From ae4f9c107f3ec593d8dbe7cc45cf0c48e05c0751 Mon Sep 17 00:00:00 2001 From: otnc Date: Tue, 14 Jul 2026 01:43:50 +0900 Subject: [PATCH] fix: report bare identifier assignment as a compile error `x be value` on an already-declared (or undeclared) bare identifier was parsed permissively and the assignment silently dropped, compiling to a bare expression statement (`x;`). Code that looked valid ran with stale values and no diagnostics. Per the RFC (6.4 Assignment), bare identifier assignment is not supported by design. This change makes the compiler say so: - parser: collect diagnostics in a new `Parser::errors` array; bare identifier assignment pushes a descriptive error suggesting const/let or compound assignment - cmd: `purus build` / `purus check` print collected errors and exit 1 - pkg: `compile()` / `check()` surface the compiler's stderr/stdout in the thrown Error instead of a generic "Command failed" message Property, index, slice, destructuring, and compound assignments are unaffected. Fixes #5 --- pkg/lib/purus-core.js | 19 +++++++++++++------ src/cmd/main/main.mbt | 18 ++++++++++++++++-- src/codegen/codegen_test.mbt | 5 ++++- src/parser/parser.mbt | 18 ++++++++++++++++-- src/parser/parser_wbtest.mbt | 25 +++++++++++++++++++++++-- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/pkg/lib/purus-core.js b/pkg/lib/purus-core.js index bb48cfa..87e1157 100644 --- a/pkg/lib/purus-core.js +++ b/pkg/lib/purus-core.js @@ -39,11 +39,17 @@ function compile(source, options = {}) { args.push("--type", "commonjs"); } args.push(tmpFile); - let result = execFileSync(process.execPath, [COMPILER_JS, ...args], { - encoding: "utf8", - stdio: ["pipe", "pipe", "pipe"], - timeout: 30000, - }); + let result; + try { + result = execFileSync(process.execPath, [COMPILER_JS, ...args], { + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"], + timeout: 30000, + }); + } catch (err) { + const detail = `${err.stdout || ""}${err.stderr || ""}`.trim(); + throw new Error(detail || `Compile error: ${err.message}`); + } result = stdlib.postProcess(result); if (header) { return `// Generated by Purus ${VERSION}\n${result}`; @@ -80,7 +86,8 @@ function check(source) { }); return true; } catch (err) { - throw new Error(`Syntax error: ${err.stderr || err.message}`); + const detail = `${err.stdout || ""}${err.stderr || ""}`.trim(); + throw new Error(`Syntax error: ${detail || err.message}`); } finally { try { fs.unlinkSync(tmpFile); diff --git a/src/cmd/main/main.mbt b/src/cmd/main/main.mbt index 88e2b99..2bcb491 100644 --- a/src/cmd/main/main.mbt +++ b/src/cmd/main/main.mbt @@ -69,7 +69,14 @@ fn cmd_build(args : Array[String]) -> Unit { filtered.push(tokens[fi]) fi += 1 } - let stmts = @parser.Parser::new(filtered).parse() + let parser = @parser.Parser::new(filtered) + let stmts = parser.parse() + if parser.errors.length() > 0 { + for msg in parser.errors { + println("Error: " + msg) + } + @sys.exit(1) + } let header = if no_header { "" } else { @@ -140,7 +147,14 @@ fn cmd_check(file : String) -> Unit { } } let tokens = @lexer.Lexer::new(src).tokenize() - let _stmts = @parser.Parser::new(tokens).parse() + let parser = @parser.Parser::new(tokens) + let _stmts = parser.parse() + if parser.errors.length() > 0 { + for msg in parser.errors { + println("Error: " + msg) + } + @sys.exit(1) + } println("OK: " + file) } diff --git a/src/codegen/codegen_test.mbt b/src/codegen/codegen_test.mbt index fc233ac..3f272a0 100644 --- a/src/codegen/codegen_test.mbt +++ b/src/codegen/codegen_test.mbt @@ -266,8 +266,11 @@ test "codegen: hyphenated ident becomes underscore" { ///| test "codegen: bare ident assignment is discarded" { + // Bare identifier assignment is a parse error (reported via Parser::errors + // and rejected by the CLI). The recovery AST still emits the identifier as + // an expression so codegen never sees the dropped assignment. let js = compile("x be 42") - assert_eq(js, "x;\n") // bare ident assignment is not supported; emits identifier as expression + assert_eq(js, "x;\n") } ///| diff --git a/src/parser/parser.mbt b/src/parser/parser.mbt index 5c2e246..051cf5c 100644 --- a/src/parser/parser.mbt +++ b/src/parser/parser.mbt @@ -4,11 +4,12 @@ pub(all) struct Parser { tokens : Array[@lexer.Token] mut pos : Int + errors : Array[String] } ///| pub fn Parser::new(tokens : Array[@lexer.Token]) -> Parser { - { tokens, pos: 0 } + { tokens, pos: 0, errors: [] } } ///| @@ -223,7 +224,20 @@ fn Parser::parse_stmt(self : Parser) -> Stmt? { // Bare identifier assignment (x be val) is not supported. // Use const/let/var for declarations. Property and index access assignments remain valid. match expr { - Ident(_) => Some(ExprStmt(expr)) // discard; bare variable assignment is removed + Ident(name) => { + self.errors.push( + "bare identifier assignment ('" + + name + + " be ...') is not supported. Declare with 'const " + + name + + " be ...' or 'let " + + name + + " be ...', or use compound assignment such as '" + + name + + " add be 1'", + ) + Some(ExprStmt(expr)) + } _ => Some(self.maybe_postfix(Assign(expr, value))) } } diff --git a/src/parser/parser_wbtest.mbt b/src/parser/parser_wbtest.mbt index 1951e83..8a1c510 100644 --- a/src/parser/parser_wbtest.mbt +++ b/src/parser/parser_wbtest.mbt @@ -45,13 +45,34 @@ test "parser: var declaration with string" { } ///| -test "parser: bare ident assignment is discarded" { - let stmts = parse("x be 10") +test "parser: bare ident assignment is a parse error" { + let parser = Parser::new(tokenize("x be 10")) + let stmts = parser.parse() assert_eq(stmts.length(), 1) match stmts[0] { ExprStmt(Ident(name)) => assert_eq(name, "x") _ => fail("expected ExprStmt(Ident) - bare assignment is not supported") } + assert_eq(parser.errors.length(), 1) + assert_true(parser.errors[0].contains("bare identifier assignment")) + assert_true(parser.errors[0].contains("'x be ...'")) +} + +///| +test "parser: bare reassignment after let is a parse error" { + let parser = Parser::new(tokenize("let y be 10\ny be 20")) + let _stmts = parser.parse() + assert_eq(parser.errors.length(), 1) + assert_true(parser.errors[0].contains("'y be ...'")) +} + +///| +test "parser: property and compound assignments report no errors" { + let parser = Parser::new( + tokenize("obj.field be 10\narr[\\0] be 1\nlet z be 0\nz add be 1"), + ) + let _stmts = parser.parse() + assert_eq(parser.errors.length(), 0) } ///|