|
| 1 | +use std::collections::BTreeMap; |
| 2 | +use std::sync::Arc; |
| 3 | +use std::sync::atomic::{AtomicU64, Ordering}; |
| 4 | + |
1 | 5 | use rsscript_sdk::{ |
2 | 6 | artifact::ArtifactVerifier, |
3 | 7 | compile::Compiler, |
4 | 8 | 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 | + }, |
6 | 15 | report::ExecutionEngineTelemetry, |
7 | | - runtime::{ExecutionRequest, NativeCostModel, NativeJitOptions, RunLimits, Runtime}, |
| 16 | + runtime::{ |
| 17 | + ExecutionRequest, NativeCostModel, NativeJitOptions, RunLimits, Runtime, TracePolicy, |
| 18 | + }, |
8 | 19 | }; |
9 | 20 |
|
10 | 21 | const CASES: &[(&str, &str)] = &[ |
@@ -32,6 +43,10 @@ const CASES: &[(&str, &str)] = &[ |
32 | 43 | "aggregate-continuation.rss", |
33 | 44 | "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 }", |
34 | 45 | ), |
| 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 | + ), |
35 | 50 | ( |
36 | 51 | "native-list-write.rss", |
37 | 52 | include_str!("../../../benchmarks/vm-jit/kernels/native_list_write_loop.rss"), |
@@ -165,6 +180,18 @@ fn native_engine_matches_the_verified_interpreter_corpus() { |
165 | 180 | "scalar work after aggregate materialization must re-enter native code; entries={continuation_entries}, yields={continuation_yields}, barriers={native_barrier_counts:?}, missed={interpreted_native_work}" |
166 | 181 | ); |
167 | 182 | } |
| 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 | + } |
168 | 195 | cases_with_native_entry += |
169 | 196 | usize::from(native_calls > 0 || osr_entries > 0 || continuation_entries > 0); |
170 | 197 | } |
@@ -252,3 +279,108 @@ fn bounded_step_accounting_matches_across_call_continuations() { |
252 | 279 | }; |
253 | 280 | assert!(continuation_entries >= 2); |
254 | 281 | } |
| 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 | +} |
0 commit comments