Skip to content
9 changes: 9 additions & 0 deletions interpreter/runtime/async.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,15 @@ pub fn setup_async_function_constructor(
}
_ => ()
}
// §27.7.2: %AsyncFunction%.[[Prototype]] is the intrinsic %Function% constructor.
let func_ctor = match env.bindings.get("Function") {
Some(binding) => binding.value
None => Null
}
match async_func_ctor {
Object(data) => data.prototype = func_ctor
_ => ()
}
// Note: AsyncFunction is NOT a global property per spec (section 19).
// It is only reachable via (async function(){}).constructor.
// We store it as an internal binding so async functions get the right prototype.
Expand Down
186 changes: 175 additions & 11 deletions interpreter/runtime/generator.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,162 @@ fn complete_generator(gen : GeneratorObject) -> Unit {
gen.try_resume_pending_error = None
}

///|
/// §20.2.1.1.1 step 17a: Returns true if expr contains a YieldExpression at
/// any level, stopping at nested function boundaries (which have their own
/// generator context and may use yield freely).
fn expr_contains_yield(expr : @ast.Expr) -> Bool {
match expr {
YieldExpr(_, _, _) => true
Binary(_, a, b, _) => expr_contains_yield(a) || expr_contains_yield(b)
Unary(_, e, _)
| UpdateExpr(_, e, _, _)
| Grouping(e, _)
| SpreadExpr(e, _) => expr_contains_yield(e)
Assign(_, e, _) => expr_contains_yield(e)
MemberAssign(obj, _, val, _) =>
expr_contains_yield(obj) || expr_contains_yield(val)
ComputedAssign(obj, key, val, _) =>
expr_contains_yield(obj) ||
expr_contains_yield(key) ||
expr_contains_yield(val)
CompoundAssign(_, a, b, _)
| Comma(a, b, _)
| WebCompatCallAssign(a, b, _) =>
expr_contains_yield(a) || expr_contains_yield(b)
Member(obj, _, _) | OptionalMember(obj, _, _) | ChainMember(obj, _, _) =>
expr_contains_yield(obj)
ComputedMember(obj, key, _)
| OptionalComputedMember(obj, key, _)
| ChainComputedMember(obj, key, _) =>
expr_contains_yield(obj) || expr_contains_yield(key)
SuperComputedMember(e, _) => expr_contains_yield(e)
Ternary(cond, t, f, _) =>
expr_contains_yield(cond) ||
expr_contains_yield(t) ||
expr_contains_yield(f)
Call(f, args, _) | NewExpr(f, args, _) | OptionalCall(f, args, _) =>
expr_contains_yield(f) ||
args.iter().any(fn(a) { expr_contains_yield(a) })
SuperCall(args, _) => args.iter().any(fn(a) { expr_contains_yield(a) })
ArrayLit(elems, _) => elems.iter().any(fn(e) { expr_contains_yield(e) })
ObjectLit(props, _) =>
props
.iter()
.any(fn(p) { expr_contains_yield(p.key) || expr_contains_yield(p.value) })
TemplateLit(_, exprs, _) =>
exprs.iter().any(fn(e) { expr_contains_yield(e) })
TaggedTemplate(tag, _, exprs, _) =>
expr_contains_yield(tag) ||
exprs.iter().any(fn(e) { expr_contains_yield(e) })
// DestructureAssign LHS pattern can contain computed keys or defaults with
// yield expressions ({[yield]: a} = {}, {a = yield} = {}) — scan both sides.
DestructureAssign(pat, rhs, _) =>
pattern_contains_yield(pat) || expr_contains_yield(rhs)
// Arrow params (including defaults) are evaluated in the enclosing scope,
// so ArrowFuncExt/AsyncArrowFuncExt are NOT full boundaries for yield.
// Simple ArrowFunc/AsyncArrowFunc have no defaults, so false is safe.
ArrowFuncExt(params, _, _, _, _) | AsyncArrowFuncExt(params, _, _, _, _) =>
params_contain_yield(params)
// Nested function boundaries — yield inside is not the outer generator's
FuncExpr(_, _, _, _, _)
| FuncExprExt(_, _, _, _, _, _)
| GeneratorExpr(_, _, _, _, _)
| GeneratorExprExt(_, _, _, _, _, _)
| AsyncFuncExpr(_, _, _, _, _)
| AsyncFuncExprExt(_, _, _, _, _, _)
| AsyncArrowFunc(_, _, _, _)
| AsyncGeneratorExpr(_, _, _, _, _)
| AsyncGeneratorExprExt(_, _, _, _, _, _)
| ArrowFunc(_, _, _, _) => false
// Class is NOT a full boundary: heritage and computed member keys are
// evaluated in the surrounding scope (ES §15.7.1 ClassElementName).
ClassExpr(_, superclass, members, _, _) => {
let heritage = match superclass {
Some(e) => expr_contains_yield(e)
None => false
}
let computed = members
.iter()
.any(fn(m) {
match m {
Method(cm) => cm.computed && expr_contains_yield(cm.key)
Field(cf) => cf.computed && expr_contains_yield(cf.key)
}
})
heritage || computed
}
_ => false
}
}

