bug: file BUG-050 (module-scoped struct literals skip missing-field check, caused our segfault)
Author:
Chris Tusa <chris.tusa@leafscale.com>
Date:
May 04, 2026 14:11
Node:
7ededc3ad0abe3be41b8bd321adeeda6d65426ef
Branch:
default
Tags:
v0.1.1
Changed files:
Diff
diff -r ca182a12ab1c -r 7ededc3ad0ab docs/BUG-050-reef-lang.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/BUG-050-reef-lang.md Mon May 04 14:11:02 2026 +0000 @@ -0,0 +1,144 @@ +# BUG: missing-field check on struct literals skipped inside module-declared files + +**Filer:** repoman v0.1.1 implementation +**Reef version:** 0.5.18 +**Found:** 2026-05-04 +**Severity:** High — silent acceptance of incomplete struct literals; uninitialized fields cause runtime segfaults when read +**Suggested ID:** BUG-050 + +--- + +## Summary + +The typechecker's "missing field in struct literal" check fires correctly in single-file programs (`reefc run foo.reef`) but is silently skipped when the same struct literal is inside a file declared as `module X`. The build succeeds. At runtime, the missing field's memory is uninitialized — for `string`-typed fields this means a null/garbage pointer, which segfaults when the field is read. + +This bit repoman v0.1.1 hard: we added a new field `output: string` to the `Registry` struct and forgot to update two existing `Registry { ... }` literals inside `module config`. Build passed, all 11 unit tests passed (because the unchanged code paths didn't read `output`), and the bug only surfaced during smoke-testing when `serialize_registry` (called by `cmd_new`'s `save`) finally read the uninitialized `output` and segfaulted. + +## Reproducer + +### Single-file (correctly errors): + +```reef +import core.result_generic as rg + +type Bar = struct + a: int + b: string + c: int +end Bar + +fn make_partial(o: Bar): rg.Result[Bar, string] + return @Result[Bar, string].Ok(Bar { + a: o.a, + c: o.c + }) +end make_partial + +proc main() + let r = make_partial(Bar { a: 1, b: "two", c: 3 }) + if rg.is_ok(r) + println("ok") + end if +end main +``` + +`reefc run repro_single.reef`: +``` +Type Error: Missing field 'b' in struct literal for 'Bar' + at repro_single.reef:line 10, column 35 +``` + +### Module-scoped (silently accepts): + +Project layout: +``` +project/ +├── reef.toml (source_dirs = ["src"]) +├── src/ +│ ├── data.reef (defines Outer, returns partial literal) +│ └── main.reef (entry, imports data) +``` + +`src/data.reef`: +```reef +module data +import core.result_generic as rg + +export + type Outer + fn make(seed: Outer): rg.Result[Outer, string] +end export + +type Inner = struct + z: int +end Inner + +type Outer = struct + schema: int + output: string + defaults: Inner + projects: [Inner] +end Outer + +fn make(seed: Outer): rg.Result[Outer, string] + return @Result[Outer, string].Ok(Outer { + schema: seed.schema, + defaults: seed.defaults, + projects: seed.projects + }) +end make + +end module +``` + +`src/main.reef`: +```reef +import data +proc main() + println("noop") +end main +``` + +`reefc build`: +``` +Building project: modtest +Entry point: src/main.reef +Build complete: build/modtest +``` + +No error. The `Outer` literal is missing `output: string` and the build succeeds. If the consumer code were extended to actually call `make()` and read the resulting `Outer.output`, it would dereference an uninitialized string pointer and segfault. + +## Repoman impact + +repoman v0.1.1's `add_project` and `update_last_sync` in `src/config.reef` returned `Registry` literals missing the new `output` field. Build was clean, unit tests passed, smoke test on `repoman new <name>` segfaulted at the registry-write step. Quoted from our log: +``` +==> incus restart veemarker +[1] 1503002 segmentation fault (core dumped) repoman new veemarker +``` + +The crash surfaced because `config.save` calls `serialize_registry` which reads `reg.output`. With uninitialized memory there, the string operation segfaults. + +The fix in repoman was a one-character-per-site addition: `output: reg.output,` in both literals. But the typechecker should have caught this at compile time, not at runtime, and not after passing all unit tests. + +We added regression tests to `tests/test_config_mutate.reef` that read `reg.output` after `add_project` and `update_last_sync` — those would have caught the bug if the typechecker doesn't. But "every codebase that adds a struct field must remember to add a regression test for that field across every literal site" is not a sustainable contract. The typechecker should enforce completeness. + +## Diagnosis + +Two code paths likely diverge between single-file mode and module mode in the typechecker. Maybe: + +- The single-file pass walks the AST and runs full struct-literal validation. +- The module pass either skips that validation pass entirely, or runs it before the struct definition is fully resolved (so all fields appear "known but not required"). + +The bug is path-dependent, not type-dependent — same struct, same literal, different validation behavior depending on whether the file declares a `module`. So I'd start by looking at any conditional in the typechecker that branches on "is this a module file" or "do we have an export block". + +## Suggested fix + +Whatever validation logic is in the single-file path needs to also run for module-declared files. Likely a one- or two-line patch in the typechecker pass dispatcher (similar shape to BUG-037's "register generics for imported modules before scanning" fix from 0.5.10). + +## Suggested test + +`reef-compiler/tests/typecheck/test_struct_missing_field_module.reef` — a two-file project (one with `module X`) where a struct literal in the module file is missing a field. Should fail to compile with `Type Error: Missing field 'X' in struct literal for 'Y'`. + +--- + +*Filed by the repoman v0.1.1 build flow.*