Skip to content

Commit 41fbab3

Browse files
committed
Implement $262.agent for test262 multi-agent Atomics tests
- Add js_agent.rs with agent thread spawning, broadcast, report queue, and receive primitives - Register __agent_* native hooks in global env (mod.rs) and dispatch them in eval.rs - Inject $262.agent shim in compose_test.js before harness files to avoid double $262 definition - Change runner.js to detect and pass needsAgent flag instead of skipping agent tests - Replace global ASYNC_WAITERS with thread-local PendingAsyncWaiterLocal for waitAsync promises - Poll resolved async waiters in js_promise poll_event_loop for cross-thread promise resolution - Keep event loop alive while pending async waiters exist (mod.rs event loop) - Fix named function expression (NFE) self-binding by creating intermediate scope at closure creation - Reset agent state per test for isolation, skip reset on agent threads
1 parent 0c723d6 commit 41fbab3

File tree

8 files changed

+460
-44
lines changed

8 files changed

+460
-44
lines changed

ci/compose_test.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ function verifyComposeStubMarkerCount(testPath, harnessIndex = {}, prependFiles
280280
return count === expected;
281281
}
282282

283-
function composeTest({testPath, repoDir, harnessIndex, prependFiles = [], needStrict = true}) {
283+
function composeTest({testPath, repoDir, harnessIndex, prependFiles = [], needStrict = true, needsAgent = false}) {
284284
// Returns { testToRun, tmpPath, cleanupTmp }
285285

286286
// prepends are file paths
@@ -367,6 +367,25 @@ function composeTest({testPath, repoDir, harnessIndex, prependFiles = [], needSt
367367

368368
// Write unique prepends
369369
PREPEND_FILES = ensureArrayDistinct(PREPEND_FILES);
370+
371+
// Inject $262.agent shim BEFORE harness files (atomicsHelper.js extends $262.agent)
372+
if (needsAgent) {
373+
outLines.push('// Inject: $262.agent shim for multi-agent tests');
374+
outLines.push('if (typeof $262 === "undefined") { var $262 = {}; }');
375+
outLines.push('if (!$262.agent) { $262.agent = {}; }');
376+
outLines.push('$262.agent.start = function(script) { __agent_start(script); };');
377+
outLines.push('$262.agent.broadcast = function(sab) {');
378+
outLines.push(' if (sab && sab.buffer) { __agent_broadcast(sab.buffer); }');
379+
outLines.push(' else { __agent_broadcast(sab); }');
380+
outLines.push('};');
381+
outLines.push('$262.agent.getReport = function() { return __agent_getReport(); };');
382+
outLines.push('$262.agent.sleep = function(ms) { __agent_sleep(ms); };');
383+
outLines.push('$262.agent.monotonicNow = function() { return __agent_monotonicNow(); };');
384+
outLines.push('$262.agent.leaving = function() { __agent_leaving(); };');
385+
outLines.push('$262.agent.report = function(val) { __agent_report(String(val)); };');
386+
outLines.push('');
387+
}
388+
370389
for (const p of PREPEND_FILES) {
371390
if (!p) continue;
372391
if (fs.existsSync(p)) {
@@ -466,7 +485,7 @@ function composeTest({testPath, repoDir, harnessIndex, prependFiles = [], needSt
466485

467486
// Inject unified $262 shim into the rebuilt file when required by test/meta
468487
const metaFixed = extractMeta(testPath);
469-
inject262Shim(lines2, testPath, metaFixed);
488+
inject262Shim(lines2, testPath, metaFixed, [], needsAgent);
470489

471490
const absTest = path.resolve(testPath);
472491
lines2.push(`// Inject: ${absTest}`);

ci/runner.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,9 @@ async function runAll(){
350350
if (/features:/.test(meta) && /Intl/.test(meta)) { skip++; log(`SKIP (feature: Intl) ${f}`); continue; }
351351
if (/\bIntl\b/.test(fs.readFileSync(f,'utf8'))) { skip++; log(`SKIP (contains Intl) ${f}`); continue; }
352352

353-
// Skip tests that require $262.agent (multi-threaded worker support)
354-
{
355-
const src = fs.readFileSync(f, 'utf8');
356-
if (/\$262\.agent\b/.test(src)) { skip++; log(`SKIP ($262.agent) ${f}`); continue; }
357-
}
353+
// Detect tests that require $262.agent (multi-threaded worker support)
354+
const _agentSrc = fs.readFileSync(f, 'utf8');
355+
const needsAgent = /\$262\.agent\b/.test(_agentSrc);
358356

359357
// handle includes
360358
const includes = parseIncludes(meta);
@@ -434,7 +432,7 @@ async function runAll(){
434432
// not inject a global "use strict" which can change eval semantics.
435433
const isModule = hasFlag(meta, 'module');
436434
const needStrict = !isModule && hasFlag(meta, 'onlyStrict');
437-
const {testToRun, tmpPath, cleanupTmp} = composeTest({testPath: f, repoDir: REPO_DIR, harnessIndex:HARNESS_INDEX, prependFiles: resolved_includes, needStrict});
435+
const {testToRun, tmpPath, cleanupTmp} = composeTest({testPath: f, repoDir: REPO_DIR, harnessIndex:HARNESS_INDEX, prependFiles: resolved_includes, needStrict, needsAgent});
438436

439437
let currentSucceeds = false;
440438
// Run test

src/core/eval.rs

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19020,10 +19020,29 @@ fn evaluate_function_expression<'gc>(
1902019020

1902119021
let is_strict = has_body_use_strict || env_strict_ancestor;
1902219022

19023+
// For Named Function Expressions (NFE), create an intermediate scope that
19024+
// contains the self-binding so the function can reference itself by name
19025+
// even when invoked indirectly (e.g., via setTimeout). Per the ES spec,
19026+
// the NFE name is bound in a scope between the outer env and the closure's
19027+
// own parameter scope.
19028+
let closure_env = if let Some(ref n) = name {
19029+
if !n.is_empty() {
19030+
let nfe_env = crate::core::new_js_object_data(mc);
19031+
nfe_env.borrow_mut(mc).prototype = Some(*env);
19032+
nfe_env.borrow_mut(mc).is_function_scope = false;
19033+
// The binding will be set to the function object after it's created (below).
19034+
nfe_env
19035+
} else {
19036+
*env
19037+
}
19038+
} else {
19039+
*env
19040+
};
19041+
1902319042
let closure_data = ClosureData {
1902419043
params: params.to_vec(),
1902519044
body: body.to_vec(),
19026-
env: Some(*env),
19045+
env: Some(closure_env),
1902719046
is_strict,
1902819047
enforce_strictness_inheritance: true,
1902919048
..ClosureData::default()
@@ -19032,6 +19051,12 @@ fn evaluate_function_expression<'gc>(
1903219051
func_obj.borrow_mut(mc).set_closure(Some(new_gc_cell_ptr(mc, closure_val)));
1903319052
match name {
1903419053
Some(n) if !n.is_empty() => {
19054+
// Set the NFE self-binding in the intermediate scope so the function
19055+
// can reference itself by name from any invocation path.
19056+
object_set_key_value(mc, &closure_env, &n, &Value::Object(func_obj))?;
19057+
if is_strict {
19058+
closure_env.borrow_mut(mc).set_non_writable(&n);
19059+
}
1903519060
let desc = create_descriptor_object(mc, &Value::String(utf8_to_utf16(&n)), false, false, true)?;
1903619061
crate::js_object::define_property_internal(mc, &func_obj, "name", &desc)?;
1903719062
}
@@ -20113,6 +20138,92 @@ pub fn call_native_function<'gc>(
2011320138
return Err(raise_type_error!("detachArrayBuffer requires an ArrayBuffer object").into());
2011420139
}
2011520140

20141+
// ─── $262.agent native hooks ───────────────────────────────────────────
20142+
if name == "__agent_start" {
20143+
let script = match args.first() {
20144+
Some(Value::String(s)) => crate::unicode::utf16_to_utf8(s),
20145+
Some(v) => crate::core::value_to_string(v),
20146+
None => String::new(),
20147+
};
20148+
crate::js_agent::agent_start(script);
20149+
return Ok(Some(Value::Undefined));
20150+
}
20151+
if name == "__agent_broadcast" {
20152+
// Accepts a SharedArrayBuffer object; extract its Arc data
20153+
if let Some(Value::Object(obj)) = args.first()
20154+
&& let Some(ab_val) = slot_get_chained(obj, &InternalSlot::ArrayBuffer)
20155+
&& let Value::ArrayBuffer(ab) = &*ab_val.borrow()
20156+
{
20157+
let ab_ref = ab.borrow();
20158+
if !ab_ref.shared {
20159+
return Err(raise_type_error!("agent.broadcast requires a SharedArrayBuffer").into());
20160+
}
20161+
let data_arc = ab_ref.data.clone();
20162+
let byte_length = ab_ref.data.lock().unwrap().len();
20163+
crate::js_agent::agent_broadcast(data_arc, byte_length);
20164+
return Ok(Some(Value::Undefined));
20165+
}
20166+
return Err(raise_type_error!("agent.broadcast requires a SharedArrayBuffer").into());
20167+
}
20168+
if name == "__agent_getReport" {
20169+
return Ok(Some(match crate::js_agent::agent_get_report() {
20170+
Some(s) => Value::String(crate::unicode::utf8_to_utf16(&s)),
20171+
None => Value::Null,
20172+
}));
20173+
}
20174+
if name == "__agent_sleep" {
20175+
let ms = match args.first() {
20176+
Some(v) => crate::core::to_number_with_env(mc, env, v)?,
20177+
None => 0.0,
20178+
};
20179+
crate::js_agent::agent_sleep(ms);
20180+
return Ok(Some(Value::Undefined));
20181+
}
20182+
if name == "__agent_monotonicNow" {
20183+
return Ok(Some(Value::Number(crate::js_agent::agent_monotonic_now())));
20184+
}
20185+
if name == "__agent_receiveBroadcast" {
20186+
// Called from within an agent thread. Blocks until broadcast is available.
20187+
// Returns a SharedArrayBuffer object wrapping the same Arc data.
20188+
let (data_arc, byte_length) = crate::js_agent::agent_receive_broadcast();
20189+
let obj = crate::core::new_js_object_data(mc);
20190+
// Set up SharedArrayBuffer prototype
20191+
if let Some(ctor_val) = crate::core::env_get(env, "SharedArrayBuffer")
20192+
&& let Value::Object(ctor_obj) = &*ctor_val.borrow()
20193+
&& let Some(p_val) = object_get_key_value(ctor_obj, "prototype")
20194+
&& let Value::Object(p_obj) = &*p_val.borrow()
20195+
{
20196+
obj.borrow_mut(mc).prototype = Some(*p_obj);
20197+
}
20198+
let buffer = new_gc_cell_ptr(
20199+
mc,
20200+
crate::core::value::JSArrayBuffer {
20201+
data: data_arc,
20202+
shared: true,
20203+
detached: false,
20204+
max_byte_length: None,
20205+
immutable: false,
20206+
},
20207+
);
20208+
slot_set(mc, &obj, InternalSlot::ArrayBuffer, &Value::ArrayBuffer(buffer));
20209+
// Store byte_length for byteLength getter
20210+
let _ = byte_length; // byte_length is already encoded in the Arc Vec
20211+
return Ok(Some(Value::Object(obj)));
20212+
}
20213+
if name == "__agent_report" {
20214+
let val = match args.first() {
20215+
Some(Value::String(s)) => crate::unicode::utf16_to_utf8(s),
20216+
Some(v) => crate::core::value_to_string(v),
20217+
None => String::new(),
20218+
};
20219+
crate::js_agent::agent_report(val);
20220+
return Ok(Some(Value::Undefined));
20221+
}
20222+
if name == "__agent_leaving" {
20223+
crate::js_agent::agent_leaving();
20224+
return Ok(Some(Value::Undefined));
20225+
}
20226+
2011620227
if name == "Array.isArray" || name == "Array.from" || name == "Array.of" || name == "Array.fromAsync" {
2011720228
let method = name.trim_start_matches("Array.");
2011820229
let v = crate::js_array::handle_array_static_method(mc, method, this_val, args, env)?;

src/core/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,26 @@ pub fn initialize_global_constructors_with_parent<'gc>(
360360
// Expose __createRealm__ as a native callable for cross-realm tests.
361361
env_set(mc, env, "__createRealm__", &Value::Function("__createRealm__".to_string()))?;
362362

363+
// $262.agent native hooks for test262 multi-agent tests
364+
object_set_key_value(mc, env, "__agent_start", &Value::Function("__agent_start".to_string()))?;
365+
object_set_key_value(mc, env, "__agent_broadcast", &Value::Function("__agent_broadcast".to_string()))?;
366+
object_set_key_value(mc, env, "__agent_getReport", &Value::Function("__agent_getReport".to_string()))?;
367+
object_set_key_value(mc, env, "__agent_sleep", &Value::Function("__agent_sleep".to_string()))?;
368+
object_set_key_value(
369+
mc,
370+
env,
371+
"__agent_monotonicNow",
372+
&Value::Function("__agent_monotonicNow".to_string()),
373+
)?;
374+
object_set_key_value(
375+
mc,
376+
env,
377+
"__agent_receiveBroadcast",
378+
&Value::Function("__agent_receiveBroadcast".to_string()),
379+
)?;
380+
object_set_key_value(mc, env, "__agent_report", &Value::Function("__agent_report".to_string()))?;
381+
object_set_key_value(mc, env, "__agent_leaving", &Value::Function("__agent_leaving".to_string()))?;
382+
363383
#[cfg(feature = "os")]
364384
crate::js_os::initialize_os_module(mc, env)?;
365385

@@ -495,6 +515,11 @@ where
495515
arena.mutate(|mc, root| {
496516
initialize_global_constructors(mc, &root.global_env)?;
497517

518+
// Reset agent state for test isolation (only on main thread, not agent threads)
519+
if !crate::js_agent::is_agent_thread() {
520+
crate::js_agent::reset_agent_state();
521+
}
522+
498523
env_set(mc, &root.global_env, "globalThis", &Value::Object(root.global_env))?;
499524
root.global_env.borrow_mut(mc).set_non_enumerable("globalThis");
500525
object_set_key_value(mc, &root.global_env, "this", &Value::Object(root.global_env))?;
@@ -673,6 +698,17 @@ where
673698
continue;
674699
}
675700

701+
// Keep event loop alive while Atomics.waitAsync promises are pending.
702+
// Background notification threads will wake us via EVENT_LOOP_WAKE.
703+
if crate::js_typedarray::has_pending_async_waiters() {
704+
let (lock, cv) = crate::js_promise::get_event_loop_wake();
705+
let mut guard = lock.lock().unwrap();
706+
*guard = false;
707+
let (_g, _res) = cv.wait_timeout(guard, std::time::Duration::from_millis(100)).unwrap();
708+
count += 1;
709+
continue;
710+
}
711+
676712
// If configured to wait for active handles (Node-like), and we have
677713
// timers/intervals registered, keep the event loop alive until
678714
// they are gone. We poll periodically and wait on the condvar

0 commit comments

Comments
 (0)