///|
/// Ensure a function body starts with "use strict" directive.
/// Returns the body unchanged if it already has one, or prepends the directive.
fn ensure_strict_body(body : Array[@ast.Stmt]) -> Array[@ast.Stmt] {
if @static_semantics.has_use_strict(body) {
body
} else {
let strict_body : Array[@ast.Stmt] = [
ExprStmt(
StringLit(
"use strict",
false,
@token.LexForm::LexNormal,
@token.Loc::default(),
),
@token.Loc::default(),
),
]
for s in body {
strict_body.push(s)
}
strict_body
}
}
///|
fn Interpreter::create_class(
self : Interpreter,
ctx : ExecContext,
name : String,
superclass_expr : @ast.Expr?,
members : Array[@ast.ClassMember],
env : Environment,
source_text : String?,
class_binding? : String? = None,
) -> Value raise Error {
// Class bodies are always strict per spec
let class_ctx : ExecContext = {
strict: true,
current_generator: ctx.current_generator,
}
// ClassDefinitionEvaluation always creates a dedicated lexical environment.
// A named class gets an immutable, initially-uninitialized binding there so
// heritage/computed-name evaluation observes the TDZ and methods retain the
// inner binding independently of the outer declaration/assignment binding.
let class_env = Environment::new(parent=Some(env))
match class_binding {
Some(binding) => class_env.def_tdz(binding, ConstBinding)
None => ()
}
// Evaluate superclass if present
let (super_ctor, super_proto) : (Value?, Value) = match superclass_expr {
Some(expr) => {
let super_val = self.eval_expr(ctx, expr, class_env)
match super_val {
Null =>
// `class C extends null {}` — legal per spec; the class is derived
// (ConstructorKind "derived") so super() is required, but calling it
// tries Construct(null, ...) which throws TypeError. Represent this
// as Some(Null) so construct_value follows the derived-class path.
(Some(Null), Null)
_ => {
if !is_constructor_value(super_val) {
raise @errors.TypeError(
message="Class extends value is not a constructor",
)
}
// Class heritage requires prototype lookup from [[Get]] semantics:
// evaluate prototype via property access (including proxies/getters)
// and require it to be Object or Null.
let super_proto = self.get_computed_property(
super_val,
String_("prototype"),
@token.Loc::default(),
)
match super_proto {
Null => (Some(super_val), super_proto)
_ =>
if is_object_value(super_proto) {
(Some(super_val), super_proto)
} else {
raise @errors.TypeError(
message="Class extends value's prototype must be an object or null",
)
}
}
}
}
}
None => (None, self.global.get("[[ObjectPrototype]]") catch { _ => Null })
}
// Create the prototype object
// Pre-insert constructor so it appears first in property enumeration order
let proto_props : Map[String, Value] = { "constructor": Undefined }
let proto_descriptors : Map[String, PropDescriptor] = Map([])
let proto_symbol_props : Map[Int, Value] = Map([])
let proto_symbol_descriptors : Map[Int, PropDescriptor] = Map([])
// Create the constructor's intrinsic own properties before evaluating class
// elements. Static elements may then redefine the configurable `name`
// property while preserving the spec-mandated insertion order.
let non_enum_configurable : PropDescriptor = {
writable: false,
enumerable: false,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
let static_props : Map[String, Value] = Map([])
static_props["length"] = Undefined
static_props["name"] = String_(name)
static_props["prototype"] = Undefined
let static_descriptors : Map[String, PropDescriptor] = Map([])
static_descriptors["length"] = non_enum_configurable
static_descriptors["name"] = non_enum_configurable
static_descriptors["prototype"] = {
writable: false,
enumerable: false,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
}
let static_symbol_props : Map[Int, Value] = Map([])
let static_symbol_descriptors : Map[Int, PropDescriptor] = Map([])
// Track constructor method
let mut ctor_fn : (Array[@ast.Param], String?, Array[@ast.Stmt])? = None
let mut ctor_param_count : Int = 0
let mut constructor_overridden : Bool = false
// Collect instance field initializers (evaluated during construction)
let instance_fields : Array[ClassFieldInit] = []
// Collect static field initializers (evaluated after class object is created)
let static_fields : Array[ClassFieldInit] = []
// Collect private instance field initializers (evaluated during construction)
let private_instance_fields : Array[ClassFieldInit] = []
// Collect private static field initializers (evaluated after class object is created)
let private_static_fields : Array[ClassFieldInit] = []
// Collect private methods for this class (shared across all instances)
let private_methods : Map[String, Value] = Map([])
// Create shared method environments before their [[HomeObject]] values exist.
// The snapshot bindings remain as legacy/direct-eval markers and as fallbacks
// for constructor-call environments; normal class methods use [[HomeObject]].
let instance_method_env : Environment = {
let menv = Environment::new(parent=Some(class_env))
let instance_super : Value = match super_ctor {
Some(_) => super_proto
None => self.global.get("[[ObjectPrototype]]") catch { _ => Null }
}
menv.def_builtin("[[SuperPrototype]]", instance_super)
menv
}
let static_method_env : Environment = {
let menv = Environment::new(parent=Some(class_env))
let static_super : Value = match super_ctor {
Some(sc) => sc
None => self.global.get("[[FunctionPrototype]]") catch { _ => Null }
}
menv.def_builtin("[[SuperPrototype]]", static_super)
menv
}
// Generate private brand symbol for this class declaration
let private_brand : Value = if name != "" {
Value::Symbol(self.realm_state.symbols.new_symbol(Some("[\{name}]")))
} else {
Value::Symbol(self.realm_state.symbols.new_symbol(Some("[anonymous]")))
}
// Bind private brand in method environments so PrivateMember/PrivateIdent
// can resolve it at eval time via the environment chain.
instance_method_env.def_builtin("[[PrivateBrand]]", private_brand)
static_method_env.def_builtin("[[PrivateBrand]]", private_brand)
// Process all members (methods and fields)
// Class bodies are always strict — computed property keys and method keys
// must be evaluated as strict code per ES2022 §15.7.
for mbr in members {
match mbr {
@ast.ClassMember::Field(f) => {
// Evaluate key now (computed field keys are evaluated in order at class definition)
// ToPropertyKey: keep Symbol as-is; coerce everything else to String once,
// so side-effectful toString/valueOf don't re-run per-construction.
let key_val : Value = match f.key {
StringLit(s, _, _, _) => String_(s)
PrivateIdent(name, _) => String_(name) // private fields: use name directly
_ => {
let v = self.eval_expr(class_ctx, f.key, class_env)
to_property_key(v, interp=Some(self))
}
}
let field_init : ClassFieldInit = {
key: key_val,
initializer: f.initializer,
closure: if f.is_static {
static_method_env
} else {
instance_method_env
},
}
if f.is_private {
if f.is_static {
private_static_fields.push(field_init)
} else {
private_instance_fields.push(field_init)
}
} else if f.is_static {
static_fields.push(field_init)
} else {
instance_fields.push(field_init)
}
}
@ast.ClassMember::StaticBlock(_) => ()
@ast.ClassMember::Method(m) => { // method branch
// Get the method key - could be string or symbol
let key_val : Value = match m.key {
StringLit(s, _, _, _) => String_(s)
PrivateIdent(name, _) => String_(name) // private methods: use name directly
_ =>
to_property_key(
self.eval_expr(class_ctx, m.key, class_env),
interp=Some(self),
)
}
let method_key_str : String = match key_val {
Symbol(_) => key_val.to_string()
_ => self.to_js_string(key_val)
}
// ES2022 §15.7.10 ClassElementEvaluation: a static MethodDefinition
// whose property key is the String "prototype" throws TypeError.
// Symbol keys are exempt (key_val is Symbol(_), method_key_str is the
// Symbol description, not the literal "prototype").
if m.is_static && key_val is String_("prototype") {
raise @errors.TypeError(
message="Class static method or accessor cannot be named 'prototype'",
)
}
// Per ES2022 §15.7.13, only a non-computed `constructor` method
// becomes the class's [[Construct]]. Computed `["constructor"]` is
// a regular prototype method.
if method_key_str == "constructor" &&
!m.is_static &&
!m.is_private &&
!m.computed {
// Extract constructor params and body
// Class bodies are always strict, so ensure constructor body has "use strict"
match m.value {
FuncExpr(_, param_names, body, _, _) => {
let strict_body = ensure_strict_body(body)
validate_function_signature(true, None, param_names, strict_body)
// Wrap plain names as Param objects (no defaults/rest/patterns)
let params : Array[@ast.Param] = param_names.map(fn(n) {
(
{
name: n,
default_val: None,
pattern: None,
is_rest_pattern: false,
} : @ast.Param)
})
ctor_fn = Some((params, None, strict_body))
ctor_param_count = param_names.length()
}
FuncExprExt(_, params, rest, body, _, _) => {
let strict_body = ensure_strict_body(body)
validate_function_signature_ext(
true,
None,
params,
rest,
strict_body,
)
ctor_fn = Some((params, rest, strict_body))
ctor_param_count = expected_argument_count_ext(params)
}
_ => ()
}
continue
}
// A method that reaches here with key "constructor" on the prototype
// (computed `["constructor"]`) overrides the spec back-reference write.
// Static "constructor" doesn't touch the prototype slot, so gate on
// !m.is_static.
if method_key_str == "constructor" && !m.is_static {
constructor_overridden = true
}
// Create the method function value
// Class methods are always strict — ensure body has "use strict"
// Use the appropriate method environment for super.method() support
let method_closure = if m.is_static {
static_method_env
} else {
instance_method_env
}
let method_val : Value = match m.value {
FuncExpr(fn_name, params, body, _, method_source_text) => {
let strict_body = ensure_strict_body(body)
validate_function_signature(true, fn_name, params, strict_body)
let func_data : FuncData = {
name: fn_name,
params,
body: strict_body,
closure: method_closure,
strict: true,
// Class methods don't get a §15.2.5 self-name binding.
has_name_binding: false,
// Class MethodDefinitions do not go through MakeConstructor.
is_method: true,
source_text: method_source_text,
}
make_func(func_data)
}
FuncExprExt(fn_name, params, rest, body, _, method_source_text) => {
let strict_body = ensure_strict_body(body)
validate_function_signature_ext(
true, fn_name, params, rest, strict_body,
)
let func_data : FuncDataExt = {
name: fn_name,
params,
rest_param: rest,
body: strict_body,
closure: method_closure,
strict: true,
has_name_binding: false,
is_method: true,
source_text: method_source_text,
}
make_func_ext(func_data)
}
// Generator/async methods: fast path with has_name_binding=false.
// Class MethodDefinitionEvaluation must not create a FunctionExpressionName
// binding; generator/async can't be stripped post-hoc so suppress at creation.
GeneratorExpr(fn_name, params, body, _, method_source_text) => {
let strict_body = ensure_strict_body(body)
validate_function_signature(true, fn_name, params, strict_body)
self.make_generator_function(
fn_name,
params,
None,
strict_body,
true,
method_closure,
has_name_binding=false,
is_method=true,
source_text=method_source_text,
)
}
GeneratorExprExt(fn_name, params, rest, body, _, method_source_text) => {
let strict_body = ensure_strict_body(body)
validate_function_signature_ext(
true, fn_name, params, rest, strict_body,
)
self.make_generator_function_ext(
fn_name,
params,
rest,
strict_body,
true,
method_closure,
has_name_binding=false,
is_method=true,
source_text=method_source_text,
)
}
AsyncFuncExpr(fn_name, params, body, _, method_source_text) => {
let strict_body = ensure_strict_body(body)
validate_function_signature(true, fn_name, params, strict_body)
self.make_async_function(
fn_name,
params,
None,
strict_body,
true,
method_closure,
has_name_binding=false,
is_method=true,
source_text=method_source_text,
)
}
AsyncFuncExprExt(fn_name, params, rest, body, _, method_source_text) => {
let strict_body = ensure_strict_body(body)
validate_function_signature_ext(
true, fn_name, params, rest, strict_body,
)
self.make_async_function_ext(
fn_name,
params,
rest,
strict_body,
true,
method_closure,
has_name_binding=false,
is_method=true,
source_text=method_source_text,
)
}
AsyncGeneratorExpr(fn_name, params, body, _, method_source_text) => {
let strict_body = ensure_strict_body(body)
validate_function_signature(true, fn_name, params, strict_body)
self.make_async_generator_function(
fn_name,
params,
None,
strict_body,
true,
method_closure,
has_name_binding=false,
is_method=true,
source_text=method_source_text,
)
}
AsyncGeneratorExprExt(
fn_name,
params,
rest,
body,
_,
method_source_text
) => {
let strict_body = ensure_strict_body(body)
validate_function_signature_ext(
true, fn_name, params, rest, strict_body,
)
self.make_async_generator_function_ext(
fn_name,
params,
rest,
strict_body,
true,
method_closure,
has_name_binding=false,
is_method=true,
source_text=method_source_text,
)
}
_ => self.eval_expr(class_ctx, m.value, method_closure)
}
// Handle getter/setter vs regular method
// Class methods are non-enumerable per spec
// Set appropriate function name with get/set prefix
match m.kind {
Get => set_function_name(method_val, "get " + method_key_str)
Set => set_function_name(method_val, "set " + method_key_str)
_ => set_function_name(method_val, method_key_str)
}
// Private methods: skip public installation, store in private_methods map
if m.is_private {
let priv_name : String = match m.key {
PrivateIdent(n, _) => n
_ => method_key_str
}
private_methods[priv_name] = method_val
continue
}
match m.kind {
Get =>
// Getter method - store as accessor descriptor
match key_val {
Symbol(sym) => {
let (props, descs) = if m.is_static {
(static_symbol_props, static_symbol_descriptors)
} else {
(proto_symbol_props, proto_symbol_descriptors)
}
props[sym.id] = Undefined
let existing = descs.get(sym.id)
descs[sym.id] = {
writable: false,
enumerable: false,
configurable: true,
getter: Some(method_val),
setter: match existing {
Some(d) => d.setter
None => None
},
is_accessor: true,
}
}
_ => {
let (props, descs) = if m.is_static {
(static_props, static_descriptors)
} else {
(proto_props, proto_descriptors)
}
props[method_key_str] = Undefined
let existing = descs.get(method_key_str)
descs[method_key_str] = {
writable: false,
enumerable: false,
configurable: true,
getter: Some(method_val),
setter: match existing {
Some(d) => d.setter
None => None
},
is_accessor: true,
}
}
}
Set =>
// Setter method - store as accessor descriptor
match key_val {
Symbol(sym) => {
let (props, descs) = if m.is_static {
(static_symbol_props, static_symbol_descriptors)
} else {
(proto_symbol_props, proto_symbol_descriptors)
}
props[sym.id] = Undefined
let existing = descs.get(sym.id)
descs[sym.id] = {
writable: false,
enumerable: false,
configurable: true,
getter: match existing {
Some(d) => d.getter
None => None
},
setter: Some(method_val),
is_accessor: true,
}
}
_ => {
let (props, descs) = if m.is_static {
(static_props, static_descriptors)
} else {
(proto_props, proto_descriptors)
}
props[method_key_str] = Undefined
let existing = descs.get(method_key_str)
descs[method_key_str] = {
writable: false,
enumerable: false,
configurable: true,
getter: match existing {
Some(d) => d.getter
None => None
},
setter: Some(method_val),
is_accessor: true,
}
}
}
_ => {
// Regular method or Init
let method_descriptor : PropDescriptor = {
writable: true,
enumerable: false,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
// 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
if m.is_static {
static_props[method_key_str] = method_val
static_descriptors[method_key_str] = method_descriptor
} else {
proto_props[method_key_str] = method_val
proto_descriptors[method_key_str] = method_descriptor
}
}
}
}
} // end Method arm
} // end match member
} // end for member in members
// Bind private methods in method environments for PrivateMember eval
let private_methods_obj : Value = Object({
bag: {
properties: private_methods,
symbol_properties: Map([]),
descriptors: Map([]),
symbol_descriptors: Map([]),
internal_slots: Map([]),
host_slots: Map([]),
},
prototype: Null,
callable: None,
class_name: "Object",
extensible: true,
arraybuffer_state: None,
})
instance_method_env.def_builtin("[[PrivateMethods]]", private_methods_obj)
static_method_env.def_builtin("[[PrivateMethods]]", private_methods_obj)
// Create prototype object
let proto = Object({
bag: {
properties: proto_props,
symbol_properties: proto_symbol_props,
descriptors: proto_descriptors,
symbol_descriptors: proto_symbol_descriptors,
internal_slots: Map([]),
host_slots: Map([]),
},
prototype: super_proto,
callable: None,
class_name: name,
extensible: true,
arraybuffer_state: None,
})
// Add constructor back-reference to prototype
// (will be set after class object is created)
// The class function's [[Prototype]] is always the super class constructor
// so that static inheritance works. For `extends null` (super_ctor = Some(Null))
// and for base classes (None), the class function inherits from Function.prototype.
let class_proto = match super_ctor {
Some(Null) | None =>
self.global.get("[[FunctionPrototype]]") catch {
_ => Null
}
Some(super_class) => super_class
}
// Create the class constructor function
let class_obj = stamp_function_realm(
Object({
bag: {
properties: static_props,
symbol_properties: static_symbol_props,
descriptors: static_descriptors,
symbol_descriptors: static_symbol_descriptors,
internal_slots: Map([]),
host_slots: Map([]),
},
prototype: class_proto,
callable: Some(
ClassConstructor({
name,
proto,
super_ctor,
ctor_fn,
closure: instance_method_env,
super_proto,
instance_fields,
private_instance_fields,
source_text,
private_brand,
private_methods,
}),
),
class_name: "Function",
extensible: true,
arraybuffer_state: None,
}),
realm_state=Some(self.realm_state),
)
// Method closures share these mutable environments, so binding the completed
// home objects here makes GetSuperBase observe their live [[Prototype]] values
// on every evaluation, including after Object.setPrototypeOf.
instance_method_env.def_builtin("[[HomeObject]]", proto)
static_method_env.def_builtin("[[HomeObject]]", class_obj)
// Set prototype.constructor to point to the class (non-enumerable per spec)
if !constructor_overridden {
match proto {
Object(proto_data) => {
proto_data.bag.properties["constructor"] = class_obj
proto_data.bag.descriptors["constructor"] = {
writable: true,
enumerable: false,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
}
_ => ()
}
}
// Add length, name, prototype properties to the class constructor per spec
match class_obj {
Object(class_data) => {
// Add length (formal parameter count) - non-writable, non-enumerable, configurable
class_data.bag.properties["length"] = Number(ctor_param_count.to_double())
class_data.bag.descriptors["length"] = non_enum_configurable
// Add prototype - non-writable, non-enumerable, non-configurable
class_data.bag.properties["prototype"] = proto
class_data.bag.descriptors["prototype"] = {
writable: false,
enumerable: false,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
}
}
_ => ()
}
// ClassDefinitionEvaluation initializes the inner immutable binding only
// after evaluating class elements, and before executing static initializers.
match class_binding {
Some(binding) => class_env.initialize(binding, class_obj)
None => ()
}
// Evaluate static field initializers (run once at class definition time).
// Class bodies are strict, but field initializers are not generator bodies.
let static_field_ctx : ExecContext = { strict: true, current_generator: None }
for sf in static_fields {
let key_str : String = match sf.key {
String_(s) => s
Symbol(sym) => sym.id.to_string() // use id for symbol-keyed static fields
_ => self.to_js_string(sf.key)
}
let value : Value = match sf.initializer {
Some(expr) => {
// Evaluate initializer with `this` = class_obj and class name in scope
let field_env = Environment::new(parent=Some(static_method_env))
field_env.def_builtin("this", class_obj)
self.eval_expr(static_field_ctx, expr, field_env)
}
None => Undefined
}
match class_obj {
Object(class_data) =>
match sf.key {
Symbol(sym) => {
if !class_data.bag.symbol_properties.contains(sym.id) &&
!class_data.extensible {
raise @errors.TypeError(
message="Cannot define static field on non-extensible object",
)
}
match class_data.bag.symbol_descriptors.get(sym.id) {
Some(d) =>
if !d.configurable {
raise @errors.TypeError(
message="Cannot redefine non-configurable static class field",
)
}
None => ()
}
class_data.bag.symbol_properties[sym.id] = value
class_data.bag.symbol_descriptors[sym.id] = {
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
}
_ => {
if !class_data.bag.properties.contains(key_str) &&
!class_data.extensible {
raise @errors.TypeError(
message="Cannot define static field on non-extensible object",
)
}
match class_data.bag.descriptors.get(key_str) {
Some(d) =>
if !d.configurable {
raise @errors.TypeError(
message="Cannot redefine non-configurable static class field",
)
}
None => ()
}
class_data.bag.properties[key_str] = value
class_data.bag.descriptors[key_str] = {
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
}
}
_ => ()
}
}
// Execute static blocks in order (after static field initialization)
// Each StaticBlock has `this` = class_obj and the class name in scope.
for mbr in members {
match mbr {
@ast.ClassMember::StaticBlock(stmts, _) => {
let block_env = Environment::new(parent=Some(static_method_env))
block_env.def_builtin("this", class_obj)
let _ = self.exec_stmts(static_field_ctx, stmts, block_env)
}
_ => ()
}
}
class_obj
}