///|
pub(all) enum Signal {
Normal(Value)
ReturnSignal(Value)
BreakSignal(String?)
ContinueSignal(String?)
}
///|
/// Microtask record for the event loop microtask queue
/// Stores a callback function and argument list to pass to it
pub(all) struct Microtask {
callback : Value // The function to call
args : Array[Value] // Arguments to pass (Promise jobs pass one, queueMicrotask passes none)
}
///|
/// Timer task for setTimeout/setInterval (task queue per WHATWG spec)
/// Timers are processed one at a time with microtask checkpoints between each
pub(all) struct TimerTask {
id : Int // Unique timer ID for clearTimeout/clearInterval
callback : Value // The function to call
args : Array[Value] // Additional arguments to pass to the callback
delay : Int // Absolute fire-at time in virtual ms (used for ordering)
period : Int // Interval repeat period (0 for setTimeout)
mut cancelled : Bool // Whether clearTimeout was called
is_interval : Bool // Whether this is a setInterval (repeats)
insertion_order : Int // Stable sort tiebreaker for equal delays
}
///|
pub(all) struct Interpreter {
output : Array[String]
global : Environment
global_this : Value
mut strict : Bool
microtask_queue : Array[Microtask] // Event loop microtask queue
timer_queue : Array[TimerTask] // Timer task queue (WHATWG task queue)
timer_id_counter : Ref[Int] // Next timer ID
timer_insertion_counter : Ref[Int] // Insertion order counter
cancelled_timer_ids : Map[Int, Bool] // IDs cancelled during callback execution
}
///|
pub fn Interpreter::new() -> Interpreter {
let global = Environment::new()
let output : Array[String] = []
// Create global object for 'this' in global context
let global_this : Value = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "global",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
setup_builtins(global, output)
// Bind 'this' and 'globalThis' in global scope
global.def_builtin("this", global_this)
global.def_builtin("globalThis", global_this)
let microtask_queue : Array[Microtask] = []
let timer_queue : Array[TimerTask] = []
let timer_id_counter : Ref[Int] = { val: 1 }
let timer_insertion_counter : Ref[Int] = { val: 0 }
let cancelled_timer_ids : Map[Int, Bool] = {}
{
output,
global,
global_this,
strict: false,
microtask_queue,
timer_queue,
timer_id_counter,
timer_insertion_counter,
cancelled_timer_ids,
}
}
///|
fn has_use_strict(stmts : Array[@ast.Stmt]) -> Bool {
if stmts.length() > 0 {
match stmts[0] {
ExprStmt(StringLit("use strict", _), _) => true
_ => false
}
} else {
false
}
}
///|
pub fn Interpreter::run(
self : Interpreter,
stmts : Array[@ast.Stmt],
) -> Value raise Error {
try {
// Check for "use strict" directive
if has_use_strict(stmts) {
self.strict = true
}
// Hoist var declarations and function declarations at global level
self.hoist_declarations(stmts, self.global)
let mut last : Value = Undefined
for stmt in stmts {
match self.exec_stmt(stmt, self.global) {
Normal(v) => last = v
ReturnSignal(_) =>
raise @errors.SyntaxError(
message="return statement outside of function",
)
BreakSignal(_) =>
raise @errors.SyntaxError(message="break statement outside of loop")
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
}
last
} catch {
e =>
if is_js_catchable_error(e) {
raise JsException(js_error_to_value_with_env(e, Some(self.global)))
} else {
raise e
}
}
}
///|
fn Interpreter::exec_stmt(
self : Interpreter,
stmt : @ast.Stmt,
env : Environment,
label? : String? = None,
) -> Signal raise Error {
match stmt {
ExprStmt(expr, _) => Normal(self.eval_expr(expr, env))
VarDecl(kind, name, init, _) => {
let bk : BindingKind = match kind {
LetKind => LetBinding
ConstKind => ConstBinding
VarKind => VarBinding
}
let value : Value = match init {
Some(expr) => self.eval_expr(expr, env)
None => Undefined
}
// For var declarations that were hoisted, use assign instead of def
if bk == VarBinding && env.bindings.contains(name) {
if init is Some(_) {
env.assign(name, value)
}
} else if (bk == LetBinding || bk == ConstBinding) &&
env.bindings.contains(name) {
// let/const hoisted with TDZ - initialize it
env.initialize(name, value)
} else {
env.def(name, value, bk)
}
Normal(Undefined)
}
Block(stmts, _) => {
let block_env = Environment::new(parent=Some(env))
// Hoist let/const with TDZ markers
hoist_block_tdz(stmts, block_env)
self.exec_stmts(stmts, block_env)
}
StmtList(stmts, _) =>
// Execute statements in same scope (no new environment)
// Used for comma-separated declarations like: var a, b, c;
self.exec_stmts(stmts, env)
IfStmt(cond, then_branch, else_branch, _) => {
let cond_val = self.eval_expr(cond, env)
if is_truthy(cond_val) {
self.exec_stmt(then_branch, env)
} else {
match else_branch {
Some(eb) => self.exec_stmt(eb, env)
None => Normal(Undefined)
}
}
}
WhileStmt(cond, body, _) => {
while is_truthy(self.eval_expr(cond, env)) {
match self.exec_stmt(body, env) {
Normal(_) => ()
ReturnSignal(v) => return ReturnSignal(v)
BreakSignal(None) => break
BreakSignal(Some(l)) =>
if label == Some(l) {
break
} else {
return BreakSignal(Some(l))
}
ContinueSignal(None) => continue
ContinueSignal(Some(l)) =>
if label == Some(l) {
continue
} else {
return ContinueSignal(Some(l))
}
}
}
Normal(Undefined)
}
ForStmt(init, cond, update, body, _) => {
let for_env = Environment::new(parent=Some(env))
match init {
Some(init_stmt) => {
let _ = self.exec_stmt(init_stmt, for_env)
}
None => ()
}
while true {
match cond {
Some(c) => if not(is_truthy(self.eval_expr(c, for_env))) { break }
None => ()
}
let mut do_continue = false
match self.exec_stmt(body, for_env) {
Normal(_) => ()
ReturnSignal(v) => return ReturnSignal(v)
BreakSignal(None) => break
BreakSignal(Some(l)) =>
if label == Some(l) {
break
} else {
return BreakSignal(Some(l))
}
ContinueSignal(None) => do_continue = true
ContinueSignal(Some(l)) =>
if label == Some(l) {
do_continue = true
} else {
return ContinueSignal(Some(l))
}
}
if do_continue {
match update {
Some(u) => {
let _ = self.eval_expr(u, for_env)
}
None => ()
}
continue
}
match update {
Some(u) => {
let _ = self.eval_expr(u, for_env)
}
None => ()
}
}
Normal(Undefined)
}
FuncDecl(name, params, body, _) => {
let func_data : FuncData = {
name: Some(name),
params,
body,
closure: env,
}
// If already hoisted, just assign; otherwise define
if env.bindings.contains(name) {
env.assign(name, make_func(func_data))
} else {
env.def(name, make_func(func_data), VarBinding)
}
Normal(Undefined)
}
ReturnStmt(expr, _) => {
let value : Value = match expr {
Some(e) => self.eval_expr(e, env)
None => Undefined
}
ReturnSignal(value)
}
BreakStmt(label, _) => BreakSignal(label)
ContinueStmt(label, _) => ContinueSignal(label)
ThrowStmt(expr, _) => {
let value = self.eval_expr(expr, env)
raise JsException(value)
}
TryCatchStmt(try_body, catch_param, catch_body, finally_body, _) =>
self.exec_try_catch(try_body, catch_param, catch_body, finally_body, env)
SwitchStmt(discriminant, cases, _) => {
let disc_val = self.eval_expr(discriminant, env)
let mut match_idx = -1
let mut default_idx = -1
for i = 0; i < cases.length(); i = i + 1 {
match cases[i].condition {
Some(test_expr) =>
if match_idx < 0 {
let test_val = self.eval_expr(test_expr, env)
if strict_equal(disc_val, test_val) {
match_idx = i
}
}
None => default_idx = i
}
}
let start = if match_idx >= 0 {
match_idx
} else if default_idx >= 0 {
default_idx
} else {
cases.length()
}
for i = start; i < cases.length(); i = i + 1 {
for stmt in cases[i].body {
match self.exec_stmt(stmt, env) {
Normal(_) => ()
BreakSignal(None) => return Normal(Undefined)
other => return other
}
}
}
Normal(Undefined)
}
DoWhileStmt(body, cond, _) => {
while true {
match self.exec_stmt(body, env) {
Normal(_) => ()
ReturnSignal(v) => return ReturnSignal(v)
BreakSignal(None) => break
BreakSignal(Some(l)) =>
if label == Some(l) {
break
} else {
return BreakSignal(Some(l))
}
ContinueSignal(None) => ()
ContinueSignal(Some(l)) =>
if label == Some(l) {
// continue — fall through to condition check
} else {
return ContinueSignal(Some(l))
}
}
if not(is_truthy(self.eval_expr(cond, env))) {
break
}
}
Normal(Undefined)
}
ForInStmt(var_kind, name, obj_expr, body, _) => {
let obj = self.eval_expr(obj_expr, env)
let keys = collect_for_in_keys(obj)
for key in keys {
let body_env = Environment::new(parent=Some(env))
match var_kind {
Some(kind) => {
let bk : BindingKind = match kind {
LetKind => LetBinding
ConstKind => ConstBinding
VarKind => VarBinding
}
body_env.def(name, String_(key), bk)
}
None => env.assign(name, String_(key))
}
match self.exec_stmt(body, body_env) {
Normal(_) => ()
ReturnSignal(v) => return ReturnSignal(v)
BreakSignal(None) => break
BreakSignal(Some(l)) =>
if label == Some(l) {
break
} else {
return BreakSignal(Some(l))
}
ContinueSignal(None) => continue
ContinueSignal(Some(l)) =>
if label == Some(l) {
continue
} else {
return ContinueSignal(Some(l))
}
}
}
Normal(Undefined)
}
ForOfStmt(var_kind, name, iterable_expr, body, loc) => {
let iterable = self.eval_expr(iterable_expr, env)
// Try to get the iterator via Symbol.iterator protocol
let iterator_sym = get_iterator_symbol()
let iterator_method = self.get_computed_property(
iterable,
Symbol(iterator_sym),
loc,
)
// Get the iterator object by calling the iterator method
let iterator = match iterator_method {
Object(data) =>
match data.callable {
Some(_) => self.call_value(iterator_method, iterable, [], loc)
None =>
raise @errors.TypeError(
message="Result of the Symbol.iterator method is not an object",
)
}
Undefined =>
// Fall back to built-in iteration for arrays and strings
// This handles cases where Symbol.iterator might not be set up yet
match iterable {
Array(arr) => make_array_iterator_value(arr)
String_(s) => make_string_iterator_value(s)
_ =>
raise @errors.TypeError(
message="\{type_of(iterable)} is not iterable (cannot read property Symbol(Symbol.iterator))",
)
}
_ =>
raise @errors.TypeError(
message="Result of the Symbol.iterator method is not an object",
)
}
// Get the next method from the iterator (walk prototype chain)
let next_method = match iterator {
Object(iter_data) =>
// First check direct properties
match iter_data.properties.get("next") {
Some(next_fn) => next_fn
None => {
// Walk prototype chain
let mut found : Value = Undefined
let mut current = iter_data.prototype
while true {
match current {
Object(proto_data) =>
match proto_data.properties.get("next") {
Some(next_fn) => {
found = next_fn
break
}
None => current = proto_data.prototype
}
_ => break
}
}
match found {
Undefined =>
raise @errors.TypeError(
message="iterator.next is not a function",
)
_ => found
}
}
}
_ => raise @errors.TypeError(message="Iterator is not an object")
}
// Iterate using the iterator protocol
while true {
// Call iterator.next()
let result = self.call_value(next_method, iterator, [], loc)
// Get 'done' and 'value' from the result
let (done, value) = match result {
Object(result_data) => {
let done_val = match result_data.properties.get("done") {
Some(v) => is_truthy(v)
None => false
}
let value_val = match result_data.properties.get("value") {
Some(v) => v
None => Undefined
}
(done_val, value_val)
}
_ =>
raise @errors.TypeError(message="Iterator result is not an object")
}
// If done, exit the loop
if done {
break
}
// Bind the value to the loop variable
let body_env = Environment::new(parent=Some(env))
match var_kind {
Some(VarKind) =>
if env.bindings.contains(name) {
env.assign(name, value)
} else {
env.def(name, value, VarBinding)
}
Some(kind) => {
let bk : BindingKind = match kind {
LetKind => LetBinding
ConstKind => ConstBinding
_ => fail("unreachable")
}
body_env.def(name, value, bk)
}
None => env.assign(name, value)
}
// Execute the loop body
match self.exec_stmt(body, body_env) {
Normal(_) => ()
ReturnSignal(v) => return ReturnSignal(v)
BreakSignal(None) => break
BreakSignal(Some(l)) =>
if label == Some(l) {
break
} else {
return BreakSignal(Some(l))
}
ContinueSignal(None) => continue
ContinueSignal(Some(l)) =>
if label == Some(l) {
continue
} else {
return ContinueSignal(Some(l))
}
}
}
Normal(Undefined)
}
ForInStmtPat(var_kind, pattern, obj_expr, body, _) => {
let obj = self.eval_expr(obj_expr, env)
let keys = collect_for_in_keys(obj)
let bk : BindingKind = match var_kind {
LetKind => LetBinding
ConstKind => ConstBinding
VarKind => VarBinding
}
for key in keys {
let body_env = Environment::new(parent=Some(env))
// var bindings go to outer env (function/global scoped), let/const to body_env
let bind_env = match var_kind {
VarKind => env
_ => body_env
}
self.bind_pattern(pattern, String_(key), bind_env, bk)
match self.exec_stmt(body, body_env) {
Normal(_) => ()
ReturnSignal(v) => return ReturnSignal(v)
BreakSignal(None) => break
BreakSignal(Some(l)) =>
if label == Some(l) {
break
} else {
return BreakSignal(Some(l))
}
ContinueSignal(None) => continue
ContinueSignal(Some(l)) =>
if label == Some(l) {
continue
} else {
return ContinueSignal(Some(l))
}
}
}
Normal(Undefined)
}
ForOfStmtPat(var_kind, pattern, iterable_expr, body, loc) => {
let iterable = self.eval_expr(iterable_expr, env)
// Try to get the iterator via Symbol.iterator protocol
let iterator_sym = get_iterator_symbol()
let iterator_method = self.get_computed_property(
iterable,
Symbol(iterator_sym),
loc,
)
// Get the iterator object
let iterator = match iterator_method {
Object(data) =>
match data.callable {
Some(_) => self.call_value(iterator_method, iterable, [], loc)
None =>
raise @errors.TypeError(
message="Result of the Symbol.iterator method is not an object",
)
}
Undefined =>
match iterable {
Array(arr) => make_array_iterator_value(arr)
String_(s) => make_string_iterator_value(s)
_ =>
raise @errors.TypeError(
message="\{type_of(iterable)} is not iterable",
)
}
_ =>
raise @errors.TypeError(
message="Result of the Symbol.iterator method is not an object",
)
}
// Get the next method
let next_method = match iterator {
Object(iter_data) =>
match iter_data.properties.get("next") {
Some(next_fn) => next_fn
None => {
let mut found : Value = Undefined
let mut current = iter_data.prototype
while true {
match current {
Object(proto_data) =>
match proto_data.properties.get("next") {
Some(next_fn) => {
found = next_fn
break
}
None => current = proto_data.prototype
}
_ => break
}
}
match found {
Undefined =>
raise @errors.TypeError(
message="iterator.next is not a function",
)
_ => found
}
}
}
_ => raise @errors.TypeError(message="Iterator is not an object")
}
let bk : BindingKind = match var_kind {
LetKind => LetBinding
ConstKind => ConstBinding
VarKind => VarBinding
}
// Iterate using the iterator protocol
while true {
let result = self.call_value(next_method, iterator, [], loc)
let (done, value) = match result {
Object(result_data) => {
let done_val = match result_data.properties.get("done") {
Some(v) => is_truthy(v)
None => false
}
let value_val = match result_data.properties.get("value") {
Some(v) => v
None => Undefined
}
(done_val, value_val)
}
_ =>
raise @errors.TypeError(message="Iterator result is not an object")
}
if done {
break
}
let body_env = Environment::new(parent=Some(env))
// var bindings go to outer env (function/global scoped), let/const to body_env
let bind_env = match var_kind {
VarKind => env
_ => body_env
}
self.bind_pattern(pattern, value, bind_env, bk)
match self.exec_stmt(body, body_env) {
Normal(_) => ()
ReturnSignal(v) => return ReturnSignal(v)
BreakSignal(None) => break
BreakSignal(Some(l)) =>
if label == Some(l) {
break
} else {
return BreakSignal(Some(l))
}
ContinueSignal(None) => continue
ContinueSignal(Some(l)) =>
if label == Some(l) {
continue
} else {
return ContinueSignal(Some(l))
}
}
}
Normal(Undefined)
}
DestructureDecl(kind, pattern, init_expr, _) => {
let bk : BindingKind = match kind {
LetKind => LetBinding
ConstKind => ConstBinding
VarKind => VarBinding
}
let value = self.eval_expr(init_expr, env)
self.bind_pattern(pattern, value, env, bk)
Normal(Undefined)
}
FuncDeclExt(name, params, rest_param, body, _) => {
let func_data : FuncDataExt = {
name: Some(name),
params,
rest_param,
body,
closure: env,
}
if env.bindings.contains(name) {
env.assign(name, make_func_ext(func_data))
} else {
env.def(name, make_func_ext(func_data), VarBinding)
}
Normal(Undefined)
}
LabeledStmt(lbl, body, _) =>
match self.exec_stmt(body, env, label=Some(lbl)) {
BreakSignal(Some(l)) =>
if l == lbl {
Normal(Undefined)
} else {
BreakSignal(Some(l))
}
other => other
}
ClassDecl(name, superclass, methods, _) => {
let class_val = self.create_class(name, superclass, methods, env)
// Class was hoisted with TDZ, initialize it now
if env.bindings.contains(name) {
env.initialize(name, class_val)
} else {
env.def(name, class_val, LetBinding)
}
Normal(Undefined)
}
}
}
///|
fn collect_for_in_keys(obj : Value) -> Array[String] {
let keys : Array[String] = []
let seen : Map[String, Bool] = {}
let mut current = obj
while true {
match current {
Object(data) => {
data.properties.each(fn(k, _v) {
if not(seen.contains(k)) {
seen[k] = true
let enumerable = match data.descriptors.get(k) {
Some(d) => d.enumerable
None => true
}
if enumerable {
keys.push(k)
}
}
})
current = data.prototype
}
Array(data) => {
for i = 0; i < data.elements.length(); i = i + 1 {
let k = i.to_string()
if not(seen.contains(k)) {
seen[k] = true
keys.push(k)
}
}
break
}
_ => break
}
}
keys
}
///|
fn Interpreter::exec_stmts(
self : Interpreter,
stmts : Array[@ast.Stmt],
env : Environment,
) -> Signal raise Error {
let mut last : Value = Undefined
for stmt in stmts {
match self.exec_stmt(stmt, env) {
Normal(v) => last = v
other => return other
}
}
Normal(last)
}
///|
fn hoist_pattern(pattern : @ast.Pattern, env : Environment) -> Unit raise Error {
match pattern {
IdentPat(name) =>
if not(env.bindings.contains(name)) {
env.def(name, Undefined, VarBinding)
}
ArrayPat(elements, rest) => {
for elem in elements {
match elem {
Some(p) => hoist_pattern(p, env)
None => ()
}
}
match rest {
Some(p) => hoist_pattern(p, env)
None => ()
}
}
ObjectPat(props, rest) => {
for prop in props {
hoist_pattern(prop.value, env)
}
match rest {
Some(p) => hoist_pattern(p, env)
None => ()
}
}
}
}
///|
fn Interpreter::hoist_declarations(
_self : Interpreter,
stmts : Array[@ast.Stmt],
env : Environment,
) -> Unit raise Error {
for stmt in stmts {
match stmt {
VarDecl(VarKind, name, _, _) =>
if not(env.bindings.contains(name)) {
env.def(name, Undefined, VarBinding)
}
FuncDecl(name, params, body, _) => {
let func_data : FuncData = {
name: Some(name),
params,
body,
closure: env,
}
if env.bindings.contains(name) {
env.assign(name, make_func(func_data))
} else {
env.def(name, make_func(func_data), VarBinding)
}
}
FuncDeclExt(name, params, rest_param, body, _) => {
let func_data : FuncDataExt = {
name: Some(name),
params,
rest_param,
body,
closure: env,
}
if env.bindings.contains(name) {
env.assign(name, make_func_ext(func_data))
} else {
env.def(name, make_func_ext(func_data), VarBinding)
}
}
DestructureDecl(VarKind, pattern, _, _) => hoist_pattern(pattern, env)
StmtList(inner_stmts, _) =>
// Recursively hoist declarations from StmtList (comma-separated decls)
for inner in inner_stmts {
match inner {
VarDecl(VarKind, name, _, _) =>
if not(env.bindings.contains(name)) {
env.def(name, Undefined, VarBinding)
}
DestructureDecl(VarKind, pattern, _, _) =>
hoist_pattern(pattern, env)
_ => ()
}
}
_ => ()
}
}
}
///|
/// Hoist let/const declarations with TDZ markers at block scope
fn hoist_block_tdz(
stmts : Array[@ast.Stmt],
env : Environment,
) -> Unit raise Error {
for stmt in stmts {
match stmt {
VarDecl(LetKind, name, _, _) =>
if not(env.bindings.contains(name)) {
env.def_tdz(name, LetBinding)
}
VarDecl(ConstKind, name, _, _) =>
if not(env.bindings.contains(name)) {
env.def_tdz(name, ConstBinding)
}
// DestructureDecl with let/const also needs TDZ
DestructureDecl(LetKind, pattern, _, _) =>
hoist_pattern_tdz(pattern, env, LetBinding)
DestructureDecl(ConstKind, pattern, _, _) =>
hoist_pattern_tdz(pattern, env, ConstBinding)
// Class declarations have TDZ (similar to let)
ClassDecl(name, _, _, _) =>
if not(env.bindings.contains(name)) {
env.def_tdz(name, LetBinding)
}
_ => ()
}
}
}
///|
/// Hoist pattern bindings with TDZ markers
fn hoist_pattern_tdz(
pattern : @ast.Pattern,
env : Environment,
kind : BindingKind,
) -> Unit raise Error {
match pattern {
IdentPat(name) =>
if not(env.bindings.contains(name)) {
env.def_tdz(name, kind)
}
ArrayPat(elements, rest) => {
for elem in elements {
match elem {
Some(p) => hoist_pattern_tdz(p, env, kind)
None => ()
}
}
match rest {
Some(p) => hoist_pattern_tdz(p, env, kind)
None => ()
}
}
ObjectPat(props, rest) => {
for prop in props {
hoist_pattern_tdz(prop.value, env, kind)
}
match rest {
Some(p) => hoist_pattern_tdz(p, env, kind)
None => ()
}
}
}
}
///|
fn Interpreter::bind_pattern(
self : Interpreter,
pattern : @ast.Pattern,
value : Value,
env : Environment,
bk : BindingKind,
) -> Unit raise Error {
match pattern {
IdentPat(name) =>
if bk == VarBinding && env.bindings.contains(name) {
env.assign(name, value)
} else if (bk == LetBinding || bk == ConstBinding) &&
env.bindings.contains(name) {
// let/const hoisted with TDZ - initialize it
env.initialize(name, value)
} else {
env.def(name, value, bk)
}
ArrayPat(elements, rest) => {
let arr_elements : Array[Value] = match value {
Array(data) => data.elements
_ => []
}
for i = 0; i < elements.length(); i = i + 1 {
match elements[i] {
Some(pat) => {
let val : Value = if i < arr_elements.length() {
arr_elements[i]
} else {
Undefined
}
self.bind_pattern(pat, val, env, bk)
}
None => () // hole — skip
}
}
match rest {
Some(rest_pat) => {
let rest_elements : Array[Value] = []
for i = elements.length(); i < arr_elements.length(); i = i + 1 {
rest_elements.push(arr_elements[i])
}
self.bind_pattern(
rest_pat,
Array({ elements: rest_elements }),
env,
bk,
)
}
None => ()
}
}
ObjectPat(props, rest) => {
let used_keys : Map[String, Bool] = {}
for prop in props {
used_keys[prop.key] = true
let val : Value = match value {
Object(data) =>
match data.properties.get(prop.key) {
Some(v) => v
None => Undefined
}
_ => Undefined
}
let final_val : Value = if val is Undefined {
match prop.default_val {
Some(default_expr) => self.eval_expr(default_expr, env)
None => Undefined
}
} else {
val
}
self.bind_pattern(prop.value, final_val, env, bk)
}
match rest {
Some(rest_pat) => {
let rest_props : Map[String, Value] = {}
let rest_symbol_props : Map[Int, Value] = {}
match value {
Object(data) => {
// Only copy own enumerable properties per ES CopyDataProperties
data.properties.each(fn(k, v) {
if not(used_keys.contains(k)) {
// Check if property is enumerable (default true if no descriptor)
let is_enumerable = match data.descriptors.get(k) {
Some(desc) => desc.enumerable
None => true
}
if is_enumerable {
rest_props[k] = v
}
}
})
// Also copy enumerable symbol properties (they are never in used_keys)
data.symbol_properties.each(fn(k, v) {
let is_enumerable = match data.symbol_descriptors.get(k) {
Some(desc) => desc.enumerable
None => true
}
if is_enumerable {
rest_symbol_props[k] = v
}
})
}
_ => ()
}
self.bind_pattern(
rest_pat,
Object({
properties: rest_props,
symbol_properties: rest_symbol_props,
prototype: Null,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
env,
bk,
)
}
None => ()
}
}
}
}
///|
fn Interpreter::assign_pattern(
self : Interpreter,
pattern : @ast.Pattern,
value : Value,
env : Environment,
) -> Unit raise Error {
match pattern {
IdentPat(name) => env.assign(name, value)
ArrayPat(elements, rest) => {
let arr_elements : Array[Value] = match value {
Array(data) => data.elements
_ => []
}
for i = 0; i < elements.length(); i = i + 1 {
match elements[i] {
Some(pat) => {
let val : Value = if i < arr_elements.length() {
arr_elements[i]
} else {
Undefined
}
self.assign_pattern(pat, val, env)
}
None => ()
}
}
match rest {
Some(rest_pat) => {
let rest_elements : Array[Value] = []
for i = elements.length(); i < arr_elements.length(); i = i + 1 {
rest_elements.push(arr_elements[i])
}
self.assign_pattern(rest_pat, Array({ elements: rest_elements }), env)
}
None => ()
}
}
ObjectPat(props, rest) => {
let used_keys : Map[String, Bool] = {}
for prop in props {
used_keys[prop.key] = true
let val : Value = match value {
Object(data) =>
match data.properties.get(prop.key) {
Some(v) => v
None => Undefined
}
_ => Undefined
}
let final_val : Value = if val is Undefined {
match prop.default_val {
Some(default_expr) => self.eval_expr(default_expr, env)
None => Undefined
}
} else {
val
}
self.assign_pattern(prop.value, final_val, env)
}
match rest {
Some(rest_pat) => {
let rest_props : Map[String, Value] = {}
let rest_symbol_props : Map[Int, Value] = {}
match value {
Object(data) => {
// Only copy own enumerable properties per ES CopyDataProperties
data.properties.each(fn(k, v) {
if not(used_keys.contains(k)) {
// Check if property is enumerable (default true if no descriptor)
let is_enumerable = match data.descriptors.get(k) {
Some(desc) => desc.enumerable
None => true
}
if is_enumerable {
rest_props[k] = v
}
}
})
// Also copy enumerable symbol properties (they are never in used_keys)
data.symbol_properties.each(fn(k, v) {
let is_enumerable = match data.symbol_descriptors.get(k) {
Some(desc) => desc.enumerable
None => true
}
if is_enumerable {
rest_symbol_props[k] = v
}
})
}
_ => ()
}
self.assign_pattern(
rest_pat,
Object({
properties: rest_props,
symbol_properties: rest_symbol_props,
prototype: Null,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
env,
)
}
None => ()
}
}
}
}
///|
fn Interpreter::exec_try_catch(
self : Interpreter,
try_body : Array[@ast.Stmt],
catch_param : String?,
catch_body : Array[@ast.Stmt]?,
finally_body : Array[@ast.Stmt]?,
env : Environment,
) -> Signal raise Error {
let try_env = Environment::new(parent=Some(env))
let result : Signal = self.exec_stmts(try_body, try_env) catch {
e =>
// Check if this is a JavaScript-catchable error
if is_js_catchable_error(e) {
match (catch_param, catch_body) {
(Some(param), Some(body)) => {
let catch_env = Environment::new(parent=Some(env))
// Convert runtime errors to proper JS Error objects with prototype chain
let error_value = js_error_to_value_with_env(e, Some(env))
catch_env.def(param, error_value, LetBinding)
self.exec_stmts(body, catch_env)
}
_ => Normal(Undefined)
}
} else {
// Re-raise non-JS errors (like MoonBit Failure errors)
raise e
}
}
match finally_body {
Some(body) => {
let finally_result = self.exec_stmts(
body,
Environment::new(parent=Some(env)),
)
match finally_result {
Normal(_) => result
other => other
}
}
None => result
}
}
///|
fn Interpreter::eval_expr(
self : Interpreter,
expr : @ast.Expr,
env : Environment,
) -> Value raise Error {
match expr {
NumberLit(n, _) => Number(n)
StringLit(s, _) => String_(s)
BoolLit(b, _) => Bool(b)
NullLit(_) => Null
UndefinedLit(_) => Undefined
Ident(name, _) => env.get(name)
Grouping(e, _) => self.eval_expr(e, env)
Binary(op, left, right, loc) => self.eval_binary(op, left, right, env, loc)
Unary(op, operand, loc) => self.eval_unary(op, operand, env, loc)
Assign(name, value_expr, _) => {
let value = self.eval_expr(value_expr, env)
env.assign(name, value)
value
}
Ternary(cond, then_expr, else_expr, _) =>
if is_truthy(self.eval_expr(cond, env)) {
self.eval_expr(then_expr, env)
} else {
self.eval_expr(else_expr, env)
}
Call(callee, args, loc) => self.eval_call(callee, args, env, loc)
Member(obj_expr, prop, loc) => self.eval_member(obj_expr, prop, env, loc)
FuncExpr(name, params, body, _) => {
let func_data : FuncData = { name, params, body, closure: env }
make_func(func_data)
}
ObjectLit(props, _) => {
let properties : Map[String, Value] = {}
let symbol_properties : Map[Int, Value] = {}
let descriptors : Map[String, PropDescriptor] = {}
for prop in props {
// Handle spread property: { ...expr }
if prop.kind == @ast.Spread {
let spread_val = self.eval_expr(prop.key, env)
match spread_val {
Object(data) => {
// Copy all enumerable own properties
for key, value in data.properties {
match data.descriptors.get(key) {
Some(desc) =>
if desc.enumerable {
properties[key] = value
}
None => properties[key] = value
}
}
// Copy symbol properties too
for sym_id, value in data.symbol_properties {
match data.symbol_descriptors.get(sym_id) {
Some(desc) =>
if desc.enumerable {
symbol_properties[sym_id] = value
}
None => symbol_properties[sym_id] = value
}
}
}
Array(arr_data) => {
// Spread array into object: only enumerable own properties
// length is non-enumerable per spec, so only copy indexed elements
for i, value in arr_data.elements {
properties[i.to_string()] = value
}
}
String_(s) => {
// Spread string into object: { ..."abc" } → { "0": "a", "1": "b", "2": "c" }
let mut idx = 0
for ch in s {
properties[idx.to_string()] = String_(ch.to_string())
idx = idx + 1
}
}
Null | Undefined => () // Skip null/undefined per spec
Number(_) | Bool(_) => () // Primitives have no enumerable own properties
_ => ()
}
continue
}
if prop.computed {
// Evaluate the computed key
let key_val = self.eval_expr(prop.key, env)
match key_val {
Symbol(sym) =>
// Symbol key - store in symbol_properties
match prop.kind {
Init =>
symbol_properties[sym.id] = self.eval_expr(prop.value, env)
Get | Set => () // Getters/setters for symbols not fully supported yet
Spread => () // handled above
}
_ => {
// Convert to string key
let key_str : String = match key_val {
String_(s) => s
Number(n) => {
let i = n.to_int()
if i.to_double() == n {
i.to_string()
} else {
n.to_string()
}
}
Bool(b) => b.to_string()
Null => "null"
Undefined => "undefined"
_ => key_val.to_string()
}
match prop.kind {
Init => properties[key_str] = self.eval_expr(prop.value, env)
Get => {
let getter = self.eval_expr(prop.value, env)
properties["__get__" + key_str] = getter
}
Set => {
let setter = self.eval_expr(prop.value, env)
properties["__set__" + key_str] = setter
}
Spread => () // handled above
}
}
}
} else {
// Static key - extract from StringLit
let key_str = match prop.key {
StringLit(s, _) => s
_ => prop.key.to_string() // fallback
}
match prop.kind {
Init => properties[key_str] = self.eval_expr(prop.value, env)
Get => {
let getter = self.eval_expr(prop.value, env)
properties["__get__" + key_str] = getter
}
Set => {
let setter = self.eval_expr(prop.value, env)
properties["__set__" + key_str] = setter
}
Spread => () // handled above
}
}
}
let obj_prototype : Value = try { env.get("[[ObjectPrototype]]") } catch { _ => Null }
Object({
properties,
symbol_properties,
prototype: obj_prototype,
callable: None,
class_name: "Object",
descriptors,
symbol_descriptors: {},
extensible: true,
})
}
ArrayLit(elements, _) => {
let vals : Array[Value] = []
for e in elements {
match e {
SpreadExpr(inner, spread_loc) => {
let val = self.eval_expr(inner, env)
// Use iterator protocol for spreading
let spread_vals = self.spread_iterable(val, spread_loc)
for v in spread_vals {
vals.push(v)
}
}
_ => vals.push(self.eval_expr(e, env))
}
}
Array({ elements: vals })
}
ComputedMember(obj_expr, key_expr, loc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
self.get_computed_property(obj, key, loc)
}
MemberAssign(obj_expr, prop, value_expr, _loc) => {
let obj = self.eval_expr(obj_expr, env)
let value = self.eval_expr(value_expr, env)
match obj {
Object(data) => {
// Check writable descriptor
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return value
}
None => ()
}
// Check extensible for new properties
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return value
}
data.properties[prop] = value
value
}
Promise(data) => {
data.properties[prop] = value
value
}
_ =>
raise @errors.TypeError(
message="Cannot set property '\{prop}' of \{type_of(obj)}",
)
}
}
ComputedAssign(obj_expr, key_expr, value_expr, _loc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let value = self.eval_expr(value_expr, env)
match obj {
Object(data) =>
// Handle Symbol keys specially
match key {
Symbol(sym) => {
// Check symbol property descriptor
match data.symbol_descriptors.get(sym.id) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property",
)
}
return value
}
None => ()
}
if not(data.symbol_properties.contains(sym.id)) &&
not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property, object is not extensible",
)
}
return value
}
data.symbol_properties[sym.id] = value
value
}
_ => {
let prop = key.to_string()
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return value
}
None => ()
}
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return value
}
data.properties[prop] = value
value
}
}
Array(data) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 {
while data.elements.length() <= i {
data.elements.push(Undefined)
}
data.elements[i] = value
}
value
}
_ => value
}
Promise(data) =>
match key {
Symbol(sym) => {
data.symbol_properties[sym.id] = value
value
}
_ => {
let prop = key.to_string()
data.properties[prop] = value
value
}
}
_ =>
raise @errors.TypeError(
message="Cannot set property of \{type_of(obj)}",
)
}
}
NewExpr(callee_expr, arg_exprs, loc) =>
self.eval_new(callee_expr, arg_exprs, env, loc)
ThisExpr(_) => env.get("this") catch { _ => Undefined }
UpdateExpr(op, operand, prefix, loc) =>
self.eval_update(op, operand, prefix, env, loc)
CompoundAssign(op, target, value_expr, loc) =>
self.eval_compound_assign(op, target, value_expr, env, loc)
Comma(left, right, _) => {
let _ = self.eval_expr(left, env)
self.eval_expr(right, env)
}
TemplateLit(strings, exprs, _) => {
let buf = StringBuilder::new()
for i = 0; i < strings.length(); i = i + 1 {
buf.write_string(strings[i])
if i < exprs.length() {
let val = self.eval_expr(exprs[i], env)
buf.write_string(val.to_string())
}
}
String_(buf.to_string())
}
TaggedTemplate(tag_expr, strings, exprs, loc) => {
let tag = self.eval_expr(tag_expr, env)
// Build the raw array (same as cooked for now - raw doesn't process escapes)
let raw_vals : Array[Value] = []
for s in strings {
raw_vals.push(String_(s))
}
let raw_arr = Array({ elements: raw_vals })
// Create the template strings object as an array-like object with raw property
let props : Map[String, Value] = {}
props["length"] = Number(strings.length().to_double())
props["raw"] = raw_arr
for i, s in strings {
props[i.to_string()] = String_(s)
}
let template_obj = Object({
properties: props,
symbol_properties: {},
prototype: try { self.global.get("[[ObjectPrototype]]") } catch {
_ => Null
},
callable: None,
class_name: "Array",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Build args: [templateObj, ...substitutions]
let args : Array[Value] = [template_obj]
for e in exprs {
args.push(self.eval_expr(e, env))
}
// Resolve this binding for member expression tags (e.g., String.raw`...`)
let this_val : Value = match tag_expr {
Member(obj_expr, _, _) | ComputedMember(obj_expr, _, _) =>
self.eval_expr(obj_expr, env)
_ => Undefined
}
self.call_value(tag, this_val, args, loc)
}
ArrowFunc(params, body, _) => {
let func_data : FuncData = { name: None, params, body, closure: env }
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(ArrowFunc(func_data)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}
ArrowFuncExt(params, rest_param, body, _) => {
let func_data : FuncDataExt = {
name: None,
params,
rest_param,
body,
closure: env,
}
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(ArrowFuncExt(func_data)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}
FuncExprExt(name, params, rest_param, body, _) => {
let func_data : FuncDataExt = {
name,
params,
rest_param,
body,
closure: env,
}
make_func_ext(func_data)
}
RegexLit(pattern, flags, _) => make_regexp_object(pattern, flags)
SpreadExpr(_, _) =>
raise @errors.SyntaxError(
message="Spread expression used outside of call or array literal",
)
DestructureAssign(pattern, value_expr, _) => {
let value = self.eval_expr(value_expr, env)
self.assign_pattern(pattern, value, env)
value
}
OptionalMember(obj_expr, prop, loc) => {
// obj?.prop - return undefined if obj is null/undefined
let obj = self.eval_expr(obj_expr, env)
match obj {
Null | Undefined => Undefined
_ => self.get_property(obj, prop, loc)
}
}
OptionalComputedMember(obj_expr, key_expr, loc) => {
// obj?.[key] - return undefined if obj is null/undefined
let obj = self.eval_expr(obj_expr, env)
match obj {
Null | Undefined => Undefined
_ => {
let key = self.eval_expr(key_expr, env)
self.get_computed_property(obj, key, loc)
}
}
}
OptionalCall(callee_expr, arg_exprs, loc) =>
// func?.(args) - return undefined if func is null/undefined
// Per ES spec, arguments are NOT evaluated if function is nullish
// Need to preserve receiver for method calls
match callee_expr {
OptionalMember(obj_expr, prop, mloc) => {
let obj = self.eval_expr(obj_expr, env)
match obj {
Null | Undefined => Undefined
_ => {
let func_val = self.get_property(obj, prop, mloc)
match func_val {
Null | Undefined => Undefined
_ => {
let args = self.eval_args_with_spread(arg_exprs, env)
self.call_value(func_val, obj, args, loc)
}
}
}
}
}
OptionalComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
match obj {
Null | Undefined => Undefined
_ => {
let key = self.eval_expr(key_expr, env)
let func_val = self.get_computed_property(obj, key, mloc)
match func_val {
Null | Undefined => Undefined
_ => {
let args = self.eval_args_with_spread(arg_exprs, env)
self.call_value(func_val, obj, args, loc)
}
}
}
}
}
Member(obj_expr, prop, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let func_val = self.get_property(obj, prop, mloc)
match func_val {
Null | Undefined => Undefined
_ => {
let args = self.eval_args_with_spread(arg_exprs, env)
self.call_value(func_val, obj, args, loc)
}
}
}
ComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let func_val = self.get_computed_property(obj, key, mloc)
match func_val {
Null | Undefined => Undefined
_ => {
let args = self.eval_args_with_spread(arg_exprs, env)
self.call_value(func_val, obj, args, loc)
}
}
}
_ => {
let callee = self.eval_expr(callee_expr, env)
match callee {
Null | Undefined => Undefined
_ => {
let args = self.eval_args_with_spread(arg_exprs, env)
self.call_value(callee, Undefined, args, loc)
}
}
}
}
ClassExpr(name, superclass, methods, _) =>
self.create_class(name.unwrap_or(""), superclass, methods, env)
SuperCall(arg_exprs, loc) => {
// super(args) - call the parent constructor
// Get the [[HomeObject]] from the current environment
let super_ctor = env.get("[[SuperConstructor]]")
let this_val = env.get("this")
let args = self.eval_args_with_spread(arg_exprs, env)
// Call the super constructor
match super_ctor {
Object(data) =>
match data.callable {
Some(
ClassConstructor(
_,
_,
_super_ctor,
ctor_fn,
ctor_env,
super_proto
)
) => {
// Create new environment for the constructor call
let call_env = Environment::new(parent=Some(ctor_env))
call_env.def_builtin("this", this_val)
// Set up super constructor chain if there's a grandparent
match _super_ctor {
Some(grand_super) => {
call_env.def_builtin("[[SuperConstructor]]", grand_super)
call_env.def_builtin("[[SuperPrototype]]", super_proto)
}
None => ()
}
// Bind parameters and execute constructor body
match ctor_fn {
Some((ctor_params, ctor_body)) => {
for i = 0; i < ctor_params.length(); i = i + 1 {
let val : Value = if i < args.length() {
args[i]
} else {
Undefined
}
call_env.def(ctor_params[i], val, LetBinding)
}
self.hoist_declarations(ctor_body, call_env)
let exec_result = self.exec_stmts(ctor_body, call_env)
// If super constructor returns an object, use it as the new 'this'
match exec_result {
ReturnSignal(v) =>
match v {
Object(_) => {
// Update 'this' in the current environment
env.assign("this", v)
return v
}
_ => ()
}
_ => ()
}
}
None => ()
}
this_val
}
_ =>
raise @errors.ReferenceError(
message="super() called but [[SuperConstructor]] is not a class at line \{loc.line}",
)
}
_ =>
raise @errors.ReferenceError(
message="super() called but no [[SuperConstructor]] in scope at line \{loc.line}",
)
}
}
SuperMember(prop, loc) => {
// super.prop - access parent prototype property
let super_proto = env.get("[[SuperPrototype]]")
match super_proto {
Null | Undefined =>
raise @errors.ReferenceError(
message="super.prop used but no [[SuperPrototype]] in scope at line \{loc.line}",
)
_ => self.get_property(super_proto, prop, loc)
}
}
SuperComputedMember(key_expr, loc) => {
// super[expr] - access parent prototype property with computed key
let super_proto = env.get("[[SuperPrototype]]")
let key = self.eval_expr(key_expr, env)
match super_proto {
Null | Undefined =>
raise @errors.ReferenceError(
message="super[expr] used but no [[SuperPrototype]] in scope at line \{loc.line}",
)
_ => self.get_computed_property(super_proto, key, loc)
}
}
NewTargetExpr(_) => {
// new.target - returns the constructor called with new, or undefined
try {
env.get("")
} catch {
_ => Undefined
}
}
}
}
///|
fn Interpreter::eval_binary(
self : Interpreter,
op : @ast.BinOp,
left_expr : @ast.Expr,
right_expr : @ast.Expr,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
// Short-circuit for &&, ||, and ??
match op {
And => {
let left = self.eval_expr(left_expr, env)
if not(is_truthy(left)) {
return left
}
self.eval_expr(right_expr, env)
}
Or => {
let left = self.eval_expr(left_expr, env)
if is_truthy(left) {
return left
}
self.eval_expr(right_expr, env)
}
NullishCoalesce => {
// ?? returns left if it's not null/undefined, otherwise right
let left = self.eval_expr(left_expr, env)
match left {
Null | Undefined => self.eval_expr(right_expr, env)
_ => left
}
}
Instanceof => {
// instanceof - ES2015+ with Symbol.hasInstance support (spec: 7.3.21 OrdinaryHasInstance)
let l = self.eval_expr(left_expr, env)
let r = self.eval_expr(right_expr, env)
match r {
Object(r_data) => {
// Step 1: Check for Symbol.hasInstance method via GetMethod (walks prototype chain)
let has_instance_sym = get_hasinstance_symbol()
let has_instance_method = get_symbol_method(r, has_instance_sym.id)
match has_instance_method {
Some(has_instance_fn) =>
// Call [Symbol.hasInstance](l) and convert result to boolean
match has_instance_fn {
Object(fn_data) =>
match fn_data.callable {
Some(_) => {
let result = self.call_value(has_instance_fn, r, [l], loc)
Bool(is_truthy(result))
}
None =>
// Symbol.hasInstance exists but is not callable - TypeError per spec
raise @errors.TypeError(
message="Symbol.hasInstance is not a function",
)
}
_ =>
// Symbol.hasInstance is not an object - TypeError per spec
raise @errors.TypeError(
message="Symbol.hasInstance is not a function",
)
}
None =>
// No Symbol.hasInstance found, use OrdinaryHasInstance
// Step 2: Check if RHS is callable (required for OrdinaryHasInstance)
match r_data.callable {
None =>
raise @errors.TypeError(
message="Right-hand side of 'instanceof' is not callable",
)
Some(_) =>
// Step 3: Get prototype property and walk LHS prototype chain
// Per OrdinaryHasInstance spec: if prototype is not an object, return false
// This handles bound functions which don't have own prototype property
match r_data.properties.get("prototype") {
Some(proto) =>
match proto {
Object(_) => instanceof_prototype_chain(l, proto)
// Non-object prototype: return false per spec
_ => Bool(false)
}
// No prototype property (e.g., bound functions): return false per spec
None => Bool(false)
}
}
}
}
_ =>
raise @errors.TypeError(
message="Right-hand side of 'instanceof' is not an object",
)
}
}
_ => {
let left = self.eval_expr(left_expr, env)
let right = self.eval_expr(right_expr, env)
eval_binary_op(op, left, right, loc)
}
}
}
///|
fn eval_binary_op(
op : @ast.BinOp,
left : Value,
right : Value,
_loc : @token.Loc,
) -> Value raise Error {
match (op, left, right) {
// Symbol cannot be converted to string implicitly
(Add, Symbol(_), _) | (Add, _, Symbol(_)) =>
raise @errors.TypeError(
message="Cannot convert a Symbol value to a string",
)
// String concatenation
(Add, String_(a), String_(b)) => String_(a + b)
(Add, String_(a), b) => String_(a + b.to_string())
(Add, a, String_(b)) => String_(a.to_string() + b)
// Numeric operations
(Add, Number(a), Number(b)) => Number(a + b)
(Sub, Number(a), Number(b)) => Number(a - b)
(Mul, Number(a), Number(b)) => Number(a * b)
(Div, Number(a), Number(b)) => Number(a / b)
(Mod, Number(a), Number(b)) => Number(a % b)
// Comparison (numeric)
(Lt, Number(a), Number(b)) => Bool(a < b)
(Gt, Number(a), Number(b)) => Bool(a > b)
(LtEq, Number(a), Number(b)) => Bool(a <= b)
(GtEq, Number(a), Number(b)) => Bool(a >= b)
// Comparison (string — lexicographic)
(Lt, String_(a), String_(b)) => Bool(a < b)
(Gt, String_(a), String_(b)) => Bool(a > b)
(LtEq, String_(a), String_(b)) => Bool(a <= b)
(GtEq, String_(a), String_(b)) => Bool(a >= b)
// Equality
(EqEqEq, l, r) => Bool(strict_equal(l, r))
(NotEqEq, l, r) => Bool(not(strict_equal(l, r)))
(EqEq, l, r) => Bool(loose_equal(l, r))
(NotEq, l, r) => Bool(not(loose_equal(l, r)))
// Exponentiation
(Exp, Number(a), Number(b)) => Number(@math.pow(a, b))
(Exp, l, r) => Number(@math.pow(to_number(l), to_number(r)))
// Arithmetic fallbacks (type coercion)
(Add, l, r) => Number(to_number(l) + to_number(r))
(Sub, l, r) => Number(to_number(l) - to_number(r))
(Mul, l, r) => Number(to_number(l) * to_number(r))
(Div, l, r) => Number(to_number(l) / to_number(r))
(Mod, l, r) => Number(to_number(l) % to_number(r))
// Comparison fallbacks (type coercion)
(Lt, l, r) => Bool(to_number(l) < to_number(r))
(Gt, l, r) => Bool(to_number(l) > to_number(r))
(LtEq, l, r) => Bool(to_number(l) <= to_number(r))
(GtEq, l, r) => Bool(to_number(l) >= to_number(r))
// Bitwise operations
(BitAnd, l, r) =>
Number(to_int32(to_number(l)).land(to_int32(to_number(r))).to_double())
(BitOr, l, r) =>
Number(to_int32(to_number(l)).lor(to_int32(to_number(r))).to_double())
(BitXor, l, r) =>
Number(to_int32(to_number(l)).lxor(to_int32(to_number(r))).to_double())
// Shift operations
(LShift, l, r) => {
let a = to_int32(to_number(l))
let shift = to_int32(to_number(r)) & 0x1f
Number((a << shift).to_double())
}
(RShift, l, r) => {
let a = to_int32(to_number(l))
let shift = to_int32(to_number(r)) & 0x1f
Number((a >> shift).to_double())
}
(URShift, l, r) => {
let a = to_int32(to_number(l))
let shift = to_int32(to_number(r)) & 0x1f
if shift == 0 {
if a < 0 {
Number(a.to_double() + 4294967296.0)
} else {
Number(a.to_double())
}
} else {
let shifted = a >> shift
let all_bits : Int = 0x7FFFFFFF
let mask = all_bits >> (shift - 1)
Number(shifted.land(mask).to_double())
}
}
// in operator - walks prototype chain
(In, l, r) =>
match r {
Object(data) =>
// Handle Symbol keys
match l {
Symbol(sym) => {
// Check own symbol property first
if data.symbol_properties.contains(sym.id) {
return Bool(true)
}
// Walk prototype chain for symbol properties
let mut current = data.prototype
while true {
match current {
Object(proto_data) =>
if proto_data.symbol_properties.contains(sym.id) {
return Bool(true)
} else {
current = proto_data.prototype
}
_ => break
}
}
Bool(false)
}
_ => {
let key = l.to_string()
// Check own property first
if data.properties.contains(key) {
return Bool(true)
}
// Walk prototype chain
let mut current = data.prototype
while true {
match current {
Object(proto_data) =>
if proto_data.properties.contains(key) {
return Bool(true)
} else {
current = proto_data.prototype
}
_ => break
}
}
Bool(false)
}
}
Array(data) => {
let idx = to_number(l).to_int() catch { _ => -1 }
Bool(idx >= 0 && idx < data.elements.length())
}
_ =>
raise @errors.TypeError(
message="Cannot use 'in' operator to search for '\{l.to_string()}' in \{type_of(r)}",
)
}
// And/Or/??/Instanceof already handled in eval_binary
(And, _, _) | (Or, _, _) | (NullishCoalesce, _, _) | (Instanceof, _, _) =>
raise @errors.InternalError(
message="Internal error: logical operators should be short-circuited",
)
}
}
///|
/// GetMethod for symbol keys - walks prototype chain to find a symbol-keyed method (spec 7.3.9)
fn get_symbol_method(obj : Value, symbol_id : Int) -> Value? {
let mut current = obj
while true {
match current {
Object(data) =>
match data.symbol_properties.get(symbol_id) {
Some(val) =>
match val {
Null | Undefined => return None
_ => return Some(val)
}
None => current = data.prototype
}
_ => return None
}
}
None
}
///|
/// Helper for instanceof: walk prototype chain to check if l's prototype chain includes target_proto
fn instanceof_prototype_chain(l : Value, target_proto : Value) -> Value {
match l {
Object(l_data) => {
let mut current = l_data.prototype
let mut found = false
while true {
match current {
Null | Undefined => break
_ =>
if strict_equal(current, target_proto) {
found = true
break
} else {
match current {
Object(data) => current = data.prototype
_ => break
}
}
}
}
Bool(found)
}
// Arrays and primitives don't have prototype chains in this representation
_ => Bool(false)
}
}
///|
fn strict_equal(a : Value, b : Value) -> Bool {
match (a, b) {
(Number(a), Number(b)) => a == b
(String_(a), String_(b)) => a == b
(Bool(a), Bool(b)) => a == b
(Null, Null) => true
(Undefined, Undefined) => true
(Object(a), Object(b)) => physical_equal(a, b)
(Array(a), Array(b)) => physical_equal(a, b)
(Symbol(a), Symbol(b)) => a.id == b.id // Symbols compare by identity
(Map(a), Map(b)) => physical_equal(a, b) // Maps compare by reference
(Set(a), Set(b)) => physical_equal(a, b) // Sets compare by reference
(Promise(a), Promise(b)) => physical_equal(a, b) // Promises compare by reference
_ => false
}
}
///|
fn loose_equal(a : Value, b : Value) -> Bool {
// Abstract Equality Comparison Algorithm (ES spec 7.2.14)
match (a, b) {
// Same type: use strict equality
(Number(x), Number(y)) => x == y
(String_(x), String_(y)) => x == y
(Bool(x), Bool(y)) => x == y
(Null, Null) => true
(Undefined, Undefined) => true
(Object(x), Object(y)) => physical_equal(x, y)
(Array(x), Array(y)) => physical_equal(x, y)
(Symbol(x), Symbol(y)) => x.id == y.id // Symbols compare by identity only
(Promise(x), Promise(y)) => physical_equal(x, y) // Promises compare by reference
// null == undefined
(Null, Undefined) | (Undefined, Null) => true
// Number == String: convert string to number
(Number(n), String_(s)) => {
let num = to_number(String_(s)) catch { _ => return false }
n == num
}
(String_(s), Number(n)) => {
let num = to_number(String_(s)) catch { _ => return false }
num == n
}
// Boolean == anything: convert boolean to number first
(Bool(bval), _) => loose_equal(Number(if bval { 1.0 } else { 0.0 }), b)
(_, Bool(bval)) => loose_equal(a, Number(if bval { 1.0 } else { 0.0 }))
// Object/Array == Number/String: try ToPrimitive (simplified: use to_string)
(Object(_), Number(_)) | (Object(_), String_(_)) => {
let prim = String_(a.to_string())
loose_equal(prim, b)
}
(Number(_), Object(_)) | (String_(_), Object(_)) => {
let prim = String_(b.to_string())
loose_equal(a, prim)
}
(Array(_), Number(_)) | (Array(_), String_(_)) => {
let prim = String_(a.to_string())
loose_equal(prim, b)
}
(Number(_), Array(_)) | (String_(_), Array(_)) => {
let prim = String_(b.to_string())
loose_equal(a, prim)
}
// Symbols don't coerce - any comparison with Symbol and different type is false
(Symbol(_), _) | (_, Symbol(_)) => false
// Everything else is false
_ => false
}
}
///|
fn Interpreter::eval_unary(
self : Interpreter,
op : @ast.UnaryOp,
operand : @ast.Expr,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
match op {
Typeof =>
match operand {
Ident(name, _) =>
if env.has(name) {
String_(type_of(env.get(name)))
} else {
String_("undefined")
}
_ => String_(type_of(self.eval_expr(operand, env)))
}
Void => {
let _ = self.eval_expr(operand, env)
Undefined
}
Delete =>
match operand {
Member(obj_expr, prop, _) => {
let obj = self.eval_expr(obj_expr, env)
match obj {
Object(data) => {
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.configurable) {
if self.strict {
raise @errors.TypeError(
message="Cannot delete property '\{prop}'",
)
}
return Bool(false)
}
None => ()
}
let _ = data.properties.remove(prop)
let _ = data.descriptors.remove(prop)
Bool(true)
}
_ => Bool(true)
}
}
ComputedMember(obj_expr, key_expr, _) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
match obj {
Object(data) => {
let k = key.to_string()
match data.descriptors.get(k) {
Some(desc) =>
if not(desc.configurable) {
if self.strict {
raise @errors.TypeError(
message="Cannot delete property '\{k}'",
)
}
return Bool(false)
}
None => ()
}
let _ = data.properties.remove(k)
let _ = data.descriptors.remove(k)
Bool(true)
}
_ => Bool(true)
}
}
_ => Bool(true)
}
_ => {
let val = self.eval_expr(operand, env)
match op {
Neg => Number(-to_number(val))
Pos => Number(to_number(val))
Not => Bool(not(is_truthy(val)))
BitNot => Number(to_int32(to_number(val)).lnot().to_double())
_ =>
raise @errors.InternalError(
message="Unexpected unary operator at line \{loc.line}, col \{loc.col}",
)
}
}
}
}
///|
/// Spread an iterable value into an array of values using the iterator protocol
fn Interpreter::spread_iterable(
self : Interpreter,
val : Value,
loc : @token.Loc,
) -> Array[Value] raise Error {
let result : Array[Value] = []
// Try to get the iterator via Symbol.iterator protocol
let iterator_sym = get_iterator_symbol()
let iterator_method = self.get_computed_property(
val,
Symbol(iterator_sym),
loc,
)
// Get the iterator object
let iterator = match iterator_method {
Object(data) =>
match data.callable {
Some(_) => self.call_value(iterator_method, val, [], loc)
None =>
raise @errors.TypeError(
message=type_of(val) +
" is not iterable (Symbol.iterator is not a function)",
)
}
Undefined =>
// No iterator method — fall back for arrays/strings which are built-in iterables
match val {
Array(arr) => {
for el in arr.elements {
result.push(el)
}
return result
}
String_(s) => {
let chars = s.to_array()
for c in chars {
let buf = StringBuilder::new()
buf.write_char(c)
result.push(String_(buf.to_string()))
}
return result
}
_ =>
raise @errors.TypeError(
message=type_of(val) + " is not iterable",
)
}
_ =>
raise @errors.TypeError(
message=type_of(val) + " is not iterable",
)
}
// Get the next method from the iterator (walk prototype chain)
let next_method = match iterator {
Object(iter_data) =>
// First check direct properties
match iter_data.properties.get("next") {
Some(next_fn) => next_fn
None => {
// Walk prototype chain
let mut found : Value = Undefined
let mut current = iter_data.prototype
while true {
match current {
Object(proto_data) =>
match proto_data.properties.get("next") {
Some(next_fn) => {
found = next_fn
break
}
None => current = proto_data.prototype
}
_ => break
}
}
match found {
Undefined =>
raise @errors.TypeError(message="iterator.next is not a function")
_ => found
}
}
}
_ => raise @errors.TypeError(message="Iterator result is not an object")
}
// Iterate using the iterator protocol
while true {
let iter_result = self.call_value(next_method, iterator, [], loc)
let (done, value) = match iter_result {
Object(result_data) => {
let done_val = match result_data.properties.get("done") {
Some(v) => is_truthy(v)
None => false
}
let value_val = match result_data.properties.get("value") {
Some(v) => v
None => Undefined
}
(done_val, value_val)
}
_ =>
raise @errors.TypeError(
message="Iterator result is not an object",
)
}
if done {
break
}
result.push(value)
}
result
}
///|
fn Interpreter::eval_args_with_spread(
self : Interpreter,
arg_exprs : Array[@ast.Expr],
env : Environment,
) -> Array[Value] raise Error {
let args : Array[Value] = []
for a in arg_exprs {
match a {
SpreadExpr(inner, loc) => {
let val = self.eval_expr(inner, env)
// Use iterator protocol for spreading
let spread_vals = self.spread_iterable(val, loc)
for v in spread_vals {
args.push(v)
}
}
_ => args.push(self.eval_expr(a, env))
}
}
args
}
///|
fn Interpreter::eval_call(
self : Interpreter,
callee_expr : @ast.Expr,
arg_exprs : Array[@ast.Expr],
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
let args : Array[Value] = self.eval_args_with_spread(arg_exprs, env)
match callee_expr {
Member(obj_expr, prop, mloc) =>
match obj_expr {
Ident("console", _) => {
let callee = self.eval_member(obj_expr, prop, env, mloc)
self.call_value(callee, Undefined, args, loc)
}
_ => {
let obj = self.eval_expr(obj_expr, env)
let func_val = self.get_property(obj, prop, mloc)
self.call_value(func_val, obj, args, loc)
}
}
ComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let func_val = self.get_computed_property(obj, key, mloc)
self.call_value(func_val, obj, args, loc)
}
_ => {
let callee = self.eval_expr(callee_expr, env)
self.call_value(callee, Undefined, args, loc)
}
}
}
///|
fn Interpreter::call_value(
self : Interpreter,
callee : Value,
this_val : Value,
args : Array[Value],
loc : @token.Loc,
) -> Value raise Error {
match callee {
Object(obj_data) =>
match obj_data.callable {
Some(UserFunc(data)) => {
let saved_strict = self.strict
let result = try {
let func_env = Environment::new(parent=Some(data.closure))
let func_strict = self.strict || has_use_strict(data.body)
if func_strict {
self.strict = true
}
func_env.def("this", this_val, LetBinding)
func_env.def("", Undefined, LetBinding)
for i = 0; i < data.params.length(); i = i + 1 {
let val : Value = if i < args.length() {
args[i]
} else {
Undefined
}
func_env.def(data.params[i], val, LetBinding)
}
// Create arguments object
let args_props : Map[String, Value] = {}
for i = 0; i < args.length(); i = i + 1 {
args_props[i.to_string()] = args[i]
}
args_props["length"] = Number(args.length().to_double())
if not(func_strict) {
args_props["callee"] = callee
}
func_env.def(
"arguments",
Object({
properties: args_props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Arguments",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
VarBinding,
)
// Hoist var declarations and function declarations within function body
self.hoist_declarations(data.body, func_env)
match data.name {
Some(name) =>
if not(func_env.bindings.contains(name)) {
func_env.def(name, callee, LetBinding)
}
None => ()
}
match self.exec_stmts(data.body, func_env) {
Normal(v) => v
ReturnSignal(v) => v
BreakSignal(_) =>
raise @errors.SyntaxError(
message="break statement outside of loop",
)
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
} catch {
e => {
self.strict = saved_strict
raise e
}
}
self.strict = saved_strict
result
}
Some(ArrowFunc(data)) => {
let func_env = Environment::new(parent=Some(data.closure))
// Arrow functions do NOT rebind this — use closure's this
for i = 0; i < data.params.length(); i = i + 1 {
let val : Value = if i < args.length() {
args[i]
} else {
Undefined
}
func_env.def(data.params[i], val, LetBinding)
}
self.hoist_declarations(data.body, func_env)
match self.exec_stmts(data.body, func_env) {
Normal(v) => v
ReturnSignal(v) => v
BreakSignal(_) =>
raise @errors.SyntaxError(
message="break statement outside of loop",
)
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
}
Some(UserFuncExt(data)) => {
let saved_strict = self.strict
let result = try {
let func_env = Environment::new(parent=Some(data.closure))
let func_strict = self.strict || has_use_strict(data.body)
if func_strict {
self.strict = true
}
func_env.def("this", this_val, LetBinding)
func_env.def("", Undefined, LetBinding)
// Bind params with defaults
let mut effective_param_count = 0
for i = 0; i < data.params.length(); i = i + 1 {
let param = data.params[i]
// Skip rest destructuring params - they're bound via rest_param
if data.rest_param is Some(rn) && param.name == rn {
continue
}
let val : Value = if effective_param_count < args.length() &&
not(args[effective_param_count] is Undefined) {
args[effective_param_count]
} else {
match param.default_val {
Some(default_expr) => self.eval_expr(default_expr, func_env)
None =>
if effective_param_count < args.length() {
args[effective_param_count]
} else {
Undefined
}
}
}
func_env.def(param.name, val, LetBinding)
// Destructure if pattern param
match param.pattern {
Some(pat) => self.bind_pattern(pat, val, func_env, LetBinding)
None => ()
}
effective_param_count = effective_param_count + 1
}
// Rest param
match data.rest_param {
Some(rest_name) => {
let rest_elements : Array[Value] = []
for i = effective_param_count; i < args.length(); i = i + 1 {
rest_elements.push(args[i])
}
let rest_val = Array({ elements: rest_elements })
func_env.def(rest_name, rest_val, LetBinding)
// Apply destructuring pattern if present
for p in data.params {
if p.name == rest_name {
match p.pattern {
Some(pat) =>
self.bind_pattern(pat, rest_val, func_env, LetBinding)
None => ()
}
break
}
}
}
None => ()
}
// Create arguments object
let args_props : Map[String, Value] = {}
for i = 0; i < args.length(); i = i + 1 {
args_props[i.to_string()] = args[i]
}
args_props["length"] = Number(args.length().to_double())
if not(func_strict) {
args_props["callee"] = callee
}
func_env.def(
"arguments",
Object({
properties: args_props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Arguments",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
VarBinding,
)
self.hoist_declarations(data.body, func_env)
match data.name {
Some(name) =>
if not(func_env.bindings.contains(name)) {
func_env.def(name, callee, LetBinding)
}
None => ()
}
match self.exec_stmts(data.body, func_env) {
Normal(v) => v
ReturnSignal(v) => v
BreakSignal(_) =>
raise @errors.SyntaxError(
message="break statement outside of loop",
)
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
} catch {
e => {
self.strict = saved_strict
raise e
}
}
self.strict = saved_strict
result
}
Some(ArrowFuncExt(data)) => {
let func_env = Environment::new(parent=Some(data.closure))
// Arrow functions do NOT rebind this
let mut effective_param_count = 0
for i = 0; i < data.params.length(); i = i + 1 {
let param = data.params[i]
// Skip rest destructuring params
if data.rest_param is Some(rn) && param.name == rn {
continue
}
let val : Value = if effective_param_count < args.length() &&
not(args[effective_param_count] is Undefined) {
args[effective_param_count]
} else {
match param.default_val {
Some(default_expr) => self.eval_expr(default_expr, func_env)
None =>
if effective_param_count < args.length() {
args[effective_param_count]
} else {
Undefined
}
}
}
func_env.def(param.name, val, LetBinding)
// Destructure if pattern param
match param.pattern {
Some(pat) => self.bind_pattern(pat, val, func_env, LetBinding)
None => ()
}
effective_param_count = effective_param_count + 1
}
// Rest param
match data.rest_param {
Some(rest_name) => {
let rest_elements : Array[Value] = []
for i = effective_param_count; i < args.length(); i = i + 1 {
rest_elements.push(args[i])
}
let rest_val = Array({ elements: rest_elements })
func_env.def(rest_name, rest_val, LetBinding)
// Apply destructuring pattern if present
for p in data.params {
if p.name == rest_name {
match p.pattern {
Some(pat) =>
self.bind_pattern(pat, rest_val, func_env, LetBinding)
None => ()
}
break
}
}
}
None => ()
}
self.hoist_declarations(data.body, func_env)
match self.exec_stmts(data.body, func_env) {
Normal(v) => v
ReturnSignal(v) => v
BreakSignal(_) =>
raise @errors.SyntaxError(
message="break statement outside of loop",
)
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
}
Some(BoundFunc(target, bound_this, bound_args)) => {
let all_args : Array[Value] = []
for a in bound_args {
all_args.push(a)
}
for a in args {
all_args.push(a)
}
self.call_value(target, bound_this, all_args, loc)
}
Some(FuncCallMethod(target)) => {
let call_this = if args.length() > 0 { args[0] } else { Undefined }
let call_args : Array[Value] = []
for i = 1; i < args.length(); i = i + 1 {
call_args.push(args[i])
}
self.call_value(target, call_this, call_args, loc)
}
Some(FuncApplyMethod(target)) => {
let call_this = if args.length() > 0 { args[0] } else { Undefined }
let call_args : Array[Value] = if args.length() > 1 {
match args[1] {
Array(data) => {
let a : Array[Value] = []
for el in data.elements {
a.push(el)
}
a
}
_ => []
}
} else {
[]
}
self.call_value(target, call_this, call_args, loc)
}
Some(MethodCallable(_, func)) => func(this_val, args)
Some(NativeCallable(_, func)) => func(args)
Some(NonConstructableCallable(_, func)) => func(args)
Some(InterpreterCallable(_, func)) => func(self, this_val, args)
Some(ConstructorOnlyCallable(name, _)) =>
raise @errors.TypeError(
message=name + " constructor cannot be invoked without 'new'",
)
Some(ClassConstructor(name, _, _, _, _, _)) =>
raise @errors.TypeError(
message="Class constructor " +
name +
" cannot be invoked without 'new'",
)
None => raise @errors.TypeError(message="is not a function")
}
_ => raise @errors.TypeError(message="is not a function")
}
}
///|
fn Interpreter::eval_member(
self : Interpreter,
obj_expr : @ast.Expr,
prop : String,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
// Special case: console.log
match obj_expr {
Ident("console", _) =>
if prop == "log" {
let output = self.output
make_native_func("log", fn(args) {
let parts : Array[String] = args.map(fn(a) { a.to_string() })
output.push(parts.join(" "))
Undefined
})
} else {
raise @errors.TypeError(message="console.\{prop} is not supported")
}
_ => {
let obj = self.eval_expr(obj_expr, env)
self.get_property(obj, prop, loc)
}
}
}
///|
fn get_bound_func_name(target : Value) -> String {
match target {
Object(td) =>
match td.callable {
Some(UserFunc(fd)) => "bound " + fd.name.unwrap_or("")
Some(UserFuncExt(fd)) => "bound " + fd.name.unwrap_or("")
Some(NativeCallable(n, _)) => "bound " + n
Some(NonConstructableCallable(n, _)) => "bound " + n
Some(_) => "bound "
None => "bound "
}
_ => "bound "
}
}
///|
fn Interpreter::get_property(
self : Interpreter,
obj : Value,
prop : String,
_loc : @token.Loc,
) -> Value raise Error {
match obj {
Object(data) =>
match data.properties.get(prop) {
Some(v) => v
None => {
// Walk prototype chain
let mut current = data.prototype
while true {
match current {
Object(proto_data) =>
match proto_data.properties.get(prop) {
Some(v) => return v
None => current = proto_data.prototype
}
_ => break
}
}
// Built-in Function methods (for callable objects)
match data.callable {
Some(callable) =>
if prop == "call" {
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(FuncCallMethod(obj)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
} else if prop == "apply" {
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(FuncApplyMethod(obj)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
} else if prop == "bind" {
let target = obj
return make_native_func("bind", fn(args) {
let bind_this = if args.length() > 0 {
args[0]
} else {
Undefined
}
let bound_args : Array[Value] = []
for i = 1; i < args.length(); i = i + 1 {
bound_args.push(args[i])
}
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(BoundFunc(target, bind_this, bound_args)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
})
} else if prop == "length" {
// Return the number of formal parameters
let param_count = match callable {
UserFunc(func_data) => func_data.params.length()
UserFuncExt(func_data) => func_data.params.length()
ArrowFunc(func_data) => func_data.params.length()
ArrowFuncExt(func_data) => func_data.params.length()
BoundFunc(target, _, bound_args) =>
match target {
Object(td) =>
match td.callable {
Some(UserFunc(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
Some(UserFuncExt(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
Some(ArrowFunc(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
Some(ArrowFuncExt(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
_ => 0
}
_ => 0
}
NativeCallable(_, _) => 0
NonConstructableCallable(_, _) => 0
FuncCallMethod(_) => 1
FuncApplyMethod(_) => 2
MethodCallable(_, _) => 0
InterpreterCallable(_, _) => 0
ConstructorOnlyCallable(_, _) => 0
ClassConstructor(_, _, _, ctor, _, _) =>
match ctor {
Some((params, _)) => params.length()
None => 0
}
}
return Number(param_count.to_double())
} else if prop == "name" {
// Return the function name
let func_name : String = match callable {
UserFunc(func_data) => func_data.name.unwrap_or("")
UserFuncExt(func_data) => func_data.name.unwrap_or("")
ArrowFunc(_) => ""
ArrowFuncExt(_) => ""
BoundFunc(target, _, _) => get_bound_func_name(target)
NativeCallable(n, _) => n
NonConstructableCallable(n, _) => n
FuncCallMethod(_) => "call"
FuncApplyMethod(_) => "apply"
MethodCallable(n, _) => n
InterpreterCallable(n, _) => n
ConstructorOnlyCallable(n, _) => n
ClassConstructor(n, _, _, _, _, _) => n
}
return String_(func_name)
} else if prop == "toString" {
// Return function string representation
let func_str : String = match callable {
UserFunc(func_data) => {
let name = func_data.name.unwrap_or("")
"function " + name + "() { [native code] }"
}
UserFuncExt(func_data) => {
let name = func_data.name.unwrap_or("")
"function " + name + "() { [native code] }"
}
ArrowFunc(_) => "() => { [native code] }"
ArrowFuncExt(_) => "() => { [native code] }"
BoundFunc(_, _, _) => "function () { [native code] }"
NativeCallable(n, _) =>
"function " + n + "() { [native code] }"
NonConstructableCallable(n, _) =>
"function " + n + "() { [native code] }"
FuncCallMethod(_) => "function call() { [native code] }"
FuncApplyMethod(_) => "function apply() { [native code] }"
MethodCallable(n, _) =>
"function " + n + "() { [native code] }"
InterpreterCallable(n, _) =>
"function " + n + "() { [native code] }"
ConstructorOnlyCallable(n, _) =>
"function " + n + "() { [native code] }"
ClassConstructor(n, _, _, _, _, _) =>
"class " + n + " { [native code] }"
}
return make_native_func("toString", fn(_args) {
String_(func_str)
})
} else if prop == "prototype" {
// Only constructable functions have a prototype property
// Non-constructable (arrow functions, bound functions, etc.) return undefined
let is_constructable = match callable {
UserFunc(_)
| UserFuncExt(_)
| NativeCallable(_, _)
| ConstructorOnlyCallable(_, _)
| ClassConstructor(_, _, _, _, _, _) => true
ArrowFunc(_)
| ArrowFuncExt(_)
| BoundFunc(_, _, _)
| NonConstructableCallable(_, _)
| FuncCallMethod(_)
| FuncApplyMethod(_)
| MethodCallable(_, _)
| InterpreterCallable(_, _) => false
}
if not(is_constructable) {
return Undefined
}
// For constructable functions, check if prototype exists
match data.properties.get("prototype") {
Some(p) => return p
None => {
// Create and memoize default prototype
let proto : Value = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
data.properties["prototype"] = proto
return proto
}
}
}
None => ()
}
// Built-in Object methods
if prop == "hasOwnProperty" {
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
MethodCallable("hasOwnProperty", fn(this_val, args) {
let key_val = if args.length() > 0 {
args[0]
} else {
Undefined
}
match this_val {
Object(d) =>
// Handle Symbol keys
match key_val {
Symbol(sym) =>
Bool(d.symbol_properties.contains(sym.id))
_ => Bool(d.properties.contains(key_val.to_string()))
}
_ => Bool(false)
}
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}
if prop == "toString" {
return make_native_func("toString", fn(_args) {
String_("[object Object]")
})
}
Undefined
}
}
Array(data) =>
if prop == "length" {
Number(data.elements.length().to_double())
} else {
let result = get_array_method_with_interp(data, prop)
match result {
Undefined => lookup_builtin_proto(self.global, "Array", prop)
_ => result
}
}
String_(s) =>
if prop == "length" {
Number(s.length().to_double())
} else {
let result = get_string_method(s, prop)
match result {
Undefined => lookup_builtin_proto(self.global, "String", prop)
_ => result
}
}
Number(_) => {
let result = get_number_method(obj, prop)
match result {
Undefined => lookup_builtin_proto(self.global, "Number", prop)
_ => result
}
}
Bool(_) => lookup_builtin_proto(self.global, "Boolean", prop)
Symbol(sym) =>
// Symbol properties: description, toString
if prop == "description" {
match sym.description {
Some(desc) => String_(desc)
None => Undefined
}
} else if prop == "toString" {
make_native_func("toString", fn(_args) {
match sym.description {
Some(desc) => String_("Symbol(\{desc})")
None => String_("Symbol()")
}
})
} else {
lookup_builtin_proto(self.global, "Symbol", prop)
}
Map(data) =>
if prop == "size" {
Number(data.entries.length().to_double())
} else {
get_map_method(data, prop)
}
Set(data) =>
if prop == "size" {
Number(data.values.length().to_double())
} else {
get_set_method(data, prop)
}
Promise(data) =>
// Check user-defined properties first, then prototype methods
match data.properties.get(prop) {
Some(v) => v
None => get_promise_method(data, prop)
}
Null | Undefined =>
raise @errors.TypeError(
message="Cannot read property '\{prop}' of \{type_of(obj)}",
)
_ => Undefined
}
}
///|
/// Look up a property on a built-in constructor's prototype (e.g. Array.prototype.constructor)
fn lookup_builtin_proto(env : Environment, ctor_name : String, prop : String) -> Value {
let ctor = try { env.get(ctor_name) } catch { _ => return Undefined }
match ctor {
Object(ctor_data) =>
match ctor_data.properties.get("prototype") {
Some(Object(proto_data)) =>
match proto_data.properties.get(prop) {
Some(v) => v
None => {
// Walk up prototype chain from the prototype
let mut current = proto_data.prototype
while true {
match current {
Object(pdata) =>
match pdata.properties.get(prop) {
Some(v) => return v
None => current = pdata.prototype
}
_ => break
}
}
Undefined
}
}
_ => Undefined
}
_ => Undefined
}
}
///|
fn Interpreter::get_computed_property(
_self : Interpreter,
obj : Value,
key : Value,
_loc : @token.Loc,
) -> Value raise Error {
// Handle Symbol keys specially
match key {
Symbol(sym) =>
match obj {
Object(data) =>
// Look up symbol property
match data.symbol_properties.get(sym.id) {
Some(v) => return v
None => {
// Walk prototype chain for symbol properties
let mut current = data.prototype
while true {
match current {
Object(proto_data) =>
match proto_data.symbol_properties.get(sym.id) {
Some(v) => return v
None => current = proto_data.prototype
}
_ => break
}
}
return Undefined
}
}
String_(s) => {
// Handle Symbol.iterator for strings
let iterator_sym = get_iterator_symbol()
if sym.id == iterator_sym.id {
// Return a function that creates a string iterator
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
MethodCallable("[Symbol.iterator]", fn(_this_val, _args) {
make_string_iterator_value(s)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}
return Undefined
}
Array(arr) => {
// Handle Symbol.iterator for arrays
let iterator_sym = get_iterator_symbol()
if sym.id == iterator_sym.id {
// Return a function that creates an array iterator
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
MethodCallable("[Symbol.iterator]", fn(_this_val, _args) {
make_array_iterator_value(arr)
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}
return Undefined
}
Promise(data) =>
match data.symbol_properties.get(sym.id) {
Some(v) => return v
None => return Undefined
}
_ => return Undefined
}
_ => ()
}
match obj {
Object(data) => {
let prop = key.to_string()
match data.properties.get(prop) {
Some(v) => v
None => {
// Walk prototype chain
let mut current = data.prototype
while true {
match current {
Object(proto_data) =>
match proto_data.properties.get(prop) {
Some(v) => return v
None => current = proto_data.prototype
}
_ => break
}
}
// Built-in Function methods (for callable objects)
match data.callable {
Some(callable) =>
if prop == "call" {
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(FuncCallMethod(obj)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
} else if prop == "apply" {
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(FuncApplyMethod(obj)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
} else if prop == "bind" {
let target = obj
return make_native_func("bind", fn(args) {
let bind_this = if args.length() > 0 {
args[0]
} else {
Undefined
}
let bound_args : Array[Value] = []
for i = 1; i < args.length(); i = i + 1 {
bound_args.push(args[i])
}
Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(BoundFunc(target, bind_this, bound_args)),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
})
} else if prop == "length" {
// Return the number of formal parameters
let param_count = match callable {
UserFunc(func_data) => func_data.params.length()
UserFuncExt(func_data) => func_data.params.length()
ArrowFunc(func_data) => func_data.params.length()
ArrowFuncExt(func_data) => func_data.params.length()
BoundFunc(target, _, bound_args) =>
match target {
Object(td) =>
match td.callable {
Some(UserFunc(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
Some(UserFuncExt(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
Some(ArrowFunc(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
Some(ArrowFuncExt(fd)) => {
let diff = fd.params.length() - bound_args.length()
if diff < 0 {
0
} else {
diff
}
}
_ => 0
}
_ => 0
}
NativeCallable(_, _) => 0
NonConstructableCallable(_, _) => 0
FuncCallMethod(_) => 1
FuncApplyMethod(_) => 2
MethodCallable(_, _) => 0
InterpreterCallable(_, _) => 0
ConstructorOnlyCallable(_, _) => 0
ClassConstructor(_, _, _, ctor, _, _) =>
match ctor {
Some((params, _)) => params.length()
None => 0
}
}
return Number(param_count.to_double())
} else if prop == "name" {
// Return the function name
let func_name : String = match callable {
UserFunc(func_data) => func_data.name.unwrap_or("")
UserFuncExt(func_data) => func_data.name.unwrap_or("")
ArrowFunc(_) => ""
ArrowFuncExt(_) => ""
BoundFunc(target, _, _) => get_bound_func_name(target)
NativeCallable(n, _) => n
NonConstructableCallable(n, _) => n
FuncCallMethod(_) => "call"
FuncApplyMethod(_) => "apply"
MethodCallable(n, _) => n
InterpreterCallable(n, _) => n
ConstructorOnlyCallable(n, _) => n
ClassConstructor(n, _, _, _, _, _) => n
}
return String_(func_name)
} else if prop == "toString" {
// Return function string representation
let func_str : String = match callable {
UserFunc(func_data) => {
let name = func_data.name.unwrap_or("")
"function " + name + "() { [native code] }"
}
UserFuncExt(func_data) => {
let name = func_data.name.unwrap_or("")
"function " + name + "() { [native code] }"
}
ArrowFunc(_) => "() => { [native code] }"
ArrowFuncExt(_) => "() => { [native code] }"
BoundFunc(_, _, _) => "function () { [native code] }"
NativeCallable(n, _) =>
"function " + n + "() { [native code] }"
NonConstructableCallable(n, _) =>
"function " + n + "() { [native code] }"
FuncCallMethod(_) => "function call() { [native code] }"
FuncApplyMethod(_) => "function apply() { [native code] }"
MethodCallable(n, _) =>
"function " + n + "() { [native code] }"
InterpreterCallable(n, _) =>
"function " + n + "() { [native code] }"
ConstructorOnlyCallable(n, _) =>
"function " + n + "() { [native code] }"
ClassConstructor(n, _, _, _, _, _) =>
"class " + n + " { [native code] }"
}
return make_native_func("toString", fn(_args) {
String_(func_str)
})
} else if prop == "prototype" {
// Only constructable functions have a prototype property
// Non-constructable (arrow functions, bound functions, etc.) return undefined
let is_constructable = match callable {
UserFunc(_)
| UserFuncExt(_)
| NativeCallable(_, _)
| ConstructorOnlyCallable(_, _)
| ClassConstructor(_, _, _, _, _, _) => true
ArrowFunc(_)
| ArrowFuncExt(_)
| BoundFunc(_, _, _)
| NonConstructableCallable(_, _)
| FuncCallMethod(_)
| FuncApplyMethod(_)
| MethodCallable(_, _)
| InterpreterCallable(_, _) => false
}
if not(is_constructable) {
return Undefined
}
// For constructable functions, check if prototype exists
match data.properties.get("prototype") {
Some(p) => return p
None => {
// Create and memoize default prototype
let proto : Value = Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
data.properties["prototype"] = proto
return proto
}
}
}
None => ()
}
// Built-in Object methods
if prop == "hasOwnProperty" {
return Object({
properties: {},
symbol_properties: {},
prototype: Null,
callable: Some(
MethodCallable("hasOwnProperty", fn(this_val, args) {
let key_val = if args.length() > 0 {
args[0]
} else {
Undefined
}
match this_val {
Object(d) =>
// Handle Symbol keys
match key_val {
Symbol(sym) =>
Bool(d.symbol_properties.contains(sym.id))
_ => Bool(d.properties.contains(key_val.to_string()))
}
_ => Bool(false)
}
}),
),
class_name: "Function",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
}
if prop == "toString" {
return make_native_func("toString", fn(_args) {
String_("[object Object]")
})
}
Undefined
}
}
}
Array(data) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 && i < data.elements.length() {
data.elements[i]
} else {
Undefined
}
}
String_("length") => Number(data.elements.length().to_double())
String_(s) => get_array_method_with_interp(data, s)
_ => Undefined
}
String_(s) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 && i < s.length() {
let buf = StringBuilder::new()
buf.write_char(s.to_array()[i])
String_(buf.to_string())
} else {
Undefined
}
}
String_("length") => Number(s.length().to_double())
String_(method_name) => get_string_method(s, method_name)
_ => Undefined
}
Number(_) =>
match key {
String_(method_name) => get_number_method(obj, method_name)
_ => Undefined
}
Promise(data) => {
// Coerce key to string via ToPropertyKey (Symbol handled above)
let prop = key.to_string()
match data.properties.get(prop) {
Some(v) => v
None => get_promise_method(data, prop)
}
}
Null | Undefined =>
raise @errors.TypeError(message="Cannot read property of \{type_of(obj)}")
_ => Undefined
}
}
///|
fn Interpreter::eval_new(
self : Interpreter,
callee_expr : @ast.Expr,
arg_exprs : Array[@ast.Expr],
env : Environment,
_loc : @token.Loc,
) -> Value raise Error {
let ctor = self.eval_expr(callee_expr, env)
let args : Array[Value] = self.eval_args_with_spread(arg_exprs, env)
match ctor {
Object(obj_data) =>
match obj_data.callable {
Some(UserFunc(data)) => {
let saved_strict = self.strict
let result = try {
let func_strict = self.strict || has_use_strict(data.body)
if func_strict {
self.strict = true
}
let proto = match obj_data.properties.get("prototype") {
Some(v) => v
None => Null
}
let new_obj = Object({
properties: {},
symbol_properties: {},
prototype: proto,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let func_env = Environment::new(parent=Some(data.closure))
func_env.def("this", new_obj, LetBinding)
func_env.def("", ctor, LetBinding)
for i = 0; i < data.params.length(); i = i + 1 {
let val : Value = if i < args.length() {
args[i]
} else {
Undefined
}
func_env.def(data.params[i], val, LetBinding)
}
// Create arguments object
let new_args_props : Map[String, Value] = {}
for i = 0; i < args.length(); i = i + 1 {
new_args_props[i.to_string()] = args[i]
}
new_args_props["length"] = Number(args.length().to_double())
if not(func_strict) {
new_args_props["callee"] = ctor
}
func_env.def(
"arguments",
Object({
properties: new_args_props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Arguments",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
VarBinding,
)
// Hoist var declarations and function declarations
self.hoist_declarations(data.body, func_env)
match data.name {
Some(name) =>
if not(func_env.bindings.contains(name)) {
func_env.def(name, ctor, LetBinding)
}
None => ()
}
let exec_result = self.exec_stmts(data.body, func_env)
match exec_result {
Normal(_) => new_obj
ReturnSignal(v) =>
match v {
Object(_) => v
_ => new_obj
}
BreakSignal(_) =>
raise @errors.SyntaxError(
message="break statement outside of loop",
)
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
} catch {
e => {
self.strict = saved_strict
raise e
}
}
self.strict = saved_strict
result
}
Some(UserFuncExt(data)) => {
let saved_strict = self.strict
let result = try {
let func_strict = self.strict || has_use_strict(data.body)
if func_strict {
self.strict = true
}
let proto = match obj_data.properties.get("prototype") {
Some(v) => v
None => Null
}
let new_obj = Object({
properties: {},
symbol_properties: {},
prototype: proto,
callable: None,
class_name: "Object",
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
let func_env = Environment::new(parent=Some(data.closure))
func_env.def("this", new_obj, LetBinding)
func_env.def("", ctor, LetBinding)
let mut effective_param_count = 0
for i = 0; i < data.params.length(); i = i + 1 {
let param = data.params[i]
// Skip rest destructuring params
if data.rest_param is Some(rn) && param.name == rn {
continue
}
let val : Value = if effective_param_count < args.length() &&
not(args[effective_param_count] is Undefined) {
args[effective_param_count]
} else {
match param.default_val {
Some(default_expr) => self.eval_expr(default_expr, func_env)
None =>
if effective_param_count < args.length() {
args[effective_param_count]
} else {
Undefined
}
}
}
func_env.def(param.name, val, LetBinding)
match param.pattern {
Some(pat) => self.bind_pattern(pat, val, func_env, LetBinding)
None => ()
}
effective_param_count = effective_param_count + 1
}
match data.rest_param {
Some(rest_name) => {
let rest_elements : Array[Value] = []
for i = effective_param_count; i < args.length(); i = i + 1 {
rest_elements.push(args[i])
}
let rest_val = Array({ elements: rest_elements })
func_env.def(rest_name, rest_val, LetBinding)
// Apply destructuring pattern if present
for p in data.params {
if p.name == rest_name {
match p.pattern {
Some(pat) =>
self.bind_pattern(pat, rest_val, func_env, LetBinding)
None => ()
}
break
}
}
}
None => ()
}
let new_args_props : Map[String, Value] = {}
for i = 0; i < args.length(); i = i + 1 {
new_args_props[i.to_string()] = args[i]
}
new_args_props["length"] = Number(args.length().to_double())
if not(func_strict) {
new_args_props["callee"] = ctor
}
func_env.def(
"arguments",
Object({
properties: new_args_props,
symbol_properties: {},
prototype: Null,
callable: None,
class_name: "Arguments",
descriptors: {},
symbol_descriptors: {},
extensible: true,
}),
VarBinding,
)
self.hoist_declarations(data.body, func_env)
match data.name {
Some(name) =>
if not(func_env.bindings.contains(name)) {
func_env.def(name, ctor, LetBinding)
}
None => ()
}
let exec_result = self.exec_stmts(data.body, func_env)
match exec_result {
Normal(_) => new_obj
ReturnSignal(v) =>
match v {
Object(_) => v
_ => new_obj
}
BreakSignal(_) =>
raise @errors.SyntaxError(
message="break statement outside of loop",
)
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
} catch {
e => {
self.strict = saved_strict
raise e
}
}
self.strict = saved_strict
result
}
Some(NativeCallable(_, func)) => func(args)
Some(NonConstructableCallable(name, _)) =>
raise @errors.TypeError(message=name + " is not a constructor")
Some(InterpreterCallable(_, func)) => func(self, Undefined, args)
Some(ConstructorOnlyCallable(_, func)) => func(self, args)
Some(ArrowFunc(_)) | Some(ArrowFuncExt(_)) =>
raise @errors.TypeError(
message="arrow functions cannot be used as constructors",
)
Some(
ClassConstructor(
class_name,
proto,
super_ctor,
ctor_fn,
closure,
super_proto
)
) => {
// Create the new instance with the class prototype
let new_obj = Object({
properties: {},
symbol_properties: {},
prototype: proto,
callable: None,
class_name,
descriptors: {},
symbol_descriptors: {},
extensible: true,
})
// Set up the constructor environment
let ctor_env = Environment::new(parent=Some(closure))
ctor_env.def_builtin("this", new_obj)
ctor_env.def("", ctor, LetBinding)
// Set up [[SuperConstructor]] and [[SuperPrototype]] for super() calls
match super_ctor {
Some(sc) => {
ctor_env.def_builtin("[[SuperConstructor]]", sc)
ctor_env.def_builtin("[[SuperPrototype]]", super_proto)
}
None => ()
}
// Execute the constructor if one was defined
match ctor_fn {
Some((params, body)) => {
// Bind parameters
for i = 0; i < params.length(); i = i + 1 {
let val : Value = if i < args.length() {
args[i]
} else {
Undefined
}
ctor_env.def(params[i], val, LetBinding)
}
// Hoist declarations and execute body
self.hoist_declarations(body, ctor_env)
let exec_result = self.exec_stmts(body, ctor_env)
match exec_result {
Normal(_) => new_obj
ReturnSignal(v) =>
match v {
Object(_) => v
_ => new_obj
}
BreakSignal(_) =>
raise @errors.SyntaxError(
message="break statement outside of loop",
)
ContinueSignal(_) =>
raise @errors.SyntaxError(
message="continue statement outside of loop",
)
}
}
None => {
// No explicit constructor - if there's a super class, call super()
// Save the derived class constructor for new.target propagation
let derived_ctor = ctor
match super_ctor {
Some(sc) =>
match sc {
Object(super_data) =>
match super_data.callable {
Some(
ClassConstructor(
_,
_,
grand_super,
ctor,
senv,
sproto
)
) => {
let super_env = Environment::new(parent=Some(senv))
super_env.def_builtin("this", new_obj)
super_env.def(
"", derived_ctor, LetBinding,
)
match grand_super {
Some(gs) => {
super_env.def_builtin("[[SuperConstructor]]", gs)
super_env.def_builtin(
"[[SuperPrototype]]", sproto,
)
}
None => ()
}
match ctor {
Some((params, body)) => {
for i = 0; i < params.length(); i = i + 1 {
let val : Value = if i < args.length() {
args[i]
} else {
Undefined
}
super_env.def(params[i], val, LetBinding)
}
self.hoist_declarations(body, super_env)
let exec_result = self.exec_stmts(body, super_env)
// If super constructor returns an object, use it
match exec_result {
ReturnSignal(v) =>
match v {
Object(_) => return v
_ => ()
}
_ => ()
}
}
None => ()
}
}
_ => ()
}
_ => ()
}
None => ()
}
new_obj
}
}
}
_ => raise @errors.TypeError(message="is not a constructor")
}
_ => raise @errors.TypeError(message="is not a constructor")
}
}
///|
fn Interpreter::create_class(
self : Interpreter,
name : String,
superclass_expr : @ast.Expr?,
methods : Array[@ast.ClassMethod],
env : Environment,
) -> Value raise Error {
// Evaluate superclass if present
let (super_ctor, super_proto) : (Value?, Value) = match superclass_expr {
Some(expr) => {
let super_val = self.eval_expr(expr, env)
match super_val {
Object(super_data) =>
match super_data.properties.get("prototype") {
Some(sp) => (Some(super_val), sp)
None => (Some(super_val), Null)
}
_ =>
raise @errors.TypeError(
message="Class extends value is not a constructor",
)
}
}
None => (None, Null)
}
// Create the prototype object
let proto_props : Map[String, Value] = {}
let proto_descriptors : Map[String, PropDescriptor] = {}
let proto_symbol_props : Map[Int, Value] = {}
let proto_symbol_descriptors : Map[Int, PropDescriptor] = {}
// Create static properties map
let static_props : Map[String, Value] = {}
let static_descriptors : Map[String, PropDescriptor] = {}
let static_symbol_props : Map[Int, Value] = {}
let static_symbol_descriptors : Map[Int, PropDescriptor] = {}
// Track constructor method
let mut ctor_fn : (Array[String], Array[@ast.Stmt])? = None
// Process all methods
for m in methods {
// Get the method key - could be string or symbol
let key_val : Value = match m.key {
StringLit(s, _) => String_(s)
_ => self.eval_expr(m.key, env)
}
let method_key_str : String = key_val.to_string()
// Check if this is the constructor
if method_key_str == "constructor" && not(m.is_static) {
// Extract constructor params and body
match m.value {
FuncExpr(_, params, body, _) => ctor_fn = Some((params, body))
FuncExprExt(_, params, _, body, _) => {
// Convert Param array to String array (ignoring defaults for now)
let param_names = params.map(fn(p) { p.name })
ctor_fn = Some((param_names, body))
}
_ => ()
}
continue
}
// Create the method function value
let method_val : Value = match m.value {
FuncExpr(fn_name, params, body, _) => {
let func_data : FuncData = { name: fn_name, params, body, closure: env }
make_func(func_data)
}
FuncExprExt(fn_name, params, rest, body, _) => {
let func_data : FuncDataExt = {
name: fn_name,
params,
rest_param: rest,
body,
closure: env,
}
make_func_ext(func_data)
}
_ => self.eval_expr(m.value, env)
}
// Handle getter/setter vs regular method
// Class methods are non-enumerable per spec
let method_descriptor : PropDescriptor = {
writable: true,
enumerable: false,
configurable: true,
}
// Store method in appropriate map based on key type (string vs symbol)
match key_val {
Symbol(sym) =>
// Symbol key - store in symbol_properties
if m.is_static {
static_symbol_props[sym.id] = method_val
static_symbol_descriptors[sym.id] = method_descriptor
} else {
proto_symbol_props[sym.id] = method_val
proto_symbol_descriptors[sym.id] = method_descriptor
}
_ => {
// String key - store in regular properties
let method_key = key_val.to_string()
if m.is_static {
static_props[method_key] = method_val
static_descriptors[method_key] = method_descriptor
} else {
proto_props[method_key] = method_val
proto_descriptors[method_key] = method_descriptor
}
}
}
}
// Create prototype object
let proto = Object({
properties: proto_props,
symbol_properties: proto_symbol_props,
prototype: super_proto,
callable: None,
class_name: name,
descriptors: proto_descriptors,
symbol_descriptors: proto_symbol_descriptors,
extensible: true,
})
// Add constructor back-reference to prototype
// (will be set after class object is created)
// Create the class constructor function
let class_obj = Object({
properties: static_props,
symbol_properties: static_symbol_props,
prototype: Null,
callable: Some(
ClassConstructor(name, proto, super_ctor, ctor_fn, env, super_proto),
),
class_name: "Function",
descriptors: static_descriptors,
symbol_descriptors: static_symbol_descriptors,
extensible: true,
})
// Set prototype.constructor to point to the class (non-enumerable per spec)
match proto {
Object(proto_data) => {
proto_data.properties["constructor"] = class_obj
proto_data.descriptors["constructor"] = {
writable: true,
enumerable: false,
configurable: true,
}
}
_ => ()
}
// Add prototype property to the class
match class_obj {
Object(class_data) => class_data.properties["prototype"] = proto
_ => ()
}
class_obj
}
///|
fn Interpreter::eval_update(
self : Interpreter,
op : @ast.UpdateOp,
operand : @ast.Expr,
prefix : Bool,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
match operand {
Ident(name, _) => {
let current = env.get(name)
let num = to_number(current)
let new_val : Value = match op {
Increment => Number(num + 1.0)
Decrement => Number(num - 1.0)
}
env.assign(name, new_val)
if prefix {
new_val
} else {
Number(num)
}
}
Member(obj_expr, prop, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let current = self.get_property(obj, prop, mloc)
let num = to_number(current)
let new_val : Value = match op {
Increment => Number(num + 1.0)
Decrement => Number(num - 1.0)
}
match obj {
Object(data) => data.properties[prop] = new_val
_ =>
raise @errors.SyntaxError(
message="Invalid update target at line \{loc.line}, col \{loc.col}",
)
}
if prefix {
new_val
} else {
Number(num)
}
}
ComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let current = self.get_computed_property(obj, key, mloc)
let num = to_number(current)
let new_val : Value = match op {
Increment => Number(num + 1.0)
Decrement => Number(num - 1.0)
}
match obj {
Object(data) => data.properties[key.to_string()] = new_val
Array(data) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 && i < data.elements.length() {
data.elements[i] = new_val
}
}
_ => ()
}
_ =>
raise @errors.SyntaxError(
message="Invalid update target at line \{loc.line}, col \{loc.col}",
)
}
if prefix {
new_val
} else {
Number(num)
}
}
_ =>
raise @errors.SyntaxError(
message="Invalid update target at line \{loc.line}, col \{loc.col}",
)
}
}
///|
fn Interpreter::eval_compound_assign(
self : Interpreter,
op : @ast.CompoundOp,
target : @ast.Expr,
value_expr : @ast.Expr,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
// Handle logical assignment operators with short-circuit semantics
match op {
LogicalAndAssign =>
return self.eval_logical_assign_and(target, value_expr, env, loc)
LogicalOrAssign =>
return self.eval_logical_assign_or(target, value_expr, env, loc)
NullishAssign =>
return self.eval_nullish_assign(target, value_expr, env, loc)
_ => ()
}
let bin_op : @ast.BinOp = match op {
AddAssign => Add
SubAssign => Sub
MulAssign => Mul
ExpAssign => Exp
DivAssign => Div
ModAssign => Mod
BitAndAssign => BitAnd
BitOrAssign => BitOr
BitXorAssign => BitXor
LShiftAssign => LShift
RShiftAssign => RShift
URShiftAssign => URShift
LogicalAndAssign | LogicalOrAssign | NullishAssign => Add // unreachable
}
match target {
Ident(name, _) => {
let current = env.get(name)
let rhs = self.eval_expr(value_expr, env)
let result = eval_binary_op(bin_op, current, rhs, loc)
env.assign(name, result)
result
}
Member(obj_expr, prop, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let current = self.get_property(obj, prop, mloc)
let rhs = self.eval_expr(value_expr, env)
let result = eval_binary_op(bin_op, current, rhs, loc)
match obj {
Object(data) => {
data.properties[prop] = result
result
}
_ =>
raise @errors.TypeError(
message="Cannot set property at line \{loc.line}, col \{loc.col}",
)
}
}
ComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let current = self.get_computed_property(obj, key, mloc)
let rhs = self.eval_expr(value_expr, env)
let result = eval_binary_op(bin_op, current, rhs, loc)
match obj {
Object(data) =>
// Handle Symbol keys specially
match key {
Symbol(sym) => {
data.symbol_properties[sym.id] = result
result
}
_ => {
data.properties[key.to_string()] = result
result
}
}
Array(data) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 && i < data.elements.length() {
data.elements[i] = result
}
result
}
_ => result
}
_ =>
raise @errors.TypeError(
message="Cannot set property at line \{loc.line}, col \{loc.col}",
)
}
}
_ =>
raise @errors.SyntaxError(
message="Invalid compound assignment target at line \{loc.line}, col \{loc.col}",
)
}
}
///|
fn Interpreter::eval_logical_assign_and(
self : Interpreter,
target : @ast.Expr,
value_expr : @ast.Expr,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
// x &&= y: if x is truthy, assign y to x; return final value
match target {
Ident(name, _) => {
let current = env.get(name)
if is_truthy(current) {
let rhs = self.eval_expr(value_expr, env)
env.assign(name, rhs)
rhs
} else {
current
}
}
Member(obj_expr, prop, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let current = self.get_property(obj, prop, mloc)
if is_truthy(current) {
let rhs = self.eval_expr(value_expr, env)
match obj {
Object(data) => {
// Check writable descriptor
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return rhs
}
None => ()
}
// Check extensible for new properties
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return rhs
}
data.properties[prop] = rhs
rhs
}
_ =>
raise @errors.TypeError(
message="Cannot set property '\{prop}' of \{type_of(obj)}",
)
}
} else {
current
}
}
ComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let current = self.get_computed_property(obj, key, mloc)
if is_truthy(current) {
let rhs = self.eval_expr(value_expr, env)
match obj {
Object(data) =>
// Handle Symbol keys specially
match key {
Symbol(sym) => {
// Check symbol descriptor
match data.symbol_descriptors.get(sym.id) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property",
)
}
return rhs
}
None => ()
}
if not(data.symbol_properties.contains(sym.id)) &&
not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property, object is not extensible",
)
}
return rhs
}
data.symbol_properties[sym.id] = rhs
rhs
}
_ => {
let prop = key.to_string()
// Check writable descriptor
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return rhs
}
None => ()
}
// Check extensible for new properties
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return rhs
}
data.properties[prop] = rhs
rhs
}
}
Array(data) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 && i < data.elements.length() {
data.elements[i] = rhs
}
rhs
}
_ => rhs
}
_ =>
raise @errors.TypeError(
message="Cannot set property of \{type_of(obj)}",
)
}
} else {
current
}
}
_ =>
raise @errors.SyntaxError(
message="Invalid assignment target at line \{loc.line}, col \{loc.col}",
)
}
}
///|
fn Interpreter::eval_logical_assign_or(
self : Interpreter,
target : @ast.Expr,
value_expr : @ast.Expr,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
// x ||= y: if x is falsy, assign y to x; return final value
match target {
Ident(name, _) => {
let current = env.get(name)
if not(is_truthy(current)) {
let rhs = self.eval_expr(value_expr, env)
env.assign(name, rhs)
rhs
} else {
current
}
}
Member(obj_expr, prop, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let current = self.get_property(obj, prop, mloc)
if not(is_truthy(current)) {
let rhs = self.eval_expr(value_expr, env)
match obj {
Object(data) => {
// Check writable descriptor
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return rhs
}
None => ()
}
// Check extensible for new properties
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return rhs
}
data.properties[prop] = rhs
rhs
}
_ =>
raise @errors.TypeError(
message="Cannot set property '\{prop}' of \{type_of(obj)}",
)
}
} else {
current
}
}
ComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let current = self.get_computed_property(obj, key, mloc)
if not(is_truthy(current)) {
let rhs = self.eval_expr(value_expr, env)
match obj {
Object(data) =>
// Handle Symbol keys specially
match key {
Symbol(sym) => {
match data.symbol_descriptors.get(sym.id) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property",
)
}
return rhs
}
None => ()
}
if not(data.symbol_properties.contains(sym.id)) &&
not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property, object is not extensible",
)
}
return rhs
}
data.symbol_properties[sym.id] = rhs
rhs
}
_ => {
let prop = key.to_string()
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return rhs
}
None => ()
}
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return rhs
}
data.properties[prop] = rhs
rhs
}
}
Array(data) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 && i < data.elements.length() {
data.elements[i] = rhs
}
rhs
}
_ => rhs
}
_ =>
raise @errors.TypeError(
message="Cannot set property of \{type_of(obj)}",
)
}
} else {
current
}
}
_ =>
raise @errors.SyntaxError(
message="Invalid assignment target at line \{loc.line}, col \{loc.col}",
)
}
}
///|
fn Interpreter::eval_nullish_assign(
self : Interpreter,
target : @ast.Expr,
value_expr : @ast.Expr,
env : Environment,
loc : @token.Loc,
) -> Value raise Error {
// x ??= y: if x is null or undefined, assign y to x; return final value
match target {
Ident(name, _) => {
let current = env.get(name)
match current {
Null | Undefined => {
let rhs = self.eval_expr(value_expr, env)
env.assign(name, rhs)
rhs
}
_ => current
}
}
Member(obj_expr, prop, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let current = self.get_property(obj, prop, mloc)
match current {
Null | Undefined => {
let rhs = self.eval_expr(value_expr, env)
match obj {
Object(data) => {
// Check writable descriptor
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return rhs
}
None => ()
}
// Check extensible for new properties
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return rhs
}
data.properties[prop] = rhs
rhs
}
_ =>
raise @errors.TypeError(
message="Cannot set property '\{prop}' of \{type_of(obj)}",
)
}
}
_ => current
}
}
ComputedMember(obj_expr, key_expr, mloc) => {
let obj = self.eval_expr(obj_expr, env)
let key = self.eval_expr(key_expr, env)
let current = self.get_computed_property(obj, key, mloc)
match current {
Null | Undefined => {
let rhs = self.eval_expr(value_expr, env)
match obj {
Object(data) =>
// Handle Symbol keys specially
match key {
Symbol(sym) => {
match data.symbol_descriptors.get(sym.id) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property",
)
}
return rhs
}
None => ()
}
if not(data.symbol_properties.contains(sym.id)) &&
not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property, object is not extensible",
)
}
return rhs
}
data.symbol_properties[sym.id] = rhs
rhs
}
_ => {
let prop = key.to_string()
match data.descriptors.get(prop) {
Some(desc) =>
if not(desc.writable) {
if self.strict {
raise @errors.TypeError(
message="Cannot assign to read only property '\{prop}'",
)
}
return rhs
}
None => ()
}
if not(data.properties.contains(prop)) && not(data.extensible) {
if self.strict {
raise @errors.TypeError(
message="Cannot add property \{prop}, object is not extensible",
)
}
return rhs
}
data.properties[prop] = rhs
rhs
}
}
Array(data) =>
match key {
Number(n) => {
let i = n.to_int()
if i >= 0 && i < data.elements.length() {
data.elements[i] = rhs
}
rhs
}
_ => rhs
}
_ =>
raise @errors.TypeError(
message="Cannot set property of \{type_of(obj)}",
)
}
}
_ => current
}
}
_ =>
raise @errors.SyntaxError(
message="Invalid assignment target at line \{loc.line}, col \{loc.col}",
)
}
}