From 109a50a155750907fbd3fc0664138745e1cc5af4 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Fri, 1 May 2026 17:58:42 -0700 Subject: [PATCH] add proper brand based private class support --- include/common.h | 1 + include/silver/compiler.h | 19 ++ include/silver/opcode.h | 9 +- src/silver/compile_ctx.c | 2 + src/silver/compiler.c | 323 ++++++++++++++++++++++++++++++++-- src/silver/engine.c | 9 +- src/silver/ops/objects.h | 4 - src/silver/ops/private.h | 226 ++++++++++++++++++++++-- tests/test_private_brands.cjs | 162 +++++++++++++++++ vendor/wirecall.wrap | 7 + 10 files changed, 722 insertions(+), 40 deletions(-) create mode 100644 tests/test_private_brands.cjs create mode 100644 vendor/wirecall.wrap diff --git a/include/common.h b/include/common.h index 1b9a5fe..8b74a7e 100644 --- a/include/common.h +++ b/include/common.h @@ -27,6 +27,7 @@ X(SLOT_PROXY_REF) \ X(SLOT_BUILTIN) \ X(SLOT_BRAND) \ + X(SLOT_PRIVATE_ELEMENTS) \ X(SLOT_DATA) \ X(SLOT_EVENT_MAX_LISTENERS) \ X(SLOT_CTOR) \ diff --git a/include/silver/compiler.h b/include/silver/compiler.h index 595b8ab..ea352d5 100644 --- a/include/silver/compiler.h +++ b/include/silver/compiler.h @@ -68,6 +68,24 @@ typedef struct sv_line_table { int count; } sv_line_table_t; +typedef struct { + const char *name; + uint32_t len; + uint8_t kind; + bool is_static; + bool has_getter; + bool has_setter; + struct sv_compiler *owner; + int local; +} sv_private_name_t; + +typedef struct sv_private_scope { + struct sv_private_scope *parent; + sv_private_name_t *names; + int count; + int cap; +} sv_private_scope_t; + typedef struct sv_compiler { ant_t *js; const char *filename; @@ -151,6 +169,7 @@ typedef struct sv_compiler { const_dedup_entry_t *const_dedup; sv_line_table_t *line_table; + sv_private_scope_t *private_scope; } sv_compiler_t; diff --git a/include/silver/opcode.h b/include/silver/opcode.h index d48df7a..01b6f87 100644 --- a/include/silver/opcode.h +++ b/include/silver/opcode.h @@ -91,9 +91,11 @@ OP_DEF( GET_LENGTH, 1, 1, 1, none) /* obj -> length */ OP_DEF( GET_FIELD_OPT, 5, 1, 1, atom) /* null-safe obj -> val */ OP_DEF( GET_ELEM_OPT, 1, 2, 1, none) /* null-safe obj key -> val */ -OP_DEF( GET_PRIVATE, 1, 2, 1, none) /* obj prop -> value */ -OP_DEF( PUT_PRIVATE, 1, 3, 0, none) /* obj value prop -> */ -OP_DEF( DEF_PRIVATE, 1, 3, 1, none) /* obj prop value -> obj */ +OP_DEF( GET_PRIVATE, 1, 2, 1, none) /* obj private_name -> value */ +OP_DEF( GET_PRIVATE_OPT, 1, 2, 1, none) /* obj private_name -> value|undefined */ +OP_DEF( PUT_PRIVATE, 1, 3, 1, none) /* obj value private_name -> value */ +OP_DEF( DEF_PRIVATE, 2, 3, 1, u8) /* obj private_name value -> obj */ +OP_DEF( HAS_PRIVATE, 1, 2, 1, none) /* obj private_name -> bool */ OP_DEF( GET_SUPER, 1, 1, 1, none) /* obj -> super */ OP_DEF( GET_SUPER_VAL, 1, 3, 1, none) /* this obj prop -> value */ @@ -229,7 +231,6 @@ OP_DEF( COPY_DATA_PROPS, 2, 3, 3, u8) /* Object.assign-like */ OP_DEF( DEFINE_CLASS, 14, 2, 2, atom_u8) /* parent ctor -> ctor proto */ OP_DEF( DEFINE_CLASS_COMP, 14, 3, 3, atom_u8) /* computed name variant */ -OP_DEF( ADD_BRAND, 1, 2, 0, none) /* this_obj home_obj -> (private brand) */ OP_DEF( TO_OBJECT, 1, 1, 1, none) /* coerce to object wrapper */ OP_DEF( TO_PROPKEY, 1, 1, 1, none) /* coerce to string/symbol */ diff --git a/src/silver/compile_ctx.c b/src/silver/compile_ctx.c index fcba723..72b7fb6 100644 --- a/src/silver/compile_ctx.c +++ b/src/silver/compile_ctx.c @@ -44,6 +44,7 @@ void sv_compile_ctx_init_root( ctx->super_local = -1; ctx->using_stack_local = -1; ctx->line_table = line_table; + ctx->private_scope = NULL; } void sv_compile_ctx_init_child( @@ -70,6 +71,7 @@ void sv_compile_ctx_init_child( ctx->super_local = -1; ctx->using_stack_local = -1; ctx->param_count = node ? node->args.count : 0; + ctx->private_scope = enclosing->private_scope; } void sv_compile_ctx_cleanup(sv_compiler_t *ctx) { diff --git a/src/silver/compiler.c b/src/silver/compiler.c index dbfc747..2605250 100644 --- a/src/silver/compiler.c +++ b/src/silver/compiler.c @@ -270,6 +270,135 @@ static inline void compile_static_property_key(sv_compiler_t *c, sv_ast_t *key) compile_expr(c, key); } +enum { + SV_COMP_PRIVATE_FIELD = 0, + SV_COMP_PRIVATE_METHOD = 1, + SV_COMP_PRIVATE_GETTER = 3, + SV_COMP_PRIVATE_SETTER = 4 +}; + +static int local_to_frame_slot(sv_compiler_t *c, int local_idx); +static int add_upvalue(sv_compiler_t *c, uint16_t index, bool is_local, bool is_const); +static void emit_get_local(sv_compiler_t *c, int local_idx); + +static inline bool is_private_name_node(const sv_ast_t *node) { + return node && node->type == N_IDENT && node->str && node->len > 0 && node->str[0] == '#'; +} + +static sv_private_name_t *private_scope_find_current( + sv_private_scope_t *scope, const char *name, uint32_t len +) { + if (!scope || !name) return NULL; + for (int i = 0; i < scope->count; i++) { + sv_private_name_t *p = &scope->names[i]; + if (p->len == len && memcmp(p->name, name, len) == 0) return p; + } + return NULL; +} + +static sv_private_name_t *private_scope_resolve( + sv_compiler_t *c, const char *name, uint32_t len +) { + for (sv_private_scope_t *scope = c->private_scope; scope; scope = scope->parent) { + sv_private_name_t *p = private_scope_find_current(scope, name, len); + if (p) return p; + } + return NULL; +} + +static bool private_scope_add( + sv_compiler_t *c, sv_private_scope_t *scope, + sv_ast_t *name, uint8_t kind, bool is_static +) { + if (!is_private_name_node(name)) return true; + + if (name->len == 12 && memcmp(name->str, "#constructor", 12) == 0) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Classes may not declare private constructor names"); + return false; + } + + sv_private_name_t *existing = private_scope_find_current(scope, name->str, name->len); + bool is_accessor = kind == SV_COMP_PRIVATE_GETTER || kind == SV_COMP_PRIVATE_SETTER; + if (existing) { + bool existing_accessor = existing->kind == SV_COMP_PRIVATE_GETTER || + existing->kind == SV_COMP_PRIVATE_SETTER; + if (!is_accessor || !existing_accessor || existing->is_static != is_static) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Duplicate private name '%.*s'", (int)name->len, name->str); + return false; + } + if ((kind == SV_COMP_PRIVATE_GETTER && existing->has_getter) || + (kind == SV_COMP_PRIVATE_SETTER && existing->has_setter)) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Duplicate private accessor '%.*s'", (int)name->len, name->str); + return false; + } + if (kind == SV_COMP_PRIVATE_GETTER) existing->has_getter = true; + if (kind == SV_COMP_PRIVATE_SETTER) existing->has_setter = true; + return true; + } + + if (scope->count >= scope->cap) { + int new_cap = scope->cap ? scope->cap * 2 : 8; + sv_private_name_t *new_names = realloc(scope->names, (size_t)new_cap * sizeof(*scope->names)); + if (!new_names) return false; + scope->names = new_names; + scope->cap = new_cap; + } + + sv_private_name_t *p = &scope->names[scope->count++]; + *p = (sv_private_name_t){ + .name = name->str, + .len = name->len, + .kind = kind, + .is_static = is_static, + .has_getter = kind == SV_COMP_PRIVATE_GETTER, + .has_setter = kind == SV_COMP_PRIVATE_SETTER, + .owner = NULL, + .local = -1 + }; + return true; +} + +static int resolve_private_upvalue(sv_compiler_t *c, sv_private_name_t *p) { + if (!c->enclosing || !p || !p->owner || p->local < 0) return -1; + + if (c->enclosing == p->owner) { + p->owner->locals[p->local].captured = true; + uint16_t slot = (uint16_t)local_to_frame_slot(p->owner, p->local); + return add_upvalue(c, slot, true, false); + } + + int upvalue = resolve_private_upvalue(c->enclosing, p); + if (upvalue == -1) return -1; + return add_upvalue(c, (uint16_t)upvalue, false, false); +} + +static bool emit_private_token(sv_compiler_t *c, sv_ast_t *name) { + sv_private_name_t *p = private_scope_resolve(c, name->str, name->len); + if (!p) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Private name '%.*s' is not declared", (int)name->len, name->str); + emit_op(c, OP_UNDEF); + return false; + } + if (!p->owner || p->local < 0) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Private name '%.*s' is not initialized", (int)name->len, name->str); + emit_op(c, OP_UNDEF); + return false; + } + if (p->owner == c) { + emit_get_local(c, p->local); + return true; + } + int upvalue = resolve_private_upvalue(c, p); + if (upvalue == -1) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Private name '%.*s' is not in scope", (int)name->len, name->str); + emit_op(c, OP_UNDEF); + return false; + } + emit_op(c, OP_GET_UPVAL); + emit_u16(c, (uint16_t)upvalue); + return true; +} + static int add_atom(sv_compiler_t *c, const char *str, uint32_t len) { const char *interned = intern_string(str, (size_t)len); const char *stored = interned; @@ -1429,6 +1558,11 @@ void compile_expr(sv_compiler_t *c, sv_ast_t *node) { } case N_IDENT: + if (is_private_name_node(node)) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Private names may only be used as class member names"); + emit_op(c, OP_UNDEF); + break; + } emit_get_var(c, node->str, node->len); break; @@ -1643,9 +1777,9 @@ void compile_binary(sv_compiler_t *c, sv_ast_t *node) { if (op == TOK_IN && node->left->type == N_IDENT && node->left->len > 0 && node->left->str[0] == '#') { - emit_constant(c, js_mkstr_permanent(c->js, node->left->str, node->left->len)); compile_expr(c, node->right); - emit_op(c, OP_IN); + emit_private_token(c, node->left); + emit_op(c, OP_HAS_PRIVATE); return; } @@ -1724,6 +1858,22 @@ void compile_update(sv_compiler_t *c, sv_ast_t *node) { emit_op(c, is_inc ? OP_POST_INC : OP_POST_DEC); emit_set_var(c, target->str, target->len, false); } + } else if (target->type == N_MEMBER && !(target->flags & 1) && is_private_name_node(target->right)) { + compile_expr(c, target->left); + emit_op(c, OP_DUP); + emit_private_token(c, target->right); + emit_op(c, OP_GET_PRIVATE); + if (prefix) { + emit_op(c, is_inc ? OP_INC : OP_DEC); + emit_private_token(c, target->right); + emit_op(c, OP_PUT_PRIVATE); + } else { + emit_op(c, is_inc ? OP_POST_INC : OP_POST_DEC); + emit_op(c, OP_SWAP_UNDER); + emit_private_token(c, target->right); + emit_op(c, OP_PUT_PRIVATE); + emit_op(c, OP_POP); + } } else if (target->type == N_MEMBER && !(target->flags & 1)) { compile_expr(c, target->left); emit_op(c, OP_DUP); @@ -1770,6 +1920,14 @@ void compile_assign(sv_compiler_t *c, sv_ast_t *node) { ); if (op == TOK_ASSIGN) { + if (target->type == N_MEMBER && !(target->flags & 1) && is_private_name_node(target->right)) { + compile_expr(c, target->left); + compile_expr(c, node->right); + emit_private_token(c, target->right); + emit_op(c, OP_PUT_PRIVATE); + return; + } + if (target->type == N_MEMBER && !(target->flags & 1)) { int atom = add_atom(c, target->right->str, target->right->len); compile_expr(c, target->left); @@ -1885,6 +2043,49 @@ void compile_assign(sv_compiler_t *c, sv_ast_t *node) { default: break; } emit_set_var(c, target->str, target->len, true); + } else if (target->type == N_MEMBER && !(target->flags & 1) && is_private_name_node(target->right)) { + if (op == TOK_LOR_ASSIGN || op == TOK_LAND_ASSIGN || + op == TOK_NULLISH_ASSIGN) { + compile_expr(c, target->left); + emit_op(c, OP_DUP); + emit_private_token(c, target->right); + emit_op(c, OP_GET_PRIVATE); + int skip = emit_jump(c, + op == TOK_LOR_ASSIGN ? OP_JMP_TRUE_PEEK : + op == TOK_LAND_ASSIGN ? OP_JMP_FALSE_PEEK : OP_JMP_NOT_NULLISH); + emit_op(c, OP_POP); + compile_expr(c, node->right); + emit_private_token(c, target->right); + emit_op(c, OP_PUT_PRIVATE); + int end = emit_jump(c, OP_JMP); + patch_jump(c, skip); + emit_op(c, OP_NIP); + patch_jump(c, end); + return; + } + + compile_expr(c, target->left); + emit_op(c, OP_DUP); + emit_private_token(c, target->right); + emit_op(c, OP_GET_PRIVATE); + compile_expr(c, node->right); + switch (op) { + case TOK_PLUS_ASSIGN: emit_op(c, OP_ADD); break; + case TOK_MINUS_ASSIGN: emit_op(c, OP_SUB); break; + case TOK_MUL_ASSIGN: emit_op(c, OP_MUL); break; + case TOK_DIV_ASSIGN: emit_op(c, OP_DIV); break; + case TOK_REM_ASSIGN: emit_op(c, OP_MOD); break; + case TOK_SHL_ASSIGN: emit_op(c, OP_SHL); break; + case TOK_SHR_ASSIGN: emit_op(c, OP_SHR); break; + case TOK_ZSHR_ASSIGN: emit_op(c, OP_USHR); break; + case TOK_AND_ASSIGN: emit_op(c, OP_BAND); break; + case TOK_XOR_ASSIGN: emit_op(c, OP_BXOR); break; + case TOK_OR_ASSIGN: emit_op(c, OP_BOR); break; + case TOK_EXP_ASSIGN: emit_op(c, OP_EXP); break; + default: break; + } + emit_private_token(c, target->right); + emit_op(c, OP_PUT_PRIVATE); } else if (target->type == N_MEMBER && !(target->flags & 1)) { int atom = add_atom(c, target->right->str, target->right->len); @@ -1980,6 +2181,12 @@ void compile_assign(sv_compiler_t *c, sv_ast_t *node) { void compile_lhs_set(sv_compiler_t *c, sv_ast_t *target, bool keep) { if (target->type == N_IDENT) { emit_set_var(c, target->str, target->len, keep); + } else if (target->type == N_MEMBER && !(target->flags & 1) && is_private_name_node(target->right)) { + (void)keep; + compile_expr(c, target->left); + emit_op(c, OP_SWAP); + emit_private_token(c, target->right); + emit_op(c, OP_PUT_PRIVATE); } else if (target->type == N_MEMBER && !(target->flags & 1)) { if (keep) emit_op(c, OP_DUP); compile_expr(c, target->left); @@ -2069,6 +2276,12 @@ static void compile_delete_optional(sv_compiler_t *c, sv_ast_t *arg) { void compile_delete(sv_compiler_t *c, sv_ast_t *node) { sv_ast_t *arg = node->right; + if ((arg->type == N_MEMBER || arg->type == N_OPTIONAL) && + arg->right && is_private_name_node(arg->right)) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Cannot delete private fields"); + emit_op(c, OP_TRUE); + return; + } if (arg->type == N_OPTIONAL) { compile_delete_optional(c, arg); } else if (arg->type == N_MEMBER && sv_node_has_optional_base(arg->left)) { @@ -2168,6 +2381,9 @@ static void compile_receiver_property_get(sv_compiler_t *c, sv_ast_t *node) { if (node->flags & 1) { compile_expr(c, node->right); emit_op(c, OP_GET_ELEM); + } else if (is_private_name_node(node->right)) { + emit_private_token(c, node->right); + emit_op(c, OP_GET_PRIVATE); } else { emit_srcpos(c, node->right); emit_atom_op(c, OP_GET_FIELD, node->right->str, node->right->len); @@ -2202,6 +2418,11 @@ static sv_call_kind_t compile_call_setup_non_optional(sv_compiler_t *c, sv_ast_t } if (callee->type == N_MEMBER && is_ident_name(callee->left, "super")) { + if (!(callee->flags & 1) && is_private_name_node(callee->right)) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Cannot access private member through super"); + emit_op(c, OP_UNDEF); + return SV_CALL_DIRECT; + } emit_op(c, OP_THIS); emit_op(c, OP_THIS); emit_get_var(c, "super", 5); @@ -2439,6 +2660,11 @@ void compile_new(sv_compiler_t *c, sv_ast_t *node) { void compile_member(sv_compiler_t *c, sv_ast_t *node) { if (is_ident_name(node->left, "super")) { + if (!(node->flags & 1) && is_private_name_node(node->right)) { + js_mkerr_typed(c->js, JS_ERR_SYNTAX, "Cannot access private member through super"); + emit_op(c, OP_UNDEF); + return; + } emit_op(c, OP_THIS); emit_get_var(c, "super", 5); if (node->flags & 1) @@ -2461,6 +2687,9 @@ void compile_member(sv_compiler_t *c, sv_ast_t *node) { if (node->flags & 1) { compile_expr(c, node->right); emit_op(c, OP_GET_ELEM); + } else if (is_private_name_node(node->right)) { + emit_private_token(c, node->right); + emit_op(c, OP_GET_PRIVATE); } else { if (node->right->len == 6 && memcmp(node->right->str, "length", 6) == 0) emit_op(c, OP_GET_LENGTH); @@ -2477,6 +2706,9 @@ void compile_optional_get(sv_compiler_t *c, sv_ast_t *node) { if (node->flags & 1) { compile_expr(c, node->right); emit_op(c, OP_GET_ELEM_OPT); + } else if (is_private_name_node(node->right)) { + emit_private_token(c, node->right); + emit_op(c, OP_GET_PRIVATE_OPT); } else { emit_srcpos(c, node->right); emit_atom_op(c, OP_GET_FIELD_OPT, node->right->str, node->right->len); @@ -4203,11 +4435,26 @@ void compile_label(sv_compiler_t *c, sv_ast_t *node) { } } - +static inline bool is_class_method_def(const sv_ast_t *m); static void emit_field_inits(sv_compiler_t *c, sv_ast_t **fields, int count) { sv_compiler_t *enc = c->enclosing; for (int i = 0; i < count; i++) { sv_ast_t *m = fields[i]; + bool is_fn = is_class_method_def(m); + if (is_private_name_node(m->left)) { + emit_op(c, OP_THIS); + emit_private_token(c, m->left); + if (is_fn) compile_func_expr(c, m->right); + else if (m->right) compile_expr(c, m->right); + else emit_op(c, OP_UNDEF); + emit_op(c, OP_DEF_PRIVATE); + if (m->flags & FN_GETTER) emit(c, SV_COMP_PRIVATE_GETTER); + else if (m->flags & FN_SETTER) emit(c, SV_COMP_PRIVATE_SETTER); + else emit(c, is_fn ? SV_COMP_PRIVATE_METHOD : SV_COMP_PRIVATE_FIELD); + emit_op(c, OP_POP); + continue; + } + emit_op(c, OP_THIS); if (m->right) compile_expr(c, m->right); else emit_op(c, OP_UNDEF); @@ -4282,6 +4529,23 @@ static void compile_class_method( emit_op(c, OP_POP); } +static void compile_private_static_element(sv_compiler_t *c, sv_ast_t *m, int ctor_local) { + bool is_fn = is_class_method_def(m); + emit_get_local(c, ctor_local); + emit_private_token(c, m->left); + if (is_fn) { + if (m->flags & FN_STATIC) m->right->flags |= FN_STATIC; + compile_func_expr(c, m->right); + } else if (m->right) compile_expr(c, m->right); + else emit_op(c, OP_UNDEF); + + emit_op(c, OP_DEF_PRIVATE); + if (m->flags & FN_GETTER) emit(c, SV_COMP_PRIVATE_GETTER); + else if (m->flags & FN_SETTER) emit(c, SV_COMP_PRIVATE_SETTER); + else emit(c, is_fn ? SV_COMP_PRIVATE_METHOD : SV_COMP_PRIVATE_FIELD); + emit_op(c, OP_POP); +} + static inline int compile_class_precompute_key(sv_compiler_t *c, sv_ast_t *key_expr) { compile_expr(c, key_expr); int loc = add_local(c, "", 0, false, c->scope_depth); @@ -4303,9 +4567,27 @@ void compile_class(sv_compiler_t *c, sv_ast_t *node) { if (node->left) compile_expr(c, node->left); else emit_op(c, OP_UNDEF); + sv_private_scope_t private_scope = { .parent = c->private_scope }; + sv_private_scope_t *saved_private_scope = c->private_scope; + c->private_scope = &private_scope; + for (int i = 0; i < node->args.count; i++) { sv_ast_t *m = node->args.items[i]; if (m->type != N_METHOD) continue; + bool is_fn = is_class_method_def(m); + bool is_private = is_private_name_node(m->left); + + if (is_private) { + uint8_t private_kind = (m->flags & FN_GETTER) ? SV_COMP_PRIVATE_GETTER : + (m->flags & FN_SETTER) ? SV_COMP_PRIVATE_SETTER : + is_fn ? SV_COMP_PRIVATE_METHOD : SV_COMP_PRIVATE_FIELD; + if (!private_scope_add(c, &private_scope, m->left, private_kind, !!(m->flags & FN_STATIC))) { + c->private_scope = saved_private_scope; + free(private_scope.names); + emit_op(c, OP_UNDEF); + return; + } + } if ( !(m->flags & FN_STATIC) && @@ -4323,9 +4605,8 @@ void compile_class(sv_compiler_t *c, sv_ast_t *node) { memcmp(m->left->str, "name", 4) == 0 ) has_static_name = true; - bool is_fn = is_class_method_def(m); - if (!(m->flags & FN_STATIC) && !is_fn) field_count++; - if (node->str && (m->flags & FN_COMPUTED) && (is_fn || (m->flags & FN_STATIC))) computed_method_count++; + if (!(m->flags & FN_STATIC) && (is_private || !is_fn)) field_count++; + if (!is_private && node->str && (m->flags & FN_COMPUTED) && (is_fn || (m->flags & FN_STATIC))) computed_method_count++; } sv_ast_t **field_inits = NULL; @@ -4347,24 +4628,34 @@ void compile_class(sv_compiler_t *c, sv_ast_t *node) { if (m->type != N_METHOD || m == ctor_method) continue; bool is_fn = is_class_method_def(m); - bool is_instance_field = !(m->flags & FN_STATIC) && !is_fn; + bool is_private = is_private_name_node(m->left); + bool needs_instance_init = !(m->flags & FN_STATIC) && (is_private || !is_fn); - if (is_instance_field) { + if (needs_instance_init) { if (field_inits) field_inits[fi] = m; - if (computed_key_locals) computed_key_locals[fi] = (m->flags & FN_COMPUTED) + if (computed_key_locals) computed_key_locals[fi] = (!is_private && (m->flags & FN_COMPUTED)) ? compile_class_precompute_key(c, m->left) : -1; fi++; continue; } - if (!method_comp_keys || !(m->flags & FN_COMPUTED)) continue; + if (is_private || !method_comp_keys || !(m->flags & FN_COMPUTED)) continue; method_comp_keys[i] = compile_class_precompute_key(c, m->left); }} int inner_name_local = -1; - if (node->str) { + bool has_class_scope = node->str || private_scope.count > 0; + if (has_class_scope) { begin_scope(c); - inner_name_local = add_local(c, node->str, node->len, true, c->scope_depth); + if (node->str) + inner_name_local = add_local(c, node->str, node->len, true, c->scope_depth); + for (int i = 0; i < private_scope.count; i++) { + sv_private_name_t *p = &private_scope.names[i]; + p->owner = c; + p->local = add_local(c, "", 0, true, c->scope_depth); + emit_op(c, OP_OBJECT); + emit_put_local(c, p->local); + } } if (ctor_method && ctor_method->right) { @@ -4493,6 +4784,10 @@ void compile_class(sv_compiler_t *c, sv_ast_t *node) { if (m->type != N_METHOD) continue; if (m == ctor_method) continue; + if (is_private_name_node(m->left)) { + if (m->flags & FN_STATIC) compile_private_static_element(c, m, ctor_local); + continue; + } bool is_fn = is_class_method_def(m); if (!is_fn && !(m->flags & FN_STATIC)) continue; @@ -4515,7 +4810,9 @@ void compile_class(sv_compiler_t *c, sv_ast_t *node) { c->locals[outer_name_local].is_tdz = false; } - if (node->str) end_scope(c); + if (has_class_scope) end_scope(c); + c->private_scope = saved_private_scope; + free(private_scope.names); } static bool ast_contains_await_expr(const sv_ast_t *node) { diff --git a/src/silver/engine.c b/src/silver/engine.c index 5276972..5ac8865 100644 --- a/src/silver/engine.c +++ b/src/silver/engine.c @@ -1019,9 +1019,11 @@ ant_value_t sv_execute_frame(sv_vm_t *vm, sv_func_t *func, ant_value_t this, ant L_GET_FIELD_OPT: { VM_CHECK(sv_op_get_field_opt(vm, js, func, ip)); NEXT(5); } L_GET_ELEM_OPT: { VM_CHECK(sv_op_get_elem_opt(vm, js, func, ip)); NEXT(1); } - L_GET_PRIVATE: { sv_op_get_private(vm, js); NEXT(1); } - L_PUT_PRIVATE: { sv_op_put_private(vm, js); NEXT(1); } - L_DEF_PRIVATE: { sv_op_def_private(vm, js); NEXT(1); } + L_GET_PRIVATE: { VM_CHECK(sv_op_get_private(vm, js)); NEXT(1); } + L_GET_PRIVATE_OPT: { VM_CHECK(sv_op_get_private_opt(vm, js)); NEXT(1); } + L_PUT_PRIVATE: { VM_CHECK(sv_op_put_private(vm, js)); NEXT(1); } + L_DEF_PRIVATE: { VM_CHECK(sv_op_def_private(vm, js, ip)); NEXT(2); } + L_HAS_PRIVATE: { VM_CHECK(sv_op_has_private(vm, js)); NEXT(1); } L_GET_SUPER: { sv_op_get_super(vm, js); NEXT(1); } L_GET_SUPER_VAL: { sv_op_get_super_val(vm, js, frame); NEXT(1); } @@ -1857,7 +1859,6 @@ ant_value_t sv_execute_frame(sv_vm_t *vm, sv_func_t *func, ant_value_t this, ant L_DEFINE_CLASS: { sv_op_define_class(vm, js, func, ip); NEXT(14); } L_DEFINE_CLASS_COMP: { sv_op_define_class_comp(vm, js, func, ip); NEXT(14); } - L_ADD_BRAND: { sv_op_add_brand(vm); NEXT(1); } L_TO_OBJECT: { VM_CHECK(sv_op_to_object(vm, js)); NEXT(1); } L_TO_PROPKEY: { sv_op_to_propkey(vm, js); NEXT(1); } diff --git a/src/silver/ops/objects.h b/src/silver/ops/objects.h index d619469..bccf14f 100644 --- a/src/silver/ops/objects.h +++ b/src/silver/ops/objects.h @@ -302,8 +302,4 @@ static inline void sv_op_define_class_comp( vm->stack[vm->sp++] = name; } -static inline void sv_op_add_brand(sv_vm_t *vm) { - vm->sp -= 2; -} - #endif diff --git a/src/silver/ops/private.h b/src/silver/ops/private.h index b8eab3b..c2935a9 100644 --- a/src/silver/ops/private.h +++ b/src/silver/ops/private.h @@ -2,31 +2,227 @@ #define SV_PRIVATE_H #include "silver/engine.h" +#include -static inline void sv_op_get_private(sv_vm_t *vm, ant_t *js) { - ant_value_t prop = vm->stack[--vm->sp]; +enum { + SV_PRIVATE_FIELD = 0, + SV_PRIVATE_METHOD = 1, + SV_PRIVATE_ACCESSOR = 2, + SV_PRIVATE_GETTER = 3, + SV_PRIVATE_SETTER = 4 +}; + +static inline ant_value_t sv_private_entry_get(ant_t *js, ant_value_t entry, ant_offset_t idx) { + return vtype(entry) == T_ARR + ? js_arr_get(js, entry, idx) + : js_mkundef(); +} + +static inline ant_value_t sv_private_entry_set(ant_t *js, ant_value_t entry, ant_offset_t idx, ant_value_t value) { + char key_buf[8]; + int key_len = snprintf(key_buf, sizeof(key_buf), "%u", (unsigned)idx); + return js_setprop(js, entry, js_mkstr(js, key_buf, (size_t)key_len), value); +} + +static inline ant_value_t sv_private_table(ant_t *js, ant_value_t obj, bool create) { + if (!is_object_type(obj)) return js_mkundef(); + ant_value_t table = js_get_slot(obj, SLOT_PRIVATE_ELEMENTS); + + if (vtype(table) == T_ARR) return table; + if (!create) return js_mkundef(); + table = js_mkarr(js); + + if (is_err(table)) return table; + js_set_slot_wb(js, obj, SLOT_PRIVATE_ELEMENTS, table); + + return table; +} + +static inline ant_value_t sv_private_cached_entry( + ant_t *js, ant_value_t table, ant_value_t token, ant_offset_t len +) { + if (!is_object_type(token)) return js_mkundef(); + ant_value_t cached = js_get_slot(token, SLOT_DATA); + if (vtype(cached) != T_NUM) return js_mkundef(); + + double idx_num = js_getnum(cached); + if (idx_num < 0 || idx_num >= (double)len) return js_mkundef(); + + ant_offset_t idx = (ant_offset_t)idx_num; + ant_value_t entry = js_arr_get(js, table, idx); + if (vtype(entry) == T_ARR && sv_private_entry_get(js, entry, 0) == token) + return entry; + return js_mkundef(); +} + +static inline void sv_private_cache_entry(ant_t *js, ant_value_t token, ant_offset_t idx) { + if (is_object_type(token)) + js_set_slot(token, SLOT_DATA, js_mknum((double)idx)); +} + +static inline ant_value_t sv_private_find_entry(ant_t *js, ant_value_t obj, ant_value_t token) { + ant_value_t table = sv_private_table(js, obj, false); + if (vtype(table) != T_ARR) return js_mkundef(); + ant_offset_t len = js_arr_len(js, table); + + ant_value_t cached = sv_private_cached_entry(js, table, token, len); + if (vtype(cached) != T_UNDEF) return cached; + + for (ant_offset_t i = 0; i < len; i++) { + ant_value_t entry = js_arr_get(js, table, i); + if (vtype(entry) == T_ARR && sv_private_entry_get(js, entry, 0) == token) { + sv_private_cache_entry(js, token, i); + return entry; + }} + + return js_mkundef(); +} + +static inline ant_value_t sv_private_make_entry( + ant_t *js, ant_value_t obj, ant_value_t token, + int kind, ant_value_t value, ant_value_t getter, ant_value_t setter +) { + ant_value_t table = sv_private_table(js, obj, true); + if (is_err(table)) return table; + + ant_value_t entry = js_mkarr(js); + if (is_err(entry)) return entry; + js_arr_push(js, entry, token); + js_arr_push(js, entry, js_mknum((double)kind)); + js_arr_push(js, entry, value); + js_arr_push(js, entry, getter); + js_arr_push(js, entry, setter); + + ant_offset_t idx = js_arr_len(js, table); + js_arr_push(js, table, entry); + sv_private_cache_entry(js, token, idx); + + return entry; +} + +static inline ant_value_t sv_private_missing(ant_t *js) { + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot access private member on object whose class did not declare it"); +} + +static inline ant_value_t sv_op_get_private_impl(sv_vm_t *vm, ant_t *js, bool optional) { + ant_value_t token = vm->stack[--vm->sp]; ant_value_t obj = vm->stack[--vm->sp]; - ant_value_t key_str = coerce_to_str(js, prop); - ant_offset_t klen; - ant_offset_t koff = vstr(js, key_str, &klen); - const char *kptr = (const char *)(uintptr_t)(koff); - vm->stack[vm->sp++] = js_getprop_fallback(js, obj, kptr); + + if (!is_object_type(obj)) { + if (optional && (vtype(obj) == T_UNDEF || vtype(obj) == T_NULL)) { + vm->stack[vm->sp++] = js_mkundef(); + return js_mkundef(); + } + return sv_private_missing(js); + } + + ant_value_t entry = sv_private_find_entry(js, obj, token); + if (vtype(entry) == T_UNDEF) return sv_private_missing(js); + + ant_value_t kind_val = sv_private_entry_get(js, entry, 1); + int kind = vtype(kind_val) == T_NUM ? (int)js_getnum(kind_val) : SV_PRIVATE_FIELD; + if (kind == SV_PRIVATE_ACCESSOR) { + ant_value_t getter = sv_private_entry_get(js, entry, 3); + if (vtype(getter) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "Private accessor has no getter"); + ant_value_t result = sv_vm_call_explicit_this(vm, js, getter, obj, NULL, 0); + if (is_err(result)) return result; + vm->stack[vm->sp++] = result; + return js_mkundef(); + } + + vm->stack[vm->sp++] = sv_private_entry_get(js, entry, 2); + return js_mkundef(); +} + +static inline ant_value_t sv_op_get_private(sv_vm_t *vm, ant_t *js) { + return sv_op_get_private_impl(vm, js, false); } -static inline void sv_op_put_private(sv_vm_t *vm, ant_t *js) { - ant_value_t prop = vm->stack[--vm->sp]; +static inline ant_value_t sv_op_get_private_opt(sv_vm_t *vm, ant_t *js) { + return sv_op_get_private_impl(vm, js, true); +} + +static inline ant_value_t sv_op_put_private(sv_vm_t *vm, ant_t *js) { + ant_value_t token = vm->stack[--vm->sp]; ant_value_t val = vm->stack[--vm->sp]; ant_value_t obj = vm->stack[--vm->sp]; - ant_value_t key_str = coerce_to_str(js, prop); - js_setprop(js, obj, key_str, val); + + if (!is_object_type(obj)) return sv_private_missing(js); + ant_value_t entry = sv_private_find_entry(js, obj, token); + if (vtype(entry) == T_UNDEF) return sv_private_missing(js); + + ant_value_t kind_val = sv_private_entry_get(js, entry, 1); + int kind = vtype(kind_val) == T_NUM ? (int)js_getnum(kind_val) : SV_PRIVATE_FIELD; + if (kind == SV_PRIVATE_FIELD) { + ant_value_t set = sv_private_entry_set(js, entry, 2, val); + if (is_err(set)) return set; + vm->stack[vm->sp++] = val; + return js_mkundef(); + } + + if (kind == SV_PRIVATE_ACCESSOR) { + ant_value_t setter = sv_private_entry_get(js, entry, 4); + if (vtype(setter) == T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "Private accessor has no setter"); + ant_value_t args[1] = { val }; + ant_value_t result = sv_vm_call_explicit_this(vm, js, setter, obj, args, 1); + if (is_err(result)) return result; + vm->stack[vm->sp++] = val; + return js_mkundef(); + } + + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot write to private method"); } -static inline void sv_op_def_private(sv_vm_t *vm, ant_t *js) { +static inline ant_value_t sv_op_def_private(sv_vm_t *vm, ant_t *js, uint8_t *ip) { + uint8_t def_kind = sv_get_u8(ip + 1); ant_value_t val = vm->stack[--vm->sp]; - ant_value_t prop = vm->stack[--vm->sp]; + ant_value_t token = vm->stack[--vm->sp]; ant_value_t obj = vm->stack[vm->sp - 1]; - ant_value_t key_str = coerce_to_str(js, prop); - js_setprop(js, obj, key_str, val); + + if (!is_object_type(obj)) return sv_private_missing(js); + ant_value_t existing = sv_private_find_entry(js, obj, token); + + if (def_kind == SV_PRIVATE_GETTER || def_kind == SV_PRIVATE_SETTER) { + ant_value_t entry = existing; + if (vtype(entry) == T_UNDEF) { + entry = sv_private_make_entry( + js, obj, token, SV_PRIVATE_ACCESSOR, + js_mkundef(), + def_kind == SV_PRIVATE_GETTER ? val : js_mkundef(), + def_kind == SV_PRIVATE_SETTER ? val : js_mkundef()); + return is_err(entry) ? entry : js_mkundef(); + } + + ant_value_t kind_val = sv_private_entry_get(js, entry, 1); + int kind = vtype(kind_val) == T_NUM ? (int)js_getnum(kind_val) : SV_PRIVATE_FIELD; + if (kind != SV_PRIVATE_ACCESSOR) + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot redefine private member"); + + ant_offset_t slot = def_kind == SV_PRIVATE_GETTER ? 3 : 4; + if (vtype(sv_private_entry_get(js, entry, slot)) != T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot redefine private accessor"); + return sv_private_entry_set(js, entry, slot, val); + } + + if (vtype(existing) != T_UNDEF) + return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot initialize private member twice"); + + ant_value_t entry = sv_private_make_entry( + js, obj, token, + def_kind == SV_PRIVATE_METHOD ? SV_PRIVATE_METHOD : SV_PRIVATE_FIELD, + val, js_mkundef(), js_mkundef()); + return is_err(entry) ? entry : js_mkundef(); +} + +static inline ant_value_t sv_op_has_private(sv_vm_t *vm, ant_t *js) { + ant_value_t token = vm->stack[--vm->sp]; + ant_value_t obj = vm->stack[--vm->sp]; + if (!is_object_type(obj)) + return js_mkerr_typed(js, JS_ERR_TYPE, "Right operand of private brand check must be an object"); + vm->stack[vm->sp++] = js_bool(vtype(sv_private_find_entry(js, obj, token)) != T_UNDEF); + return js_mkundef(); } #endif diff --git a/tests/test_private_brands.cjs b/tests/test_private_brands.cjs new file mode 100644 index 0000000..0ee3eb3 --- /dev/null +++ b/tests/test_private_brands.cjs @@ -0,0 +1,162 @@ +const assert = (cond, msg) => { + if (!cond) throw new Error(msg); +}; + +const assertThrows = (fn, name, msg) => { + let threw = false; + try { + fn(); + } catch (err) { + threw = name === undefined || (err && err.name === name); + } + if (!threw) throw new Error(msg); +}; + +class A { + #x = 1; + + #m() { + return this.#x + 1; + } + + get #g() { + return this.#x + 2; + } + + set #g(value) { + this.#x = value; + } + + has(value) { + return #x in value; + } + + read(value) { + return value.#x; + } + + write(value) { + this.#g = value; + } + + call() { + return this.#m(); + } + + get value() { + return this.#g; + } +} + +class B { + #x = 99; + + has(value) { + return #x in value; + } +} + +const a = new A(); +const b = new B(); + +assert(a.has(a) === true, "own private brand should be present"); +assert(a.has({}) === false, "plain object should not have private brand"); +assert(a.has(b) === false, "same spelling in another class should be distinct"); +assert(b.has(b) === true, "other class should keep its own private brand"); +assertThrows(() => a.has(1), "TypeError", "private brand check right operand should be object"); +assertThrows(() => a.read({}), "TypeError", "wrong receiver private get should throw"); + +a.write(10); +assert(a.value === 12, "private accessors should read and write hidden state"); +assert(a.call() === 11, "private methods should be callable"); +assert(a["#x"] === undefined, "private field should not be a public string property"); +assert(Object.keys(a).indexOf("#x") === -1, "private field should not be enumerable"); +assert(Reflect.ownKeys(a).indexOf("#x") === -1, "private field should not be reflected"); + +class S { + static #v = 3; + + static #inc() { + return ++this.#v; + } + + static has(value) { + return #v in value; + } + + static run() { + return this.#inc(); + } +} + +assert(S.has(S) === true, "static private brand should be on the constructor"); +assert(S.has(new S()) === false, "static private brand should not be on instances"); +assert(S.run() === 4, "static private methods should access static private fields"); + +let baseSeen = false; + +class Base { + #base = 1; + + constructor() { + baseSeen = #base in this; + } + + hasBase(value) { + return #base in value; + } +} + +class Derived extends Base { + #derived = 2; + + hasDerived(value) { + return #derived in value; + } +} + +const d = new Derived(); +assert(baseSeen === true, "base private fields should be initialized during construction"); +assert(d.hasBase(d) === true, "derived instances should keep base private brands"); +assert(d.hasDerived(d) === true, "derived instances should keep derived private brands"); + +assertThrows(() => Function("class Bad { #x; #x; }"), "SyntaxError", "duplicate private field should be syntax error"); +assertThrows(() => Function("class Bad { #constructor; }"), "SyntaxError", "#constructor should be rejected"); +assertThrows(() => Function("class Bad { m() { return this.#missing; } }"), "SyntaxError", "undeclared private name should be syntax error"); + +const makeBox = Function("return class { #x = 1; has(value) { return #x in value; } read(value) { return value.#x; } };"); +const Box1 = makeBox(); +const Box2 = makeBox(); +const box1 = new Box1(); +const box2 = new Box2(); +assert(box1.has(box2) === false, "private brands from separately compiled classes should not collide"); +assertThrows(() => box1.read(box2), "TypeError", "same source offsets from separate compiles should stay distinct"); + +class ReturnObjectBase { + constructor(value) { + return value; + } +} + +class OtherStamp extends ReturnObjectBase { + #other = 1; +} + +class OffsetStamp extends ReturnObjectBase { + #x = 2; + + getX() { + return this.#x; + } +} + +const offsetZero = new OffsetStamp({}); +const offsetOne = {}; +new OtherStamp(offsetOne); +new OffsetStamp(offsetOne); + +assert(OffsetStamp.prototype.getX.call(offsetZero) === 2, "cached private lookup should work at offset 0"); +assert(OffsetStamp.prototype.getX.call(offsetOne) === 2, "cached private lookup should validate and recover at offset 1"); +assert(OffsetStamp.prototype.getX.call(offsetZero) === 2, "cached private lookup should validate when returning to offset 0"); + +console.log("private brand tests ok"); diff --git a/vendor/wirecall.wrap b/vendor/wirecall.wrap new file mode 100644 index 0000000..d087e22 --- /dev/null +++ b/vendor/wirecall.wrap @@ -0,0 +1,7 @@ +[wrap-git] +url = https://github.com/theMackabu/wirecall.git +revision = head +depth = 1 + +[provide] +wirecall = wirecall_dep -- 2.51.2