Skip to content

Commit 9d145bc

Browse files
committed
Make callable values Program-owned
1 parent ad3f72c commit 9d145bc

15 files changed

Lines changed: 41 additions & 133 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ The compiler keeps direct host dispatch separate from script callable dispatch.
674674
- `ret` completes the active typed continuation: root halt, caller resume, or host return.
675675

676676
At runtime, direct `call` uses `Vm::execute_host_call`, while `callvalue` validates the callable's
677-
Program instance, prototype, schema, arity, and frame layout before dispatch. VMBC v9 carries the
677+
prototype, schema, arity, and frame layout before dispatch. VMBC v9 carries the
678678
script-function, prototype, function-region, and root-binding tables. See
679679
[`docs/callable-runtime.md`](docs/callable-runtime.md) for the bytecode, lifecycle, callback, and
680680
optimized-backend contracts.

docs/callable-runtime.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RustScript bytecode format version 9 introduces runtime script call frames and f
99
- callable environments are bound through the internal builtin call path; callable creation adds no bytecode opcode.
1010
- `ret` completes the active script frame. A nested frame leaves exactly one result at the caller segment base, using `null` when the body produced no value. Root `ret` keeps the historical program-result stack behavior.
1111

12-
VMBC v9 is a hard format boundary. Decoders reject older versions. The stream includes script-function entry ranges, callable prototypes, function regions, and root callable bindings. PDRC recordings and AOT artifacts use their corresponding bumped format/ABI versions and include callable metadata in cache identity.
12+
VMBC v9 is a hard format boundary. Decoders reject older versions. The stream includes script-function entry ranges, callable prototypes, function regions, and root callable bindings. PDRC v4 recordings and AOT artifacts use their corresponding bumped format/ABI versions and include callable metadata in cache identity.
1313

1414
## Runtime model
1515

@@ -26,11 +26,11 @@ Branches are restricted to the active function region. Validation rejects cross-
2626

2727
## Callable identity and lifetime
2828

29-
A callable contains the owning program-instance ID, prototype ID, kind, and optional environment. Capture-free function items compare by program/prototype identity. Closures compare by runtime identity. Callable constants are forbidden; functions are initialized from program metadata and closures are materialized at their declaration site.
29+
A callable contains its prototype ID, kind, and optional environment. The Program/Store owns the callable lifetime. Capture-free function items compare by prototype identity inside that Program; closures compare by runtime environment identity. Callable constants are forbidden; functions are initialized from Program metadata and closures are materialized at their declaration site.
3030

31-
Resetting or replacing the VM program assigns a new program-instance ID. Calling a value from an older instance reports `StaleCallable`.
31+
Reset clears Program runtime values and rebinds root function items from Program metadata. Program replacement cancels and releases the old Store callback registry before dropping the old Program. Raw callable values are Program-local and are not portable across Program/Store boundaries.
3232

33-
`Vm::invoke_callable` is the synchronous host-entry API for a callable retained after the root program halts. For resumable work, `Vm::start_callable` returns `VmStatus`; after `Yielded` or `Waiting`, continue with `Vm::resume` and read the completed value with `Vm::take_callable_result`. `ScriptCallback::start` and `Store::take_callback_result` expose the same flow for typed callbacks. `Store::script_callback` validates the Store identity, Program instance, arity, and copied callable schema and returns a typed `ScriptCallback<Args, Ret>`. A callback can invoke directly, create a `Send` queued invocation on another thread, unsubscribe all aliases, or enter the Store FIFO through `enqueue_callback`; queue errors propagate and no implicit coalescing occurs. `Vm::shutdown` clears queued work, runtime values and host resources before invalidating every exported callable.
33+
`Vm::invoke_callable` is the synchronous host-entry API for a callable retained while the current Program is active. For resumable work, `Vm::start_callable` returns `VmStatus`; after `Yielded` or `Waiting`, continue with `Vm::resume` and read the completed value with `Vm::take_callable_result`. `ScriptCallback::start` and `Store::take_callback_result` expose the same flow for typed callbacks. `Store::script_callback` validates Store ownership, arity, and the copied callable schema and returns a typed `ScriptCallback<Args, Ret>` with no stale identity field. A callback can invoke directly, create a `Send` queued invocation on another thread, unsubscribe all aliases, or enter the Store FIFO through `enqueue_callback`; queue errors propagate and no implicit coalescing occurs. `Vm::shutdown` clears queued work, runtime values and host resources before invalidating every exported callback through the Store registry.
3434