///|
// Walk a binding pattern for yield expressions in default values and computed
// property keys. Property name strings (e.g. the "yield" in {yield: x}) are
// not expressions and are intentionally not flagged here.
fn pattern_contains_yield(pat : @ast.Pattern) -> Bool {
match pat {
IdentPat(_) => false
DefaultPat(inner, default_expr) =>
pattern_contains_yield(inner) || expr_contains_yield(default_expr)
ArrayPat(elements, rest) => {
let elems_yield = elements
.iter()
.any(fn(e) {
match e {
Some(p) => pattern_contains_yield(p)
None => false
}
})
let rest_yield = match rest {
Some(r) => pattern_contains_yield(r)
None => false
}
elems_yield || rest_yield
}
AssignTarget(e) => expr_contains_yield(e)
ObjectPat(props, rest) => {
let props_yield = props
.iter()
.any(fn(prop) {
// Check computed key expression (e.g. {[yield]: x}), shorthand or
// keyed default (e.g. {x = yield} or {a: b = yield}), and nested pat.
let computed_yield = match prop.computed_key {
Some(e) => expr_contains_yield(e)
None => false
}
let default_yield = match prop.default_val {
Some(e) => expr_contains_yield(e)
None => false
}
computed_yield || default_yield || pattern_contains_yield(prop.value)
})
let rest_yield = match rest {
Some(r) => pattern_contains_yield(r)
None => false
}
props_yield || rest_yield
}
}
}

