///|
/// Promise implementation for JavaScript engine
/// Follows ECMAScript spec with microtask/job queue integration
/// Uses host environment event loop for JS compilation target
///
/// Event Loop Architecture:
/// - For JS compilation (host environment): Uses host event loop via FFI
/// - For WASM compilation: Deferred - will use browser's event loop
/// - For native compilation: Deferred - will need custom event loop implementation
///
/// Reference: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-jobs
///|
/// Reference to Promise.prototype for method lookup
let promise_prototype_ref : Ref[Value?] = { val: None }
///|
/// Get Promise.prototype methods for a Promise instance
pub fn get_promise_method(_data : PromiseData, prop : String) -> Value {
match promise_prototype_ref.val {
Some(proto) =>
match proto {
Object(proto_data) =>
match proto_data.properties.get(prop) {
Some(m) => m
None => Undefined
}
_ => Undefined
}
None => Undefined
}
}
///|
/// Enqueue a microtask to be executed when the current execution context completes
/// This implements HostEnqueuePromiseJob from the ECMAScript spec
pub fn Interpreter::enqueue_microtask(
self : Interpreter,
callback : Value,
args : Array[Value],
) -> Unit {
self.microtask_queue.push({ callback, args })
}
///|
/// Run all pending microtasks until the queue is empty
/// This is called after each task (script execution) completes
/// Implements the microtask checkpoint from WHATWG Event Loop spec
pub fn Interpreter::run_microtasks(self : Interpreter) -> Unit raise Error {
// Process microtasks until queue is empty
// Note: new microtasks can be enqueued during execution
// Use index-based draining to avoid O(n²) from repeated remove(0)
let mut head = 0
while head < self.microtask_queue.length() {
let task = self.microtask_queue[head]
head = head + 1
// Call the callback with queued arguments
match task.callback {
Object(obj_data) =>
match obj_data.callable {
Some(_) => {
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let _ = self.call_value(task.callback, Undefined, task.args, loc)
}
None => ()
}
_ => ()
}
}
// Clear processed microtasks
self.microtask_queue.clear()
}
///|
/// Create resolve and reject capability functions for a promise
/// Returns (resolve_func, reject_func)
fn create_resolving_functions(
_interp : Interpreter,
promise_data : PromiseData,
) -> (Value, Value) {
// Track whether already resolved (prevents multiple resolution)
let already_resolved : Ref[Bool] = { val: false }
// Create resolve function
let resolve_func = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("resolve", fn(i, _this, args) {
if already_resolved.val {
return Undefined
}
already_resolved.val = true
let resolution = if args.length() > 0 { args[0] } else { Undefined }
// Check for self-resolution (ECMAScript spec requirement)
match resolution {
Promise(pd) =>
if physical_equal(pd, promise_data) {
// Self-resolution: reject with TypeError
let error = make_type_error("Chaining cycle detected for promise")
reject_promise(i, promise_data, error)
return Undefined
}
_ => ()
}
// Thenable assimilation (ECMAScript §25.6.1.3.2 steps 8-13)
// If resolution is not an object, fulfill directly.
// Otherwise, perform Get(resolution, "then") and check if callable.
// This applies uniformly to both native Promises and plain objects
// — the spec has no fast-path for native promises.
match resolution {
Promise(_) | Object(_) => {
// Get(resolution, "then") walks the prototype chain and
// invokes accessors per ECMAScript spec
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let then_val = i.get_property(resolution, "then", loc) catch {
e => {
// If Get throws, reject the promise with the error
let err = js_error_to_value(e)
reject_promise(i, promise_data, err)
return Undefined
}
}
match then_val {
Object(then_od) =>
match then_od.callable {
Some(_) => {
// Thenable: enqueue PromiseResolveThenableJob
let (res, rej) = create_resolving_functions(i, promise_data)
let job = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("PromiseResolveThenableJob", fn(
i2,
_t,
_a,
) {
try {
let _ = i2.call_value(
then_val,
resolution,
[res, rej],
loc,
)
} catch {
e => {
let err = js_error_to_value(e)
let _ = i2.call_value(rej, Undefined, [err], loc)
}
}
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
i.enqueue_microtask(job, [])
}
None => fulfill_promise(i, promise_data, resolution)
}
_ => fulfill_promise(i, promise_data, resolution)
}
}
_ => fulfill_promise(i, promise_data, resolution)
}
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Create reject function
let reject_func = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("reject", fn(i, _this, args) {
if already_resolved.val {
return Undefined
}
already_resolved.val = true
let reason = if args.length() > 0 { args[0] } else { Undefined }
reject_promise(i, promise_data, reason)
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
(resolve_func, reject_func)
}
///|
/// Fulfill a promise with the given value
/// Triggers all fulfill reactions as microtasks
fn fulfill_promise(
interp : Interpreter,
promise_data : PromiseData,
value : Value,
) -> Unit {
// Can only transition from Pending
guard promise_data.state is Pending else { return }
promise_data.state = Fulfilled
promise_data.result = value
// Trigger all fulfill reactions
for reaction in promise_data.fulfill_reactions {
enqueue_promise_reaction_job(interp, reaction, value)
}
// Clear reaction queues
promise_data.fulfill_reactions.clear()
promise_data.reject_reactions.clear()
}
///|
/// Reject a promise with the given reason
/// Triggers all reject reactions as microtasks
fn reject_promise(
interp : Interpreter,
promise_data : PromiseData,
reason : Value,
) -> Unit {
// Can only transition from Pending
guard promise_data.state is Pending else { return }
promise_data.state = Rejected
promise_data.result = reason
// Trigger all reject reactions
for reaction in promise_data.reject_reactions {
enqueue_promise_reaction_job(interp, reaction, reason)
}
// Clear reaction queues
promise_data.fulfill_reactions.clear()
promise_data.reject_reactions.clear()
}
///|
/// Enqueue a promise reaction job (microtask)
/// This implements NewPromiseReactionJob from ECMAScript spec
fn enqueue_promise_reaction_job(
interp : Interpreter,
reaction : PromiseReaction,
argument : Value,
) -> Unit {
// Create a job that will execute the reaction handler
let job_callback = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("PromiseReactionJob", fn(i, _this, args) {
let value = if args.length() > 0 { args[0] } else { Undefined }
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
// If handler is None, use identity (fulfill) or thrower (reject)
match reaction.handler {
Some(handler) => {
// Try to call the handler
let result = i.call_value(handler, Undefined, [value], loc) catch {
e => {
// Handler threw - reject the dependent promise
let error_value = js_error_to_value(e)
let _ = i.call_value(
reaction.reject,
Undefined,
[error_value],
loc,
)
return Undefined
}
}
// Resolve the dependent promise with handler result
let _ = i.call_value(reaction.resolve, Undefined, [result], loc)
Undefined
}
None =>
// No handler - pass through value/reason
match reaction.reaction_type {
Fulfill => {
let _ = i.call_value(reaction.resolve, Undefined, [value], loc)
Undefined
}
Reject => {
let _ = i.call_value(reaction.reject, Undefined, [value], loc)
Undefined
}
}
}
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
interp.enqueue_microtask(job_callback, [argument])
}
///|
/// Setup Promise constructor and prototype methods
pub fn setup_promise(env : Environment) -> Unit {
// Promise prototype with then, catch, finally
let promise_proto_props : Map[String, Value] = {}
// Promise.prototype.then(onFulfilled, onRejected)
promise_proto_props["then"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("then", fn(interp, this_val, args) {
match this_val {
Promise(promise_data) => {
let on_fulfilled = if args.length() > 0 {
match args[0] {
Object(od) =>
match od.callable {
Some(_) => Some(args[0])
None => None
}
_ => None
}
} else {
None
}
let on_rejected = if args.length() > 1 {
match args[1] {
Object(od) =>
match od.callable {
Some(_) => Some(args[1])
None => None
}
_ => None
}
} else {
None
}
// Mark as handled if onRejected is provided
if on_rejected is Some(_) {
promise_data.is_handled = true
}
// Create new promise for the result
let result_promise_data = new_promise_data()
let result_promise : Value = Promise(result_promise_data)
// Create resolving functions for the result promise
let (resolve, reject) = create_resolving_functions(
interp, result_promise_data,
)
// Create reactions
let fulfill_reaction : PromiseReaction = {
handler: on_fulfilled,
resolve,
reject,
reaction_type: Fulfill,
}
let reject_reaction : PromiseReaction = {
handler: on_rejected,
resolve,
reject,
reaction_type: Reject,
}
// Depending on promise state, either queue reactions or enqueue jobs
match promise_data.state {
Pending => {
promise_data.fulfill_reactions.push(fulfill_reaction)
promise_data.reject_reactions.push(reject_reaction)
}
Fulfilled =>
enqueue_promise_reaction_job(
interp,
fulfill_reaction,
promise_data.result,
)
Rejected => {
if on_rejected is Some(_) {
promise_data.is_handled = true
}
enqueue_promise_reaction_job(
interp,
reject_reaction,
promise_data.result,
)
}
}
result_promise
}
_ =>
raise @errors.TypeError(
message="Promise.prototype.then requires a Promise",
)
}
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise.prototype.catch(onRejected)
promise_proto_props["catch"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("catch", fn(interp, this_val, args) {
// catch(onRejected) is just then(undefined, onRejected)
let then_method = match this_val {
Promise(pd) => get_promise_method(pd, "then")
_ =>
raise @errors.TypeError(
message="Promise.prototype.catch requires a Promise",
)
}
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let on_rejected = if args.length() > 0 { args[0] } else { Undefined }
interp.call_value(then_method, this_val, [Undefined, on_rejected], loc)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise.prototype.finally(onFinally)
promise_proto_props["finally"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("finally", fn(interp, this_val, args) {
// Ensure this_val is a Promise
let promise_data = match this_val {
Promise(pd) => pd
_ =>
raise @errors.TypeError(
message="Promise.prototype.finally requires a Promise",
)
}
let on_finally = if args.length() > 0 { args[0] } else { Undefined }
let is_callable = match on_finally {
Object(od) => od.callable is Some(_)
_ => false
}
if not(is_callable) {
// If onFinally is not callable, pass through
let then_method = get_promise_method(promise_data, "then")
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
return interp.call_value(
then_method,
this_val,
[on_finally, on_finally],
loc,
)
}
// Create wrapper functions that call onFinally then pass through value
// Per ECMAScript §27.2.5.3, we must await the result of onFinally
// by chaining through Promise.resolve(onFinally_result).then(...)
let value_thunk = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("finallyValueThunk", fn(i, _this, inner_args) {
let value = if inner_args.length() > 0 {
inner_args[0]
} else {
Undefined
}
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
// Call onFinally
let finally_result = i.call_value(on_finally, Undefined, [], loc)
// Wrap result via PromiseResolve algorithm
let result_promise = promise_resolve_value(i, finally_result)
// Create a thunk that returns the original value
let return_value_thunk = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("returnValue", fn(_i2, _t, _a) { value }),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Chain: Promise.resolve(finally_result).then(() => value)
let then_method = match result_promise {
Promise(pd) => get_promise_method(pd, "then")
_ => Undefined
}
i.call_value(
then_method,
result_promise,
[return_value_thunk],
loc,
)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let thrower_thunk = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("finallyThrowerThunk", fn(
i,
_this,
inner_args,
) {
let reason = if inner_args.length() > 0 {
inner_args[0]
} else {
Undefined
}
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
// Call onFinally
let finally_result = i.call_value(on_finally, Undefined, [], loc)
// Wrap result via PromiseResolve algorithm
let result_promise = promise_resolve_value(i, finally_result)
// Create a thunk that re-throws the original reason
let throw_reason_thunk = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("throwReason", fn(_i2, _t, _a) {
raise JsException(reason)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Chain: Promise.resolve(finally_result).then(() => { throw reason })
let then_method = match result_promise {
Promise(pd) => get_promise_method(pd, "then")
_ => Undefined
}
i.call_value(
then_method,
result_promise,
[throw_reason_thunk],
loc,
)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let then_method = get_promise_method(promise_data, "then")
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
interp.call_value(
then_method,
this_val,
[value_thunk, thrower_thunk],
loc,
)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let promise_proto = Object({
properties: promise_proto_props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Promise",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Store prototype reference for method lookup
promise_prototype_ref.val = Some(promise_proto)
// Promise constructor
let promise_ctor_props : Map[String, Value] = {}
// Promise.resolve(value)
promise_ctor_props["resolve"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("resolve", fn(interp, _this, args) {
let value = if args.length() > 0 { args[0] } else { Undefined }
promise_resolve_value(interp, value)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise.reject(reason)
promise_ctor_props["reject"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("reject", fn(_interp, _this, args) {
let reason = if args.length() > 0 { args[0] } else { Undefined }
let promise_data : PromiseData = {
state: Rejected,
result: reason,
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
Promise(promise_data)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise.all(iterable)
promise_ctor_props["all"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("all", fn(interp, _this, args) {
let iterable = if args.length() > 0 { args[0] } else { Undefined }
// Get array of promises
let promises = get_promise_array(interp, iterable)
let count = promises.length()
if count == 0 {
// Empty array - return resolved promise with empty array
let promise_data : PromiseData = {
state: Fulfilled,
result: Array({ elements: [] }),
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
return Promise(promise_data)
}
// Create result promise
let result_data = new_promise_data()
let result_promise = Promise(result_data)
let (resolve, reject) = create_resolving_functions(interp, result_data)
let bookkeeping_resolve = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
NativeCallable("allBookkeepingResolve", fn(_args) { Undefined }),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Track results
let results : Array[Value] = Array::make(count, Undefined)
let remaining : Ref[Int] = { val: count }
for i = 0; i < count; i = i + 1 {
let promise = promises[i]
let index = i
// Ensure it's a promise (wrap if not)
let p = match promise {
Promise(_) => promise
_ => {
let pd : PromiseData = {
state: Fulfilled,
result: promise,
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
Promise(pd)
}
}
// Create handlers for this promise
let on_fulfilled = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("allOnFulfilled", fn(i2, _this2, args2) {
let value = if args2.length() > 0 {
args2[0]
} else {
Undefined
}
results[index] = value
remaining.val = remaining.val - 1
if remaining.val == 0 {
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let _ = i2.call_value(
resolve,
Undefined,
[Array({ elements: results })],
loc,
)
}
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Attach handlers
match p {
Promise(pd) => {
pd.is_handled = true
match pd.state {
Pending => {
let fulfill_reaction : PromiseReaction = {
handler: Some(on_fulfilled),
resolve: bookkeeping_resolve,
reject,
reaction_type: Fulfill,
}
let reject_reaction : PromiseReaction = {
handler: None,
resolve: bookkeeping_resolve,
reject,
reaction_type: Reject,
}
pd.fulfill_reactions.push(fulfill_reaction)
pd.reject_reactions.push(reject_reaction)
}
Fulfilled =>
enqueue_promise_reaction_job(
interp,
{
handler: Some(on_fulfilled),
resolve: bookkeeping_resolve,
reject,
reaction_type: Fulfill,
},
pd.result,
)
Rejected =>
enqueue_promise_reaction_job(
interp,
{ handler: None, resolve, reject, reaction_type: Reject },
pd.result,
)
}
}
_ => ()
}
}
result_promise
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise.race(iterable)
promise_ctor_props["race"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("race", fn(interp, _this, args) {
let iterable = if args.length() > 0 { args[0] } else { Undefined }
let promises = get_promise_array(interp, iterable)
// Create result promise
let result_data = new_promise_data()
let result_promise = Promise(result_data)
let (resolve, reject) = create_resolving_functions(interp, result_data)
for promise in promises {
// Ensure it's a promise
let p = match promise {
Promise(_) => promise
_ => {
let pd : PromiseData = {
state: Fulfilled,
result: promise,
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
Promise(pd)
}
}
match p {
Promise(pd) => {
pd.is_handled = true
match pd.state {
Pending => {
let fulfill_reaction : PromiseReaction = {
handler: None,
resolve,
reject,
reaction_type: Fulfill,
}
let reject_reaction : PromiseReaction = {
handler: None,
resolve,
reject,
reaction_type: Reject,
}
pd.fulfill_reactions.push(fulfill_reaction)
pd.reject_reactions.push(reject_reaction)
}
Fulfilled =>
enqueue_promise_reaction_job(
interp,
{ handler: None, resolve, reject, reaction_type: Fulfill },
pd.result,
)
Rejected =>
enqueue_promise_reaction_job(
interp,
{ handler: None, resolve, reject, reaction_type: Reject },
pd.result,
)
}
}
_ => ()
}
}
result_promise
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise.allSettled(iterable)
promise_ctor_props["allSettled"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("allSettled", fn(interp, _this, args) {
let iterable = if args.length() > 0 { args[0] } else { Undefined }
let promises = get_promise_array(interp, iterable)
let count = promises.length()
if count == 0 {
let promise_data : PromiseData = {
state: Fulfilled,
result: Array({ elements: [] }),
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
return Promise(promise_data)
}
let result_data = new_promise_data()
let result_promise = Promise(result_data)
let (resolve, _reject) = create_resolving_functions(interp, result_data)
let bookkeeping_resolve = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
NativeCallable("allSettledBookkeepingResolve", fn(_args) {
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let results : Array[Value] = Array::make(count, Undefined)
let remaining : Ref[Int] = { val: count }
for i = 0; i < count; i = i + 1 {
let promise = promises[i]
let index = i
let p = match promise {
Promise(_) => promise
_ => {
let pd : PromiseData = {
state: Fulfilled,
result: promise,
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
Promise(pd)
}
}
let on_fulfilled = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("allSettledOnFulfilled", fn(
i2,
_this2,
args2,
) {
let value = if args2.length() > 0 {
args2[0]
} else {
Undefined
}
let result_props : Map[String, Value] = {}
result_props["status"] = String_("fulfilled")
result_props["value"] = value
results[index] = Object({
properties: result_props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
remaining.val = remaining.val - 1
if remaining.val == 0 {
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let _ = i2.call_value(
resolve,
Undefined,
[Array({ elements: results })],
loc,
)
}
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let on_rejected = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("allSettledOnRejected", fn(
i2,
_this2,
args2,
) {
let reason = if args2.length() > 0 {
args2[0]
} else {
Undefined
}
let result_props : Map[String, Value] = {}
result_props["status"] = String_("rejected")
result_props["reason"] = reason
results[index] = Object({
properties: result_props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
remaining.val = remaining.val - 1
if remaining.val == 0 {
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let _ = i2.call_value(
resolve,
Undefined,
[Array({ elements: results })],
loc,
)
}
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Create a dummy resolve that won't be used (allSettled never rejects)
let dummy_reject = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
NativeCallable("dummyReject", fn(_args) { Undefined }),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
match p {
Promise(pd) => {
pd.is_handled = true
match pd.state {
Pending => {
pd.fulfill_reactions.push({
handler: Some(on_fulfilled),
resolve: bookkeeping_resolve,
reject: dummy_reject,
reaction_type: Fulfill,
})
pd.reject_reactions.push({
handler: Some(on_rejected),
resolve: bookkeeping_resolve,
reject: dummy_reject,
reaction_type: Reject,
})
}
Fulfilled =>
enqueue_promise_reaction_job(
interp,
{
handler: Some(on_fulfilled),
resolve: bookkeeping_resolve,
reject: dummy_reject,
reaction_type: Fulfill,
},
pd.result,
)
Rejected =>
enqueue_promise_reaction_job(
interp,
{
handler: Some(on_rejected),
resolve: bookkeeping_resolve,
reject: dummy_reject,
reaction_type: Reject,
},
pd.result,
)
}
}
_ => ()
}
}
result_promise
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise.any(iterable)
promise_ctor_props["any"] = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("any", fn(interp, _this, args) {
let iterable = if args.length() > 0 { args[0] } else { Undefined }
let promises = get_promise_array(interp, iterable)
let count = promises.length()
if count == 0 {
// Empty array - reject with AggregateError
let promise_data : PromiseData = {
state: Rejected,
result: make_aggregate_error("All promises were rejected", []),
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
return Promise(promise_data)
}
let result_data = new_promise_data()
let result_promise = Promise(result_data)
let (resolve, reject) = create_resolving_functions(interp, result_data)
let bookkeeping_resolve = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
NativeCallable("anyBookkeepingResolve", fn(_args) { Undefined }),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let errors : Array[Value] = Array::make(count, Undefined)
let remaining : Ref[Int] = { val: count }
for i = 0; i < count; i = i + 1 {
let promise = promises[i]
let index = i
let p = match promise {
Promise(_) => promise
_ => {
let pd : PromiseData = {
state: Fulfilled,
result: promise,
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
Promise(pd)
}
}
let on_rejected = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("anyOnRejected", fn(i2, _this2, args2) {
let reason = if args2.length() > 0 {
args2[0]
} else {
Undefined
}
errors[index] = reason
remaining.val = remaining.val - 1
if remaining.val == 0 {
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let agg_error = make_aggregate_error(
"All promises were rejected", errors,
)
let _ = i2.call_value(reject, Undefined, [agg_error], loc)
}
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
match p {
Promise(pd) => {
pd.is_handled = true
match pd.state {
Pending => {
pd.fulfill_reactions.push({
handler: None,
resolve,
reject,
reaction_type: Fulfill,
})
pd.reject_reactions.push({
handler: Some(on_rejected),
resolve: bookkeeping_resolve,
reject,
reaction_type: Reject,
})
}
Fulfilled =>
enqueue_promise_reaction_job(
interp,
{ handler: None, resolve, reject, reaction_type: Fulfill },
pd.result,
)
Rejected =>
enqueue_promise_reaction_job(
interp,
{
handler: Some(on_rejected),
resolve: bookkeeping_resolve,
reject,
reaction_type: Reject,
},
pd.result,
)
}
}
_ => ()
}
}
result_promise
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Promise constructor itself - requires 'new' keyword
let promise_ctor = Object({
properties: promise_ctor_props,
symbol_properties: {},
prototype: Null,
callable: Some(
ConstructorOnlyCallable("Promise", fn(interp, args) {
let executor = if args.length() > 0 { args[0] } else { Undefined }
// Validate executor is callable
match executor {
Object(od) =>
match od.callable {
Some(_) => ()
None =>
raise @errors.TypeError(
message="Promise resolver undefined is not a function",
)
}
_ =>
raise @errors.TypeError(
message="Promise resolver undefined is not a function",
)
}
// Create new promise
let promise_data = new_promise_data()
let promise = Promise(promise_data)
// Create resolving functions
let (resolve, reject) = create_resolving_functions(interp, promise_data)
// Call executor synchronously
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
try {
let _ = interp.call_value(executor, Undefined, [resolve, reject], loc)
} catch {
e => {
// If executor throws, reject the promise
let error_value = js_error_to_value(e)
let _ = interp.call_value(reject, Undefined, [error_value], loc)
}
}
promise
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Set prototype property on constructor
match promise_ctor {
Object(od) => od.properties["prototype"] = promise_proto
_ => ()
}
env.def_builtin("Promise", promise_ctor)
// Set Promise.prototype.constructor = Promise
match promise_proto {
Object(data) => {
data.properties["constructor"] = promise_ctor
data.descriptors["constructor"] = { writable: true, enumerable: false, configurable: true }
}
_ => ()
}
// queueMicrotask(callback) - schedules a microtask
// Part of the Web Platform APIs, used for scheduling microtasks directly
env.def_builtin(
"queueMicrotask",
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("queueMicrotask", fn(interp, _this, args) {
let callback = if args.length() > 0 { args[0] } else { Undefined }
// Validate callback is callable
match callback {
Object(od) =>
match od.callable {
Some(_) => interp.enqueue_microtask(callback, [])
None =>
raise @errors.TypeError(
message="queueMicrotask requires a function argument",
)
}
_ =>
raise @errors.TypeError(
message="queueMicrotask requires a function argument",
)
}
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
)
}
///|
/// Setup timer builtins: setTimeout, clearTimeout, setInterval, clearInterval
/// These implement the WHATWG Timer APIs
/// Reference: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers
pub fn setup_timers(env : Environment) -> Unit {
// setTimeout(callback, delay, ...args) -> timerId
env.def_builtin(
"setTimeout",
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("setTimeout", fn(interp, _this, args) {
let callback = if args.length() > 0 { args[0] } else { Undefined }
// Validate callback is callable
match callback {
Object(od) =>
match od.callable {
Some(_) => ()
None =>
raise @errors.TypeError(
message="setTimeout requires a function as first argument",
)
}
_ =>
raise @errors.TypeError(
message="setTimeout requires a function as first argument",
)
}
let delay = if args.length() > 1 {
match args[1] {
Number(n) => {
let d = n.to_int()
if d < 0 {
0
} else {
d
}
}
_ => 0
}
} else {
0
}
// Collect extra arguments to pass to callback
let extra_args : Array[Value] = []
for i = 2; i < args.length(); i = i + 1 {
extra_args.push(args[i])
}
let id = interp.timer_id_counter.val
interp.timer_id_counter.val = id + 1
let order = interp.timer_insertion_counter.val
interp.timer_insertion_counter.val = order + 1
interp.timer_queue.push({
id,
callback,
args: extra_args,
delay,
period: 0,
cancelled: false,
is_interval: false,
insertion_order: order,
})
Number(id.to_double())
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
)
// clearTimeout(id)
env.def_builtin(
"clearTimeout",
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("clearTimeout", fn(interp, _this, args) {
let id = if args.length() > 0 {
match args[0] {
Number(n) => n.to_int()
_ => return Undefined
}
} else {
return Undefined
}
// Mark timer as cancelled in queue
for timer in interp.timer_queue {
if timer.id == id {
timer.cancelled = true
break
}
}
// Also record in cancelled set so run_timers won't re-enqueue
// an interval whose callback called clearInterval on itself
interp.cancelled_timer_ids[id] = true
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
)
// setInterval(callback, delay, ...args) -> timerId
env.def_builtin(
"setInterval",
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("setInterval", fn(interp, _this, args) {
let callback = if args.length() > 0 { args[0] } else { Undefined }
match callback {
Object(od) =>
match od.callable {
Some(_) => ()
None =>
raise @errors.TypeError(
message="setInterval requires a function as first argument",
)
}
_ =>
raise @errors.TypeError(
message="setInterval requires a function as first argument",
)
}
let delay = if args.length() > 1 {
match args[1] {
Number(n) => {
let d = n.to_int()
if d < 0 {
0
} else {
d
}
}
_ => 0
}
} else {
0
}
let extra_args : Array[Value] = []
for i = 2; i < args.length(); i = i + 1 {
extra_args.push(args[i])
}
let id = interp.timer_id_counter.val
interp.timer_id_counter.val = id + 1
let order = interp.timer_insertion_counter.val
interp.timer_insertion_counter.val = order + 1
interp.timer_queue.push({
id,
callback,
args: extra_args,
delay,
period: delay,
cancelled: false,
is_interval: true,
insertion_order: order,
})
Number(id.to_double())
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
)
// clearInterval(id) - same semantics as clearTimeout
env.def_builtin(
"clearInterval",
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
InterpreterCallable("clearInterval", fn(interp, _this, args) {
let id = if args.length() > 0 {
match args[0] {
Number(n) => n.to_int()
_ => return Undefined
}
} else {
return Undefined
}
for timer in interp.timer_queue {
if timer.id == id {
timer.cancelled = true
break
}
}
// Also record in cancelled set so run_timers won't re-enqueue
// an interval whose callback called clearInterval on itself
interp.cancelled_timer_ids[id] = true
Undefined
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
)
}
///|
/// Run the event loop: process all pending timer tasks with
/// microtask checkpoints between each (per WHATWG event loop spec).
/// Timers are sorted by (delay, insertion_order) so they fire in the
/// correct relative order. After each timer callback, microtasks are
/// drained before the next timer fires.
///
/// For setInterval, the timer is re-enqueued after each invocation
/// until cancelled. A safety limit prevents infinite loops.
pub fn Interpreter::run_timers(self : Interpreter) -> Unit raise Error {
// Safety limit to prevent infinite loops from setInterval
let max_iterations = 10000
let mut iterations = 0
while self.timer_queue.length() > 0 && iterations < max_iterations {
iterations = iterations + 1
// Sort timers by delay (ascending), then by insertion order (ascending)
self.timer_queue.sort_by(fn(a, b) {
if a.delay != b.delay {
a.delay - b.delay
} else {
a.insertion_order - b.insertion_order
}
})
// Take the first (soonest) timer
let timer = self.timer_queue.remove(0)
if timer.cancelled {
continue
}
// Execute the timer callback
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let _ = self.call_value(timer.callback, Undefined, timer.args, loc)
// Microtask checkpoint after each task (per WHATWG event loop spec)
self.run_microtasks()
// If this is setInterval and not cancelled, re-enqueue.
// Check both the timer's cancelled flag and cancelled_timer_ids,
// because clearInterval called inside the callback can't mark the
// already-removed timer struct — it records in cancelled_timer_ids instead.
if timer.is_interval &&
not(timer.cancelled) &&
not(self.cancelled_timer_ids.contains(timer.id)) {
let order = self.timer_insertion_counter.val
self.timer_insertion_counter.val = order + 1
self.timer_queue.push({
id: timer.id,
callback: timer.callback,
args: timer.args,
delay: timer.delay + timer.period,
period: timer.period,
cancelled: false,
is_interval: true,
insertion_order: order,
})
}
}
// Clear the cancelled set for this run
self.cancelled_timer_ids.clear()
}
///|
/// Helper to extract array of values from iterable using iterator protocol
fn get_promise_array(
interp : Interpreter,
iterable : Value,
) -> Array[Value] raise Error {
match iterable {
Undefined | Null =>
raise @errors.TypeError(
message="Cannot read properties of " +
type_of(iterable) +
" (reading 'Symbol(Symbol.iterator)')",
)
_ => {
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
interp.spread_iterable(iterable, loc)
}
}
}
///|
/// PromiseResolve algorithm (ECMAScript §27.2.4.7)
/// If value is already a Promise, return it; otherwise resolve a new Promise with it
fn promise_resolve_value(
interp : Interpreter,
value : Value,
) -> Value raise Error {
match value {
Promise(_) => value
_ => {
let pd : PromiseData = {
state: Pending,
result: Undefined,
fulfill_reactions: [],
reject_reactions: [],
is_handled: false,
properties: {},
symbol_properties: {},
}
let promise = Promise(pd)
let (resolve, _reject) = create_resolving_functions(interp, pd)
let loc : @token.Loc = { line: 0, col: 0, offset: 0 }
let _ = interp.call_value(resolve, Undefined, [value], loc)
promise
}
}
}
///|
/// Helper to create a TypeError value (for promise rejection)
fn make_type_error(message : String) -> Value {
let props : Map[String, Value] = {}
props["name"] = String_("TypeError")
props["message"] = String_(message)
Object({
properties: props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "TypeError",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}
///|
/// Helper to create an AggregateError value
fn make_aggregate_error(message : String, errors : Array[Value]) -> Value {
let props : Map[String, Value] = {}
props["name"] = String_("AggregateError")
props["message"] = String_(message)
props["errors"] = Array({ elements: errors })
Object({
properties: props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "AggregateError",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}