From 950ab35bc484c7ad8a1847678c2b551a5bd2044e Mon Sep 17 00:00:00 2001 From: otnc Date: Tue, 14 Jul 2026 01:47:52 +0900 Subject: [PATCH] fix: allow keywords as property names after `.` and `\.` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Property access parsed names with a fixed allowlist of keywords, and every other keyword fell through to "unknown" — so `promise.then[f]` compiled to `p.unknown(f)`. The entire Promise chaining API (`then` / `catch` / `finally`) and any keyword-named member was broken. Add `keyword_text` mapping every keyword token back to its source text and use it in property position (after `.` and `\.`), matching JavaScript, which allows reserved words as property names. Notes: - `protected` and `function` lex to the same tokens as `private` / `fn`, so `obj.protected` compiles to `obj.private` (pre-existing aliasing; unchanged by this fix) - binding positions (destructuring, parameters) keep the previous stricter allowlist Fixes #6 --- src/codegen/codegen_test.mbt | 14 +++++ src/parser/parser.mbt | 118 +++++++++++++++++++++++++++++++++++ src/parser/parser_expr.mbt | 4 +- src/parser/parser_wbtest.mbt | 40 ++++++++++++ 4 files changed, 174 insertions(+), 2 deletions(-) diff --git a/src/codegen/codegen_test.mbt b/src/codegen/codegen_test.mbt index fc233ac..35d9497 100644 --- a/src/codegen/codegen_test.mbt +++ b/src/codegen/codegen_test.mbt @@ -1261,3 +1261,17 @@ test "codegen: semicolon string empty" { let js = compile("const s be //;;//") assert_eq(js, "const s = \"\";\n") } + +///| +test "codegen: keyword property names (promise chain)" { + let js = compile("p.then[f].catch[g].finally[h]") + assert_eq(js, "p.then(f).catch(g).finally(h);\n") +} + +///| +test "codegen: keyword property access" { + let js = compile("const t be obj.default") + assert_eq(js, "const t = obj.default;\n") + let js2 = compile("const u be obj\\.then") + assert_eq(js2, "const u = obj?.then;\n") +} diff --git a/src/parser/parser.mbt b/src/parser/parser.mbt index 5c2e246..3572779 100644 --- a/src/parser/parser.mbt +++ b/src/parser/parser.mbt @@ -290,6 +290,124 @@ fn Parser::parse_ident_name(self : Parser) -> String { } } +///| +/// Map a keyword token back to its source text. Returns None for +/// non-keyword tokens (identifiers, literals, punctuation). +fn keyword_text(tok : @lexer.TokenKind) -> String? { + match tok { + Const_ => Some("const") + Let_ => Some("let") + Var_ => Some("var") + Be => Some("be") + Add => Some("add") + Sub => Some("sub") + Mul => Some("mul") + Div => Some("div") + Fdiv => Some("fdiv") + Mod => Some("mod") + Neg => Some("neg") + Pow => Some("pow") + Eq => Some("eq") + Neq => Some("neq") + Lt => Some("lt") + Gt => Some("gt") + Le => Some("le") + Ge => Some("ge") + And => Some("and") + Or => Some("or") + Not => Some("not") + Coal => Some("coal") + Band => Some("band") + Bor => Some("bor") + Bxor => Some("bxor") + Bnot => Some("bnot") + Shl => Some("shl") + Shr => Some("shr") + Ushr => Some("ushr") + Fn => Some("fn") + Return_ => Some("return") + To => Some("to") + If_ => Some("if") + Elif => Some("elif") + Else_ => Some("else") + Unless => Some("unless") + Then_ => Some("then") + While_ => Some("while") + Until => Some("until") + Do_ => Some("do") + Yield_ => Some("yield") + For_ => Some("for") + In_ => Some("in") + Range_ => Some("range") + Break_ => Some("break") + Continue_ => Some("continue") + Match_ => Some("match") + When_ => Some("when") + Switch_ => Some("switch") + Case_ => Some("case") + Pipe_ => Some("pipe") + As => Some("as") + Of => Some("of") + Gives => Some("gives") + Typeof_ => Some("typeof") + Void_ => Some("void") + Instanceof_ => Some("instanceof") + New_ => Some("new") + Delete_ => Some("delete") + This_ => Some("this") + Async_ => Some("async") + Await_ => Some("await") + Try_ => Some("try") + Catch_ => Some("catch") + Finally_ => Some("finally") + Throw_ => Some("throw") + Import_ => Some("import") + From_ => Some("from") + Export_ => Some("export") + Default_ => Some("default") + Require_ => Some("require") + Use_ => Some("use") + Mod_ => Some("namespace") + Public_ => Some("public") + All_ => Some("all") + Type_ => Some("type") + With_ => Some("with") + True_ => Some("true") + False_ => Some("false") + Null_ => Some("null") + Nil_ => Some("nil") + Undefined_ => Some("undefined") + Nan_ => Some("nan") + Infinity_ => Some("infinity") + List_ => Some("list") + Object_ => Some("object") + Class_ => Some("class") + Extends_ => Some("extends") + Super_ => Some("super") + Static_ => Some("static") + Private_ => Some("private") + Get_ => Some("get") + Set_ => Some("set") + Blank => Some("blank") + _ => None + } +} + +///| +/// Parse a name in property position (after `.` or `\.`). +/// Any keyword is a valid property name there, matching JavaScript, +/// which allows reserved words after `.` (e.g. `promise.then`, +/// `promise.catch`, `promise.finally`). +fn Parser::parse_property_name(self : Parser) -> String { + match keyword_text(self.peek()) { + Some(name) => { + self.pos += 1 + name + } + None => self.parse_ident_name() + } +} + ///| fn Parser::parse_var_decl(self : Parser, kind : DeclKind) -> Stmt { self.pos += 1 // skip const/let/var diff --git a/src/parser/parser_expr.mbt b/src/parser/parser_expr.mbt index 73d343e..1c7a416 100644 --- a/src/parser/parser_expr.mbt +++ b/src/parser/parser_expr.mbt @@ -395,7 +395,7 @@ fn Parser::parse_postfix(self : Parser) -> Expr { match self.peek() { Dot => { self.pos += 1 - let field = self.parse_ident_name() + let field = self.parse_property_name() if self.peek() == LBracket { if self.is_computed_bracket() { // Computed access on property: obj.field[\expr] @@ -443,7 +443,7 @@ fn Parser::parse_postfix(self : Parser) -> Expr { } OptDot => { self.pos += 1 - let field = self.parse_ident_name() + let field = self.parse_property_name() if self.peek() == LBracket { if self.is_computed_bracket() { self.pos += 1 // skip [ diff --git a/src/parser/parser_wbtest.mbt b/src/parser/parser_wbtest.mbt index 1951e83..46389d8 100644 --- a/src/parser/parser_wbtest.mbt +++ b/src/parser/parser_wbtest.mbt @@ -489,3 +489,43 @@ test "parser: multiple statements" { let stmts = parse("const x be 1\nconst y be 2\nconst z be x add y") assert_eq(stmts.length(), 3) } + +///| +test "parser: keyword as property name (then/catch/finally)" { + let stmts = parse("p.then[f]") + assert_eq(stmts.length(), 1) + match stmts[0] { + ExprStmt(MethodCall(Ident(obj), field, args)) => { + assert_eq(obj, "p") + assert_eq(field, "then") + assert_eq(args.length(), 1) + } + _ => fail("expected MethodCall with field 'then'") + } + match parse("p.catch[f]")[0] { + ExprStmt(MethodCall(_, field, _)) => assert_eq(field, "catch") + _ => fail("expected MethodCall with field 'catch'") + } + match parse("p.finally[f]")[0] { + ExprStmt(MethodCall(_, field, _)) => assert_eq(field, "finally") + _ => fail("expected MethodCall with field 'finally'") + } +} + +///| +test "parser: keyword as property access (dot / optional dot)" { + match parse("const t be obj.if")[0] { + VarDecl(Const_, _, DotAccess(Ident(o), field)) => { + assert_eq(o, "obj") + assert_eq(field, "if") + } + _ => fail("expected DotAccess with field 'if'") + } + match parse("const t be obj\\.then")[0] { + VarDecl(Const_, _, OptDotAccess(Ident(o), field)) => { + assert_eq(o, "obj") + assert_eq(field, "then") + } + _ => fail("expected OptDotAccess with field 'then'") + } +}