///|
fn params_contain_yield(params : Array[@ast.Param]) -> Bool {
params
.iter()
.any(fn(p) {
let default_has_yield = match p.default_val {
Some(expr) => expr_contains_yield(expr)
None => false
}
let pattern_has_yield = match p.pattern {
Some(pat) => pattern_contains_yield(pat)
None => false
}
default_has_yield || pattern_has_yield
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

///|
/// Set up the GeneratorFunction constructor on the given interpreter.
/// GeneratorFunction("a", "yield a") creates a generator function,
Expand Down Expand Up @@ -240,22 +396,22 @@ pub fn setup_generator_function_constructor(
length=1,
realm_state=env.realm_state,
fn(interp, _this, args) raise {
// Last arg is body, previous args are parameter names. With no
// arguments, the synthesized body and parameter list are both empty.
let body_str = if args.length() == 0 {
""
} else {
match args[args.length() - 1] {
String_(s) => s
_ => args[args.length() - 1].to_string()
}
}
// §20.2.1.1.1: coerce params first (step 5a), body last (step 6).
// Last arg is body, previous args are parameter names.
let param_count = if args.length() == 0 { 0 } else { args.length() - 1 }
let param_parts : Array[String] = []
for i = 0; i < param_count; i = i + 1 {
match args[i] {
String_(s) => param_parts.push(s)
_ => param_parts.push(args[i].to_string())
_ => param_parts.push(interp.to_js_string(args[i]))
}
}
let body_str = if args.length() == 0 {
""
} else {
match args[args.length() - 1] {
String_(s) => s
_ => interp.to_js_string(args[args.length() - 1])
}
}
let params_str = param_parts.join(",")
Expand All @@ -282,6 +438,14 @@ pub fn setup_generator_function_constructor(
)
}
GeneratorDeclExt(_, params, rest_param, body, _, source_text) => {
// §20.2.1.1.1 step 17a: YieldExpression in generator params → SyntaxError.
// The parser now enters generator context for params, so `yield` in
// default values is parsed as YieldExpr and caught here via AST walk.
if params_contain_yield(params) {
raise @errors.SyntaxError(
message="Generator function parameters may not contain yield expressions",
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let body_strict = @static_semantics.has_use_strict(body)
validate_function_constructor_params_ext(params, rest_param, body)
interp.validate_block_early_errors(body, body_strict)
Expand Down
95 changes: 88 additions & 7 deletions interpreter/stdlib/builtins_function.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,42 @@ fn func_kind_prefix(class_name : String) -> String {
}
}

///|
// §20.2.1.1.1 step 27a: create a .prototype object that inherits from
// Object.prototype. Extracted so all three Function-constructor return paths
// stay consistent if the construction ever changes.
fn make_function_prototype(realm_state : @runtime.RealmState) -> Value {
let object_proto = @runtime.get_obj_proto(realm_state=Some(realm_state))
Value::Object({
bag: @runtime.PropertyBag(),
prototype: object_proto,
callable: None,
class_name: "Object",
extensible: true,
arraybuffer_state: None,
})
}

///|
// §20.2.1.1.1 step 27a: wire result.prototype.constructor = result so the
// constructed function's prototype correctly back-references the function.
fn wire_constructor_to_prototype(proto : Value, result : Value) -> Unit {
match proto {
Object(proto_data) => {
proto_data.bag.properties["constructor"] = result
proto_data.bag.descriptors["constructor"] = {
writable: true,
enumerable: false,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
}
_ => ()
}
}

///|
/// ES262 §10.2.4 ExpectedArgumentCount for extended parameter lists:
/// count parameters before the first default initializer or rest parameter.
Expand Down Expand Up @@ -422,9 +458,11 @@ fn setup_function_builtins(
if prog.stmts.length() > 0 {
match prog.stmts[0] {
FuncDecl(_, params, body, _, _) => {
let proto = make_function_prototype(realm_state)
let fn_props : Map[String, Value] = {}
fn_props["length"] = Value::Number(params.length().to_double())
fn_props["name"] = Value::String_("anonymous")
fn_props["prototype"] = proto
// Validate static early errors against the function's strict context.
let body_strict = @static_semantics.has_use_strict(body)
@runtime.validate_function_constructor_params(
Expand All @@ -434,12 +472,23 @@ fn setup_function_builtins(
body,
)
interp.validate_block_early_errors(body, body_strict)
return @runtime.stamp_function_realm(
let result = @runtime.stamp_function_realm(
Value::Object({
bag: {
properties: fn_props,
symbol_properties: {},
descriptors: { "length": nf_desc, "name": nf_desc },
descriptors: {
"length": nf_desc,
"name": nf_desc,
"prototype": {
writable: true,
enumerable: false,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
},
},
symbol_descriptors: {},
internal_slots: {},
},
Expand All @@ -463,24 +512,39 @@ fn setup_function_builtins(
}),
realm_state=Some(realm_state),
)
wire_constructor_to_prototype(proto, result)
return result
}
FuncDeclExt(_, params, rest_param, body, _, _) => {
let proto = make_function_prototype(realm_state)
let fn_props : Map[String, Value] = {}
let fn_length = expected_argument_count_ext(params)
fn_props["length"] = Value::Number(fn_length.to_double())
fn_props["name"] = Value::String_("anonymous")
fn_props["prototype"] = proto
// Validate static early errors against the function's strict context.
let body_strict = @static_semantics.has_use_strict(body)
@runtime.validate_function_constructor_params_ext(
params, rest_param, body,
)
interp.validate_block_early_errors(body, body_strict)
return @runtime.stamp_function_realm(
let result = @runtime.stamp_function_realm(
Value::Object({
bag: {
properties: fn_props,
symbol_properties: {},
descriptors: { "length": nf_desc, "name": nf_desc },
descriptors: {
"length": nf_desc,
"name": nf_desc,
"prototype": {
writable: true,
enumerable: false,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
},
},
symbol_descriptors: {},
internal_slots: {},
},
Expand All @@ -504,20 +568,35 @@ fn setup_function_builtins(
}),
realm_state=Some(realm_state),
)
wire_constructor_to_prototype(proto, result)
return result
}
_ => ()
}
}
// Fallback: empty function
@runtime.stamp_function_realm(
// Fallback: empty function (defensive — synthesized source always parses)
let fb_proto = make_function_prototype(realm_state)
let fb_result = @runtime.stamp_function_realm(
Value::Object({
bag: {
properties: {
"length": Value::Number(0.0),
"name": Value::String_("anonymous"),
"prototype": fb_proto,
},
symbol_properties: {},
descriptors: { "length": nf_desc, "name": nf_desc },
descriptors: {
"length": nf_desc,
"name": nf_desc,
"prototype": {
writable: true,
enumerable: false,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
},
},
symbol_descriptors: {},
internal_slots: {},
},
Expand All @@ -529,6 +608,8 @@ fn setup_function_builtins(
}),
realm_state=Some(realm_state),
)
wire_constructor_to_prototype(fb_proto, fb_result)
fb_result
}),
),
class_name: "Function",
Expand Down
Loading