|

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.*