3535
PDRC recordings preserve full execution-frame metadata. Callable environments use identity-table encoding, so aliases still share one environment after decode.
3636

pd-vm-nostd/src/error.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ pub enum VmError {
1313
InvalidLocal(u8),
1414
InvalidCall(u16),
1515
InvalidCallable,
16-
StaleCallable,
1716
InvalidCallablePrototype(u32),
1817
CallStackOverflow,
1918
InvalidCallArity {
@@ -49,7 +48,6 @@ impl fmt::Display for VmError {
4948
Self::InvalidLocal(index) => write!(f, "invalid local index: {index}"),
5049
Self::InvalidCall(index) => write!(f, "invalid call index: {index}"),
5150
Self::InvalidCallable => f.write_str("callvalue operand is not callable"),
52-
Self::StaleCallable => f.write_str("callable belongs to another program instance"),
5351
Self::InvalidCallablePrototype(index) => {
5452
write!(f, "invalid callable prototype: {index}")
5553
}

pd-vm-nostd/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub use program::{
2020
CallablePrototype, CallableTarget, FunctionRegion, HostImport, OpCode, Program,
2121
RootCallableBinding, ScriptFunction, ValueType,
2222
};
23-
pub use value::{CallableEnvironment, CallableKind, CallableValue, ProgramInstanceId, Value};
23+
pub use value::{CallableEnvironment, CallableKind, CallableValue, Value};
2424
pub use vm::{Vm, VmResult, VmStatus};
2525
pub use vmbc::decode_program;
2626

pd-vm-nostd/src/value.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ pub type SharedString = Rc<String>;
77
pub type SharedBytes = Rc<Vec<u8>>;
88
pub type SharedArray = Rc<Vec<Value>>;
99
pub type SharedMap = Rc<Vec<(Value, Value)>>;
10-
pub type ProgramInstanceId = u64;
1110
pub type SharedCallable = Rc<CallableValue>;
1211
pub type CallableEnvironment = Rc<RefCell<Vec<Value>>>;
1312

@@ -20,7 +19,6 @@ pub enum CallableKind {
2019

2120
#[derive(Clone, Debug)]
2221
pub struct CallableValue {
23-
pub program_instance: ProgramInstanceId,
2422
pub prototype_id: u32,
2523
pub kind: CallableKind,
2624
pub env: Option<CallableEnvironment>,
@@ -52,9 +50,7 @@ impl PartialEq for Value {
5250
(Self::Map(lhs), Self::Map(rhs)) => lhs == rhs,
5351
(Self::Callable(lhs), Self::Callable(rhs)) => {
5452
if lhs.env.is_none() && rhs.env.is_none() {
55-
lhs.program_instance == rhs.program_instance
56-
&& lhs.prototype_id == rhs.prototype_id
57-
&& lhs.kind == rhs.kind
53+
lhs.prototype_id == rhs.prototype_id && lhs.kind == rhs.kind
5854
} else {
5955
Rc::ptr_eq(lhs, rhs)
6056
}

pd-vm-nostd/src/vm.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ pub struct Vm<C = ()> {
4242
host_dispatcher: Option<HostDispatcher<C>>,
4343
context: C,
4444
fuel: Option<u64>,
45-
program_instance: u64,
4645
frames: Vec<ExecutionFrame>,
4746
}
4847

@@ -86,7 +85,6 @@ impl<C> Vm<C> {
8685
host_dispatcher,
8786
context,
8887
fuel: None,
89-
program_instance: 1,
9088
frames: Vec::new(),
9189
};
9290
vm.initialize_root_callables();
@@ -105,7 +103,6 @@ impl<C> Vm<C> {
105103
let slot = binding.local_slot as usize;
106104
if let Some(local) = self.locals.get_mut(slot) {
107105
*local = Value::Callable(Rc::new(CallableValue {
108-
program_instance: self.program_instance,
109106
prototype_id: binding.prototype_id,
110107
kind: prototype.kind,
111108
env: None,
@@ -136,7 +133,6 @@ impl<C> Vm<C> {
136133
}
137134
let env: CallableEnvironment = Rc::new(RefCell::new(captures));
138135
Ok(Value::Callable(Rc::new(CallableValue {
139-
program_instance: self.program_instance,
140136
prototype_id,
141137
kind: prototype.kind,
142138
env: Some(env),
@@ -157,9 +153,6 @@ impl<C> Vm<C> {
157153
Value::Callable(callable) => callable,
158154
_ => return Err(VmError::InvalidCallable),
159155
};
160-
if callable.program_instance != self.program_instance {
161-
return Err(VmError::StaleCallable);
162-
}
163156
let prototype = self
164157
.program
165158
.callable_prototypes()
@@ -215,7 +208,6 @@ impl<C> Vm<C> {
215208
if slot < prototype.frame_local_count {
216209
self.locals[local_base + slot] =
217210
Value::Callable(Rc::new(CallableValue {
218-
program_instance: self.program_instance,
219211
prototype_id: binding.prototype_id,
220212
kind: binding_prototype.kind,
221213
env: None,

plans/function_as_value_plan.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@
2323
**Complete:**
2424

2525
- Add Program-owned callable prototypes, script entry points, parameter/local layouts, capture layouts, source metadata, and instantiated generic signatures.
26-
- Add `Value::Callable { program_instance, prototype_id, kind, env }`; it must not own `Arc<Program>`.
26+
- Add Program-owned `Value::Callable { prototype_id, kind, env }`. Callable values follow their Program/Store lifetime and never retain an old `Program`.
2727
- Add frame-relative locals, a real execution-frame stack, and one `Ret` rule: complete the active frame and follow its typed continuation. Root execution uses an explicit `Halt` continuation; nested RSS calls use `ResumeBytecode`; Rust invocation uses `ReturnToHost`. Never infer return meaning from an empty call stack.
2828
- Add `CallValue(argc)` as the common RSS function-item/closure invocation path; retain existing `Call` for direct host/builtin imports.
2929
- Initialize capture-free named bindings from Program metadata.
3030
- Add one shared environment-binding path for every capture-bearing callable. Introduce a dedicated runtime operation only if existing VM operations cannot bind captures.
31-
- Validate `ProgramInstanceId` before prototype lookup; replaced/reset Program handles return `StaleCallable`.
31+
- Keep callable values inside their owning Program/Store. Program replacement and reset invalidate the old callable registry as a lifecycle operation; callable dispatch does not compare stale identities.
3232

33-
**Acceptance:** Named calls, recursion, closure calls, arity failures, stale handles, return values, yield/pending, and resume all use real frames.
33+
**Acceptance:** Named calls, recursion, closure calls, arity failures, return values, yield/pending, and resume all use real frames.
3434

3535
## 3. Integrate captures, types, and generics
3636

@@ -59,7 +59,7 @@
5959
- Release environments correctly on clone, overwrite, `Drop`, unwind, collection removal, return, cancellation, and reset; reject ownership cycles until cycle collection exists.
6060
- On Program replacement, REPL installation, or reuse reset: cancel callback work, remove listeners, clear callable roots/persisted callables, drop the old Program, and invalidate all external old handles.
6161
- Add Rust APIs to resolve exported RSS functions and invoke function items or closures through one resumable path.
62-
- Add typed `ScriptCallback<Args, Ret>` with Store identity, Program instance ID, and copied callable schema.
62+
- Add typed `ScriptCallback<Args, Ret>` with Store ownership and copied callable schema. It follows the Program registry and stores no stale Program identity.
6363
- Serialize callbacks through the Store queue; define FIFO/coalescing, cross-thread enqueue, unsubscribe, teardown, error, and return-value policies.
6464
- Verify that the final Rust-held callback releases captures exactly once.
6565

plans/real_script_call_frames_plan.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ First-class value typing, generic schemas, lifecycle, and retained callback APIs
2929
- Apply a hard internal format break; no backward decoder, migration, opcode reinterpretation, or compatibility branch is required:
3030
- bump VMBC `ENCODE_VERSION` from 8 to 9 and accept only version 9;
3131
- bump AOT artifact `VERSION` from 2 to 3 and native `ABI_VERSION` from 1 to 2 because Program/VM/native layouts and return control flow change;
32-
- bump debugger recording `PDRC` from 2 to 3, accept only version 3, and remove legacy version-1 recording acceptance;
32+
- bump debugger recording `PDRC` from 2 to 4, accept only version 4, and reject legacy versions 1 through 3;
3333
- add the bytecode ABI revision to Program/native trace cache keys together with callable tables, function regions, and slot layouts so old in-memory entries cannot match;
3434
- regenerate internal fixtures/artifacts and make every old format fail with its existing unsupported-version error.
3535

@@ -42,8 +42,8 @@ First-class value typing, generic schemas, lifecycle, and retained callback APIs
4242
**Complete:**
4343

4444
- Add `Program.script_functions` and `Program.callable_prototypes` containing entry IP, arity, frame-local count, parameter slots, slot-location layout, capture layout, source metadata, and instantiated generic signature.
45-
- Add `Value::Callable { program_instance, prototype_id, kind, env }`; it stores no `Arc<Program>`.
46-
- Assign a fresh `ProgramInstanceId` on Program installation/replacement/reset. `CallValue` validates it before prototype lookup and returns `StaleCallable` for old handles.
45+
- Add Program-owned `Value::Callable { prototype_id, kind, env }`; it stores no `Arc<Program>` and never outlives the owning Program/Store registry.
46+
- On Program installation/replacement/reset, clear runtime callable values and invalidate external callbacks through the owning Store registry. Callable dispatch does not compare stale identities.
4747
- Add an execution-frame stack. Each frame records:
4848
- typed continuation (`Halt`, `ResumeBytecode`, or `ReturnToHost`);
4949
- prototype/function identity;
@@ -52,7 +52,7 @@ First-class value typing, generic schemas, lifecycle, and retained callback APIs
5252
- active callable/environment root;
5353
- recursion, fuel/epoch, suspension, and debug state required for exact resume.
5454
- Resolve logical locals through metadata such as `SlotLocation::Frame(offset)` or `SlotLocation::Capture(cell)`. A logical slot must map to exactly one storage class in each prototype.
55-
- On `CallValue`, validate value kind, Program instance, arity, and recursion limit; preserve caller state; allocate callee locals; bind arguments/self/environment; then enter the target.
55+
- On `CallValue`, validate value kind, prototype, arity, and recursion limit; preserve caller state; allocate callee locals; bind arguments/self/environment; then enter the target.
5656
- On `Ret`, normalize the result, release callee roots/locals, restore the typed continuation, and either resume bytecode, return to Rust, or report halt.
5757
- Emit root code and every RSS function body once in one code blob, with explicit root/function regions.
5858
- Validate `Br` and `Brfalse` targets remain inside their current root/function region; cross-function control flow is valid only through `CallValue` and `Ret`.
@@ -93,7 +93,7 @@ First-class value typing, generic schemas, lifecycle, and retained callback APIs
9393
- `Br`/`Brfalse` cannot cross root/function regions;
9494
- `Ldc` cannot deserialize Program-bound callable constants;
9595
- `Pop` and `Dup` apply normal callable/environment clone/drop ownership;
96-
- `Ceq` compares function items by Program instance/prototype identity and closure aliases by callable/environment identity; separate closure evaluations compare unequal;
96+
- `Ceq` compares function items by prototype identity inside their owning Program and closure aliases by callable/environment identity; separate closure evaluations compare unequal;
9797
- arithmetic, bitwise, ordering, and shift opcodes reject callable operands through existing type-error paths.
9898
- Remove or redesign the current interpreter `Call + Ret` fusion. It may run only when typed continuation/frame semantics, fuel ticks, epoch checks, result normalization, and lifecycle behavior remain identical to unfused execution.
9999
- Change the current interpreter mapping from unconditional `Ret -> Halted` to frame completion.
@@ -112,7 +112,7 @@ First-class value typing, generic schemas, lifecycle, and retained callback APIs
112112

113113
- Update assembler/disassembler, operand decoding, VMBC validation, no-std execution, AOT bundles, REPL replacement, recording/replay, formatter output, and source diagnostics.
114114
- Add function-region/prototype/frame data to debug info, stack traces, current-frame local inspection, and Rust invocation status.
115-
- Add focused tests for root `Ret`, nested `Ret`, Rust-root `Ret`, `CallValue` stack shape, stale handles, arity/type errors, region-confined branches, slot-location validation, callable equality, `Pop`/`Dup` lifecycle, and rewritten `Call + Ret` optimization.
115+
- Add focused tests for root `Ret`, nested `Ret`, Rust-root `Ret`, `CallValue` stack shape, Program-owned callback invalidation, arity/type errors, region-confined branches, slot-location validation, callable equality, `Pop`/`Dup` lifecycle, and rewritten `Call + Ret` optimization.
116116
- Add interpreter/JIT/AOT parity tests for direct/dynamic calls, recursion, captures, host calls, errors, yield/pending/resume, cancellation, reset, and exact drop counts.
117117
- Instrument native tests to assert zero callable-induced side exits, trace breaks, interpreter handoffs, and rejected valid AOT Programs.
118118
- Run focused suites during implementation, then formatting, workspace tests, Clippy with `-D warnings`, and release builds.

src/builtins/runtime/core.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,6 @@ mod tests {
940940
#[test]
941941
fn callable_map_keys_are_rejected() {
942942
let callable = Value::Callable(Arc::new(crate::CallableValue {
943-
program_instance: 1,
944943
prototype_id: 0,
945944
kind: crate::CallableKind::FunctionItem,
946945
env: None,

src/bytecode.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ pub type SharedBytes = Arc<Vec<u8>>;
1212
pub type SharedArray = Arc<Vec<Value>>;
1313
pub type SharedMap = Arc<VmMap>;
1414
pub type SharedCallable = Arc<CallableValue>;
15-
pub type ProgramInstanceId = u64;
1615

1716
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1817
pub enum CallableKind {
@@ -65,7 +64,6 @@ pub struct CallableEnvironment {
6564

6665
#[derive(Clone, Debug)]
6766
pub struct CallableValue {
68-
pub program_instance: ProgramInstanceId,
6967
pub prototype_id: u32,
7068
pub kind: CallableKind,
7169
pub env: Option<Arc<CallableEnvironment>>,
@@ -296,7 +294,6 @@ fn hash_map_key(value: &Value, state: &mut impl Hasher) {
296294
}
297295
Value::Callable(callable) => {
298296
7u8.hash(state);
299-
callable.program_instance.hash(state);
300297
callable.prototype_id.hash(state);
301298
callable.kind.hash(state);
302299
callable.env.as_ref().map(Arc::as_ptr).hash(state);
@@ -378,7 +375,6 @@ pub(crate) fn hash_value(value: &Value, state: &mut impl Hasher) {
378375
}
379376
Value::Callable(callable) => {
380377
7u8.hash(state);
381-
callable.program_instance.hash(state);
382378
callable.prototype_id.hash(state);
383379
callable.kind.hash(state);
384380
callable.env.as_ref().map(Arc::as_ptr).hash(state);
@@ -503,10 +499,7 @@ impl PartialEq for Value {
503499
}
504500

505501
fn callable_value_eq(lhs: &CallableValue, rhs: &CallableValue) -> bool {
506-
if lhs.program_instance != rhs.program_instance
507-
|| lhs.prototype_id != rhs.prototype_id
508-
|| lhs.kind != rhs.kind
509-
{
502+
if lhs.prototype_id != rhs.prototype_id || lhs.kind != rhs.kind {
510503
return false;
511504
}
512505
match (&lhs.env, &rhs.env) {

0 commit comments

Comments
 (0)