test: lock down dependency diagnostic provenance
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user