fix(ir): validate const-expression typed module-const initializers [F0.7]

Attempt 1 rejected only LITERAL initializers that mismatch a typed module
const's annotation; a const-EXPRESSION initializer escaped, so the same
issue-0088 root remained for `M :: 2; N : string : M + 2` — accepted at exit 0,
folding `[N]s64` to 4 and printing N as an integer.

Root cause: `registerTypedModuleConst` validated only the enumerated literal
node kinds; any other kind fell through to `else => {}`, and pass 0
pre-registers binary_op/unary_op consts as a `.s64` placeholder that was never
reconciled with the annotation.

Fix — validate by TYPE, not by node kind:
- lower.zig: `registerTypedModuleConst` now covers literals AND const-expressions
  (binary_op/unary_op) through one path. `typedConstInitFits` keeps the literal
  arms and routes any non-literal through the new `constExprInitFits`, which
  compares the initializer's INFERRED type (`inferExprType`, the existing
  type-inference facility — no second const evaluator) to the annotation with the
  same integer/float compatibility. A mismatch emits the `type mismatch` diagnostic
  (a const-expression is described by its inferred type, e.g. "an integer
  expression") and evicts the pass-0 placeholder; a match registers the const at
  its resolved annotation type (the same `put` the literal path always did), so a
  const-expression folds and emits at its declared type.
- `literalKindName` → `initializerDescription` (+ `constExprDescription`) so the
  message is accurate for both a literal and a const-expression initializer.

Regression:
- examples/1143: extended with `E : string : M + 2` and `V : string : -M`
  (const-expr mismatches → exit 1, pinned diagnostics).
- examples/0162: extended with `KE : s64 : M + 2` (used as a count + printed) and
  `WE : f32 : M + 2` (over-rejection guard — valid const-exprs still work).
- program_index.test.zig: count-gate test extended with a binary_op value node
  declared `string` (must not fold as a count).

Docs: specs.md §3 + readme.md generalized from "initializer literal" to cover
constant expressions; issues/0088 RESOLVED banner updated.
This commit is contained in:
agra
2026-06-05 07:51:16 +03:00
parent 156edf8e28
commit 454ea06bd4
9 changed files with 188 additions and 88 deletions

View File

@@ -281,6 +281,19 @@ test "moduleConstInt gates the fold on the declared type, not the initializer no
try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, &table, "OK"));
try std.testing.expect(pi.moduleConstInt(&map, &table, "STR") == null);
try std.testing.expect(pi.moduleConstInt(&map, &table, "BOOLEAN") == null);
// The same gate holds for a const-EXPRESSION value node (`M + 2`), not just
// a bare literal: a `string`-typed const whose initializer is a foldable
// integer expression must still never fold as a count (issue 0088 attempt 2 —
// the const-expression leak). `KEXPR : s64 : M + 2` (numeric type) folds; the
// same expression declared `string` does not.
var m_lit = nLit(2);
var add2 = nLit(2);
var expr_val = nBin(.add, &m_lit, &add2);
try map.put("KEXPR", .{ .value = &expr_val, .ty = .s64 });
try map.put("STREXPR", .{ .value = &expr_val, .ty = .string });
try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, &table, "KEXPR"));
try std.testing.expect(pi.moduleConstInt(&map, &table, "STREXPR") == null);
}
test "evalConstIntExpr folds an integral float literal, halts on a fractional one" {