Skip to content

Commit e7dbb48

Browse files
authored
Make methods, getters, and setters non-constructible (#21)
Concise methods, getters, setters, and class methods are created without a [[Construct]] internal method (§10.2 / §15.x), so `new obj.method()`, `new (getter)()`, and `new c.foo()` must throw — but they didn't, because a method was indistinguishable from a normal function once compiled. Store the [[Construct]] capability on FuncTemplate as a positive `is_constructor` bit (the user-function analogue of NativeFunction's `constructible`; cf. QuickJS `is_constructor` / JSC `ConstructAbility`), computed at emit time from the function's syntactic kind: True for normal function declarations/expressions and class constructors; False for arrows, generators, async functions, and methods/getters/setters (object-literal and class). do_construct now gates on `!is_constructor` instead of re-deriving arrow/generator/async, and object.is_constructor reads the bit directly.
1 parent da3c64a commit e7dbb48

9 files changed

Lines changed: 130 additions & 21 deletions

File tree

src/arc/compiler.gleam

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ fn compile_module_with_scope(
8989
False,
9090
False,
9191
False,
92+
// Script / module body — not a constructor.
93+
False,
9294
None,
9395
resolved.this_slot,
9496
)
@@ -214,6 +216,8 @@ pub fn compile_eval_direct(
214216
False,
215217
False,
216218
False,
219+
// Eval body — not a constructor.
220+
False,
217221
None,
218222
resolved.this_slot,
219223
))
@@ -388,6 +392,8 @@ fn compile_script(
388392
False,
389393
False,
390394
False,
395+
// Script / module body — not a constructor.
396+
False,
391397
None,
392398
resolved.this_slot,
393399
)
@@ -522,6 +528,7 @@ fn compile_child(
522528
child.is_derived_constructor,
523529
child.is_generator,
524530
child.is_async,
531+
child.is_constructor,
525532
local_names,
526533
resolved.this_slot,
527534
)

