Skip to content

Commit aff3d2b

Browse files
committed
test(jit): prove provider and await continuations
1 parent 53437e9 commit aff3d2b

2 files changed

Lines changed: 139 additions & 2 deletions

File tree

crates/rsscript-sdk/tests/native_jit_differential.rs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
use std::collections::BTreeMap;
2+
use std::sync::Arc;
3+
use std::sync::atomic::{AtomicU64, Ordering};
4+
15
use rsscript_sdk::{
26
artifact::ArtifactVerifier,
37
compile::Compiler,
48
operation::CancellationToken,
5-
provider_api::ProviderRegistry,
9+
provider_api::{
10+
BlockingBehavior, CancellationBehavior, DataEffect, ExternalSymbol, FunctionSignature,
11+
ParameterSignature, ProviderCallMode, ProviderDescriptor, ProviderError,
12+
ProviderErrorMapping, ProviderFunction, ProviderFunctionDescriptor, ProviderRegistry,
13+
RUNTIME_ABI_VERSION, ResourceCleanupContract, WireInterpreterFn, WireValue,
14+
},
615
report::ExecutionEngineTelemetry,
7-
runtime::{ExecutionRequest, NativeCostModel, NativeJitOptions, RunLimits, Runtime},
16+
runtime::{
17+
ExecutionRequest, NativeCostModel, NativeJitOptions, RunLimits, Runtime, TracePolicy,
18+
},
819
};
920

