Skip to content

Latest commit

 

History

History
166 lines (112 loc) · 19.3 KB

File metadata and controls

166 lines (112 loc) · 19.3 KB

LambdaJS — Classes

Last verified against tree: 2026-08-13 (initial stamp from git history)

Part of the LambdaJS detailed-design set. This document covers the class layer that LambdaJS builds on top of the ordinary object/property/prototype machinery: how a class declaration is collected into a JsClassEntry at compile time, how the constructor's this.prop = writes drive shape pre-allocation, how methods land on the prototype (vs. static methods on the class object), how super()/super.method() and new.target are wired, how private # members are mangled and brand-checked, and how a handful of builtins are subclassable.

Object representation, property attributes, [[Get]]/[[Set]], the prototype walk, and shape pre-allocation storage are owned by JS_06 — Objects, Properties & Prototypes; this document links to JS_06 rather than restating those mechanics. A source class constructor is a JsFunction with explicit call/construct capability; its instance prototype and fields are ordinary shaped objects with immutable metadata under D3.4.7.

Primary sources: lambda/js/js_mir_context.hpp (JsClassEntry, JsClassMethodEntry, JsStaticFieldEntry, JsInstanceFieldEntry, the ctor_prop_* arrays on JsFuncCollected, the collection caps), lambda/js/js_mir_function_collection_class_inference.cpp (class collection, jm_scan_ctor_props, jm_find_class), lambda/js/js_mir_statement_lowering.cpp (class-function emission, prototype/method install, constructor prologue/body/epilogue, new C() lowering), lambda/js/js_mir_expression_lowering.cpp (jm_class_private_name, super.x lowering), lambda/js/js_runtime.cpp (js_construct_value, js_super_call_native, brand helpers, active new.target), lambda/js/js_class.h, and lambda/js/js_object_meta.h (class metadata). Audience: engine developers. Convention: file:line references drift; confirm against symbol names.


1. Purpose & scope

ES2022 classes are lowered to constructor functions, prototype objects, and property descriptors. A class binding is a JsFunction with explicit call and construct entries; the constructor's class/brand policy is carried by the immutable metadata of its runtime shapes, while instance prototypes remain ordinary objects. The public prototype property and internal prototype resolver are separate, and no name/sentinel property grants callability.

The compiler does the heavy lifting in a pre-pass (jm_collect_functions recursing into class bodies, js_mir_function_collection_class_inference.cpp:680) that fills one JsClassEntry per class, then emits the class object and all instance/static plumbing during statement lowering. Constructor field inference (the A5 this.prop scan, §3) feeds the shape pre-allocation owned by JS_06 §10. The earliest typed-AST phase that classifies constructors and derived-ness is in JS_01 — Compilation Pipeline; generators-as-methods are in JS_08 — Iterators & Generators.


2. Class collection & JsClassEntry

JsClassEntry and method/field records

jm_collect_functions reserves a JsClassEntry slot the moment it sees a class node and then recurses into the body so nested functions/classes get collected with the right parent_index (js_mir_function_collection_class_inference.cpp:684). The struct (js_mir_context.hpp:313) holds fixed-capacity arrays:

Field Cap Meaning
methods[128] 128 every method/accessor/constructor, method_count live
static_fields[16] 16 static x = … definitions (JsStaticFieldEntry, :294)
instance_fields[32] 32 non-static field initializers (JsInstanceFieldEntry, :304)
static_blocks[8] 8 static { … } block bodies
constructor points into methods[] or NULL
superclass resolved parent JsClassEntry* (NULL for extends <expr> / builtins)

Each JsClassMethodEntry (:281) records name, the collected fc (JsFuncCollected*), param_count, and the booleans is_constructor/is_static/is_getter/is_setter/computed plus key_expr for computed keys. The collection loop walks the class body's statement list (:703): field definitions feed static_fields[]/instance_fields[] (private names mangled via jm_class_private_name, §6), method definitions feed methods[], and static {} blocks feed static_blocks[]. The constructor's body is fed to jm_scan_ctor_props to populate the A5 field arrays (§3, :871).

