reflection foundation, tuples, debug.print

This commit is contained in:
2026-07-14 23:45:30 +02:00
parent 267947e79d
commit 0b2055d64b
24 changed files with 126598 additions and 115394 deletions
+275 -13
View File
@@ -2258,7 +2258,12 @@ milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(t, string(stdout), "io-ok\n")
testing.expect_value(t, string(stdout),
"io-ok 37 {bro}\n" +
"bounds=-128/255 -32768/65535 -2147483648/4294967295 -9223372036854775808/18446744073709551615 " +
"-9223372036854775808/18446744073709551615 -128/127 -128/255 -32768/65535 -2147483648/4294967295 " +
"-9223372036854775808/18446744073709551615 -9223372036854775808/18446744073709551615 0/0 1/1 -1/1\n",
)
}
@(test)
@@ -2266,6 +2271,7 @@ milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) {
cases := [?]string{
"main func(value i32) void {}\n",
"main func(left, right i32) void {}\n",
"main func($value i32) void {}\n",
}
for text in cases {
source_file := source.Source{path="test.bro", text=text}
@@ -2279,7 +2285,7 @@ milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) {
for diagnostic in diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"take no parameters or one @std/io Io",
"take no parameters or one @std/process Init",
)
}
testing.expect(t, found)
@@ -2292,6 +2298,239 @@ milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) {
}
}
@(test)
milestone_37_rejects_direct_io_main_parameter :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-direct-io-main"
main_path := "/tmp/brolang-test-direct-io-main/main.bro"
text := `io :: import "@std/io"
main func(system io.Io) void {
_ = system
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"take no parameters or one @std/process Init",
)
}
testing.expect(t, loaded)
testing.expect(t, found)
}
@(test)
milestone_37_validates_process_init_and_hidden_system_provider :: proc(t: ^testing.T) {
Case :: struct {
io_source: string,
process_source: string,
message: string,
}
cases := []Case{
{
io_source=`Io :: struct { value i32 }
system func() Io { return Io {value = 0} }
`,
process_source=`io :: import "@std/io"
Init :: struct { io io.Io }
`,
message="does not provide the required 'hide system func() Io'",
},
{
io_source=`Io :: struct { value i32 }
hide system func() Io { return Io {value = 0} }
`,
process_source=`io :: import "@std/io"
Init :: struct { io io.Io, extra i32 }
`,
message="Init must be an auto-layout record containing exactly 'io io.Io'",
},
}
for test_case, index in cases {
root := fmt.tprintf("/tmp/brolang-test-process-schema-%d", index)
app := fmt.tprintf("%s/app", root)
io_dir := fmt.tprintf("%s/std/io", root)
process_dir := fmt.tprintf("%s/std/process", root)
main_source: string = `process :: import "@std/process"
main func(init process.Init) void { _ = init }
`
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, os2.make_directory_all(app) == nil)
testing.expect(t, os2.make_directory_all(io_dir) == nil)
testing.expect(t, os2.make_directory_all(process_dir) == nil)
testing.expect(t, os.write_entire_file(
fmt.tprintf("%s/main.bro", app), transmute([]byte)main_source,
))
testing.expect(t, os.write_entire_file(
fmt.tprintf("%s/io.bro", io_dir), transmute([]byte)test_case.io_source,
))
testing.expect(t, os.write_entire_file(
fmt.tprintf("%s/process.bro", process_dir), transmute([]byte)test_case.process_source,
))
sources := source.init_store()
diagnostics := source.init_store_diagnostics(&sources)
symbols := symbol.init_table()
ast_module, loaded := loader.load(app, &sources, &diagnostics, &symbols, project_root_path=root)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.message)
}
testing.expect(t, loaded)
testing.expect(t, found)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
source.destroy_store(&sources)
}
}
@(test)
milestone_37_tuples_reflection_inline_for_and_debug_print_compile_and_run :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(
"examples/programs/tuples", &sources, &diagnostics, &symbols, project_root_path=".",
)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, !strings.contains(llvm_text, "FormatToken"))
testing.expect(t, !strings.contains(llvm_text, "parse_format"))
testing.expect(t, !strings.contains(llvm_text, "FieldInfo"))
testing.expect(t, !strings.contains(llvm_text, "RecordInfo"))
output := "/tmp/brolang-test-tuples"
defer _ = os.remove(output)
status := compiler_core.compile_package(
"examples/programs/tuples", output, nil, target.DEFAULT, cimport.Options{}, ".",
)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(t, string(stdout), "")
testing.expect_value(t, string(stderr), "tuple=40/bro, limits=-9223372036854775808/18446744073709551615")
}
@(test)
milestone_37_format_errors_are_reported_at_comptime :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-format-errors"
main_path := "/tmp/brolang-test-format-errors/main.bro"
text := `io :: import "@std/io"
process :: import "@std/process"
main func(init process.Init) void {
writer io.Writer :: io.Writer {impl = init.io, stream = .stdout}
stored :: typeinfo!(i32)
_ = stored
io.print(writer, "", 1) catch |_| {}
io.print(writer, "{s}", {1,}) catch |_| {}
io.print(writer, "{d}", {"bro",}) catch |_| {}
io.print(writer, "{d}{d}", {1,}) catch |_| {}
io.print(writer, "{d}", {1, 2}) catch |_| {}
io.print(writer, "{x}", {}) catch |_| {}
io.print(writer, "}", {}) catch |_| {}
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := [7]bool{}
for diagnostic in diagnostics.items {
message := diagnostic.message
found[0] = found[0] || strings.contains(message, "arguments must be a tuple")
found[1] = found[1] || strings.contains(message, "cannot implicitly convert i8 to []u8")
found[2] = found[2] || strings.contains(message, "'{d}' requires an integer")
found[3] = found[3] || strings.contains(message, "argument count does not match")
found[4] = found[4] || strings.contains(message, "unknown specifier")
found[5] = found[5] || strings.contains(message, "unmatched '}'")
found[6] = found[6] || strings.contains(message, "compile-time-only metadata")
}
testing.expect(t, loaded)
for present in found {
testing.expect(t, present)
}
}
@(test)
milestone_37_inline_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) {
text := `main func() void {
total i32 = 0
inline for {1, 2} |value| {
if total == 0 {
break
}
total += value
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"break or continue targeting an inline loop must be compile-time-resolvable",
)
}
testing.expect(t, found)
}
@(test)
milestone_33_rejects_an_incompatible_runtime_write_declaration :: proc(t: ^testing.T) {
text := `write c_func(_ c_int, _ c_int, _ c_ulong) c_long
@@ -2931,10 +3170,10 @@ main func() void {
message := diagnostic.message
found_runtime_arg = found_runtime_arg || strings.contains(message, "must be a compile-time integer expression")
found_missing = found_missing || strings.contains(message, "cannot infer comptime parameter 'N'")
found_extra = found_extra || strings.contains(message, "expects 1 arguments with explicit comptime parameters or 0 with inferred comptime parameters, got 2")
found_extra = found_extra || strings.contains(message, "has no unique complete argument mapping")
found_negative = found_negative || strings.contains(message, "integer constant -1 does not fit in usize")
found_range = found_range || strings.contains(message, "integer constant 300 does not fit in u8")
found_bad_type = found_bad_type || strings.contains(message, "requires a concrete integer type")
found_bad_type = found_bad_type || strings.contains(message, "requires type, a concrete integer type, or immutable []u8")
found_assignment = found_assignment || strings.contains(message, "cannot assign comptime parameter 'N'")
found_address = found_address || strings.contains(message, "'&' requires an addressable location")
}
@@ -2996,7 +3235,7 @@ main func() void {
message := diagnostic.message
found_conflict = found_conflict || strings.contains(message, "conflicting inference for comptime parameter 'T': i32 and u32")
found_unknown = found_unknown || strings.contains(message, "cannot infer comptime parameter 'T'")
found_partial = found_partial || strings.contains(message, "expects 3 arguments with explicit comptime parameters or 1 with inferred comptime parameters, got 2")
found_partial = found_partial || strings.contains(message, "cannot infer comptime parameter 'N'")
if strings.contains(message, "cannot infer comptime parameter 'T'") {
unrecoverable += 1
}
@@ -3008,19 +3247,38 @@ main func() void {
}
@(test)
comptime_params_must_form_leading_prefix :: proc(t: ^testing.T) {
comptime_params_may_be_interleaved_and_are_erased_from_the_abi :: proc(t: ^testing.T) {
text := `valid func($T type, $N usize, value T) [N]T {
result [N]T = undefined
_ = value
return result
}
runtime_first func(value i32, $T type, $N usize) T { return value }
runtime_first func(value T, $T type, $N usize) T { return value }
split func($T type, value T, $N usize) [N]T {
result [N]T = undefined
_ = value
return result
}
main func() void {}
from_result func($T type) T {
value T = undefined
return value
}
choose_mapping func($A usize, value i32, $B usize) [A]u8 {
result [A]u8 = undefined
_ = value
_ = B
return result
}
main func() void {
_ = runtime_first(42, i32, 4)
_ = runtime_first(42, _, 4)
_ = split(i32, 42, 4)
_ = split(42, 4)
value i32 = from_result()
chosen [1]u8 :: choose_mapping(7, 2)
_ = value
_ = chosen
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
@@ -3034,13 +3292,17 @@ main func() void {}
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
count := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "comptime parameters must form a leading parameter prefix") {
count += 1
for function in hir_module.functions {
name := symbol.resolve(&symbols, function.name)
if name == "runtime_first" || name == "split" {
testing.expect_value(t, len(function.params), 1)
} else if name == "from_result" {
testing.expect_value(t, len(function.params), 0)
} else if name == "choose_mapping" {
testing.expect_value(t, len(function.params), 1)
}
}
testing.expect_value(t, count, 2)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)