src/arc/compiler/emit.gleam

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ pub type CompiledChild {
9393
is_derived_constructor: Bool,
9494
is_generator: Bool,
9595
is_async: Bool,
96+
/// Stored [[Construct]] capability: True for normal functions and class
97+
/// constructors; False for arrows, generators, async, and methods /
98+
/// getters / setters. (cf. QuickJS `is_constructor` / JSC `ConstructAbility`.)
99+
is_constructor: Bool,
96100
/// True if this function contains a syntactic `eval(...)` call with
97101
/// identifier callee. Such functions get all locals boxed and a
98102
/// name→index table stored on FuncTemplate so direct eval can see them.
@@ -1026,6 +1030,8 @@ fn collect_hoisted_funcs(
10261030
False,
10271031
is_gen,
10281032
is_async,
1033+
// Function declaration: a constructor unless gen/async.
1034+
!is_gen && !is_async,
10291035
)
10301036
let #(e, idx) = add_child_function(e, child)
10311037
#(e, [#(name, idx), ..funcs])
@@ -1287,6 +1293,7 @@ fn compile_function_body(
12871293
is_arrow: Bool,
12881294
is_generator: Bool,
12891295
is_async: Bool,
1296+
is_constructor: Bool,
12901297
) -> CompiledChild {
12911298
let stmts = case body {
12921299
ast.BlockStatement(s) -> s
@@ -1429,6 +1436,7 @@ fn compile_function_body(
14291436
is_derived_constructor: False,
14301437
is_generator:,
14311438
is_async:,
1439+
is_constructor:,
14321440
has_eval_call: e.has_eval_call,
14331441
references_this: e.references_this,
14341442
)
@@ -2398,7 +2406,17 @@ fn emit_expr(e: Emitter, expr: ast.Expression) -> Result(Emitter, EmitError) {
23982406
// Function expression
23992407
ast.FunctionExpression(name, params, body, is_gen, is_async) -> {
24002408
let child =
2401-
compile_function_body(e, name, params, body, False, is_gen, is_async)
2409+
compile_function_body(
2410+
e,
2411+
name,
2412+
params,
2413+
body,
2414+
False,
2415+
is_gen,
2416+
is_async,
2417+
// Normal function expression: a constructor unless gen/async.
2418+
!is_gen && !is_async,
2419+
)
24022420
let #(e, idx) = add_child_function(e, child)
24032421
Ok(emit_ir(e, IrMakeClosure(idx)))
24042422
}
@@ -2411,7 +2429,16 @@ fn emit_expr(e: Emitter, expr: ast.Expression) -> Result(Emitter, EmitError) {
24112429
ast.ArrowBodyBlock(stmt) -> stmt
24122430
}
24132431
let child =
2414-
compile_function_body(e, None, params, body_stmt, True, False, is_async)
2432+
compile_function_body(
2433+
e,
2434+
None,
2435+
params,
2436+
body_stmt,
2437+
True,
2438+
False,
2439+
is_async,
2440+
False,
2441+
)
24152442
let #(e, idx) = add_child_function(e, child)
24162443
Ok(emit_ir(e, IrMakeClosure(idx)))
24172444
}
@@ -2693,6 +2720,8 @@ fn emit_named_expr(
26932720
False,
26942721
is_gen,
26952722
is_async,
2723+
// Named function expression: a constructor unless gen/async.
2724+
!is_gen && !is_async,
26962725
)
26972726
let #(e, idx) = add_child_function(e, child)
26982727
Ok(emit_ir(e, IrMakeClosure(idx)))
@@ -2713,6 +2742,8 @@ fn emit_named_expr(
27132742
True,
27142743
False,
27152744
is_async,
2745+
// Arrow function — not a constructor.
2746+
False,
27162747
)
27172748
let #(e, idx) = add_child_function(e, child)
27182749
Ok(emit_ir(e, IrMakeClosure(idx)))
@@ -2727,6 +2758,36 @@ fn emit_named_expr(
27272758

27282759
/// Emit one property in an object literal. Object is already on the stack.
27292760
/// All handlers leave the object on the stack for the next property.
2761+
/// Compile an object-literal *method* value — a concise method, getter, or
2762+
/// setter — so it is NOT a constructor (methods/accessors have no
2763+
/// [[Construct]], so `new o.m()` must throw). These values are always
2764+
/// FunctionExpressions; fall back defensively for anything unexpected.
2765+
fn emit_method_value(
2766+
e: Emitter,
2767+
value: ast.Expression,
2768+
name: Option(String),
2769+
) -> Result(Emitter, EmitError) {
2770+
case value {
2771+
ast.FunctionExpression(_, params, body, is_gen, is_async) -> {
2772+
let child =
2773+
compile_function_body(
2774+
e,
2775+
name,
2776+
params,
2777+
body,
2778+
False,
2779+
is_gen,
2780+
is_async,
2781+
// method / getter / setter — not a constructor
2782+
False,
2783+
)
2784+
let #(e, idx) = add_child_function(e, child)
2785+
Ok(emit_ir(e, IrMakeClosure(idx)))
2786+
}
2787+
_ -> emit_expr(e, value)
2788+
}
2789+
}
2790+
27302791
fn emit_object_property(
27312792
e: Emitter,
27322793
prop: ast.Property,
@@ -2739,16 +2800,22 @@ fn emit_object_property(
27392800
value:,
27402801
kind: ast.Init,
27412802
computed: False,
2803+
method:,
27422804
..,
27432805
)
27442806
| ast.Property(
27452807
key: ast.StringExpression(name),
27462808
value:,
27472809
kind: ast.Init,
27482810
computed: False,
2811+
method:,
27492812
..,
27502813
) -> {
2751-
use e <- result.map(emit_named_expr(e, value, name))
2814+
// `{ m() {} }` is a method (not a constructor); `{ x: v }` is data.
2815+
use e <- result.map(case method {
2816+
True -> emit_method_value(e, value, Some(name))
2817+
False -> emit_named_expr(e, value, name)
2818+
})
27522819
emit_ir(e, IrDefineField(name))
27532820
}
27542821

@@ -2760,19 +2827,26 @@ fn emit_object_property(
27602827
value:,
27612828
kind: ast.Init,
27622829
computed: False,
2830+
method:,
27632831
..,
27642832
) -> {
27652833
let e = push_const(e, JsNumber(Finite(n)))
2766-
use e <- result.map(emit_expr(e, value))
2834+
use e <- result.map(case method {
2835+
True -> emit_method_value(e, value, None)
2836+
False -> emit_expr(e, value)
2837+
})
27672838
emit_ir(e, IrDefineFieldComputed)
27682839
}
27692840

27702841
// Computed key: {[expr]: value}
27712842
// Emit key, emit value, IrDefineFieldComputed — pops both, keeps obj.
27722843
// The VM handles ToPropertyKey (Symbol preserved, else ToString).
2773-
ast.Property(key:, value:, kind: ast.Init, computed: True, ..) -> {
2844+
ast.Property(key:, value:, kind: ast.Init, computed: True, method:, ..) -> {
27742845
use e <- result.try(emit_expr(e, key))
2775-
use e <- result.map(emit_expr(e, value))
2846+
use e <- result.map(case method {
2847+
True -> emit_method_value(e, value, None)
2848+
False -> emit_expr(e, value)
2849+
})
27762850
emit_ir(e, IrDefineFieldComputed)
27772851
}
27782852

@@ -2800,7 +2874,7 @@ fn emit_object_property(
28002874
computed: False,
28012875
..,
28022876
) -> {
2803-
use e <- result.map(emit_named_expr(e, value, "get " <> name))
2877+
use e <- result.map(emit_method_value(e, value, Some("get " <> name)))
28042878
emit_ir(e, IrDefineAccessor(name, opcode.Getter))
28052879
}
28062880

@@ -2819,20 +2893,20 @@ fn emit_object_property(
28192893
computed: False,
28202894
..,
28212895
) -> {
2822-
use e <- result.map(emit_named_expr(e, value, "set " <> name))
2896+
use e <- result.map(emit_method_value(e, value, Some("set " <> name)))
28232897
emit_ir(e, IrDefineAccessor(name, opcode.Setter))
28242898
}
28252899

28262900
// Computed or exotic-key getter/setter: { get [expr]() {} }
28272901
// Stack: emit key, emit fn → DefineAccessorComputed
28282902
ast.Property(key:, value:, kind: ast.Get, ..) -> {
28292903
use e <- result.try(emit_expr(e, key))
2830-
use e <- result.map(emit_expr(e, value))
2904+
use e <- result.map(emit_method_value(e, value, None))
28312905
emit_ir(e, IrDefineAccessorComputed(opcode.Getter))
28322906
}
28332907
ast.Property(key:, value:, kind: ast.Set, ..) -> {
28342908
use e <- result.try(emit_expr(e, key))
2835-
use e <- result.map(emit_expr(e, value))
2909+
use e <- result.map(emit_method_value(e, value, None))
28362910
emit_ir(e, IrDefineAccessorComputed(opcode.Setter))
28372911
}
28382912

@@ -4152,6 +4226,8 @@ fn compile_derived_class(
41524226
False,
41534227
False,
41544228
False,
4229+
// Class constructor — IS a constructor.
4230+
True,
41554231
)
41564232
// Mark as derived constructor
41574233
let child = CompiledChild(..child, is_derived_constructor: True)
@@ -4236,6 +4312,8 @@ fn emit_class_methods(
42364312
False,
42374313
is_gen,
42384314
is_async,
4315+
// Class method / getter / setter — not a constructor.
4316+
False,
42394317
)
42404318
let #(e, idx) = add_child_function(e, child)
42414319
let e = emit_ir(e, IrMakeClosure(idx))
@@ -4287,7 +4365,17 @@ fn emit_computed_class_method(
42874365
}
42884366
use e <- result.try(emit_expr(e, key))
42894367
let child =
4290-
compile_function_body(e, None, params, body, False, is_gen, is_async)
4368+
compile_function_body(
4369+
e,
4370+
None,
4371+
params,
4372+
body,
4373+
False,
4374+
is_gen,
4375+
is_async,
4376+
// Computed class method / getter / setter — not a constructor.
4377+
False,
4378+
)
42914379
let #(e, idx) = add_child_function(e, child)
42924380
let e = emit_ir(e, IrMakeClosure(idx))
42934381
let e = case kind {
@@ -4410,6 +4498,8 @@ fn compile_base_class(
44104498
False,
44114499
False,
44124500
False,
4501+
// Class constructor — IS a constructor.
4502+
True,
44134503
)
44144504
let #(e, ctor_idx) = add_child_function(e, child)
44154505

src/arc/compiler/resolve.gleam

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub fn resolve(
5151
is_derived_constructor: Bool,
5252
is_generator: Bool,
5353
is_async: Bool,
54+
is_constructor: Bool,
5455
local_names: Option(List(#(String, Int))),
5556
this_slot: Option(Int),
5657
) -> FuncTemplate {
@@ -69,6 +70,7 @@ pub fn resolve(
6970
is_derived_constructor:,
7071
is_generator:,
7172
is_async:,
73+
is_constructor:,
7274
local_names:,
7375
this_slot:,
7476
)

src/arc/vm/exec/call.gleam

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,10 +1333,12 @@ pub fn do_construct(
13331333
dispatch_fn: DispatchNativeFn,
13341334
) -> Result(State, #(StepResult, JsValue, State)) {
13351335
case heap.read(state.heap, ctor_ref) {
1336+
// Functions lacking [[Construct]] — arrows, generators, async functions,
1337+
// and methods/getters/setters — throw when used with `new` (§7.2.4). The
1338+
// capability is stored on the template (set at compile from the syntactic
1339+
// kind); class constructors and normal functions carry it.
13361340
Some(ObjectSlot(kind: FunctionObject(func_template:, ..), ..))
1337-
if func_template.is_arrow
1338-
|| func_template.is_generator
1339-
|| func_template.is_async
1341+
if !func_template.is_constructor
13401342
->
13411343
state.throw_type_error(
13421344
state,

src/arc/vm/exec/interpreter.gleam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ fn construct_fn_callback(
112112
is_derived_constructor: False,
113113
is_generator: False,
114114
is_async: False,
115+
is_constructor: False,
115116
local_names: None,
116117
this_slot: None,
117118
)

src/arc/vm/ops/object.gleam

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,14 +1652,12 @@ pub fn is_constructor(heap: Heap, value: JsValue) -> Bool {
16521652
JsObject(ref) ->
16531653
case heap.read(heap, ref) {
16541654
// ECMAScript function: a constructor unless it's an arrow, generator,
1655-
// or async function — those lack [[Construct]] (§10.2). Derived from
1656-
// the function's own template (intrinsic, already-stored data).
1657-
// NOTE: concise methods/getters/setters are also non-constructors per
1658-
// spec, but FuncTemplate doesn't yet flag them (follow-up).
1655+
// ECMAScript function: read the [[Construct]] capability stored on its
1656+
// template, set at compile time from the function's syntactic kind
1657+
// (normal functions + class constructors carry it; arrows, generators,
1658+
// async functions, and methods/getters/setters do not).
16591659
Some(ObjectSlot(kind: FunctionObject(func_template:, ..), ..)) ->
1660-
!func_template.is_arrow
1661-
&& !func_template.is_generator
1662-
&& !func_template.is_async
1660+
func_template.is_constructor
16631661
// Built-in function: read the [[Construct]] capability stored on the
16641662
// slot — set True for constructor intrinsics at allocation, and copied
16651663
// from its target by `bind` (§10.4.1.3 step 6). A native's dispatch tag

src/arc/vm/value.gleam

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ pub type FuncTemplate {
103103
is_derived_constructor: Bool,
104104
is_generator: Bool,
105105
is_async: Bool,
106+
/// Stored [[Construct]] capability (ES2024 §7.2.4), the user-function
107+
/// analogue of NativeFunction's `constructible`. True for normal function
108+
/// declarations/expressions and class constructors; False for arrows,
109+
/// generators, async functions, and methods/getters/setters. Computed at
110+
/// compile time from the function's syntactic kind (cf. QuickJS
111+
/// `is_constructor` / JSC `ConstructAbility`).
112+
is_constructor: Bool,
106113
/// Present only for functions that contain a direct eval call.
107114
/// Maps variable name → local slot index. All such locals are boxed
108115
/// (BoxSlot refs), so direct eval can read/write them by index.

test/heap_test.gleam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fn dummy_template() -> FuncTemplate {
2424
is_derived_constructor: False,
2525
is_generator: False,
2626
is_async: False,
27+
is_constructor: False,
2728
local_names: None,
2829
this_slot: None,
2930
)

test/vm_test.gleam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fn make_func(
6161
is_derived_constructor: False,
6262
is_generator: False,
6363
is_async: False,
64+
is_constructor: True,
6465
local_names: None,
6566
this_slot: None,
6667
)

0 commit comments

Comments
 (0)