The whole-module caps are JS_MIR_MAX_COLLECTED_CLASSES == 4096 and JS_MIR_MAX_COLLECTED_FUNCTIONS == 32768 (js_mir_context.hpp:128); overflowing the class array sets class_collection_overflow_logged and silently stops collecting. jm_find_class (:1591) is a linear name scan over class_entries[], and jm_class_name_is_unique (js_mir_statement_lowering.cpp:2168) guards the static-new fast path: a duplicated class name falls back to runtime dispatch.


3. Constructor compilation

The compiler distinguishes the constructor function, the public prototype object, and the shape blueprint derived from the constructor body. The constructor is a JsFunction; the prototype/shape objects carry their own immutable metadata under D3.4.7 rather than hidden class-map fields.

A5 ctor field scan. jm_scan_ctor_props (js_mir_function_collection_class_inference.cpp:1501) walks the constructor's top-level statements and records each this.<name> = <rhs> assignment into the constructor fc's parallel arrays ctor_prop_ptrs[16]/ctor_prop_lens[16]/ctor_prop_types[16]/ctor_prop_param_idx[16] (js_mir_context.hpp:244). It stops at the first return/throw (so only unconditional prologue writes count), detects typed-array RHS (jm_detect_typed_array_new) and a scalar field type (jm_detect_ctor_field_type), and maps a field to a constructor parameter index when the RHS is a bare param identifier (P4b). The cap is 16 distinct fields.

Prologue — create object + set proto + A5 shape pre-alloc. For a statically-resolved new C(args) (js_mir_statement_lowering.cpp:2747), when ctor_prop_count > 0 the compiler emits js_constructor_create_object_shaped (or the _cached variant once shape_cache_ptr is allocated) passing the ctor-prop name/len arrays — this pre-allocates the instance's TypeMap so every this.prop = write in the body hits a known slot. js_constructor_create_object_shaped_cached (js_runtime.cpp:2566) captures the freshly-built TypeMap* into the per-class shape_cache on first new; subsequent instances share the blueprint. The slot read/write helpers js_get_shaped_slot/js_set_shaped_slot (:2584/:2613) and the storage mechanics are documented in JS_06 §10. When the class has instance fields or is a superclass, pre-shaping is skipped and js_new_object is used so js_property_set manages the shape dynamically. The prologue creates the instance with the class/prototype metadata and calls js_set_prototype(obj, C.prototype) (:2845); no public class-name marker is written.

Body — this.prop =. Inside the body, this is a normal receiver; this.prop = v lowers to either a shaped-slot write (when the slot was pre-allocated) or js_property_set. Instance-field initializers run before the constructor body, base-class-first, with this bound to the partially-constructed object (:2850).

Epilogue / return rules. After the constructor call, js_new_check_constructor_return(obj, result) implements the ES rule: if the constructor explicitly returned an object (MAP/ARRAY/ELEMENT/FUNC/OBJECT/VMAP) that value becomes the instance, otherwise the freshly-created obj is used. Per D6.2.2v2, all dynamic new, Reflect.construct, bound construction, Proxy construction, and super() paths pass newTarget as an explicit js_construct_value operand. The selected construct entry scopes the active binding read by js_get_new_target and restores it on every success/ERROR exit; there is no pending handoff.


4. Methods on prototype vs. static methods

The class-object emission (js_mir_statement_lowering.cpp:4679 for the function-body path, mirrored in the Phase-3 module pre-pass in js_mir_module_batch_lowering.cpp) builds two targets:

  • Instance methods install onto the prototype object (class_proto_obj, created at :4801 and exposed as public prototype). The own-instance-method loop (:5031) skips is_constructor/is_static, builds the method function (a closure via jm_build_closure_for_method when it captures, else js_new_method_function), names it (get /set prefix for accessors), marks it a method (js_mark_method_func), sets its home class, and installs it with jm_emit_install_method_or_accessor — which routes getters/setters into a JsAccessorPair and plain methods into a non-enumerable data slot (accessor storage is JS_06 §3).
  • Static methods install directly onto the class object cls_obj (:4870), with inherited static methods copied base-first from the superclass chain (:4816) before own statics override them. static {} blocks lower via jm_emit_class_static_block (:2128), executing the block with this/the class binding in scope.
  • Static fields lower via jm_emit_class_static_field (:2044), evaluating initializers (computed keys allowed) onto the class object.

