// Builtins package: constructors, prototypes, and global-object seeding for
// the ECMAScript standard library skeleton.
//
// This is Step 9's core deliverable. M1 only lands the objects that AC
// requires:
//
// - Object / Object.prototype
// - Function.prototype (no `Function` constructor — `new Function(...)`
// needs eval, which is M2+)
// - Array.prototype (fresh `[]` links its proto here)
// - String.prototype (skeleton; `test_op2` uses `instanceof String`)
// - Error / Error.prototype
// - TypeError, RangeError, SyntaxError, ReferenceError (constructors and
// prototypes, each of whose proto is Error.prototype)
//
// M3 will inflate this package with the real `Object.prototype.toString`,
// `Array.prototype.push`, etc. — for M1 the prototypes are near-empty:
// they carry `name` / `message` where JS spec demands it and otherwise
// serve as identity anchors for `instanceof`.
//
// Design notes:
//
// - Prototypes are built with `Object::new(Shape::new(), Null)`, so a fresh
// prototype's `[[Prototype]]` starts as `Null`. We then use `set_proto`
// to link them appropriately (e.g. Function.prototype's proto is
// Object.prototype).
// - Constructors are `@value.NativeFunction`. Their `.prototype` is wired
// via `set_prototype` after both objects exist.
// - `install_into(globals)` registers each constructor as a global by name
// plus the value-shaped globals (`undefined`, `NaN`, `Infinity`,
// `globalThis`).
///|
/// The M1 builtin registry. Owns the prototype objects and constructor
/// values that get exposed on the global object.
///
/// This struct is intentionally not `pub(all)`: the vm package only needs
/// to construct one, read the prototype references, and install into a
/// globals object. Field access outside `builtins` goes through accessors.
pub struct Builtins {
object_proto : @value.Object
function_proto : @value.Object
array_proto : @value.Object
string_proto : @value.Object
error_proto : @value.Object
type_error_proto : @value.Object
range_error_proto : @value.Object
syntax_error_proto : @value.Object
reference_error_proto : @value.Object
object_ctor : @value.NativeFunction
string_ctor : @value.NativeFunction
error_ctor : @value.NativeFunction
type_error_ctor : @value.NativeFunction
range_error_ctor : @value.NativeFunction
syntax_error_ctor : @value.NativeFunction
reference_error_ctor : @value.NativeFunction
}
///|
/// Accessor: Object.prototype. Used by the VM when it materialises a fresh
/// object literal (`{}` or `new Object`) — the new object's `[[Prototype]]`
/// must be Object.prototype so `hasOwnProperty` etc. work in later
/// milestones.
pub fn Builtins::object_proto(self : Builtins) -> @value.Object {
self.object_proto
}
///|
/// Accessor: Function.prototype. Not used by VM opcodes yet in M1 (function
/// values don't have proto links to Function.prototype until we grow real
/// callable objects — Step 9 lays the pieces, later milestones connect
/// them). Exposed for symmetry.
pub fn Builtins::function_proto(self : Builtins) -> @value.Object {
self.function_proto
}
///|
/// Accessor: Array.prototype. The VM's `OP_NEW_ARRAY` links a fresh array's
/// proto here.
pub fn Builtins::array_proto(self : Builtins) -> @value.Object {
self.array_proto
}
///|
/// Accessor: String.prototype. Used for `String` instance links (M3 grows
/// this to include real string methods).
pub fn Builtins::string_proto(self : Builtins) -> @value.Object {
self.string_proto
}
///|
/// Accessor: Error.prototype.
pub fn Builtins::error_proto(self : Builtins) -> @value.Object {
self.error_proto
}
///|
/// Accessor: TypeError.prototype.
pub fn Builtins::type_error_proto(self : Builtins) -> @value.Object {
self.type_error_proto
}
///|
/// Accessor: RangeError.prototype.
pub fn Builtins::range_error_proto(self : Builtins) -> @value.Object {
self.range_error_proto
}
///|
/// Accessor: SyntaxError.prototype.
pub fn Builtins::syntax_error_proto(self : Builtins) -> @value.Object {
self.syntax_error_proto
}
///|
/// Accessor: ReferenceError.prototype.
pub fn Builtins::reference_error_proto(self : Builtins) -> @value.Object {
self.reference_error_proto
}
///|
/// Create a fresh set of M1 builtin objects. The order of operations is
/// tightly coupled to prototype-chain rules — read the inline comments to
/// see why each `set_proto` / `set_prototype` call happens where it does.
pub fn Builtins::new() -> Builtins {
// Step 1: allocate the root prototype (Object.prototype).
// Object.prototype's [[Prototype]] is Null. All other prototypes point
// here directly or through the Error prototype chain.
let object_proto = @value.Object::new(@value.Shape::new(), @value.Null)
// Step 2: sibling prototypes whose proto is Object.prototype.
let function_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(object_proto),
)
let array_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(object_proto),
)
let string_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(object_proto),
)
let error_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(object_proto),
)
// Step 3: Error subclass prototypes. Their proto is Error.prototype so
// `new TypeError() instanceof Error` is `true` per JS spec.
let type_error_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(error_proto),
)
let range_error_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(error_proto),
)
let syntax_error_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(error_proto),
)
let reference_error_proto = @value.Object::new(
@value.Shape::new(),
@value.Object(error_proto),
)
// Step 4: seed the `name` and `message` properties on each Error-family
// prototype. JS spec: `Error.prototype.name === "Error"`,
// `Error.prototype.message === ""`. `String.prototype`, `Array.prototype`,
// `Object.prototype` do NOT get these — they carry (eventually) their own
// method tables instead.
error_proto.add_property(
"name",
@value.Str("Error"),
@value.ATTR_DEFAULT_DATA,
)
error_proto.add_property("message", @value.Str(""), @value.ATTR_DEFAULT_DATA)
type_error_proto.add_property(
"name",
@value.Str("TypeError"),
@value.ATTR_DEFAULT_DATA,
)
type_error_proto.add_property(
"message",
@value.Str(""),
@value.ATTR_DEFAULT_DATA,
)
range_error_proto.add_property(
"name",
@value.Str("RangeError"),
@value.ATTR_DEFAULT_DATA,
)
range_error_proto.add_property(
"message",
@value.Str(""),
@value.ATTR_DEFAULT_DATA,
)
syntax_error_proto.add_property(
"name",
@value.Str("SyntaxError"),
@value.ATTR_DEFAULT_DATA,
)
syntax_error_proto.add_property(
"message",
@value.Str(""),
@value.ATTR_DEFAULT_DATA,
)
reference_error_proto.add_property(
"name",
@value.Str("ReferenceError"),
@value.ATTR_DEFAULT_DATA,
)
reference_error_proto.add_property(
"message",
@value.Str(""),
@value.ATTR_DEFAULT_DATA,
)
// Step 5: allocate the constructor NativeFunctions.
let object_ctor = @value.NativeFunction::new("Object", true, object_impl)
let string_ctor = @value.NativeFunction::new("String", true, string_impl)
let error_ctor = @value.NativeFunction::new("Error", true, error_impl)
let type_error_ctor = @value.NativeFunction::new(
"TypeError", true, error_impl,
)
let range_error_ctor = @value.NativeFunction::new(
"RangeError", true, error_impl,
)
let syntax_error_ctor = @value.NativeFunction::new(
"SyntaxError", true, error_impl,
)
let reference_error_ctor = @value.NativeFunction::new(
"ReferenceError", true, error_impl,
)
// Step 6: wire the .prototype on each constructor. The VM's OP_CONSTRUCT
// reads this to set the new receiver's [[Prototype]].
object_ctor.set_prototype(@value.Object(object_proto))
string_ctor.set_prototype(@value.Object(string_proto))
error_ctor.set_prototype(@value.Object(error_proto))
type_error_ctor.set_prototype(@value.Object(type_error_proto))
range_error_ctor.set_prototype(@value.Object(range_error_proto))
syntax_error_ctor.set_prototype(@value.Object(syntax_error_proto))
reference_error_ctor.set_prototype(@value.Object(reference_error_proto))
// Step 7: wire each Error-family prototype's `constructor` property back
// to its constructor. JS: `Error.prototype.constructor === Error`.
error_proto.add_property(
"constructor",
@value.NativeFn(error_ctor),
@value.ATTR_DEFAULT_DATA,
)
type_error_proto.add_property(
"constructor",
@value.NativeFn(type_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
range_error_proto.add_property(
"constructor",
@value.NativeFn(range_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
syntax_error_proto.add_property(
"constructor",
@value.NativeFn(syntax_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
reference_error_proto.add_property(
"constructor",
@value.NativeFn(reference_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
object_proto.add_property(
"constructor",
@value.NativeFn(object_ctor),
@value.ATTR_DEFAULT_DATA,
)
string_proto.add_property(
"constructor",
@value.NativeFn(string_ctor),
@value.ATTR_DEFAULT_DATA,
)
{
object_proto,
function_proto,
array_proto,
string_proto,
error_proto,
type_error_proto,
range_error_proto,
syntax_error_proto,
reference_error_proto,
object_ctor,
string_ctor,
error_ctor,
type_error_ctor,
range_error_ctor,
syntax_error_ctor,
reference_error_ctor,
}
}
///|
/// Attach every M1 builtin to a global object. Called from `Engine::new`
/// after the empty globals object is constructed. The identity of `globals`
/// is what `globalThis` points at, so we register it last.
pub fn Builtins::install_into(self : Builtins, globals : @value.Object) -> Unit {
// Constructors.
globals.add_property(
"Object",
@value.NativeFn(self.object_ctor),
@value.ATTR_DEFAULT_DATA,
)
globals.add_property(
"String",
@value.NativeFn(self.string_ctor),
@value.ATTR_DEFAULT_DATA,
)
globals.add_property(
"Error",
@value.NativeFn(self.error_ctor),
@value.ATTR_DEFAULT_DATA,
)
globals.add_property(
"TypeError",
@value.NativeFn(self.type_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
globals.add_property(
"RangeError",
@value.NativeFn(self.range_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
globals.add_property(
"SyntaxError",
@value.NativeFn(self.syntax_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
globals.add_property(
"ReferenceError",
@value.NativeFn(self.reference_error_ctor),
@value.ATTR_DEFAULT_DATA,
)
// Value-shaped globals.
globals.add_property("undefined", @value.Undefined, @value.ATTR_DEFAULT_DATA)
globals.add_property("NaN", @value.Number(js_nan()), @value.ATTR_DEFAULT_DATA)
globals.add_property(
"Infinity",
@value.Number(js_infinity()),
@value.ATTR_DEFAULT_DATA,
)
// `globalThis` is the global object itself.
globals.add_property(
"globalThis",
@value.Object(globals),
@value.ATTR_DEFAULT_DATA,
)
}
// ---------------------------------------------------------------------------
// Constructor impls
// ---------------------------------------------------------------------------
///|
/// `new Object()` / `Object()` — for M1 both variants return an empty object
/// whose proto is Object.prototype. When called with `new`, the VM has
/// already allocated the receiver with the right proto and passes it as
/// `this_val`; we simply return it. When called without `new`,
/// `this_val` will be Undefined, and we allocate a fresh proto-less object
/// here. (Step 9 doesn't have access to `Object.prototype` from inside the
/// impl closure — the VM's OP_CONSTRUCT does the wiring; on plain call we
/// return Undefined and let the caller ignore the return.)
///
/// The signature must be a static function because MoonBit closures over
/// non-`Copy` captures aren't valid in native-function bodies. Anything the
/// impl needs to reach must come through `this_val` or `args`.
fn object_impl(
this_val : @value.JSValue,
_args : Array[@value.JSValue],
) -> Result[@value.JSValue, @value.NativeError] {
match this_val {
Object(_) => Ok(this_val)
_ => {
// Called without `new`. Return an empty object; the caller can wire
// proto after. (M1 doesn't currently exercise this path.)
let obj = @value.Object::new(@value.Shape::new(), @value.Null)
Ok(@value.Object(obj))
}
}
}
///|
/// `new Error(message)` / `Error(message)` — sets `this.message` to
/// `String(args[0])` when args has content. The VM has already created the
/// receiver with the right proto (Error.prototype / TypeError.prototype /
/// etc.), so this impl only needs to install the message.
///
/// Returns `Ok(Undefined)` so `adjust_ctor_return` uses `this_val` — that
/// preserves the receiver with all its wired properties. Called without
/// `new`, `this_val` will be `Undefined`; we still fabricate a plain
/// object and return it as an Object value (the VM's non-`new` fallthrough
/// won't reach adjust_ctor_return).
///
/// The subclass-name is embedded via the prototype chain: the VM wires
/// `Object::new(shape, proto=Error/TypeError/...prototype)` before invoking
/// this impl, so `error_obj.name` reads through the prototype and yields
/// the right subclass name.
fn error_impl(
this_val : @value.JSValue,
args : Array[@value.JSValue],
) -> Result[@value.JSValue, @value.NativeError] {
match this_val {
Object(obj) => {
if args.length() > 0 {
let msg = value_to_display_string(args[0])
if not_undefined(args[0]) {
obj.add_property("message", @value.Str(msg), @value.ATTR_DEFAULT_DATA)
}
}
Ok(@value.Undefined)
}
// Called without `new`. JS spec: same as with `new`. We fabricate a
// proto-less object here; this path isn't exercised by M1 AC.
_ => {
let obj = @value.Object::new(@value.Shape::new(), @value.Null)
if args.length() > 0 && not_undefined(args[0]) {
let msg = value_to_display_string(args[0])
obj.add_property("message", @value.Str(msg), @value.ATTR_DEFAULT_DATA)
}
Ok(@value.Object(obj))
}
}
}
///|
/// `new String(x)` / `String(x)` — M1 stub. When called with `new`, the VM
/// has already materialised a receiver whose proto is String.prototype; we
/// leave it alone so `instance instanceof String` succeeds. When called
/// without `new`, coerce the argument to a JS string primitive and return
/// it (`String(123)` → `"123"`). The zero-arg case yields `""` per spec.
///
/// This is enough to satisfy the M1 AC's use of `a instanceof String`
/// (test_op2). Real String wrapping (with `.valueOf` / index access on the
/// wrapper object) is deferred to M3.
fn string_impl(
this_val : @value.JSValue,
args : Array[@value.JSValue],
) -> Result[@value.JSValue, @value.NativeError] {
match this_val {
Object(_) =>
// Called via `new`. Preserve the pre-allocated receiver; M1 doesn't
// wire an internal [[StringData]] slot.
Ok(@value.Undefined)
_ => {
// Plain call `String(x)` → coerce to string primitive.
let s = if args.length() == 0 {
""
} else {
value_to_display_string(args[0])
}
Ok(@value.Str(s))
}
}
}
///|
/// True iff `v` is not `Undefined`. Used by error constructor to skip the
/// message assignment when no argument was supplied (`new Error()` should
/// leave `message` inherited from the prototype as "").
fn not_undefined(v : @value.JSValue) -> Bool {
match v {
Undefined => false
_ => true
}
}
///|
/// Coerce a JSValue to its display string form for error messages. The full
/// `ToString` operation lives in the vm package (with proper NaN / -0 /
/// scientific-notation handling); builtins is intentionally kept separate
/// from `vm`, so we duplicate a minimal subset here. M3 can lift this into
/// a shared helper once the layering settles.
fn value_to_display_string(v : @value.JSValue) -> String {
match v {
Undefined => "undefined"
Null => "null"
Bool(true) => "true"
Bool(false) => "false"
Int32(i) => i.to_string()
Number(d) => d.to_string()
Str(s) => s
Object(_) => "[object Object]"
Function(f) => "function " + f.name() + "() { [native code] }"
NativeFn(nf) => "function " + nf.name() + "() { [native code] }"
}
}
///|
/// A JS NaN value. Duplicated from vm (`@double.not_a_number`) to keep this
/// package free of a math dependency; we only need one constant.
fn js_nan() -> Double {
0.0 / 0.0
}
///|
/// JS +Infinity.
fn js_infinity() -> Double {
1.0 / 0.0
}