1021
const CASES: &[(&str, &str)] = &[
@@ -32,6 +43,10 @@ const CASES: &[(&str, &str)] = &[
3243
"aggregate-continuation.rss",
3344
"struct AggregateBox { value: Int } fn main() -> Int { let boxed = AggregateBox(value: 13); let extracted = boxed.value; let a = extracted * 3; let b = a + 11; let c = b * 5; return c }",
3445
),
46+
(
47+
"await-continuation.rss",
48+
"async fn boundary(value: Int) -> Int { return value + 4 } async fn main() -> Int { let a = 7; let b = a * 3; let c = b + 11; task_group { async let pending = boundary(value: c); let d = await pending; let e = d * 5; let f = e - 9; let g = f + 2; return g } }",
49+
),
3550
(
3651
"native-list-write.rss",
3752
include_str!("../../../benchmarks/vm-jit/kernels/native_list_write_loop.rss"),
@@ -165,6 +180,18 @@ fn native_engine_matches_the_verified_interpreter_corpus() {
165180
"scalar work after aggregate materialization must re-enter native code; entries={continuation_entries}, yields={continuation_yields}, barriers={native_barrier_counts:?}, missed={interpreted_native_work}"
166181
);
167182
}
183+
if *file == "await-continuation.rss" {
184+
assert!(
185+
continuation_entries >= 2 && continuation_yields >= 2,
186+
"scalar work around await must use native continuations; entries={continuation_entries}, yields={continuation_yields}, barriers={native_barrier_counts:?}"
187+
);
188+
assert!(
189+
native_barrier_counts
190+
.get("await")
191+
.is_some_and(|count| *count >= 1),
192+
"await must remain a VM-owned barrier"
193+
);
194+
}
168195
cases_with_native_entry +=
169196
usize::from(native_calls > 0 || osr_entries > 0 || continuation_entries > 0);
170197
}
@@ -252,3 +279,108 @@ fn bounded_step_accounting_matches_across_call_continuations() {
252279
};
253280
assert!(continuation_entries >= 2);
254281
}
282+
283+
#[test]
284+
fn provider_barrier_executes_once_and_reenters_native() {
285+
const SOURCE: &str = "module app\nuse host.math.*\nfn main() -> Int { let a = 7; let b = a * 3; let c = b + 11; let d = adjust(value: read c); let e = d * 5; let f = e - 9; let g = f + 2; return g }";
286+
const INTERFACE: &str = "module host.math\npub fn adjust(value: read Int) -> Int\n";
287+
288+
let symbol = ExternalSymbol::new("host.math.adjust").expect("test symbol is valid");
289+
let signature = FunctionSignature {
290+
parameters: vec![ParameterSignature {
291+
name: "value".into(),
292+
effect: DataEffect::Read,
293+
ty: "Int".into(),
294+
retained: false,
295+
}],
296+
result: "Int".into(),
297+
asynchronous: false,
298+
};
299+
let descriptor = ProviderDescriptor {
300+
provider_id: "jit.test.math".into(),
301+
provider_version: "1".into(),
302+
supported_abi: vec![RUNTIME_ABI_VERSION],
303+
record_layouts: Vec::new(),
304+
variant_layouts: Vec::new(),
305+
functions: vec![ProviderFunctionDescriptor {
306+
symbol: symbol.clone(),
307+
signature: signature.clone(),
308+
entry: "adjust".into(),
309+
call_mode: ProviderCallMode::Sync,
310+
blocking: BlockingBehavior::NonBlocking,
311+
cancellation: CancellationBehavior::NotApplicable,
312+
thread_safe: true,
313+
reentrant: true,
314+
resource_cleanup: ResourceCleanupContract::None,
315+
error_mapping: ProviderErrorMapping::StructuredV1,
316+
}],
317+
};
318+
let calls = Arc::new(AtomicU64::new(0));
319+
let provider_calls = Arc::clone(&calls);
320+
let mut providers = ProviderRegistry::default();
321+
providers
322+
.register(
323+
&descriptor,
324+
BTreeMap::from([(
325+
symbol,
326+
ProviderFunction {
327+
signature,
328+
callable: WireInterpreterFn::new(move |args| match args.as_slice() {
329+
[WireValue::Int { value }] => {
330+
provider_calls.fetch_add(1, Ordering::SeqCst);
331+
Ok(WireValue::Int { value: value + 4 })
332+
}
333+
_ => Err(ProviderError::invalid_argument(
334+
"adjust expects one Int argument",
335+
)),
336+
}),
337+
},
338+
)]),
339+
)
340+
.expect("test Provider matches its descriptor");
341+
342+
let built = Compiler
343+
.compile_with_interfaces(&[("main.rss", SOURCE)], &[("math.rssi", INTERFACE)])
344+
.expect("provider continuation source compiles");
345+
let admitted = ArtifactVerifier
346+
.verify(built)
347+
.expect("provider continuation artifact verifies")
348+
.admit_trusted_input();
349+
let linked = Runtime::new(providers)
350+
.link(&admitted)
351+
.expect("test Provider links");
352+
353+
let interpreter = linked.execute(ExecutionRequest::default().trace(TracePolicy::MetadataOnly));
354+
assert_eq!(calls.load(Ordering::SeqCst), 1);
355+
let native = linked.execute(
356+
ExecutionRequest::default()
357+
.trace(TracePolicy::MetadataOnly)
358+
.native_jit(NativeJitOptions {
359+
cost_model: NativeCostModel::Off,
360+
..NativeJitOptions::default()
361+
}),
362+
);
363+
assert_eq!(calls.load(Ordering::SeqCst), 2);
364+
assert_eq!(native.outcome(), interpreter.outcome());
365+
assert_eq!(
366+
native.usage.steps_consumed,
367+
interpreter.usage.steps_consumed
368+
);
369+
assert_eq!(native.provider_call_traces.len(), 1);
370+
let ExecutionEngineTelemetry::Native {
371+
continuation_entries,
372+
continuation_yields,
373+
native_barrier_counts,
374+
..
375+
} = native.telemetry.engine
376+
else {
377+
panic!("Provider continuation must report native telemetry");
378+
};
379+
assert!(continuation_entries >= 2);
380+
assert!(continuation_yields >= 2);
381+
assert!(
382+
native_barrier_counts
383+
.get("external_call")
384+
.is_some_and(|count| *count >= 1)
385+
);
386+
}

docs/spec/native-jit-contract.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ currently keep every continuation in the interpreter.
5252
coexist in the VM frame: continuation marshalling validates only the exact
5353
register footprint of the selected scalar region, so scalar work after an
5454
interpreter-materialized aggregate can re-enter native code safely.
55+
- Provider calls and `await` are exercised as normal mixed-mode boundaries by
56+
interpreter/native differential tests. The VM executes each boundary exactly
57+
once, preserves Provider traces and scheduler semantics, then probes the next
58+
scalar continuation. Generated code never re-enters the interpreter or spans a
59+
suspension.
5560
- Structural compilation work is bounded by `JitLimits` before Cranelift code
5661
generation. Instruction, register, CFG-edge, operand, analysis-word, deopt,
5762
memo-scope, callee, and recursive-group counts have deterministic limits.

0 commit comments

Comments
 (0)