The prototype carries a non-enumerable constructor back-pointer (js_set_default_constructor_property) and immutable class metadata. Built-in prototype methods (e.g. Array.prototype.map) are installed as realm-local properties when the intrinsic prototype is populated; they participate in the same ordinary property walk described in JS_06 §8 and JS_10 — Standard Built-in Library.


5. Inheritance, super() & super.method()

When a class has a resolved superclass JsClassEntry, the prototype's [[Prototype]] is linked to the parent's prototype (js_set_prototype(last_proto, sp_obj), js_mir_statement_lowering.cpp:4981), while superclass and constructor-body state stays in the rooted JsFunction class carrier. js_check_class_prototype_parent/js_check_class_heritage_constructor (js_runtime.cpp:11472/:11480) enforce IsConstructor and a valid .prototype. An extends <expr> whose target is not a collected class (a builtin, a member expression, null) takes the runtime-resolved heritage path (:5001); extends null links the prototype to null and makes a later super() fail IsConstructor.

super(args) in a derived constructor lowers to js_super_call_native(callee, this, args, argc) (js_runtime.cpp:11522, emitted at js_mir_expression_lowering.cpp:6772):

super() dispatch

  1. reject values without a construct capability with a TypeError;
  2. resolve the parent constructor value and call js_construct_value(parent, args, argc, activeNewTarget, ...);
  3. let the parent's stored construct entry allocate its required internal slots and choose activeNewTarget.prototype; source-class constructors use the same function capability boundary;
  4. bind the object result as derived this, preserving ERROR and object-return semantics.

super.method(args) fetches the method via js_super_property_get walking the home class's prototype parent, then calls it with the current this (js_mir_expression_lowering.cpp:6828, :6875); super.x/super.x = go through js_super_property_get/js_super_property_set (:700/:750).


6. Private members

A #-prefixed name is rewritten to a __private_ prefix at parse time; jm_is_private_name (js_mir_expression_lowering.cpp:101) tests for len > 10 && starts-with "__private_". To keep two classes' identically-spelled private names distinct, jm_class_private_name (:105) re-mangles to __private_<classIndex>_<suffix> where classIndex is the JsClassEntry's position in class_entries[] — so #x in class slot 3 becomes __private_3_x. Private fields, methods, and static fields all carry this mangled name. Enumeration filters __private_ keys (JS_06 §9), so private members never surface in Object.keys/for-in.

Brand checks. Private access on an object that never went through the class body throws TypeError, not undefined. Private keys are NamePool-owned private identities; class metadata and explicit private-brand ownership are kept separate from public property spelling under D3.4.7. Derived instances are branded after super() by js_init_class_instance_fields.


7. Computed property names

A computed key ([expr]) is stored on the relevant entry as computed = true with the original key_expr AST. At install time the key is evaluated via jm_transpile_box_item(key_expr) rather than a string literal — for instance methods (js_mir_statement_lowering.cpp:5052), static methods (:4875), static fields (jm_emit_class_static_field, :2046), and instance fields (:1986). Because a computed key may itself contain a yield when the class sits inside a generator, the compiler spills proto_obj/cls_obj/fn_item across the key evaluation (jm_gen_spill_save/jm_gen_spill_load, :5055). Phase-5C removed the legacy __get_/__set_ key wrapping; getters/setters now install through the accessor-pair path regardless of whether the key is computed.


8. Subclassable builtins

