test: lock down dependency diagnostic provenance

This commit is contained in:
2026-10-25 21:36:13 +01:00
parent d834ff9587
commit ac7ab63b9e
5 changed files with 122 additions and 2 deletions
+2
View File
@@ -59,6 +59,8 @@ the backend (`@source/llvm`) consumes the existing typed IR, emits native functi
run the bootstrap suite with `bro test`. after `bro build`, run `python3 tests/native.py` for executable-level interpreter comparisons at both `-O0` and `-O2`, including traps, source diagnostics, array ownership, calling conventions, and CLI error handling.
dependency provenance checks cover lexer, parser, resolver, checker and lowering diagnostics, plus overflow, bounds, local initialization and global-cycle traps through transitive calls. they assert originating filenames and line/byte-column positions, compare reached and untaken paths, and run generated executables with the source tree temporarily removed.
the native bootstrap accepts either a no-argument `main` or `main proc(init process.Init)`. it supplies `init.io` and `init.args` from native startup `argc`/`argv`. startup scans each argument once into a heap-allocated array of borrowed zero-terminated slices (`process.Args.values: [][;0]u8`), without copying strings. the descriptor array and borrowed strings remain valid until `main` returns; startup then frees the descriptors, including on a fallible return. allocation failure exits with status `2`. `init.args.values.len` includes the executable name, and `init.args.values[index]` directly accesses an argument after a length guard. callers can supply their own `process.Args { values = slices[..] }` for testing.
`process.run(allocator, arguments)` starts a child synchronously using Darwin `posix_spawnp`, searches `PATH`, and inherits the environment and standard streams. argument strings are borrowed zero-terminated slices; no shell parsing occurs. it releases temporary argument descriptors on every return, retries interrupted waits, and returns the exit code or `128 + signal` for signal termination.
+1 -1
View File
@@ -22,7 +22,7 @@ needs records, pointers, generics and C calls.
- [x] load directory packages once per canonical path, with deterministic file discovery, package scopes and root-only entry selection. follow Bro's existing relative and project-root `@` import rules; diagnose cycles rather than recurse indefinitely.
- [x] finish postfix namespace selection and resolve file-local import aliases without injecting dependency names into the importing package; bare and aliased import declarations are already parsed for dependency discovery.
- [x] reuse ordinary scalar signature checking, function values and IR calls for selected declarations; namespaces have no runtime representation.
- [ ] preserve originating filenames and spans in dependency diagnostics and native runtime traps; retain demand-driven diagnostic recovery.
- [x] preserve originating filenames and spans in dependency diagnostics and native runtime traps; retain demand-driven diagnostic recovery.
- [ ] verify sibling source files, transitive/shared imports, same-spelled declarations in different packages, alias conflicts, missing packages/members, argument errors and dependency-file diagnostics. preserve existing single-file commands.
visibility follows Bro's public-by-default contract; unsupported visibility
+51
View File
@@ -67,3 +67,54 @@ keeps_package_declarations_distinct_and_ir_independent test {
try testing.expect_equal(i64(42), left_result.value.data.integer)
try testing.expect_equal(i64(6), right_result.value.data.integer)
}
dependency_traps_keep_original_spans_and_remain_demand_driven test {
mains [2][]u8 :: [
"dep :: import \"../dependency\"\nmain proc() int { return dep.fail(false) }\n",
"dep :: import \"../dependency\"\nmain proc() int { return dep.fail(true) }\n",
]
prefix :: "\nfail proc(take bool) int {\n if take {\n return "
for (mains) |main, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module ir.Module := undefined
dependency_source SourceId := undefined
original diagnostics.DiagnosticId := undefined
{
session := init(mem.c_allocator)
defer deinit(&session)
root :: try add_package(&session)
dependency :: try add_package(&session)
root_bytes := try owned_text(main)
defer arraylist.deinit(&root_bytes)
dependency_bytes := try owned_text(
"\nfail proc(take bool) int {\n if take {\n return missing\n }\n return 42\n}\n",
)
defer arraylist.deinit(&dependency_bytes)
root_source :: try add_source(&session, root, "app/main.hon", &root_bytes, &diagnostic_store)
dependency_source = try add_source(&session, dependency, "dependency/fail.hon", &dependency_bytes, &diagnostic_store)
try arraylist.append(&session.imports, Import{
node = declaration(&session, root_source, 0).node,
target = dependency,
})
module = try compile(&session, root, &diagnostic_store)
}
defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original = diagnostics.DiagnosticId(0)
diagnostic :: diagnostics.get(&diagnostic_store, original)?
try testing.expect_equal(diagnostics.Code.unknown_name, diagnostic.code)
try testing.expect_equal(dependency_source, diagnostic.span.source)
try testing.expect_equal(u32(prefix.len), diagnostic.span.start)
try testing.expect_equal(u32(prefix.len + 7), diagnostic.span.end)
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
if (index == 0) {
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
try testing.expect_equal(original, outcome.trap?)
}
}
}
+3 -1
View File
@@ -5,11 +5,13 @@ import "@std/arraylist"
SourceError :: enum { source_too_large }
Error :: alias fs.Error
Error :: alias (
fs.Error
| SourceError
| io.OpenError
| io.CloseError
| io.ReadError
)
@hide:file
copy proc(allocator mem.Allocator, text []u8) []mut u8 ! mem.AllocError {
+65
View File
@@ -429,6 +429,70 @@ fail proc() int { return 9223372036854775807 + 1 }
return len(cases) + 4
def check_dependency_provenance(temporary, executable):
project = temporary / "provenance"
app = project / "app"
dependency = project / "dependency"
app.mkdir(parents=True)
dependency.mkdir()
main = app / "main.hon"
source = dependency / "diagnostic.hon"
# A transitive call must report the leaf's location, not either importing file.
bridge = project / "bridge"
bridge.mkdir()
(bridge / "main.hon").write_text(
'import "../dependency"\nfail proc() int { return dependency.fail() }\n'
)
cases = [
("fail proc() int {\n return `\n}\n", 4, 12, True),
("fail proc() int {\n return (1\n}\n", 4, 14, True),
("fail proc() int {\n text :: \"é\" return missing\n}\n", 4, 25, True),
("fail proc() int {\n if (1) return 0\n return 42\n}\n", 4, 9, True),
("fail proc() int {\n return 9223372036854775808\n}\n", 4, 12, True),
("fail proc() int {\n return 9223372036854775807 + 1\n}\n", 4, 12, False),
("fail proc() int {\n a :: [42]\n return a[1]\n}\n", 5, 12, False),
("fail proc() int {\n before :: after\n after :: 42\n return before\n}\n", 4, 15, False),
("a int :: b\nb int :: a\nfail proc() int { return a }\n", 4, 10, False),
]
for text, line, column, static in cases:
source.write_text("\n# utf8: é\n" + text)
location = f"{source.resolve()}:{line}:{column}: ".encode()
for reached in (False, True):
main.write_text(
'import "../bridge"\nmain proc() int {\n'
f' if ({str(reached).lower()}) return bridge.fail()\n'
' return 42\n}\n'
)
interpreted = run(str(HONEY), "run", str(app))
assert interpreted.returncode == int(static or reached), outcome(interpreted)
assert interpreted.stdout == (b"" if reached else b"42\n"), outcome(interpreted)
if static or reached:
label = b"runtime trap: " if reached else b"error: "
assert interpreted.stderr.startswith(location + label), outcome(interpreted)
assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted)
else:
assert interpreted.stderr == b"", outcome(interpreted)
emitted = run(str(HONEY), "emit-llvm", str(app))
assert emitted.returncode == int(static), outcome(emitted)
expected_diagnostics = interpreted.stderr.replace(b"runtime trap: ", b"error: ") if static else b""
assert emitted.stderr == expected_diagnostics, outcome(emitted)
for optimization in ("-O0", "-O2"):
built = run(str(HONEY), "build", str(app), "-o", str(executable), optimization)
assert outcome(built) == (int(static), b"", expected_diagnostics), outcome(built)
# Generated diagnostics must not reopen sources at runtime.
hidden = project.with_name("hidden-provenance")
project.rename(hidden)
try:
compiled = run(str(executable))
assert outcome(compiled) == outcome(interpreted), (
optimization, outcome(interpreted), outcome(compiled)
)
finally:
hidden.rename(project)
executable.unlink()
return len(cases) * 2
def main():
with tempfile.TemporaryDirectory(prefix="honey native ' ; $") as directory:
temporary = Path(directory)
@@ -444,6 +508,7 @@ def main():
count += 1
count += check_packages(temporary, executable)
count += check_namespace_packages(temporary, executable)
count += check_dependency_provenance(temporary, executable)
source = ROOT / "programs/factorial.hon"
emitted = run(str(HONEY), "emit-llvm", str(source))
assert emitted.returncode == 0 and emitted.stderr == b"", outcome(emitted)