harden compiler parsing, recovery, and deep-expression handling
This commit is contained in:
+255
-2
@@ -176,6 +176,113 @@ main :: func() void {}
|
||||
testing.expect(t, module.functions[3].has_body)
|
||||
}
|
||||
|
||||
@(test)
|
||||
parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) {
|
||||
text := `c :: 5
|
||||
x :: c
|
||||
foreign :: c func() i32
|
||||
broken :: c 5
|
||||
main :: func() void {}
|
||||
`
|
||||
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)
|
||||
module := parser.parse(&stream, &source_file, &diagnostics)
|
||||
defer ast.destroy_module(&module)
|
||||
|
||||
c_symbol := symbol.intern(&symbols, "c")
|
||||
for tok in stream.items {
|
||||
if tok.span.start < len(text) && text[tok.span.start:tok.span.end] == "c" {
|
||||
testing.expect_value(t, tok.kind, token.Kind.Identifier)
|
||||
testing.expect_value(t, tok.symbol, c_symbol)
|
||||
}
|
||||
}
|
||||
testing.expect_value(t, len(module.globals), 3)
|
||||
testing.expect_value(t, module.exprs[module.globals[1].expr].name, c_symbol)
|
||||
testing.expect_value(t, module.exprs[module.globals[2].expr].name, c_symbol)
|
||||
testing.expect(t, module.functions[0].c_abi)
|
||||
testing.expect_value(t, len(diagnostics.items), 1)
|
||||
testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline"))
|
||||
}
|
||||
|
||||
@(test)
|
||||
pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) {
|
||||
source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain :: func() void {}\n"}
|
||||
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)
|
||||
module := parser.parse(&stream, &source_file, &diagnostics)
|
||||
defer ast.destroy_module(&module)
|
||||
|
||||
root := module.exprs[module.globals[0].expr]
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect_value(t, root.kind, ast.Expr_Kind.Add)
|
||||
testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Add)
|
||||
testing.expect_value(t, module.exprs[root.right].integer, i64(3))
|
||||
}
|
||||
|
||||
nested_expression_source :: proc(call: bool, depth: int) -> string {
|
||||
builder := strings.builder_make()
|
||||
defer strings.builder_destroy(&builder)
|
||||
if call {
|
||||
strings.write_string(&builder, "identity :: func(value i32) i32 { return value }\nvalue :: ")
|
||||
for _ in 0..<depth {
|
||||
strings.write_string(&builder, "identity(")
|
||||
}
|
||||
} else {
|
||||
strings.write_string(&builder, "value :: ")
|
||||
for _ in 0..<depth {
|
||||
strings.write_byte(&builder, '(')
|
||||
}
|
||||
}
|
||||
strings.write_byte(&builder, '1')
|
||||
for _ in 0..<depth {
|
||||
strings.write_byte(&builder, ')')
|
||||
}
|
||||
strings.write_string(&builder, "\nmain :: func() void {}\n")
|
||||
return strings.clone(strings.to_string(builder))
|
||||
}
|
||||
|
||||
parse_nesting_result :: proc(text: string) -> (count: int, found_budget: bool) {
|
||||
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)
|
||||
module := parser.parse(&stream, &source_file, &diagnostics)
|
||||
defer ast.destroy_module(&module)
|
||||
for diagnostic in diagnostics.items {
|
||||
found_budget = found_budget || strings.contains(diagnostic.message, "expression nesting exceeds 256 levels")
|
||||
}
|
||||
return len(diagnostics.items), found_budget
|
||||
}
|
||||
|
||||
@(test)
|
||||
parser_enforces_explicit_expression_nesting_budget :: proc(t: ^testing.T) {
|
||||
modes := [?]bool{false, true}
|
||||
for call in modes {
|
||||
at_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING)
|
||||
defer delete(at_limit)
|
||||
count, found := parse_nesting_result(at_limit)
|
||||
testing.expect_value(t, count, 0)
|
||||
testing.expect(t, !found)
|
||||
|
||||
over_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING+1)
|
||||
defer delete(over_limit)
|
||||
_, found = parse_nesting_result(over_limit)
|
||||
testing.expect(t, found)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) {
|
||||
options, valid := parse_cli_args([]string{
|
||||
@@ -1078,8 +1185,6 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
|
||||
|
||||
expected := []string{
|
||||
"/usr/bin/env",
|
||||
"ZIG_LOCAL_CACHE_DIR=/tmp/brolang-zig-cache",
|
||||
"ZIG_GLOBAL_CACHE_DIR=/tmp/brolang-zig-global-cache",
|
||||
"zig",
|
||||
"cc",
|
||||
"-Wno-override-module",
|
||||
@@ -1097,6 +1202,154 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: ^testing.T) {
|
||||
store := source.init_store()
|
||||
defer source.destroy_store(&store)
|
||||
bytes := make([]byte, len("one\ntwo\n"))
|
||||
copy(bytes, "one\ntwo\n")
|
||||
source_id := source.add_source_owned(&store, "owned.bro", bytes)
|
||||
bytes[0] = 'O'
|
||||
|
||||
diagnostics := source.init_store_diagnostics(&store)
|
||||
defer source.destroy_diagnostics(&diagnostics)
|
||||
span := source.Span{file=source_id, start=4, end=7}
|
||||
first := source.add(&diagnostics, span, "same")
|
||||
second := source.addf(&diagnostics, span, "%s", "same")
|
||||
other := source.add(&diagnostics, span, "other")
|
||||
formatted := source.format(&diagnostics, other)
|
||||
defer delete(formatted)
|
||||
|
||||
testing.expect_value(t, store.items[source_id].text, "One\ntwo\n")
|
||||
testing.expect_value(t, len(store.items[source_id].line_starts), 3)
|
||||
testing.expect_value(t, first, second)
|
||||
testing.expect_value(t, len(diagnostics.items), 2)
|
||||
testing.expect(t, strings.contains(formatted, "owned.bro:2:1:"))
|
||||
}
|
||||
|
||||
@(test)
|
||||
maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) {
|
||||
source_file := source.Source{path="test.bro", text="value :: 9223372036854775807\nmain :: func() void {}\n"}
|
||||
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)
|
||||
module := parser.parse(&stream, &source_file, &diagnostics)
|
||||
defer ast.destroy_module(&module)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect_value(t, module.exprs[module.globals[0].expr].integer, i64(9223372036854775807))
|
||||
}
|
||||
|
||||
@(test)
|
||||
malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) {
|
||||
module := ir.init_module()
|
||||
defer ir.destroy_module(&module)
|
||||
instructions := make([]ir.Instruction, 3)
|
||||
instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=-1, b=-1, diagnostic=-1}
|
||||
instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=-1, b=-1, diagnostic=-1}
|
||||
instructions[2] = ir.Instruction{op=.Return, type=types.I32, a=1, b=-1, diagnostic=-1}
|
||||
append(&module.functions, ir.Function{
|
||||
link_name=strings.clone("main"),
|
||||
calling_convention=.C,
|
||||
implementation=.Definition,
|
||||
linkage=.External,
|
||||
is_main=true,
|
||||
result=types.I32,
|
||||
instructions=instructions,
|
||||
})
|
||||
source_file := source.Source{path="test.bro", text=""}
|
||||
diagnostics := source.init_diagnostics(&source_file)
|
||||
defer source.destroy_diagnostics(&diagnostics)
|
||||
symbols := symbol.init_table()
|
||||
defer symbol.destroy_table(&symbols)
|
||||
text := llvm.emit(&module, &diagnostics, &symbols)
|
||||
defer delete(text)
|
||||
|
||||
testing.expect(t, strings.contains(text, "call void @bro.trap"))
|
||||
testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86"))
|
||||
testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766"))
|
||||
testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn"))
|
||||
testing.expect(t, !strings.contains(text, "%v-1"))
|
||||
llvm_path := "/tmp/brolang-test-malformed-recovery.ll"
|
||||
output := "/tmp/brolang-test-malformed-recovery"
|
||||
defer _ = os.remove(llvm_path)
|
||||
defer _ = os.remove(output)
|
||||
testing.expect(t, os.write_entire_file(llvm_path, transmute([]byte)text))
|
||||
testing.expect(t, backend.compile(llvm_path, output))
|
||||
}
|
||||
|
||||
@(test)
|
||||
hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testing.T) {
|
||||
builder := strings.builder_make()
|
||||
defer strings.builder_destroy(&builder)
|
||||
strings.write_string(&builder, "sum :: func(value i32) i32 { return value")
|
||||
for _ in 0..<100_000 {
|
||||
strings.write_string(&builder, " + 1")
|
||||
}
|
||||
strings.write_string(&builder, " }\nmain :: func() void { _ = sum(0) }\n")
|
||||
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
|
||||
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)
|
||||
ir_module := lower.lower(&hir_module)
|
||||
defer ir.destroy_module(&ir_module)
|
||||
|
||||
instruction_count := 0
|
||||
for function in ir_module.functions {
|
||||
instruction_count += len(function.instructions)
|
||||
}
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect(t, instruction_count > 100_000)
|
||||
}
|
||||
|
||||
@(test)
|
||||
deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) {
|
||||
count := 50_000
|
||||
ast_module := ast.init_module()
|
||||
defer ast.destroy_module(&ast_module)
|
||||
source_file := source.Source{path="test.bro", text=""}
|
||||
diagnostics := source.init_diagnostics(&source_file)
|
||||
defer source.destroy_diagnostics(&diagnostics)
|
||||
symbols := symbol.init_table()
|
||||
defer symbol.destroy_table(&symbols)
|
||||
name := symbol.intern(&symbols, "value")
|
||||
state := checker.Checker{
|
||||
ast_module=&ast_module,
|
||||
diagnostics=&diagnostics,
|
||||
symbols=&symbols,
|
||||
module=hir.init_module(),
|
||||
allocator=context.allocator,
|
||||
}
|
||||
defer hir.destroy_module(&state.module)
|
||||
defer delete(state.cycle_stack)
|
||||
|
||||
for id in 0..<count {
|
||||
append(&ast_module.globals, ast.Global{name=name})
|
||||
dependencies: [dynamic]int
|
||||
dependencies.allocator = context.allocator
|
||||
append(&dependencies, (id+1)%count)
|
||||
append(&state.module.globals, hir.Global{name=name, dependencies=dependencies})
|
||||
}
|
||||
states := make([]u8, count)
|
||||
defer delete(states)
|
||||
checker.detect_global_cycles_visit(&state, 0, states)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 1)
|
||||
for global in state.module.globals {
|
||||
testing.expect(t, global.problematic)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-mutable"
|
||||
|
||||
Reference in New Issue
Block a user