class MyArr extends Array {} cannot start life as a plain Map — the instance needs the builtin's internal behaviour (exotic length, primitive slot, typed-array backing) and the subclass prototype. js_construct_value is the sole dynamic boundary: intrinsic constructors carry direct typed construct bodies, and the concrete body allocates its internal-slot-bearing instance using the explicit newTarget prototype. Bound and Proxy construct entries forward the same operand. Source classes are function values, so construction enters the ordinary capability path and delegates inherited builtin construction back into js_construct_value (D6.2.2v2).

The Array-extends case also has a compile-time fast path: when the (sole, unique) superclass identifier is Array, new emits js_array_new(0) directly (js_mir_statement_lowering.cpp:2800). Exotic get/set behaviour on the resulting instances is JS_12 — TypedArrays.


9. Method-call lowering boundary

An ordinary obj.method(args) always evaluates the receiver, performs the observable property Get, evaluates arguments, and calls the resulting value with obj as this. The former P7 receiver-class/property-name direct lane was removed: a known source class and spelling do not prove current function identity after replacement, deletion, accessor installation, prototype mutation, or Proxy interception. jm_resolve_native_call now permits native/direct lowering only for exact lexical function bindings; future JR8 feedback may devirtualize a proven callee identity with a guard and ordinary fallback (D6.2.2v2).


Known Issues & Future Improvements

  1. Class-expression inner-name scope leak. The immutable inner class-name binding is tracked via inner_module_var_index and alias_name on JsClassEntry, but the lowering for named class expressions writes the class object into module-var slots driven by name (js_mir_statement_lowering.cpp:4718+) rather than a fully isolated lexical scope; an inner name can be observable beyond the strict class-body scope the spec mandates.
  2. Boolean/String subclass primitive-slot loss. Subclasses route through the builtin [[Construct]] (js_builtin_super_constructs_via_construct, js_runtime.cpp:11448) to get [[BooleanData]]/[[StringData]], but because the instance is then re-prototyped and merged via js_object_assign, edge cases where the primitive internal slot must survive super()-then-field-init are fragile.
  3. TypedArray subclass species/resize. Construction now preserves explicit newTarget and stored element policy, but @@species-driven resize on subclassed typed arrays is not fully wired through every resizing operation.
  4. Private brand ownership. Private names use NamePool private identity; public spelling prefixes cannot forge a class brand. Derived-class deferred initialization remains governed by js_init_class_instance_fields.
  5. Fixed method/field caps. methods[128], static_fields[16], instance_fields[32], static_blocks[8], and ctor_prop_*[16] (js_mir_context.hpp:317+) are hard limits; a class exceeding any of them silently truncates (e.g. field-definition collection guards static_field_count < 16 at js_mir_function_collection_class_inference.cpp:707). Improvement: grow these to dynamic arrays like JsFuncCollected.captures.
  6. Metadata boundary. Class identity is resolved through immutable TypeMap::js_meta and js_object_meta(Item) under D3.4.7. Constructor and prototype relationships are ordinary properties/internal prototype state; spelling-based class probes and callable class Maps are retired.

Appendix A — Source map

File Responsibility (this doc)
lambda/js/js_mir_context.hpp JsClassEntry, JsClassMethodEntry, JsStaticFieldEntry, JsInstanceFieldEntry, ctor_prop_* arrays, collection caps.
lambda/js/js_mir_function_collection_class_inference.cpp Class collection, jm_scan_ctor_props (A5), jm_find_class; exact-binding-only native-call resolution.
lambda/js/js_mir_statement_lowering.cpp Class-object emission, prototype/method/static install, static {} blocks, constructor prologue/body/epilogue, new C() lowering.
lambda/js/js_mir_module_batch_lowering.cpp Phase-3 top-level class pre-pass (mirror of the function-body path).
lambda/js/js_mir_expression_lowering.cpp jm_is_private_name/jm_class_private_name, super(...)/super.x lowering.
lambda/js/js_runtime.cpp js_constructor_create_object[_shaped[_cached]], common js_construct_value, js_super_call_native, brand helpers, constructor return rules, active new.target.
lambda/js/js_class.h JsClass enum + identity helpers (shared with JS_06).

Appendix B — Related documents