ReviewOS

home-lang/home

fix(types): replace the `Void`-means-unknown escape hatch with a real `Unknown` that fails closed

#779
Closed chris opened this 9 days ago ·
9 days ago

Problem

The Home checker uses Void as "type not known", and checkAssignable accepts Void against any expected type ("gradual typing"). Unknown type names, unknown generics, tuple destructuring, imported namespaces, closures and match expressions all degrade to Void, so errors silently disappear. Type already has an .Unknown variant (~:618) that the checker does not use.

Evidence (origin/main @ d4b219507)

Scope

  • Introduce a distinct internal error/unknown type that is not assignable everywhere (and suppresses cascades).
  • An unresolved type name is a diagnostic, not Void.
  • Type tuple destructuring, closures and match results properly (match: join of all arms).

Acceptance criteria

  • let x: Nonexistent = 1 is an error.
  • let (a, b) = (1, "s"); a + b is an error.
  • foo.anything on an import alias is an error when anything is not exported.
  • home check examples/ re-baselined, with remaining diagnostics triaged.

Depends on: #778 Part of #814.

9 days ago

First checkpoint pushed to origin/main: b090da922 (fix(types): separate unknown from void types (#779)).

Unknown declared and generic type names now resolve to .Unknown, not unit .Void. Declaration sites emit a primary Unknown type '…' diagnostic; follow-on comparisons suppress cascades only for .Unknown, while real void no longer acts as a universal wildcard.

Focused parser-to-checker regressions prove let value: Nonexistent = 1 is rejected and a genuine fn noop() -> void { return } remains valid.

Guarded verification: zig build test -Dfilter=types_ -j1 --summary all — 14/14 passed, zero leaks, 631 MB peak. Commit contains exactly three files and is pushed with sole author/committer glennmichael123 <gtorregosa@gmail.com>.

Issue remains open for tuple destructuring, closure typing, match-expression joins, and import-member resolution.

9 days ago

Second checkpoint pushed to origin/main as bec0808bc (fix(types): preserve tuple destructuring types (#779)).

  • Tuple destructuring now preserves each RHS tuple element type.
  • Non-tuple RHS values and tuple/binding arity mismatches are rejected.
  • Mixed numeric/string + is rejected instead of silently concatenated.
  • Tuple-destructure AST binding names are now freed during teardown.

Verification:

  • guarded checker suite: 17/17 passed, zero leaks, 612 MB peak RSS
  • guarded parser suite: direct parser 145/145; all selected parser artifacts passed, 293 MB peak RSS
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: bec0808bc884b84acaec94d6ff46f04668960937

The issue remains open for closure typing, match-expression joins, import-member resolution, and the examples rebaseline.

9 days ago

Match-expression checkpoint pushed to origin/main as edddec83c (fix(types): validate match expression arms (#779)).

  • The checker now visits every match-expression arm instead of inferring only the first arm.
  • Every arm is checked in an isolated lexical scope; whole-value and tuple bindings are available to that arm's guard and body without leaking to siblings.
  • Guards must be boolean.
  • Arm result types must form a compatible join, and an outer expected type is propagated into every arm.
  • Empty match expressions and literal/range/tuple patterns that disagree with the matched value are rejected.

Verification under the 3,840 MB process-tree ceiling:

  • type-checker artifacts passed, 627 MB peak footprint
  • parser artifact passed, 261 MB peak footprint
  • codegen artifact passed, 159 MB peak footprint
  • zig fmt --check and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: edddec83c198d4939e2fb2786537990d0dcefc7d

The issue remains open for closure typing, import-member resolution, and the examples rebaseline.

9 days ago

Closure-typing checkpoint pushed to origin/main as 396966a05 (fix(types): infer closure function types (#779)).

  • Typed closures now synthesize concrete Function types instead of void.
  • Parameter and return annotations are recursively converted, including references, pointers, function/closure types, and supported generic containers.
  • Contextual function hints supply omitted parameter/return types; untyped parameters without context fail closed with a primary diagnostic.
  • Closure bodies are checked in isolated child scopes while captured names resolve through the parent scope.
  • Calls enforce the synthesized parameter and return types.
  • Activated closure AST teardown now recursively frees bodies, annotations, parameters, and captures; rejected inference no longer double-frees tracked parameter slices.

Verification under the 3,840 MB process-tree ceiling:

  • type-checker artifacts passed with zero leak/crash output, 658 MB peak footprint
  • AST artifact passed, 160 MB peak footprint
  • parser artifact passed, 282 MB peak footprint
  • codegen artifact passed, 160 MB peak footprint
  • static diff/format checks passed for the touched AST/test files; the large checker file retains only its known pre-existing formatter drift outside this patch
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 396966a05030adf823242dca95978162ed7903c7

The issue remains open for import-member resolution and the examples rebaseline.

9 days ago

Import-namespace checkpoint pushed to origin/main as 5c5a572c6 (fix(types): validate imported namespace members (#779)).

  • home check now passes its I/O context into TypeChecker; imports are actually read instead of silently short-circuiting.
  • Import aliases and implicit module names are typed namespaces built from public functions, structs, enums, and typed public values.
  • Namespace function calls preserve real parameter/return types and enforce arity and argument compatibility.
  • Missing members and non-public members now produce diagnostics instead of passing through void.
  • Imported parser/program state, module-cache keys, export names, field/variant slices, and failed parser-resolution paths now have explicit ownership and teardown.

Regression fixture proves:

  • support.exported(1) is accepted and returns i32.
  • support.anything is rejected.
  • support.hidden is rejected when hidden is not public.

Verification under the fixed 3,840 MB process-tree ceiling:

  • full type-checker artifacts passed with no leaks, 640 MB peak footprint (the synthetic parser source still emits its existing eager-resolution warning, but all 30 checker assertions pass)
  • parser artifact passed, 310 MB peak footprint
  • codegen artifact passed, 171 MB peak footprint
  • AST artifact passed, 133 MB peak footprint
  • git diff --check and zig ast-check src/main.zig passed
  • full native debug link was attempted twice and correctly stopped by the guard at 3,847 MB and 4,277/4,336 MB; the ceiling was not raised or bypassed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 5c5a572c644869c29c743ee07433c408e0f46d30

All three focused semantic acceptance cases are now covered. The issue remains open for the broader remaining Void-as-unknown sites and the guarded home check examples/ rebaseline once a native link can complete within the memory policy.

9 days ago

Fail-closed value-context checkpoint pushed to origin/main as 37209536d (fix(types): reject void in value contexts (#779)).

  • Failed initializer synthesis now records .Unknown, never unit .Void.
  • .Unknown suppresses follow-on diagnostics in binary expressions, conditions, patterns, and function arguments.
  • Real void values are rejected in arithmetic, conditions, bitwise/logical operations, switch/match patterns, and typed function arguments.
  • This removes the old tuple/generic comments and behavior that treated unit as a gradual-typing wildcard.

New regressions prove noop() + 1, if noop(), and consume(noop()) are errors while an unresolved declared type still produces the primary error without a cascade.

Verification under the 3,840 MB ceiling:

  • full type-checker artifacts passed, 582 MB peak footprint
  • codegen artifact passed, 179 MB peak footprint
  • static diff and test-file format checks passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 37209536d6f55cdeb2052ca967a7f61bdfa3bc2a

#779 remains open for the remaining container/member/index/unary Void escape sites and the examples rebaseline.

9 days ago

Container/member/unary fail-closed checkpoint pushed to origin/main as a07d6dd7e (fix(types): reject void container operations (#779)).

  • Index and slice operands now suppress cascades only for .Unknown; unit values and unit indices are rejected.
  • Member and safe-navigation access propagate .Unknown, while void.member and other invalid receivers produce diagnostics.
  • Unary !, negation, and bitwise-not no longer accept unit as an unknown value.
  • try now requires Result or Optional; only .Unknown propagates without another diagnostic.
  • Parser ownership is balanced when an indexed RangeExpr is lowered into a SliceExpr; the transferred wrapper is freed without touching its moved endpoints.

New regressions cover indexing/slicing, direct/safe member access, unary operators, and try on void.

Verification under the fixed 3,840 MB ceiling:

  • full type-checker artifacts: 38/38 assertions passed with zero leaks, 582 MB peak
  • parser artifact passed, 290 MB peak
  • codegen artifact passed, 179 MB peak
  • static diff/test-format checks passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: a07d6dd7e90dfa8328e27a8541b2197f78dbd8e0

#779 remains open for the smaller remaining Void fallback sites and the guarded examples rebaseline.

9 days ago

Aggregate fail-closed checkpoint pushed to origin/main as daf57c410 (fix(types): reject void aggregate coercions (#779)).

  • Integer zero and string literals no longer coerce to the concrete void type.
  • String patterns and string-to-array coercions no longer treat void array elements as byte elements.
  • Nested array coercion uses .Unknown—not .Void—to suppress follow-on mismatches.
  • Typed array literals reject unit-valued elements.
  • If-expression joins reject mixed unit/value branches while still suppressing cascades from .Unknown.

New regressions prove non-unit assignments to void, mixed void if-expression branches, unit elements in typed arrays, and array-of-void to array-of-value coercion all fail closed.

Verification under the fixed 3,840 MB ceiling:

  • focused type-checker suite: 42/42 passed, zero leak output, 585 MB peak footprint
  • codegen regression gate passed, 143 MB peak footprint
  • test-file format checks and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: daf57c410ef89c12251b74e550ce725b49c97cbc

#779 remains open for the remaining unresolved call/static/struct fallback sites and the guarded examples rebaseline.

9 days ago

Nested unknown-type checkpoint pushed to origin/main as 4bf29cda4 (fix(types): reject nested unknown types (#779)).

  • Declared types are now checked recursively for .Unknown inside arrays, maps, functions, results, tuples, unions, references, optionals, and advanced type wrappers.
  • Named structs and enums remain recursion leaves because their fields and variants are validated at declaration sites; this avoids cycles in recursive nominal types.
  • [Missing], &Missing, Result<i32, Missing>, and HashMap<string, Missing> now emit the primary unknown-type diagnostic instead of passing because their outer container was recognized.

Verification under the fixed 3,840 MB ceiling:

  • focused type-checker suite: 46/46 passed, zero leak output, 589 MB peak footprint
  • codegen regression gate passed, 161 MB peak footprint
  • test-file format checks and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 4bf29cda4ff0a5ad02f2cddc73576a612b0d1cf7

#779 remains open for unresolved value/call/static fallbacks and the guarded examples rebaseline.

9 days ago

Unresolved-value checkpoint pushed to origin/main as 5b6b0bf0e (fix(types): reject unresolved value calls (#779)).

  • Capitalized identifiers no longer bypass undefined-variable checking by degrading to void.
  • Unknown direct function calls now visit positional and named arguments, emit an Undefined function diagnostic, and produce .Unknown rather than unit.
  • Calls to missing enum variants now emit a precise diagnostic instead of falling through as unit.
  • Type-environment rebinding now updates the existing owned entry instead of leaking a duplicate key.
  • AST teardown now recursively frees named call arguments and their backing slices for both regular and static calls.

The new regressions initially exposed both ownership bugs; they were fixed in this same batch rather than suppressing leak detection.

Verification under the fixed 3,840 MB ceiling:

  • focused type-checker suite: 50/50 passed, zero leaks, 585 MB peak footprint
  • AST regression gate passed, 160 MB peak footprint
  • codegen regression gate passed, 160 MB peak footprint
  • git diff --check passed; touched test files pass formatting, while ast.zig retains only its pre-existing Zig 0.17 @intFromEnum → @backingInt formatter drift outside this patch
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 5b6b0bf0e4753edcbdae3372d0398daf9d72699d

#779 remains open for the remaining method/static/struct fallbacks and the guarded examples rebaseline.

9 days ago

Concrete callable/member checkpoint pushed to origin/main as 1f8776b8a (fix(types): reject invalid value calls (#779)).

  • Calling a bound non-function value now emits Value '…' is not callable and produces .Unknown rather than unit.
  • Unknown methods on strings and arrays now emit receiver-specific diagnostics instead of falling through as unit.
  • void {} is rejected as an invalid struct construction; the obsolete Void-means-generic struct-literal escape hatch is gone.
  • The broader struct-method fallback remains in place until method signatures are represented in StructType; this avoids rejecting valid user-defined methods merely because metadata is currently incomplete.

Verification under the fixed 3,840 MB ceiling:

  • focused type-checker suite: 54/54 passed, zero leaks, 647 MB peak footprint
  • codegen regression gate passed, 105 MB peak footprint
  • the memory guard waited for an existing test262-bin lock holder before running, so no heavy-process overlap occurred
  • test-file formatting and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 1f8776b8ae910ae38ea2a51314a636e7fbfc6d71

#779 remains open for proper struct/static method signature metadata and the guarded examples rebaseline.

9 days ago

Inline-method typing checkpoint pushed to origin/main as bab1c830a (feat(types): preserve inline method signatures (#779)).

  • StructType now carries typed inline-method metadata alongside fields.
  • Both declaration passes synthesize method parameter and return types, with declared-type validation in the checking pass.
  • The implicit self receiver is excluded from call-site arity and parameter matching.
  • Self in a method parameter or return position resolves to the owning nominal struct.
  • Calls through struct values now preserve method return types and validate argument count and compatibility rather than falling through as unit.

Verification under the fixed 3,840 MB ceiling:

  • focused type-checker suite: 57/57 passed, zero leaks, 657 MB peak footprint
  • codegen regression gate passed, 82 MB peak footprint
  • the checker waited behind the existing guarded zig-js Test262 run and started only after that shared lock released; no heavy-process overlap occurred
  • test-file formatting and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: bab1c830a36731c482728b714b405a61c7d1de0d

#779 remains open for inherent/trait implementation method metadata, static-call cleanup, and the guarded examples rebaseline.

9 days ago

Implementation-method typing checkpoint pushed to origin/main as 6a2dfa283 (feat(types): preserve implementation methods (#779)).

  • Inherent and trait impl methods are merged into their nominal struct after the declaration pass, independent of source order.
  • Rechecking replaces matching signatures rather than duplicating methods, preserving methods from multiple implementation blocks.
  • Method bodies resolve self and Self against the implementation/extension target instead of diagnosing them as unknown types.
  • The shared function-body checker now handles ordinary functions and owner-aware methods consistently.
  • Inline struct method bodies are now checked as well as their signatures.
  • Calls through inherent and trait implementations preserve return types and enforce arity and argument compatibility.

Verification under the fixed 3,840 MB ceiling:

  • focused type-checker suite: 61/61 passed, zero leaks, 592 MB peak footprint
  • codegen regression gate passed, 158 MB peak footprint
  • test-file formatting and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 6a2dfa283ecea6cd1223bfe342a69b5c3420ece5

#779 remains open for static-call cleanup, imported method metadata, and the guarded examples rebaseline.

9 days ago

Imported-method typing checkpoint pushed to origin/main as ede7d398a (feat(types): preserve imported methods (#779)).

  • Public inline methods are now copied into imported struct metadata before the imported parser/program/source are released.
  • Imported struct and method names receive checker-owned storage; private methods are not exported.
  • Imported method signatures reuse the owner-aware pipeline, so self is excluded from call arity and parameter/return types remain concrete.
  • Named imports of public structs now preserve method return types and reject incompatible arguments.

Verification under the fixed 3,840 MB ceiling:

  • focused type-checker suite: 63/63 passed, zero leaks, 588 MB peak footprint
  • codegen regression gate passed, 158 MB peak footprint
  • the synthetic import fixture emitted its existing eager-resolution warning once per import test; all five import assertions passed
  • test-file formatting and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: ede7d398a8749cd92e48537f5491f10538d3735e

#779 remains open for static-call cleanup and the guarded examples rebaseline.

9 days ago

Static-call checkpoint pushed to origin/main as 1ed2e8787 (feat(types): validate static method calls (#779)).

  • The parser now preserves whether member syntax used :: and lowers Type::method(...) into the existing dedicated StaticCallExpr node instead of erasing it into an ordinary member call.
  • Static struct calls use the method signatures added in the preceding checkpoints, preserving returns and validating arity/arguments.
  • Static enum constructors validate payload arity and type, and missing variants diagnose precisely.
  • Unknown static-call types, missing static methods, and static calls on non-struct/non-enum values now produce .Unknown plus a primary diagnostic rather than unit.
  • Positional and named arguments remain owned by the resulting call node; the transferred temporary member tree is explicitly released.

Verification under the fixed 3,840 MB ceiling:

  • exact main parser suite: 146/146 passed, 266 MB peak footprint
  • focused type-checker suite: 66/66 passed, zero leaks, 647 MB peak footprint
  • exact AST suite: 14/14 passed, 164 MB peak footprint
  • exact codegen suite: 12/12 passed, 161 MB peak footprint
  • test-file formatting and git diff --check passed
  • author and committer: glennmichael123 <gtorregosa@gmail.com>
  • remote origin/main: 1ed2e878724fe589c0746fe754aa5129632536ef

This closes the remaining static-call escape hatch. #779 remains open for the guarded home check examples/ rebaseline and any diagnostics that rebaseline exposes.

9 days ago

Examples rebaseline status after 1ed2e8787:

  • The newest native runtime binary predates the #779 checker work (home-debug is from 2026-09-23), so it cannot provide authoritative home check examples/ results.
  • One incremental zig build debug -j1 --summary all attempt was run through the shared lock with the fixed 3,840 MB process-tree ceiling.
  • The guard stopped it at 4,121 MB (host low-water 32%) with exit 125.
  • The cap was not raised, the guard was not bypassed, and no second heavy task was running.

The semantic checker/parser/AST/codegen changes remain fully verified by their focused artifacts, but the final examples acceptance box stays open until a current native binary can link within the memory policy. I am moving to another issue rather than repeatedly launching the same over-budget link.

9 days ago

Pushed 65cdf9d (fix(types): validate variadic output calls). The checker now registers println alongside print and infers every variadic output argument, so errors nested inside print/println calls can no longer be skipped. Guarded WASI verification: control-flow checker 64/64, assert checker 31/31, both below 700 MB. Examples rebaseline: test_bidirectional.home, test_move_semantics.home, and comptime_test.home now pass; array_slice.home dropped from 7 type errors to 1 unrelated undefined variable.

9 days ago

Pushed 9a5a46d4c (fix(parser): preserve unbounded array slices) directly to origin/main. The final known examples/array_slice.home diagnostic was real: array[..] unconditionally invoked the expression parser for an end bound, so ] became a bogus operand and the following slice6 binding disappeared. The parser now represents a full slice with both bounds null. Permanent parser coverage checks the AST, and the checker regression mirrors every slice form and every println call from the example. Guarded verification: parser 1,060/1,060 at 535 MB peak; focused type artifacts 65/65 plus cached companion suites at 348 MB peak; test-file format and git diff --check clean. The codegen gate exposed an independent committed-HEAD import break, now tracked as #822. Keeping #779 open until scripts/check-examples.sh can run against a freshly linked current binary under the unchanged 3,840 MB policy.

9 days ago

Examples rebaseline follow-up: the shared panic(message) blocker is now implemented in two direct-to-main commits, b663c1e0a (checker signature fn(string) -> never) and 922a50a85 (native lowering). Focused guarded suites pass 71/71 checker tests and 19/19 codegen tests. I am rebuilding the no-JSC compiler under the unchanged 3,840 MB ceiling before reporting the exact examples delta.

9 days ago

Fresh examples acceptance after b663c1e0a + 922a50a85: 34 pass / 5 fail / 0 skipped (up from 30/9), 190 MB peak under the guard. All five files previously blocked by undefined panic now pass. Remaining failures are narrowly classified: async_basic.home and async.home infer awaited values as void; ownership_test.home falsely moves a string through print; test_macros.home rejects logical-not around an expanded comparison; strings.test.hm falsely treats result as dropped. Continuing with focused fixes rather than weakening the examples gate.

9 days ago

Pushed 32875fc0d directly to origin/main for #829. Async calls now have semantic Future<T> results and await unwraps the payload instead of falling through to void. Guarded checker 72/72, no-JSC build 2,819 MB, examples now 36 pass / 3 fail / 0 skipped. Remaining files: ownership_test.home, test_macros.home, and strings.test.hm.

9 days ago

Pushed a5faf23a2 directly to origin/main for #830. It isolates ownership/drop state across functions and it bodies and fixes the ownership example through explicit cloning rather than weakening string move semantics. Guarded checker 74/74, current build 2,841 MB, examples now 38/39. Only test_macros.home remains, with the expanded assert! condition rejected by logical-not typing.

9 days ago

Completed on main. The last acceptance blocker was fixed by 4bf5d6720 (#831), and the current evidence now covers every checkbox:

  • unresolved declared types are rejected (control_flow_checker_test 34/74)
  • tuple element types survive destructuring and mixed addition is rejected (35/74)
  • missing import-alias members are rejected (50/74)
  • freshly built home-debug checks all examples: 39/39, 0 failed, 0 skipped

Final guarded verification:

  • control-flow checker: 74/74, 341 MB build/test peak
  • parser: 150/150
  • assertion checker: passing
  • no-JSC debug build: passing, 2,825 MB peak
  • examples: 39/39, 190 MB peak

All work was pushed directly to origin/main; no acceptance gate was weakened.

Sign in to comment on this issue.