Skip to content

Commit 3945af6

Browse files
committed
feat(compiler): finalize frame-local typing and ownership
1 parent f288b7d commit 3945af6

66 files changed

Lines changed: 14094 additions & 931 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.rs

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,9 +1201,9 @@ fn render_callable_consts(callables: &[&CallableDecl]) -> String {
12011201
for param in &callable.params {
12021202
writeln!(
12031203
&mut out,
1204-
" CallableParam {{ name: {:?}, ty: CallableParamType::{}, optional: {} }},",
1204+
" CallableParam {{ name: {:?}, ty: {}, optional: {} }},",
12051205
param.name,
1206-
callable_param_variant(&param.ty_label),
1206+
callable_param_expr(&param.ty_label),
12071207
param.optional
12081208
)
12091209
.unwrap();
@@ -1626,18 +1626,38 @@ fn callable_const_base(callable: &CallableDecl) -> String {
16261626
to_shouty_snake(&format!("{prefix}_{}", callable.rust_ident))
16271627
}
16281628

1629-
fn callable_param_variant(label: &str) -> &'static str {
1629+
pub(crate) fn callable_param_expr(label: &str) -> String {
16301630
match label {
1631-
"any" => "Any",
1632-
"null" => "Null",
1633-
"int" => "Int",
1634-
"float" => "Float",
1635-
"bool" => "Bool",
1636-
"string" => "String",
1637-
"bytes" => "Bytes",
1638-
"array" => "Array",
1639-
"map" => "Map",
1640-
"number" => "Number",
1631+
"any" => "CallableParamType::Any".to_string(),
1632+
"null" => "CallableParamType::Null".to_string(),
1633+
"int" => "CallableParamType::Int".to_string(),
1634+
"float" => "CallableParamType::Float".to_string(),
1635+
"bool" => "CallableParamType::Bool".to_string(),
1636+
"string" => "CallableParamType::String".to_string(),
1637+
"bytes" => "CallableParamType::Bytes".to_string(),
1638+
"array" => "CallableParamType::Array".to_string(),
1639+
"map" => "CallableParamType::Map".to_string(),
1640+
"number" => "CallableParamType::Number".to_string(),
1641+
other if other.starts_with("fn(") => {
1642+
let (params, result) = other
1643+
.strip_prefix("fn(")
1644+
.and_then(|value| value.split_once(") -> "))
1645+
.unwrap_or_else(|| panic!("invalid callable schema '{other}'"));
1646+
let params = if params.is_empty() {
1647+
Vec::new()
1648+
} else {
1649+
params
1650+
.split(", ")
1651+
.map(callable_param_expr)
1652+
.collect::<Vec<_>>()
1653+
};
1654+
let result = callable_param_expr(result);
1655+
format!(
1656+
"CallableParamType::Callable(CallableType {{ params: &[{}], return_type: &{} }})",
1657+
params.join(", "),
1658+
result
1659+
)
1660+
}
16411661
other => panic!("unsupported callable param type '{other}'"),
16421662
}
16431663
}
@@ -2063,7 +2083,7 @@ fn static_return_type_label(output: &ReturnType) -> String {
20632083
value_type_from_label(&return_type_label(output)).to_string()
20642084
}
20652085

2066-
fn type_label(ty: &Type) -> String {
2086+
pub(crate) fn type_label(ty: &Type) -> String {
20672087
match ty {
20682088
Type::Group(group) => type_label(&group.elem),
20692089
Type::Paren(paren) => type_label(&paren.elem),
@@ -2101,6 +2121,7 @@ fn type_label(ty: &Type) -> String {
21012121
"Array" | "VmArray" | "VmArrayRef" | "VmArrayHandle" => "array".to_string(),
21022122
"Map" | "VmMap" | "VmMapRef" | "VmMapHandle" => "map".to_string(),
21032123
"Number" | "NumberValue" => "number".to_string(),
2124+
"VmCallable" => callable_type_label(segment),
21042125
"Unknown" | "UnknownValue" => "unknown".to_string(),
21052126
"CallOutcome" => "unknown".to_string(),
21062127
"Option" => {
@@ -2129,6 +2150,25 @@ fn type_label(ty: &Type) -> String {
21292150
}
21302151
}
21312152

2153+
fn callable_type_label(segment: &syn::PathSegment) -> String {
2154+
let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
2155+
panic!("VmCallable requires a function signature");
2156+
};
2157+
let Some(syn::GenericArgument::Type(Type::BareFn(function))) = args.args.first() else {
2158+
panic!("VmCallable requires fn(...) -> ...");
2159+
};
2160+
let params = function
2161+
.inputs
2162+
.iter()
2163+
.map(|input| type_label(&input.ty))
2164+
.collect::<Vec<_>>();
2165+
let result = match &function.output {
2166+
ReturnType::Default => "null".to_string(),
2167+
ReturnType::Type(_, ty) => type_label(ty),
2168+
};
2169+
format!("fn({}) -> {result}", params.join(", "))
2170+
}
2171+
21322172
fn type_label_for_vec(segment: &syn::PathSegment) -> String {
21332173
let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
21342174
panic!("Vec<T> requires one generic argument");
@@ -2303,3 +2343,26 @@ fn find_matching_paren(source: &str) -> usize {
23032343
}
23042344
panic!("unterminated macro invocation");
23052345
}
2346+
2347+
#[cfg(test)]
2348+
mod callable_schema_tests {
2349+
use super::*;
2350+
use syn::parse_quote;
2351+
2352+
#[test]
2353+
fn build_metadata_renders_typed_callable_parameters() {
2354+
let ty: Type = parse_quote!(VmCallable<fn(VmMap) -> VmMap>);
2355+
assert_eq!(type_label(&ty), "fn(map) -> map");
2356+
assert_eq!(
2357+
callable_param_expr("fn(map) -> map"),
2358+
"CallableParamType::Callable(CallableType { params: &[CallableParamType::Map], return_type: &CallableParamType::Map })"
2359+
);
2360+
2361+
let float_ty: Type = parse_quote!(VmCallable<fn(f64) -> f64>);
2362+
assert_eq!(type_label(&float_ty), "fn(float) -> float");
2363+
assert_eq!(
2364+
callable_param_expr("fn(float) -> float"),
2365+
"CallableParamType::Callable(CallableType { params: &[CallableParamType::Float], return_type: &CallableParamType::Float })"
2366+
);
2367+
}
2368+
}

docs/callable-runtime.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
# Script call frames and callable values
22

3-
RustScript bytecode format version 11 (VMBC v11) introduces runtime script call frames, first-class callable values, and the static builtin ID catalog.
3+
RustScript bytecode format version 12 (VMBC v12) carries runtime script call frames, first-class callable values, the static builtin ID catalog, and the direct script-call opcode. Version 11 introduced frames, callable values, and the static catalog; version 12 adds `callscript` for statically resolved named calls.
44

55
## Bytecode contract
66

77
- `call <import:u16> <argc:u8>` remains the direct host/builtin operation; the `u16` operand is an explicit static builtin call index from the catalog (or a host-import slot) — never a count-derived offset.
88
- `callvalue <argc:u8>` consumes a stack segment in `callee, arg0, ..., argN` order.
9+
- `callscript <prototype_id:u32> <argc:u8>` calls a statically resolved named script function by prototype ID. It consumes only `argc` arguments; no callable value is taken from the stack, so environment-free named functions can be called without a hidden callable local.
910
- callable environments are bound through the internal builtin call path; callable creation adds no bytecode opcode.
1011
- `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.
1112

12-
VMBC v11 is a hard format boundary. Decoders reject all earlier versions (v10 and below) with a deterministic unsupported-version error; there is no compatibility decoder and no old-ID alias. The stream includes script-function entry ranges, callable prototypes, function regions, root callable bindings, and call indices drawn from the static builtin catalog. PDRC v6 recordings and AOT artifacts (format 7, ABI 7) use their corresponding bumped versions and include callable metadata in cache identity.
13+
### Call ownership
14+
15+
The three call opcodes differ in who owns the callee and what the frame must provide:
16+
17+
- `call` — the callee is owned by the static builtin catalog (or the host-import slot). The frame contributes only `argc` arguments; there is no callable value anywhere in the program.
18+
- `callvalue` — the callee is a `Value::Callable` owned by the caller operand stack at the call site, and remains the caller's responsibility after the call. This path carries environments, closures, and any callable whose identity or capture state is runtime-valued.
19+
- `callscript` — the callee is owned by program callable metadata (the prototype table). The frame contributes only `argc` arguments and no callable value, but unlike `call` the callee is a script function rather than a builtin, so the call enters a new script frame with its own local base.
20+
21+
VMBC v12 is a hard format boundary. Decoders reject all earlier versions (v11 and below) with a deterministic unsupported-version error; there is no compatibility decoder and no old-ID alias. The stream includes script-function entry ranges, callable prototypes, function regions, root callable bindings, and call indices drawn from the static builtin catalog. PDRC v6 recordings and AOT artifacts (format 8, ABI 8) use their corresponding bumped versions and include callable metadata in cache identity.
1322

1423
## Static builtin IDs
1524

@@ -18,7 +27,7 @@ Every VM-visible builtin (ordinary, internal, and special-call) has one explicit
1827
- **Immutable explicit IDs.** IDs never change once assigned. Adding or reordering catalog entries never renumbers existing entries; new builtins take the next free ID in their documented block (extension `0x0000..=0xFF8F` for future builtins and host imports, special-call `0xFF90..=0xFFA1`, ordinary `0xFFA2..=0xFFFF`). The reserved sentinel gap `0xFF90..=0xFF92` stays unassigned.
1928
- **Build-time validation.** The build fails on duplicate IDs, duplicate source names, duplicate Rust variants, out-of-block IDs, class/gate inconsistencies, a discovered runtime callable without an explicit ID, or a catalog entry without a runtime callable.
2029
- **Shared std/no-std IDs.** `pd-vm-nostd` dispatches on the same static indices through the checked-in generated mirror `pd-vm-nostd/src/generated_builtin_ids.rs`; the workspace test `static_builtin_ids_are_frozen` fails when the mirror drifts from the catalog.
21-
- **One-time format break.** The static ID migration bumped VMBC to v11 (and the internal bytecode ABI to 11). Older VMBC versions are rejected, never decoded.
30+
- **Format breaks are permanent.** The static ID migration bumped VMBC to v11 (and the internal bytecode ABI to 11); the `callscript` opcode break bumped both to v12. Versions below the current format are rejected, never decoded.
2231

2332
## Runtime model
2433

@@ -29,10 +38,24 @@ Each script invocation owns:
2938
- frame-local count;
3039
- active prototype and callable identity.
3140

32-
Arguments, captures, named callable bindings, and the self binding are installed before control moves to the function entry. Recursive calls therefore allocate independent local storage and are limited to 1,024 script frames.
41+
Arguments, captures, hidden callable bindings for materialized named functions, and the self binding are installed before control moves to the function entry. Recursive calls therefore allocate independent local storage and are limited to 1,024 script frames.
3342

3443
Branches are restricted to the active function region. Validation rejects cross-region targets before execution, and the interpreter repeats the check at runtime.
3544

45+
## Frame-local allocation and callable materialization
46+
47+
Each script invocation frame is an independent local-address space with its own `local_base`. Locals that are live at the same time inside one frame interfere and receive distinct relative slot numbers; locals that belong to different frames never interfere and may reuse the same relative slot number, because the runtime frame bases already separate them. A statically resolved named call keeps the caller's argument slots and post-call values live in the caller frame, while the callee body's locals are analyzed inside the callee frame.
48+
49+
Named functions receive a hidden callable slot only when runtime `Value::Callable` identity is actually required:
50+
51+
- the function is exported under the `ExportedCallable { local_slot }` contract;
52+
- the function is referenced as a value (stored, passed, or returned);
53+
- the function captures an environment;
54+
- a dynamic call site can target the function (invoked slot or argument flow into an invoked parameter);
55+
- the function's runtime self identity is required by a capturing or dynamic recursion path.
56+
57+
Functions that only receive plain direct calls — including non-capturing direct recursion — are lowered through `callscript` by prototype ID and consume no hidden callable local. The compiler reports the aggregate frame-local count (data slots plus materialized callable slots) in `FrameLocalLimitExceeded` diagnostics, so overflow reports real counts instead of a sentinel. Genuine same-frame pressure beyond 256 simultaneous locals keeps failing until wide local bytecode lands.
58+
3659
## Callable identity and lifetime
3760

3861
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.
@@ -57,8 +80,8 @@ Polling drives execution and provides backpressure: at most one event item is bu
5780

5881
## Optimized backends
5982

60-
Whole-program AOT and Trace JIT use the same builtin call path (static catalog IDs) for environment binding and native frame dispatch for `callvalue`. Script-frame entry and return preserve frame-relative locals and typed continuations.
83+
Whole-program AOT and Trace JIT use the same builtin call path (static catalog IDs) for environment binding, native frame dispatch for `callvalue`, and prototype-direct native dispatch for `callscript`. Script-frame entry and return preserve frame-relative locals and typed continuations.
6184

6285
## Embedded runtime
6386

64-
`pd-vm-nostd` decodes the same VMBC v11 callable metadata and executes callable binding, `callvalue`, recursive frames, captures, and direct host targets using `core` plus `alloc`, dispatching on the identical static builtin IDs via its checked-in generated mirror.
87+
`pd-vm-nostd` decodes the same VMBC v12 callable metadata and executes callable binding, `callvalue`, `callscript`, recursive frames, captures, and direct host targets using `core` plus `alloc`, dispatching on the identical static builtin IDs via its checked-in generated mirror.

pd-host-function/src/lib.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@ fn type_label(ty: &Type) -> Result<String, Error> {
531531
"Array" | "VmArray" | "VmArrayRef" | "VmArrayHandle" => Ok("array".to_string()),
532532
"Map" | "VmMap" | "VmMapRef" | "VmMapHandle" => Ok("map".to_string()),
533533
"Number" | "NumberValue" => Ok("number".to_string()),
534+
"VmCallable" => callable_type_label(segment),
534535
"Unknown" | "UnknownValue" => Ok("unknown".to_string()),
535536
"CallOutcome" => Ok("unknown".to_string()),
536537
"Option" => {
@@ -575,6 +576,31 @@ fn type_label(ty: &Type) -> Result<String, Error> {
575576
}
576577
}
577578

579+
fn callable_type_label(segment: &syn::PathSegment) -> Result<String, Error> {
580+
let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
581+
return Err(Error::new_spanned(
582+
&segment.arguments,
583+
"VmCallable requires a function signature",
584+
));
585+
};
586+
let Some(syn::GenericArgument::Type(Type::BareFn(function))) = args.args.first() else {
587+
return Err(Error::new_spanned(
588+
args,
589+
"VmCallable requires fn(...) -> ...",
590+
));
591+
};
592+
let params = function
593+
.inputs
594+
.iter()
595+
.map(|input| type_label(&input.ty))
596+
.collect::<Result<Vec<_>, _>>()?;
597+
let result = match &function.output {
598+
ReturnType::Default => "null".to_string(),
599+
ReturnType::Type(_, ty) => type_label(ty)?,
600+
};
601+
Ok(format!("fn({}) -> {result}", params.join(", ")))
602+
}
603+
578604
fn type_label_for_vec(segment: &syn::PathSegment) -> Result<String, Error> {
579605
let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
580606
return Err(Error::new_spanned(
@@ -672,8 +698,8 @@ fn uses_taken_extractor(ty: &Type) -> bool {
672698

673699
#[cfg(test)]
674700
mod tests {
675-
use super::expand_pd_host_function;
676-
use syn::{ItemFn, Meta, Token, parse_quote, punctuated::Punctuated};
701+
use super::{expand_pd_host_function, type_label};
702+
use syn::{ItemFn, Meta, Token, Type, parse_quote, punctuated::Punctuated};
677703

678704
#[test]
679705
fn accepts_host_call_result_from_the_function_signature() {
@@ -779,4 +805,23 @@ mod tests {
779805
.contains("parameters must be owned and 'static")
780806
);
781807
}
808+
809+
#[test]
810+
fn callable_wrapper_preserves_parameter_and_result_schema() {
811+
let ty: Type = parse_quote!(VmCallable<fn(VmMap) -> VmMap>);
812+
assert_eq!(type_label(&ty).unwrap(), "fn(map) -> map");
813+
let attr: Punctuated<Meta, Token![,]> = parse_quote!(name = "test::stream");
814+
let item: ItemFn = parse_quote! {
815+
/// Starts a synthetic callable stream.
816+
fn stream(callback: VmCallable<fn(VmMap) -> VmMap>) -> VmResult<CallOutcome> {
817+
todo!()
818+
}
819+
};
820+
let expanded = expand_pd_host_function(attr, item).unwrap().to_string();
821+
assert!(expanded.contains("VmCallable < fn (VmMap) -> VmMap >"));
822+
assert!(expanded.contains("borrow_arg"));
823+
824+
let float_ty: Type = parse_quote!(VmCallable<fn(f64) -> f64>);
825+
assert_eq!(type_label(&float_ty).unwrap(), "fn(float) -> float");
826+
}
782827
}

pd-vm-nostd/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ compiler, parser, CLI, debugger, JIT/AOT backends, filesystem support, and opera
66

77
## Runtime surface
88

9-
- VMBC v11 decoding with script-call and callable metadata
9+
- VMBC v12 decoding with environment-free `CallScript` direct script calls alongside dynamic callable calls
1010
- stack, local, and recursive script-frame execution for direct bytecode opcodes
1111
- instruction fuel with pause/resume support
1212
- synchronous named host bindings and dynamic host dispatch

0 commit comments

Comments
 (0)