Merge pull request 'feat/selfhost-sema-error-recovery' (#1) from feat/selfhost-sema-error-recovery into main
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
Reviewed-on: https://git.bara-lang.org/bux-lang/bux-lang/pulls/1
This commit is contained in:
@@ -140,6 +140,12 @@ test-errors: ensure-buxc
|
|||||||
@chmod +x tests/error_golden/run.sh
|
@chmod +x tests/error_golden/run.sh
|
||||||
@tests/error_golden/run.sh ./$(OUT)
|
@tests/error_golden/run.sh ./$(OUT)
|
||||||
|
|
||||||
|
.PHONY: test-error-recovery
|
||||||
|
test-error-recovery: ensure-buxc selfhost
|
||||||
|
@echo "=== Selfhost multi-error recovery test ==="
|
||||||
|
@chmod +x _test_error_recovery/run.sh
|
||||||
|
@_test_error_recovery/run.sh
|
||||||
|
|
||||||
test-stdlib: ensure-buxc
|
test-stdlib: ensure-buxc
|
||||||
@echo "=== Stdlib golden tests ==="
|
@echo "=== Stdlib golden tests ==="
|
||||||
@chmod +x tests/stdlib_golden/run.sh
|
@chmod +x tests/stdlib_golden/run.sh
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "error_recovery"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression: buxc2 must report ALL independent semantic errors in one run.
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT="$(cd "$HERE/.." && pwd)"
|
||||||
|
BUXC2="$ROOT/build/selfhost/build/buxc2"
|
||||||
|
export BUX_STDLIB="$ROOT/lib"
|
||||||
|
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "=== building selfhost (buxc2) ==="
|
||||||
|
(cd "$ROOT" && make selfhost)
|
||||||
|
fi
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "error: buxc2 not found at $BUXC2" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$(cd "$HERE" && "$BUXC2" check src/Main.bux 2>&1 || true)"
|
||||||
|
echo "$out"
|
||||||
|
|
||||||
|
grep -q "cannot assign String to int" <<<"$out"
|
||||||
|
grep -q "cannot assign int to bool" <<<"$out"
|
||||||
|
grep -q "undeclared identifier 'undefined_variable'" <<<"$out"
|
||||||
|
|
||||||
|
echo "PASS: all independent semantic errors reported in one run"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
func Main() -> int {
|
||||||
|
let x: int = "hello";
|
||||||
|
let y: bool = 42;
|
||||||
|
let z: int = undefined_variable;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
# Self-hosted Sema Error Recovery Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make the self-hosted Bux compiler (`buxc2`) report all independent semantic errors in a single run instead of stopping after the first one, using a `tyUnknown` error sentinel.
|
||||||
|
|
||||||
|
**Architecture:** Work entirely in the self-hosted compiler (`src/sema.bux`, `src/cli.bux`). Semantic errors are recorded via `Sema_EmitError`; error paths return a `tyUnknown` sentinel (through a new `Sema_EmitExprError` helper) so parent expressions and sibling statements keep being checked. Codegen is still skipped whenever `Sema_HasError` is true.
|
||||||
|
|
||||||
|
**Tech Stack:** Bux (self-hosted compiler source), Bash test fixtures, `make selfhost` (bootstrap `buxc` compiles `src/*.bux` into `build/selfhost/build/buxc2`).
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-07-28-selfhost-sema-error-recovery-design.md`
|
||||||
|
|
||||||
|
**Repo layout facts the engineer must know:**
|
||||||
|
- Self-hosted compiler sources: `src/*.bux` (entry: `src/main.bux`; pipeline wired in `src/cli.bux`).
|
||||||
|
- `make selfhost` copies `src/*.bux` to `build/selfhost/src/`, renames `main.bux` → `Main.bux`, and builds with the bootstrap `./buxc`. Result binary: `build/selfhost/build/buxc2`.
|
||||||
|
- `buxc2 check <dir>` type-checks a package (needs `BUX_STDLIB=<repo>/lib` exported when run from a foreign directory).
|
||||||
|
- Type kind constants live in `src/types.bux` (`tyUnknown = 0`, `tyVoid = 1`, `tyBool = 2`, `tyStr = 9`, `tyInt = 14`, `tyFloat64 = 21`, `tyPointer = 22`, `tyNamed = 26`, `tyFunc = 28`).
|
||||||
|
- `Sema_CheckStmt` / `Sema_CheckExpr` are in `src/sema.bux` (statement checking starts ~line 1425, expression checking ~line 819).
|
||||||
|
- **Do not use `if` as an expression** in `src/*.bux` — the bootstrap parser rejects it (that is exactly what Task 0 fixes in `hir_lower.bux`). Ternary `cond ? a : b` is ALSO unsupported by the bootstrap parser (postfix `?` is always consumed as the try-operator); use statement form instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 0: Unblock `make selfhost` (prerequisite)
|
||||||
|
|
||||||
|
`make selfhost` currently fails because `src/hir_lower.bux:2726` uses `if` as an expression, which the bootstrap parser does not support. Replace it with the supported ternary form. This task exists only so that every later task is verifiable end-to-end with `buxc2`.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/hir_lower.bux:2726`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Verify the failure first**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -20`
|
||||||
|
Expected: FAIL — parse error `expected expression` at `src/hir_lower.bux:2726:50` pointing at the `if`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace the if-expression with statement form**
|
||||||
|
|
||||||
|
NOTE (corrected during execution): the bootstrap parser supports NEITHER `if`-expressions NOR ternary `cond ? a : b` — the postfix loop in `bootstrap/parser.nim:794-796` consumes any `?` as the try-operator, making the ternary branch unreachable. Use statement form instead.
|
||||||
|
|
||||||
|
In `src/hir_lower.bux`, change line 2726 from:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let operandTypeExpr: *TypeExpr = if operandExpr != null as *Expr { operandExpr.refType } else { null as *TypeExpr };
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
var operandTypeExpr: *TypeExpr = null as *TypeExpr;
|
||||||
|
if operandExpr != null as *Expr { operandTypeExpr = operandExpr.refType; }
|
||||||
|
```
|
||||||
|
|
||||||
|
(`operandTypeExpr` is only read afterwards inside the same block, so `var` instead of `let` is semantically safe.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify selfhost now builds**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -5`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===` and the binary exists:
|
||||||
|
|
||||||
|
Run: `test -x build/selfhost/build/buxc2 && echo buxc2-ok`
|
||||||
|
Expected: `buxc2-ok`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/hir_lower.bux
|
||||||
|
git commit -m "fix: rewrite if-expression as statements in hir_lower (unblocks make selfhost)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add `Sema_EmitExprError` sentinel helper
|
||||||
|
|
||||||
|
Introduce one explicit helper for the "record an error, mark the expression with a `?` type, return `tyUnknown`" pattern, and convert the two existing expression error sites that already return `tyUnknown` (undeclared identifier, `self` outside method) to use it. This locks in the convention the rest of the plan relies on.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/sema.bux` (after line 94; and lines ~852, ~880)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test fixture**
|
||||||
|
|
||||||
|
Create `_test_error_recovery/` (fixture content is finalized in Task 4; for now create a minimal package):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p _test_error_recovery/src
|
||||||
|
cat > _test_error_recovery/bux.toml <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "error_recovery"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cat > _test_error_recovery/src/Main.bux <<'EOF'
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
PrintLine(undefined_variable);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected: exit=1 and the output contains `undeclared identifier 'undefined_variable'`. (This passes today; the fixture proves the sentinel path keeps working after the refactor.)
|
||||||
|
|
||||||
|
NOTE (corrected during execution): the self-hosted `check` command takes a FILE path, not a directory (`Cli_Check` reads the file directly), and single-file check mode does NOT resolve `import`s — so `PrintLine` would also appear as undeclared. The final fixture (Task 3) therefore avoids imports entirely.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the helper to `src/sema.bux`**
|
||||||
|
|
||||||
|
Immediately after `Sema_EmitError` (after line 94), insert:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, msg);
|
||||||
|
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
te.kind = tekNamed;
|
||||||
|
te.typeName = "?";
|
||||||
|
expr.refType = te;
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Convert the undeclared-identifier site**
|
||||||
|
|
||||||
|
In `Sema_CheckExpr` (`ekIdent` branch, ~line 849-854), change:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let sym: Symbol = Scope_Lookup(sema.scope, expr.strValue);
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
||||||
|
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
||||||
|
let errMsg2: String = String_Concat(errMsg, "'");
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, errMsg2);
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let sym: Symbol = Scope_Lookup(sema.scope, expr.strValue);
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
||||||
|
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
||||||
|
let errMsg2: String = String_Concat(errMsg, "'");
|
||||||
|
return Sema_EmitExprError(sema, expr, errMsg2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Convert the `self` site**
|
||||||
|
|
||||||
|
In the `ekSelf` branch (~line 877-882), change:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, "self outside method");
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
||||||
|
return Sema_EmitExprError(sema, expr, "self outside method");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rebuild selfhost and re-run the fixture**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -3`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===`
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected: exit=1 and output still contains `undeclared identifier 'undefined_variable'`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/sema.bux
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "feat(selfhost-sema): add Sema_EmitExprError sentinel helper"
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: `_test_*/` is gitignored, but tracked fixtures exist (e.g. `_test_error_snippet/`), so `git add -f` is the established way to commit a new fixture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Arithmetic errors return the sentinel (no cascading diagnostics)
|
||||||
|
|
||||||
|
Today a failed arithmetic expression emits an error but then returns `tyInt`/`tyFloat64`, which lets follow-on code treat the bad expression as a valid number and produce derived errors. Return `tyUnknown` via the helper instead.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/sema.bux` (`ekBinary` arithmetic tail, ~lines 961-965)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the fixture with a cascade probe**
|
||||||
|
|
||||||
|
Replace `_test_error_recovery/src/Main.bux` with (no imports — single-file check mode does not resolve them):
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Main() -> int {
|
||||||
|
let bad: int = "a" + 1;
|
||||||
|
let z: int = undefined_variable;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected BEFORE the fix: exit=1, output contains `arithmetic requires numeric operands` and `undeclared identifier 'undefined_variable'`. (If extra derived errors about `bad` appear, this task removes them in Task 3 interplay; the key assertion is that both independent errors appear in ONE run.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Return the sentinel from the arithmetic error path**
|
||||||
|
|
||||||
|
In `Sema_CheckExpr`, `ekBinary` branch, change:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, "arithmetic requires numeric operands");
|
||||||
|
}
|
||||||
|
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
||||||
|
return tyInt;
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
||||||
|
return Sema_EmitExprError(sema, expr, "arithmetic requires numeric operands");
|
||||||
|
}
|
||||||
|
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
||||||
|
return tyInt;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `Sema_IsNumeric` returns `true` for `tyUnknown` (see `src/sema.bux:181`), so an already-broken operand does not re-trigger this error. That is the intended cascade suppression.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rebuild and verify both independent errors appear in one run**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -3`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===`
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1 | tee /tmp/err_recovery.out; echo "exit=$?"`
|
||||||
|
|
||||||
|
Run: `grep -c "arithmetic requires numeric operands" /tmp/err_recovery.out && grep -c "undeclared identifier" /tmp/err_recovery.out`
|
||||||
|
Expected: `1` and `1` (each error reported exactly once, in the same run).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/sema.bux
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "feat(selfhost-sema): arithmetic errors return tyUnknown sentinel"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add annotation-vs-initializer assignment checking
|
||||||
|
|
||||||
|
The self-hosted sema currently never checks `let x: T = init` for type agreement (the bootstrap compiler reports `cannot assign String to int`; `buxc2` silently accepts it and lets the C compiler fail later). Add the check in `Sema_CheckStmt` (`skLet`), emitting the same message shape as the bootstrap compiler, and keep registering the variable so checking continues.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/sema.bux` (new strict-numeric predicate after `Sema_IsBool` ~line 190; `skLet` branch ~lines 1443-1462)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the fixture with two assignment errors**
|
||||||
|
|
||||||
|
Replace `_test_error_recovery/src/Main.bux` with:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = "hello";
|
||||||
|
let y: bool = 42;
|
||||||
|
let z: int = undefined_variable;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1; echo "exit=$?"`
|
||||||
|
Expected BEFORE the fix: exit=1, output contains `undeclared identifier 'undefined_variable'` but NO `cannot assign` lines (this is the failing assertion the task fixes).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a strict numeric predicate**
|
||||||
|
|
||||||
|
`Sema_IsNumeric` (`src/sema.bux:181`) deliberately treats `tyNamed`/`tyTypeParam` as numeric (operator overloading). Assignment checking must not. After `Sema_IsBool` (~line 190), insert:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_IsStrictNumeric(kind: int) -> bool {
|
||||||
|
if kind == tyInt8 || kind == tyInt16 || kind == tyInt32 || kind == tyInt64 || kind == tyInt { return true; }
|
||||||
|
if kind == tyUInt8 || kind == tyUInt16 || kind == tyUInt32 || kind == tyUInt64 || kind == tyUInt { return true; }
|
||||||
|
if kind == tyFloat32 || kind == tyFloat64 { return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add a display-name helper for diagnostics**
|
||||||
|
|
||||||
|
After `Sema_IsStrictNumeric`, insert:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_TypeNameForDiag(te: *TypeExpr, kind: int) -> String {
|
||||||
|
if te != null as *TypeExpr {
|
||||||
|
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
|
||||||
|
return String_Concat(te.pointerPointee.typeName, "*");
|
||||||
|
}
|
||||||
|
if !String_Eq(te.typeName, "") { return te.typeName; }
|
||||||
|
}
|
||||||
|
if kind == tyBool { return "bool"; }
|
||||||
|
if kind == tyStr { return "String"; }
|
||||||
|
if kind == tyInt { return "int"; }
|
||||||
|
if kind == tyInt64 { return "int64"; }
|
||||||
|
if kind == tyUInt { return "uint"; }
|
||||||
|
if kind == tyFloat64 { return "float64"; }
|
||||||
|
if kind == tyPointer { return "*void"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Emit the assignment error in `skLet`**
|
||||||
|
|
||||||
|
In `Sema_CheckStmt`, `skLet` branch, insert the check AFTER the annotation/inference block (after the `} else if stmt.child1 != null as *Expr && stmt.child1.refType != null as *TypeExpr {` block closes, i.e. right before `sym.isMutable = stmt.boolValue;` ~line 1462):
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Assignment check: annotation vs initializer (skip when either side is unknown)
|
||||||
|
if stmt.refStmtType != null as *TypeExpr && initType != tyUnknown {
|
||||||
|
let annotKind: int = Sema_ResolveType(sema, stmt.refStmtType);
|
||||||
|
if annotKind != tyUnknown {
|
||||||
|
var mismatch: bool = false;
|
||||||
|
if initType == tyNamed && annotKind == tyNamed {
|
||||||
|
// Both named: kinds are equal, compare type names (when available).
|
||||||
|
if stmt.child1 != null as *Expr && stmt.child1.refType != null as *TypeExpr {
|
||||||
|
if !String_Eq(stmt.child1.refType.typeName, "") &&
|
||||||
|
!String_Eq(stmt.child1.refType.typeName, stmt.refStmtType.typeName) {
|
||||||
|
mismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if initType != annotKind {
|
||||||
|
let numericOk: bool = Sema_IsStrictNumeric(initType) && Sema_IsStrictNumeric(annotKind);
|
||||||
|
let initIsPtr: bool = initType == tyPointer || initType == tyStr;
|
||||||
|
let annotIsPtr: bool = annotKind == tyPointer || annotKind == tyStr;
|
||||||
|
let ptrOk: bool = initIsPtr && annotIsPtr;
|
||||||
|
if !numericOk && !ptrOk {
|
||||||
|
mismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mismatch {
|
||||||
|
let gotName: String = Sema_TypeNameForDiag(stmt.child1.refType, initType);
|
||||||
|
let wantName: String = Sema_TypeNameForDiag(stmt.refStmtType, annotKind);
|
||||||
|
let msg: String = String_Concat("cannot assign ",
|
||||||
|
String_Concat(gotName, String_Concat(" to ", wantName)));
|
||||||
|
Sema_EmitError(sema, stmt.line, stmt.column, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Important: the variable is still registered with the annotation type on the next lines (`sym.typeKind = Sema_ResolveType(...)` already ran), so subsequent uses of `x` type-check as the annotated type and do not cascade.
|
||||||
|
|
||||||
|
Known limitation (documented, not fixed here): struct-literal initializers (`ekStructInit`) do not set `expr.refType`, so two different named struct types are only compared by kind; name comparison only applies when `child1.refType` is available.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rebuild and verify all three independent errors appear in one run**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make selfhost 2>&1 | tail -3`
|
||||||
|
Expected: `=== Self-hosted compiler built successfully ===`
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux/_test_error_recovery && BUX_STDLIB=/home/ziko/z-git/bux/bux/lib ../build/selfhost/build/buxc2 check src/Main.bux 2>&1 | tee /tmp/err_recovery.out; echo "exit=$?"`
|
||||||
|
|
||||||
|
Run: `grep -c "cannot assign String to int" /tmp/err_recovery.out; grep -c "cannot assign int to bool" /tmp/err_recovery.out; grep -c "undeclared identifier 'undefined_variable'" /tmp/err_recovery.out`
|
||||||
|
Expected: `1`, `1`, `1` — three independent errors in a single run.
|
||||||
|
|
||||||
|
Also verify valid code still passes (no false positives). Use an import-free file (check mode does not resolve imports):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /tmp/hello_ok/src
|
||||||
|
cat > /tmp/hello_ok/src/Main.bux <<'EOF'
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = 5;
|
||||||
|
let y: bool = true;
|
||||||
|
let s: String = "ok";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
BUX_STDLIB=/home/ziko/z-git/bux/bux/lib /home/ziko/z-git/bux/bux/build/selfhost/build/buxc2 check /tmp/hello_ok/src/Main.bux 2>&1; echo "exit=$?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `exit=0` (no errors for valid code).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/sema.bux
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "feat(selfhost-sema): check annotation vs initializer type in let/var"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Regression runner for multi-error recovery
|
||||||
|
|
||||||
|
Lock the behavior in with a runnable script so regressions are caught by one command.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `_test_error_recovery/run.sh`
|
||||||
|
- Modify: `Makefile` (add `test-error-recovery` target)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the runner script**
|
||||||
|
|
||||||
|
Create `_test_error_recovery/run.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression: buxc2 must report ALL independent semantic errors in one run.
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT="$(cd "$HERE/.." && pwd)"
|
||||||
|
BUXC2="$ROOT/build/selfhost/build/buxc2"
|
||||||
|
export BUX_STDLIB="$ROOT/lib"
|
||||||
|
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "=== building selfhost (buxc2) ==="
|
||||||
|
(cd "$ROOT" && make selfhost)
|
||||||
|
fi
|
||||||
|
if [[ ! -x "$BUXC2" ]]; then
|
||||||
|
echo "error: buxc2 not found at $BUXC2" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$(cd "$HERE" && "$BUXC2" check src/Main.bux 2>&1 || true)"
|
||||||
|
echo "$out"
|
||||||
|
|
||||||
|
grep -q "cannot assign String to int" <<<"$out"
|
||||||
|
grep -q "cannot assign int to bool" <<<"$out"
|
||||||
|
grep -q "undeclared identifier 'undefined_variable'" <<<"$out"
|
||||||
|
|
||||||
|
echo "PASS: all independent semantic errors reported in one run"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `chmod +x _test_error_recovery/run.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run it — must pass**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && ./_test_error_recovery/run.sh`
|
||||||
|
Expected: ends with `PASS: all independent semantic errors reported in one run`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the Makefile target**
|
||||||
|
|
||||||
|
In `Makefile`, add after the `test-errors` target block (after line 141):
|
||||||
|
|
||||||
|
```make
|
||||||
|
test-error-recovery: ensure-buxc selfhost
|
||||||
|
@echo "=== Selfhost multi-error recovery test ==="
|
||||||
|
@chmod +x _test_error_recovery/run.sh
|
||||||
|
@_test_error_recovery/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the target**
|
||||||
|
|
||||||
|
Run: `cd /home/ziko/z-git/bux/bux && make test-error-recovery`
|
||||||
|
Expected: `PASS: all independent semantic errors reported in one run`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Negative check — valid code still compiles through buxc2**
|
||||||
|
|
||||||
|
Run: `BUX_STDLIB=/home/ziko/z-git/bux/bux/lib /home/ziko/z-git/bux/bux/build/selfhost/build/buxc2 check /tmp/hello_ok/src/Main.bux && echo "exit=$?"`
|
||||||
|
Expected: `exit=0`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add Makefile
|
||||||
|
git add -f _test_error_recovery/
|
||||||
|
git commit -m "test: selfhost multi-error recovery regression fixture"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review notes
|
||||||
|
|
||||||
|
- Spec coverage: sentinel helper (Task 1), cascade suppression (Task 2), statement-level continuation with new assignment diagnostics (Task 3), CLI keeps skipping codegen on `hasError` (existing behavior, asserted by fixtures exiting 1 before codegen), regression test (Task 4). The `Sema_CheckStmt` early-return audit found no statement-level early returns that skip sibling statements, so no Task is needed for that; the `Sema_CheckReturnLifetime` internal `return`s only exit that helper and are correct.
|
||||||
|
- Type consistency: `Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int`, `Sema_IsStrictNumeric(kind: int) -> bool`, `Sema_TypeNameForDiag(te: *TypeExpr, kind: int) -> String` are used with the same signatures in every task.
|
||||||
|
- Known blockers/limitations: struct-literal `refType` is not set, so named-vs-named assignment checking is best-effort; bootstrap compiler behavior is unchanged.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Self-hosted compiler: semantic error recovery
|
||||||
|
|
||||||
|
**Date:** 2026-07-28
|
||||||
|
**Scope:** Self-hosted Bux compiler only (`src/sema.bux`, `src/cli.bux`)
|
||||||
|
**Status:** Approved design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
When the self-hosted compiler performs semantic analysis, it must report **all independent semantic errors** in a single run instead of stopping after the first one. Users should see every type mismatch, undeclared identifier, return-type error, etc., before the compiler exits with failure.
|
||||||
|
|
||||||
|
## Current state
|
||||||
|
|
||||||
|
- `src/sema.bux` already collects diagnostics in `Sema.diags` via `Sema_EmitError` and sets `Sema.hasError`.
|
||||||
|
- `Sema_Analyze` walks every top-level declaration, so errors in different functions are all collected.
|
||||||
|
- However, inside a single function, several `Sema_EmitError` call sites are followed by an early `return` (or by returning `tyUnknown` in a way that aborts the parent expression). This truncates checking of the remaining statements/sub-expressions and hides errors.
|
||||||
|
- `src/cli.bux` correctly prints all collected semantic diagnostics and skips HIR lowering / C codegen when `Sema_HasError` is true.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Implement **expression-level recovery with an error sentinel** (`tyUnknown`):
|
||||||
|
|
||||||
|
- Every error path in `Sema_CheckExpr` must return `tyUnknown` and must not abort the parent check.
|
||||||
|
- Every error path in `Sema_CheckStmt` must record the diagnostic and then continue with the next statement (no early `return` that skips the rest of the block/function).
|
||||||
|
- `tyUnknown` is already treated as numeric-compatible in `Sema_IsNumeric`; audit the other predicates and error sites so `tyUnknown` suppresses cascading errors rather than causing them.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No changes to the bootstrap Nim compiler (`bootstrap/*.nim`).
|
||||||
|
- No parser recovery in this work item; parser errors still stop the pipeline before semantic analysis.
|
||||||
|
- No "best-effort codegen": if semantic errors exist, HIR lowering and C generation are still skipped.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Sema context
|
||||||
|
|
||||||
|
`Sema` keeps its existing diagnostic storage:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
struct Sema {
|
||||||
|
// ... existing fields ...
|
||||||
|
diagCount: int;
|
||||||
|
diags: *SemaDiag;
|
||||||
|
hasError: bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Sema_EmitError` remains the low-level reporter.
|
||||||
|
|
||||||
|
### Error sentinel helper
|
||||||
|
|
||||||
|
Add a small helper for expression-level errors:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
func Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, msg);
|
||||||
|
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
te.kind = tekNamed;
|
||||||
|
te.typeName = "?";
|
||||||
|
expr.refType = te;
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Callers use it like:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
return Sema_EmitExprError(sema, expr, "undeclared identifier 'foo'");
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes the "record error + return sentinel" pattern explicit and harder to get wrong.
|
||||||
|
|
||||||
|
### Statement-level recovery
|
||||||
|
|
||||||
|
Audit `Sema_CheckStmt`:
|
||||||
|
|
||||||
|
- After any `Sema_EmitError` inside a statement handler, the handler must fall through to the normal `return` at the end of that branch so that the caller (`Sema_CheckBlock` / `Sema_Analyze`) continues with the next statement.
|
||||||
|
- Example: `if` condition not `bool` must still check `then`/`else` blocks before returning.
|
||||||
|
|
||||||
|
### Expression-level recovery
|
||||||
|
|
||||||
|
Audit `Sema_CheckExpr`:
|
||||||
|
|
||||||
|
- Binary/unary/call/index expressions: always check both/all children before deciding whether the parent expression has a valid type.
|
||||||
|
- When a child returns `tyUnknown`, the parent must return `tyUnknown` (or the already-known result type for comparisons) without emitting a second, derived error.
|
||||||
|
- `Sema_IsNumeric` already returns `true` for `tyUnknown`; keep that behavior. Check `Sema_IsBool` and any custom predicates that might emit follow-up errors on `tyUnknown`.
|
||||||
|
|
||||||
|
### CLI behavior
|
||||||
|
|
||||||
|
`src/cli.bux` keeps the current flow:
|
||||||
|
|
||||||
|
1. `Sema_Analyze(mod)`
|
||||||
|
2. If `Sema_HasError(sema)` → print every `sema.diags[i]` with `Diagnostic_Print` and return failure (`""` / exit code 1).
|
||||||
|
3. Only if no errors → `HirLower_LowerModule` → `CBackend_Generate`.
|
||||||
|
|
||||||
|
This guarantees that code generation never runs on an AST with semantic errors.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Source
|
||||||
|
→ Lexer
|
||||||
|
→ Parser
|
||||||
|
→ Macro expand
|
||||||
|
→ Sema_Analyze
|
||||||
|
├─ Sema_CollectGlobals
|
||||||
|
└─ for each func: Sema_CheckStmt / Sema_CheckExpr
|
||||||
|
├─ error → Sema_EmitError / Sema_EmitExprError → tyUnknown → continue
|
||||||
|
└─ ok → normal type
|
||||||
|
→ if hasError: print all diags, exit 1
|
||||||
|
→ else: HirLower → CBackend → cc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling rules
|
||||||
|
|
||||||
|
1. **Never panic/abort** for user-source errors.
|
||||||
|
2. **Never skip** checking the rest of a block because one statement failed.
|
||||||
|
3. **Never emit a cascading error** on an expression whose type is already `tyUnknown`.
|
||||||
|
4. **Do not generate code** when `hasError` is true.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Create `_test_error_recovery/`:
|
||||||
|
|
||||||
|
- `src/Main.bux` contains multiple independent semantic errors, e.g.:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let x: int = "hello";
|
||||||
|
let y: bool = 42;
|
||||||
|
PrintInt(x + y);
|
||||||
|
PrintLine(undefined_variable);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected `buxc2 check` (self-hosted) output: all semantic errors listed in one run.
|
||||||
|
|
||||||
|
### Known blocker
|
||||||
|
|
||||||
|
`make selfhost` currently fails because the bootstrap parser rejects `if` used as an expression in `src/hir_lower.bux:2726`. The changes in this design are in `src/sema.bux` and `src/cli.bux`, so they can be syntax-checked with the bootstrap parser, but the new recovery behavior cannot be exercised end-to-end until that parser gap is fixed.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Changing `Sema_CheckStmt`/`Sema_CheckExpr` control flow may accidentally alter valid-code behavior; keep the diff minimal and only move/eliminate early `return`s that follow error emission.
|
||||||
|
- `tyUnknown` suppression logic relies on existing predicates; missing one predicate may produce noisy cascading diagnostics.
|
||||||
|
- Self-hosted compiler cannot be built right now, so runtime verification of `buxc2` diagnostics is blocked.
|
||||||
+5
-1
@@ -682,6 +682,9 @@ module HirLower {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Lcx_SubstEnumName(ctx: *LowerCtx, name: String) -> String {
|
func Lcx_SubstEnumName(ctx: *LowerCtx, name: String) -> String {
|
||||||
|
// HIR nodes may carry null names (e.g. hStore without typeName);
|
||||||
|
// Nim tolerated nil strings, bux_strlen(NULL) segfaults.
|
||||||
|
if name == null as String { return name; }
|
||||||
// If name is a bare generic enum name, mangle to concrete name
|
// If name is a bare generic enum name, mangle to concrete name
|
||||||
var ge: *Decl = Lcx_FindGenericEnum(ctx, name);
|
var ge: *Decl = Lcx_FindGenericEnum(ctx, name);
|
||||||
if ge != null as *Decl {
|
if ge != null as *Decl {
|
||||||
@@ -2723,7 +2726,8 @@ module HirLower {
|
|||||||
if stmt.child1 != null as *Expr && stmt.child1.kind == ekTry {
|
if stmt.child1 != null as *Expr && stmt.child1.kind == ekTry {
|
||||||
let tryExpr: *Expr = stmt.child1;
|
let tryExpr: *Expr = stmt.child1;
|
||||||
let operandExpr: *Expr = tryExpr.child1;
|
let operandExpr: *Expr = tryExpr.child1;
|
||||||
let operandTypeExpr: *TypeExpr = if operandExpr != null as *Expr { operandExpr.refType } else { null as *TypeExpr };
|
var operandTypeExpr: *TypeExpr = null as *TypeExpr;
|
||||||
|
if operandExpr != null as *Expr { operandTypeExpr = operandExpr.refType; }
|
||||||
var typeName: String = "Result";
|
var typeName: String = "Result";
|
||||||
var errTag: String = "Result_Err";
|
var errTag: String = "Result_Err";
|
||||||
var okField: String = "Ok_0";
|
var okField: String = "Ok_0";
|
||||||
|
|||||||
+68
-5
@@ -93,6 +93,15 @@ module Sema {
|
|||||||
sema.hasError = true;
|
sema.hasError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Sema_EmitExprError(sema: *Sema, expr: *Expr, msg: String) -> int {
|
||||||
|
Sema_EmitError(sema, expr.line, expr.column, msg);
|
||||||
|
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
||||||
|
te.kind = tekNamed;
|
||||||
|
te.typeName = "?";
|
||||||
|
expr.refType = te;
|
||||||
|
return tyUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Symbol zero-init helper (bootstrap C backend does not zero-init structs)
|
// Symbol zero-init helper (bootstrap C backend does not zero-init structs)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -189,6 +198,33 @@ module Sema {
|
|||||||
return kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32;
|
return kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strict numeric predicate for assignment checking: unlike Sema_IsNumeric,
|
||||||
|
// this does NOT treat tyNamed/tyTypeParam as numeric.
|
||||||
|
func Sema_IsStrictNumeric(kind: int) -> bool {
|
||||||
|
if kind == tyInt8 || kind == tyInt16 || kind == tyInt32 || kind == tyInt64 || kind == tyInt { return true; }
|
||||||
|
if kind == tyUInt8 || kind == tyUInt16 || kind == tyUInt32 || kind == tyUInt64 || kind == tyUInt { return true; }
|
||||||
|
if kind == tyFloat32 || kind == tyFloat64 { return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display-name helper for assignment diagnostics.
|
||||||
|
func Sema_TypeNameForDiag(te: *TypeExpr, kind: int) -> String {
|
||||||
|
if te != null as *TypeExpr {
|
||||||
|
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
|
||||||
|
return String_Concat(te.pointerPointee.typeName, "*");
|
||||||
|
}
|
||||||
|
if !String_Eq(te.typeName, "") { return te.typeName; }
|
||||||
|
}
|
||||||
|
if kind == tyBool { return "bool"; }
|
||||||
|
if kind == tyStr { return "String"; }
|
||||||
|
if kind == tyInt { return "int"; }
|
||||||
|
if kind == tyInt64 { return "int64"; }
|
||||||
|
if kind == tyUInt { return "uint"; }
|
||||||
|
if kind == tyFloat64 { return "float64"; }
|
||||||
|
if kind == tyPointer { return "*void"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Block checking helper
|
// Block checking helper
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -849,8 +885,7 @@ module Sema {
|
|||||||
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
||||||
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
let errMsg: String = String_Concat("undeclared identifier '", expr.strValue);
|
||||||
let errMsg2: String = String_Concat(errMsg, "'");
|
let errMsg2: String = String_Concat(errMsg, "'");
|
||||||
Sema_EmitError(sema, expr.line, expr.column, errMsg2);
|
return Sema_EmitExprError(sema, expr, errMsg2);
|
||||||
return tyUnknown;
|
|
||||||
}
|
}
|
||||||
if sym.refType != null as *TypeExpr {
|
if sym.refType != null as *TypeExpr {
|
||||||
expr.refType = sym.refType;
|
expr.refType = sym.refType;
|
||||||
@@ -877,8 +912,7 @@ module Sema {
|
|||||||
if kind == ekSelf {
|
if kind == ekSelf {
|
||||||
let sym: Symbol = Scope_Lookup(sema.scope, "self");
|
let sym: Symbol = Scope_Lookup(sema.scope, "self");
|
||||||
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "self outside method");
|
return Sema_EmitExprError(sema, expr, "self outside method");
|
||||||
return tyUnknown;
|
|
||||||
}
|
}
|
||||||
return sym.typeKind;
|
return sym.typeKind;
|
||||||
}
|
}
|
||||||
@@ -959,7 +993,7 @@ module Sema {
|
|||||||
if op == tkAmpAmp || op == tkPipePipe || op == tkBang { return tyBool; }
|
if op == tkAmpAmp || op == tkPipePipe || op == tkBang { return tyBool; }
|
||||||
// Arithmetic returns wider type
|
// Arithmetic returns wider type
|
||||||
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "arithmetic requires numeric operands");
|
return Sema_EmitExprError(sema, expr, "arithmetic requires numeric operands");
|
||||||
}
|
}
|
||||||
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
||||||
return tyInt;
|
return tyInt;
|
||||||
@@ -1459,6 +1493,35 @@ module Sema {
|
|||||||
sym.typeName = stmt.child1.refType.typeName;
|
sym.typeName = stmt.child1.refType.typeName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Assignment check: annotation vs initializer (skip when either side is unknown)
|
||||||
|
if stmt.refStmtType != null as *TypeExpr && initType != tyUnknown {
|
||||||
|
let annotKind: int = Sema_ResolveType(sema, stmt.refStmtType);
|
||||||
|
if annotKind != tyUnknown {
|
||||||
|
var mismatch: bool = false;
|
||||||
|
if initType == tyNamed && annotKind == tyNamed {
|
||||||
|
// Both named: kinds are equal, compare type names (when available).
|
||||||
|
if stmt.child1 != null as *Expr && stmt.child1.refType != null as *TypeExpr {
|
||||||
|
if !String_Eq(stmt.child1.refType.typeName, "") &&
|
||||||
|
!String_Eq(stmt.child1.refType.typeName, stmt.refStmtType.typeName) {
|
||||||
|
mismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if initType != annotKind {
|
||||||
|
let numericOk: bool = Sema_IsStrictNumeric(initType) && Sema_IsStrictNumeric(annotKind);
|
||||||
|
let boolOk: bool = Sema_IsBool(initType) && Sema_IsBool(annotKind);
|
||||||
|
if !numericOk && !boolOk {
|
||||||
|
mismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mismatch {
|
||||||
|
let gotName: String = Sema_TypeNameForDiag(stmt.child1.refType, initType);
|
||||||
|
let wantName: String = Sema_TypeNameForDiag(stmt.refStmtType, annotKind);
|
||||||
|
let msg: String = String_Concat("cannot assign ",
|
||||||
|
String_Concat(gotName, String_Concat(" to ", wantName)));
|
||||||
|
Sema_EmitError(sema, stmt.line, stmt.column, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
sym.isMutable = stmt.boolValue;
|
sym.isMutable = stmt.boolValue;
|
||||||
sym.isPublic = false;
|
sym.isPublic = false;
|
||||||
sym.decl = null as *Decl;
|
sym.decl = null as *Decl;
|
||||||
|
|||||||
Reference in New Issue
Block a user