diff --git a/TODO.md b/TODO.md index 1343bc4..d825369 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,13 @@ - for global initialization cycles, report also starting and ending lines +# compiler hardening follow-ups + +- migrate spans, AST/HIR/IR ids, and diagnostics from `int` to compact integer types +- prune unreachable function specializations before HIR construction and emission +- support unary minus, including the signed i64 minimum literal boundary +- move ignored example binaries into a dedicated build directory and remove `.review_tmp` + # milestones 1. interop type foundation diff --git a/benchmarks/symbols/README.md b/benchmarks/symbols/README.md index 6edd77f..34f56ce 100644 --- a/benchmarks/symbols/README.md +++ b/benchmarks/symbols/README.md @@ -25,3 +25,7 @@ Results captured on 2026-06-10 with Odin `dev-2026-02:b942f72cb`: | Diagnostics | 0 | 0 | The measured run reduced token size by 14.3% and peak tracked memory by 9.8%. + +After the iterative compiler-hardening work on 2026-06-12, the same benchmark +reported 13,222,443 peak bytes and 40,117 allocations. The reusable traversal +stacks keep allocation count effectively unchanged from the interning baseline. diff --git a/compiler/backend/backend.odin b/compiler/backend/backend.odin index 4413e86..cae10bd 100644 --- a/compiler/backend/backend.odin +++ b/compiler/backend/backend.odin @@ -19,8 +19,6 @@ build_command :: proc( command: [dynamic]string command.allocator = allocator append_owned(&command, "/usr/bin/env", allocator) - append_owned(&command, "ZIG_LOCAL_CACHE_DIR=/tmp/brolang-zig-cache", allocator) - append_owned(&command, "ZIG_GLOBAL_CACHE_DIR=/tmp/brolang-zig-global-cache", allocator) append_owned(&command, "zig", allocator) append_owned(&command, "cc", allocator) append_owned(&command, "-Wno-override-module", allocator) diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index e09d045..04e411f 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -60,6 +60,11 @@ Checker :: struct { global_types: []types.Type, constants: []Constant, template_diagnostics: []int, + constant_stack: [dynamic]Constant_Frame, + expr_stack: [dynamic]int, + infer_stack: [dynamic]Infer_Frame, + build_stack: [dynamic]Build_Expr_Frame, + cycle_stack: [dynamic]Cycle_Frame, main_symbol: symbol.Id, sink_symbol: symbol.Id, allocator: mem.Allocator, @@ -69,38 +74,75 @@ symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string { return symbol.resolve(checker.symbols, id) } +Constant_Frame :: struct { + expr: int, + stage: u8, +} + eval_constant :: proc(checker: ^Checker, expr_id: int) -> Constant { if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { return Constant{kind = .Not_Constant} } - if checker.constants[expr_id].kind != .Unknown { - return checker.constants[expr_id] + stack := checker.constant_stack + clear_dynamic_array(&stack) + defer { + clear_dynamic_array(&stack) + checker.constant_stack = stack } - expr := checker.ast_module.exprs[expr_id] - result: Constant - switch expr.kind { - case .Integer: - result = Constant{kind = .Value, value = i128(expr.integer)} - case .Add: - left := eval_constant(checker, expr.left) - right := eval_constant(checker, expr.right) + append(&stack, Constant_Frame{expr=expr_id}) + + for len(stack) > 0 { + frame_index := len(stack)-1 + frame := stack[frame_index] + if checker.constants[frame.expr].kind != .Unknown { + _ = pop(&stack) + continue + } + expr := checker.ast_module.exprs[frame.expr] + if expr.kind != .Add { + result := Constant{kind = .Not_Constant} + if expr.kind == .Integer { + result = Constant{kind = .Value, value = i128(expr.integer)} + } + checker.constants[frame.expr] = result + _ = pop(&stack) + continue + } + if frame.stage == 0 { + stack[frame_index].stage = 1 + if expr.left >= 0 && expr.left < len(checker.ast_module.exprs) && + checker.constants[expr.left].kind == .Unknown { + append(&stack, Constant_Frame{expr=expr.left}) + } + continue + } + if frame.stage == 1 { + stack[frame_index].stage = 2 + if expr.right >= 0 && expr.right < len(checker.ast_module.exprs) && + checker.constants[expr.right].kind == .Unknown { + append(&stack, Constant_Frame{expr=expr.right}) + } + continue + } + left := Constant{kind = .Not_Constant} + right := Constant{kind = .Not_Constant} + if expr.left >= 0 && expr.left < len(checker.constants) { + left = checker.constants[expr.left] + } + if expr.right >= 0 && expr.right < len(checker.constants) { + right = checker.constants[expr.right] + } + result := Constant{kind = .Not_Constant} if left.kind == .Overflow || right.kind == .Overflow { result = Constant{kind = .Overflow} - } else if left.kind != .Value || right.kind != .Value { - result = Constant{kind = .Not_Constant} - } else { + } else if left.kind == .Value && right.kind == .Value { value, overflow := intrinsics.overflow_add(left.value, right.value) - if overflow { - result = Constant{kind = .Overflow} - } else { - result = Constant{kind = .Value, value = value} - } + result = Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} } - case .Invalid, .Name, .Call: - result = Constant{kind = .Not_Constant} + checker.constants[frame.expr] = result + _ = pop(&stack) } - checker.constants[expr_id] = result - return result + return checker.constants[expr_id] } fits_signed_type :: proc(value: i128, target: types.Type) -> bool { @@ -264,26 +306,29 @@ contains_name :: proc(names: []symbol.Id, name: symbol.Id) -> bool { } mark_expr_imports_used :: proc(checker: ^Checker, expr_id, file: int) { - if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { - return + stack := checker.expr_stack + clear_dynamic_array(&stack) + defer { + clear_dynamic_array(&stack) + checker.expr_stack = stack } - expr := checker.ast_module.exprs[expr_id] - switch expr.kind { - case .Name: - if symbol.is_valid(expr.qualifier) { + append(&stack, expr_id) + for len(stack) > 0 { + id := pop(&stack) + if id < 0 || id >= len(checker.ast_module.exprs) { + continue + } + expr := checker.ast_module.exprs[id] + if (expr.kind == .Name || expr.kind == .Call) && symbol.is_valid(expr.qualifier) { _ = find_import(checker, file, expr.qualifier, true) } - case .Call: - if symbol.is_valid(expr.qualifier) { - _ = find_import(checker, file, expr.qualifier, true) + switch expr.kind { + case .Call: + append(&stack, ..expr.args) + case .Add: + append(&stack, expr.left, expr.right) + case .Invalid, .Integer, .Name: } - for arg in expr.args { - mark_expr_imports_used(checker, arg, file) - } - case .Add: - mark_expr_imports_used(checker, expr.left, file) - mark_expr_imports_used(checker, expr.right, file) - case .Invalid, .Integer: } } @@ -445,80 +490,138 @@ ensure_spec :: proc(checker: ^Checker, template: int, actual_args: []types.Type) return index } +Infer_Frame :: struct { + expr: int, + stage: u8, + left: types.Type, + arg_index: int, + args: []types.Type, + template: int, +} + infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local, pkg := 0, file := 0) -> types.Type { - if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { - return types.INVALID + stack := checker.infer_stack + clear_dynamic_array(&stack) + defer { + for frame in stack { + delete(frame.args, checker.allocator) + } + clear_dynamic_array(&stack) + checker.infer_stack = stack } - constant := eval_constant(checker, expr_id) - if constant.kind == .Overflow || (constant.kind == .Value && !fits_i64(constant.value)) { - return types.I64 + append(&stack, Infer_Frame{expr=expr_id, template=-1}) + last := types.INVALID + + for len(stack) > 0 { + frame_index := len(stack)-1 + frame := stack[frame_index] + if frame.expr < 0 || frame.expr >= len(checker.ast_module.exprs) { + last = types.INVALID + _ = pop(&stack) + continue + } + expr := checker.ast_module.exprs[frame.expr] + if frame.stage == 0 { + constant := eval_constant(checker, frame.expr) + if constant.kind == .Overflow || (constant.kind == .Value && !fits_i64(constant.value)) { + last = types.I64 + _ = pop(&stack) + continue + } + if constant.kind == .Value { + last = types.smallest_signed_for_literal(i64(constant.value)) + _ = pop(&stack) + continue + } + switch expr.kind { + case .Invalid: + last = types.INVALID + _ = pop(&stack) + case .Integer: + last = types.smallest_signed_for_literal(expr.integer) + _ = pop(&stack) + case .Name: + last = types.INVALID + if !symbol.is_valid(expr.qualifier) { + last = find_infer_local(locals, expr.name) + } + if !types.is_valid(last) { + target_pkg, available := expr_package(checker, expr, pkg, file) + if available { + global := find_global(checker, expr.name, target_pkg) + if global >= 0 { + last = checker.global_types[global] + } + } + } + _ = pop(&stack) + case .Add: + stack[frame_index].stage = 1 + append(&stack, Infer_Frame{expr=expr.left, template=-1}) + case .Call: + target_pkg, available := expr_package(checker, expr, pkg, file) + template := -1 + if available { + template = find_template(checker, expr.name, target_pkg) + } + if template < 0 { + last = types.INVALID + _ = pop(&stack) + continue + } + if checker.template_diagnostics[template] >= 0 { + declared := type_from_syntax(checker.ast_module.functions[template].result) + last = declared if declared.kind == .Concrete || declared.kind == .Void else types.INVALID + _ = pop(&stack) + continue + } + stack[frame_index].template = template + stack[frame_index].args = make([]types.Type, len(expr.args), checker.allocator) + stack[frame_index].stage = 3 + if len(expr.args) > 0 { + append(&stack, Infer_Frame{expr=expr.args[0], template=-1}) + } + } + continue + } + if frame.stage == 1 { + stack[frame_index].left = last + stack[frame_index].stage = 2 + append(&stack, Infer_Frame{expr=expr.right, template=-1}) + continue + } + if frame.stage == 2 { + last = types.widest(frame.left, last) + _ = pop(&stack) + continue + } + if frame.stage == 3 { + if frame.arg_index < len(expr.args) { + stack[frame_index].args[frame.arg_index] = last + stack[frame_index].arg_index += 1 + if frame.arg_index+1 < len(expr.args) { + append(&stack, Infer_Frame{expr=expr.args[frame.arg_index+1], template=-1}) + continue + } + } + function := checker.ast_module.functions[frame.template] + if can_specialize(function, stack[frame_index].args) { + spec := ensure_spec(checker, frame.template, stack[frame_index].args) + last = checker.specs[spec].result + } else { + declared := type_from_syntax(function.result) + if function.pkg == 0 && function.name == checker.main_symbol && function.result == .Int { + last = types.I32 + } else { + last = declared if declared.kind == .Concrete || declared.kind == .Void else types.INVALID + } + } + delete(stack[frame_index].args, checker.allocator) + stack[frame_index].args = nil + _ = pop(&stack) + } } - if constant.kind == .Value { - return types.smallest_signed_for_literal(i64(constant.value)) - } - expr := checker.ast_module.exprs[expr_id] - switch expr.kind { - case .Invalid: - return types.INVALID - case .Integer: - return types.smallest_signed_for_literal(expr.integer) - case .Name: - if !symbol.is_valid(expr.qualifier) { - local_type := find_infer_local(locals, expr.name) - if types.is_valid(local_type) { - return local_type - } - } - target_pkg, available := expr_package(checker, expr, pkg, file) - if !available { - return types.INVALID - } - global := find_global(checker, expr.name, target_pkg) - if global >= 0 { - return checker.global_types[global] - } - return types.INVALID - case .Add: - left := infer_expr(checker, expr.left, locals, pkg, file) - right := infer_expr(checker, expr.right, locals, pkg, file) - return types.widest(left, right) - case .Call: - target_pkg, available := expr_package(checker, expr, pkg, file) - if !available { - return types.INVALID - } - template := find_template(checker, expr.name, target_pkg) - if template < 0 { - return types.INVALID - } - if checker.template_diagnostics[template] >= 0 { - declared := type_from_syntax(checker.ast_module.functions[template].result) - if declared.kind == .Concrete || declared.kind == .Void { - return declared - } - return types.INVALID - } - args := make([]types.Type, len(expr.args), checker.allocator) - for arg, index in expr.args { - args[index] = infer_expr(checker, arg, locals, pkg, file) - } - function := checker.ast_module.functions[template] - if !can_specialize(function, args) { - delete(args, checker.allocator) - declared := type_from_syntax(function.result) - if function.pkg == 0 && function.name == checker.main_symbol && function.result == .Int { - return types.I32 - } - if declared.kind == .Concrete || declared.kind == .Void { - return declared - } - return types.INVALID - } - spec := ensure_spec(checker, template, args) - delete(args, checker.allocator) - return checker.specs[spec].result - } - return types.INVALID + return last } infer_spec_result :: proc(checker: ^Checker, spec_id: int) -> types.Type { @@ -752,6 +855,17 @@ build_constant_expr :: proc( ) } +Build_Expr_Frame :: struct { + expr: int, + expected: types.Type, + stage: u8, + left: int, + arg_index: int, + built_args: []int, + arg_types: []types.Type, + template: int, +} + build_expr :: proc( checker: ^Checker, expr_id: int, @@ -762,173 +876,190 @@ build_expr :: proc( pkg := 0, file := 0, ) -> int { - if expr_id < 0 || expr_id >= len(checker.ast_module.exprs) { - id := source.add(checker.diagnostics, source.Span{}, "missing expression") - return invalid_hir_expr(checker, source.Span{}, id) + stack := checker.build_stack + clear_dynamic_array(&stack) + defer { + for frame in stack { + delete(frame.built_args, checker.allocator) + delete(frame.arg_types, checker.allocator) + } + clear_dynamic_array(&stack) + checker.build_stack = stack } - expr := checker.ast_module.exprs[expr_id] - constant := eval_constant(checker, expr_id) - if constant.kind == .Value || constant.kind == .Overflow { - return build_constant_expr(checker, expr, constant, expected) - } - switch expr.kind { - case .Invalid: - return invalid_hir_expr(checker, expr.span, expr.diagnostic) - case .Integer: - unreachable() - case .Name: - if !symbol.is_valid(expr.qualifier) { - if local, ok := find_build_local(locals, expr.name); ok { - return add_hir_expr( + append(&stack, Build_Expr_Frame{expr=expr_id, expected=expected, template=-1}) + last := -1 + + for len(stack) > 0 { + frame_index := len(stack)-1 + frame := stack[frame_index] + if frame.expr < 0 || frame.expr >= len(checker.ast_module.exprs) { + id := source.add(checker.diagnostics, source.Span{}, "missing expression") + last = invalid_hir_expr(checker, source.Span{}, id) + _ = pop(&stack) + continue + } + expr := checker.ast_module.exprs[frame.expr] + if frame.stage == 0 { + constant := eval_constant(checker, frame.expr) + if constant.kind == .Value || constant.kind == .Overflow { + last = build_constant_expr(checker, expr, constant, frame.expected) + _ = pop(&stack) + continue + } + switch expr.kind { + case .Invalid, .Integer: + last = invalid_hir_expr(checker, expr.span, expr.diagnostic) + _ = pop(&stack) + case .Name: + last = -1 + if !symbol.is_valid(expr.qualifier) { + if local, ok := find_build_local(locals, expr.name); ok { + last = add_hir_expr(checker, hir.Expr{ + kind=.Local, span=expr.span, type=local.type, target=local.id, + left=-1, right=-1, diagnostic=-1, + }) + } + } + if last < 0 { + target_pkg, available := expr_package(checker, expr, pkg, file, true) + if !available { + id := add_package_resolution_diagnostic(checker, expr, file) + last = invalid_hir_expr(checker, expr.span, id) + } else if global := find_global(checker, expr.name, target_pkg); global >= 0 { + add_unique(global_reads, global) + last = add_hir_expr(checker, hir.Expr{ + kind=.Global, span=expr.span, type=checker.global_types[global], + target=global, left=-1, right=-1, diagnostic=-1, + }) + } else { + id := add_name_resolution_diagnostic(checker, expr, target_pkg) + last = invalid_hir_expr(checker, expr.span, id) + } + } + _ = pop(&stack) + case .Add: + stack[frame_index].stage = 1 + append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=-1}) + case .Call: + target_pkg, available := expr_package(checker, expr, pkg, file, true) + if !available { + id := add_package_resolution_diagnostic(checker, expr, file) + last = invalid_hir_expr(checker, expr.span, id) + _ = pop(&stack) + continue + } + template := find_template(checker, expr.name, target_pkg) + if template < 0 { + id := add_call_resolution_diagnostic(checker, expr, target_pkg) + last = invalid_hir_expr(checker, expr.span, id) + _ = pop(&stack) + continue + } + if checker.template_diagnostics[template] >= 0 { + last = invalid_hir_expr(checker, expr.span, checker.template_diagnostics[template]) + _ = pop(&stack) + continue + } + if len(expr.args) != len(checker.ast_module.functions[template].params) { + id := source.addf( + checker.diagnostics, + expr.span, + "function '%s' expects %d arguments, got %d", + symbol_text(checker, expr.name), + len(checker.ast_module.functions[template].params), + len(expr.args), + ) + last = invalid_hir_expr(checker, expr.span, id) + _ = pop(&stack) + continue + } + stack[frame_index].template = template + stack[frame_index].built_args = make([]int, len(expr.args), checker.allocator) + stack[frame_index].arg_types = make([]types.Type, len(expr.args), checker.allocator) + stack[frame_index].stage = 3 + if len(expr.args) > 0 { + arg_expected := type_from_syntax(checker.ast_module.functions[template].params[0].type) + if arg_expected.kind != .Concrete { + arg_expected = types.INVALID + } + append(&stack, Build_Expr_Frame{expr=expr.args[0], expected=arg_expected, template=-1}) + } + } + continue + } + if frame.stage == 1 { + stack[frame_index].left = last + stack[frame_index].stage = 2 + append(&stack, Build_Expr_Frame{expr=expr.right, expected=types.INVALID, template=-1}) + continue + } + if frame.stage == 2 { + left := frame.left + right := last + result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) + if !types.is_signed(result) { + id := source.add(checker.diagnostics, expr.span, "addition requires compatible signed integers") + last = invalid_hir_expr(checker, expr.span, id) + } else { + left = coerce_expr(checker, left, result, checker.module.exprs[left].span) + right = coerce_expr(checker, right, result, checker.module.exprs[right].span) + last = add_hir_expr(checker, hir.Expr{ + kind=.Add, span=expr.span, type=result, left=left, right=right, + target=-1, diagnostic=-1, + }) + } + _ = pop(&stack) + continue + } + if frame.stage == 3 { + if frame.arg_index < len(expr.args) { + stack[frame_index].built_args[frame.arg_index] = last + stack[frame_index].arg_types[frame.arg_index] = checker.module.exprs[last].type + stack[frame_index].arg_index += 1 + if frame.arg_index+1 < len(expr.args) { + next := frame.arg_index+1 + arg_expected := type_from_syntax(checker.ast_module.functions[frame.template].params[next].type) + if arg_expected.kind != .Concrete { + arg_expected = types.INVALID + } + append(&stack, Build_Expr_Frame{expr=expr.args[next], expected=arg_expected, template=-1}) + continue + } + } + spec := ensure_spec(checker, frame.template, stack[frame_index].arg_types) + delete(stack[frame_index].arg_types, checker.allocator) + stack[frame_index].arg_types = nil + for _, index in stack[frame_index].built_args { + stack[frame_index].built_args[index] = coerce_expr( checker, - hir.Expr { - kind = .Local, - span = expr.span, - type = local.type, - target = local.id, - left = -1, - right = -1, - diagnostic = -1, - }, + stack[frame_index].built_args[index], + checker.specs[spec].args[index], + checker.module.exprs[stack[frame_index].built_args[index]].span, ) } - } - target_pkg, available := expr_package(checker, expr, pkg, file, true) - if !available { - id := add_package_resolution_diagnostic(checker, expr, file) - return invalid_hir_expr(checker, expr.span, id) - } - global := find_global(checker, expr.name, target_pkg) - if global >= 0 { - add_unique(global_reads, global) - return add_hir_expr( - checker, - hir.Expr { - kind = .Global, - span = expr.span, - type = checker.global_types[global], - target = global, - left = -1, - right = -1, - diagnostic = -1, - }, - ) - } - id := add_name_resolution_diagnostic(checker, expr, target_pkg) - return invalid_hir_expr(checker, expr.span, id) - case .Add: - left := build_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) - right := build_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) - left_type := checker.module.exprs[left].type - right_type := checker.module.exprs[right].type - result := types.widest(left_type, right_type) - if !types.is_signed(result) { - id := source.add( - checker.diagnostics, - expr.span, - "addition requires compatible signed integers", - ) - return invalid_hir_expr(checker, expr.span, id) - } - left = coerce_expr(checker, left, result, checker.module.exprs[left].span) - right = coerce_expr(checker, right, result, checker.module.exprs[right].span) - return add_hir_expr( - checker, - hir.Expr { - kind = .Add, - span = expr.span, - type = result, - left = left, - right = right, - target = -1, - diagnostic = -1, - }, - ) - case .Call: - target_pkg, available := expr_package(checker, expr, pkg, file, true) - if !available { - id := add_package_resolution_diagnostic(checker, expr, file) - return invalid_hir_expr(checker, expr.span, id) - } - template := find_template(checker, expr.name, target_pkg) - if template < 0 { - id := add_call_resolution_diagnostic(checker, expr, target_pkg) - return invalid_hir_expr(checker, expr.span, id) - } - if checker.template_diagnostics[template] >= 0 { - return invalid_hir_expr(checker, expr.span, checker.template_diagnostics[template]) - } - if len(expr.args) != len(checker.ast_module.functions[template].params) { - id := source.addf( - checker.diagnostics, - expr.span, - "function '%s' expects %d arguments, got %d", - symbol_text(checker, expr.name), - len(checker.ast_module.functions[template].params), - len(expr.args), - ) - return invalid_hir_expr(checker, expr.span, id) - } - built_args := make([]int, len(expr.args), checker.allocator) - arg_types := make([]types.Type, len(expr.args), checker.allocator) - for arg, index in expr.args { - arg_expected := type_from_syntax(checker.ast_module.functions[template].params[index].type) - if arg_expected.kind != .Concrete { - arg_expected = types.INVALID + add_unique(calls, spec) + result := checker.specs[spec].result + if !types.is_valid(result) { + id := source.addf( + checker.diagnostics, + expr.span, + "could not resolve result type for specialization of '%s'", + symbol_text(checker, expr.name), + ) + delete(stack[frame_index].built_args, checker.allocator) + stack[frame_index].built_args = nil + last = invalid_hir_expr(checker, expr.span, id) + } else { + last = add_hir_expr(checker, hir.Expr{ + kind=.Call, span=expr.span, type=result, target=spec, + left=-1, right=-1, args=stack[frame_index].built_args, diagnostic=-1, + }) + stack[frame_index].built_args = nil } - built_args[index] = build_expr( - checker, - arg, - locals, - global_reads, - calls, - arg_expected, - pkg, - file, - ) - arg_types[index] = checker.module.exprs[built_args[index]].type + _ = pop(&stack) } - spec := ensure_spec(checker, template, arg_types) - delete(arg_types, checker.allocator) - for _, index in built_args { - built_args[index] = coerce_expr( - checker, - built_args[index], - checker.specs[spec].args[index], - checker.module.exprs[built_args[index]].span, - ) - } - add_unique(calls, spec) - result := checker.specs[spec].result - if !types.is_valid(result) { - id := source.addf( - checker.diagnostics, - expr.span, - "could not resolve result type for specialization of '%s'", - symbol_text(checker, expr.name), - ) - delete(built_args, checker.allocator) - return invalid_hir_expr(checker, expr.span, id) - } - return add_hir_expr( - checker, - hir.Expr { - kind = .Call, - span = expr.span, - type = result, - target = spec, - left = -1, - right = -1, - args = built_args, - diagnostic = -1, - }, - ) } - return invalid_hir_expr( - checker, - expr.span, - source.add(checker.diagnostics, expr.span, "invalid expression"), - ) + return last } make_link_name :: proc(checker: ^Checker, spec_id: int) -> string { @@ -1022,7 +1153,7 @@ build_function :: proc(checker: ^Checker, spec_id: int) { result = spec.result, locals = hir_locals[:], body = body[:], - direct_global_reads = global_reads[:], + direct_global_reads = global_reads, calls = calls[:], problematic = problematic, diagnostic = checker.template_diagnostics[spec.template], @@ -1387,7 +1518,7 @@ build_function :: proc(checker: ^Checker, spec_id: int) { result = spec.result, locals = hir_locals[:], body = body[:], - direct_global_reads = global_reads[:], + direct_global_reads = global_reads, calls = calls[:], problematic = problematic, diagnostic = -1, @@ -1396,24 +1527,31 @@ build_function :: proc(checker: ^Checker, spec_id: int) { delete(locals) } -expr_problematic :: proc(module: ^hir.Module, expr_id: int) -> bool { - if expr_id < 0 || expr_id >= len(module.exprs) { - return true +expr_problematic :: proc(checker: ^Checker, expr_id: int) -> bool { + module := &checker.module + stack := checker.expr_stack + clear_dynamic_array(&stack) + defer { + clear_dynamic_array(&stack) + checker.expr_stack = stack } - expr := module.exprs[expr_id] - if expr.kind == .Invalid { - return true - } - if expr.left >= 0 && expr_problematic(module, expr.left) { - return true - } - if expr.right >= 0 && expr_problematic(module, expr.right) { - return true - } - for arg in expr.args { - if expr_problematic(module, arg) { + append(&stack, expr_id) + for len(stack) > 0 { + id := pop(&stack) + if id < 0 || id >= len(module.exprs) { return true } + expr := module.exprs[id] + if expr.kind == .Invalid { + return true + } + if expr.left >= 0 { + append(&stack, expr.left) + } + if expr.right >= 0 { + append(&stack, expr.right) + } + append(&stack, ..expr.args) } return false } @@ -1477,10 +1615,10 @@ build_globals :: proc(checker: ^Checker) { expr = expr, static_value = static_value, is_static = is_static, - dependencies = dependencies[:], + dependencies = dependencies, calls = calls[:], - direct_problem = expr_problematic(&checker.module, expr), - problematic = expr_problematic(&checker.module, expr), + direct_problem = expr_problematic(checker, expr), + problematic = expr_problematic(checker, expr), diagnostic = diagnostic, }, ) @@ -1544,17 +1682,13 @@ resolve_call_targets :: proc(checker: ^Checker) { } } -append_unique_slice :: proc(values: ^[]int, value: int, allocator: mem.Allocator) -> bool { +append_unique_slice :: proc(values: ^[dynamic]int, value: int) -> bool { for existing in values^ { if existing == value { return false } } - replacement := make([]int, len(values^) + 1, allocator) - copy(replacement, values^) - replacement[len(values^)] = value - delete(values^, allocator) - values^ = replacement + append(values, value) return true } @@ -1572,11 +1706,7 @@ propagate_global_reads :: proc(checker: ^Checker) { continue } for global_id in checker.module.functions[callee].direct_global_reads { - if append_unique_slice( - &function.direct_global_reads, - global_id, - checker.allocator, - ) { + if append_unique_slice(&function.direct_global_reads, global_id) { changed = true } } @@ -1593,37 +1723,68 @@ propagate_global_reads :: proc(checker: ^Checker) { continue } for dependency in checker.module.functions[function_id].direct_global_reads { - _ = append_unique_slice(&global.dependencies, dependency, checker.allocator) + _ = append_unique_slice(&global.dependencies, dependency) } } } } +Cycle_Frame :: struct { + global: int, + next_dependency: int, +} + detect_global_cycles_visit :: proc(checker: ^Checker, global_id: int, states: []u8) { if states[global_id] == 2 { return } - if states[global_id] == 1 { - id := source.addf( - checker.diagnostics, - checker.ast_module.globals[global_id].span, - "global initialization cycle involving '%s'", - symbol_text(checker, checker.module.globals[global_id].name), - ) - checker.module.globals[global_id].diagnostic = id - checker.module.globals[global_id].problematic = true - return + stack := checker.cycle_stack + clear_dynamic_array(&stack) + defer { + clear_dynamic_array(&stack) + checker.cycle_stack = stack } - states[global_id] = 1 - for dependency in checker.module.globals[global_id].dependencies { - if dependency >= 0 && dependency < len(states) { - detect_global_cycles_visit(checker, dependency, states) - if checker.module.globals[dependency].problematic { - checker.module.globals[global_id].problematic = true - } + append(&stack, Cycle_Frame{global=global_id}) + for len(stack) > 0 { + frame_index := len(stack)-1 + frame := &stack[frame_index] + if states[frame.global] == 0 { + states[frame.global] = 1 } + dependencies := checker.module.globals[frame.global].dependencies + if frame.next_dependency >= len(dependencies) { + states[frame.global] = 2 + if checker.module.globals[frame.global].problematic && frame_index > 0 { + checker.module.globals[stack[frame_index-1].global].problematic = true + } + _ = pop(&stack) + continue + } + dependency := dependencies[frame.next_dependency] + frame.next_dependency += 1 + if dependency < 0 || dependency >= len(states) { + continue + } + if states[dependency] == 1 { + id := source.addf( + checker.diagnostics, + checker.ast_module.globals[dependency].span, + "global initialization cycle involving '%s'", + symbol_text(checker, checker.module.globals[dependency].name), + ) + checker.module.globals[dependency].diagnostic = id + checker.module.globals[dependency].problematic = true + checker.module.globals[frame.global].problematic = true + continue + } + if states[dependency] == 2 { + if checker.module.globals[dependency].problematic { + checker.module.globals[frame.global].problematic = true + } + continue + } + append(&stack, Cycle_Frame{global=dependency}) } - states[global_id] = 2 } synthesize_trap_main :: proc(checker: ^Checker) { @@ -1700,6 +1861,11 @@ check :: proc( allocator = allocator, } checker.specs.allocator = allocator + checker.constant_stack.allocator = allocator + checker.expr_stack.allocator = allocator + checker.infer_stack.allocator = allocator + checker.build_stack.allocator = allocator + checker.cycle_stack.allocator = allocator build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.constants = make([]Constant, len(ast_module.exprs), allocator) @@ -1718,6 +1884,11 @@ check :: proc( delete(checker.global_types, allocator) delete(checker.constants, allocator) delete(checker.template_diagnostics, allocator) + delete(checker.constant_stack) + delete(checker.expr_stack) + delete(checker.infer_stack) + delete(checker.build_stack) + delete(checker.cycle_stack) } for function, index in ast_module.functions { diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 76d859f..b9226ae 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -79,7 +79,7 @@ Function :: struct { result: types.Type, locals: []Local, body: []int, - direct_global_reads: []int, + direct_global_reads: [dynamic]int, calls: []int, problematic: bool, diagnostic: int, @@ -91,7 +91,7 @@ Global :: struct { expr: int, static_value: i64, is_static: bool, - dependencies: []int, + dependencies: [dynamic]int, calls: []int, direct_problem: bool, problematic: bool, @@ -125,11 +125,11 @@ destroy_module :: proc(module: ^Module) { delete(function.params, module.allocator) delete(function.locals, module.allocator) delete(function.body, module.allocator) - delete(function.direct_global_reads, module.allocator) + delete(function.direct_global_reads) delete(function.calls, module.allocator) } for global in module.globals { - delete(global.dependencies, module.allocator) + delete(global.dependencies) delete(global.calls, module.allocator) } delete(module.exprs) diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index fed62d3..6f8d35d 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -14,7 +14,6 @@ is_identifier_continue :: proc(value: byte) -> bool { keyword_kind :: proc(text: string) -> token.Kind { switch text { - case "c": return .Keyword_C case "func": return .Keyword_Func case "import": return .Keyword_Import case "return": return .Keyword_Return diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index fad4596..6192ab4 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -40,9 +40,37 @@ function_result_type :: proc(function: ir.Function) -> string { return llvm_type(function.result) } -write_operand :: proc(builder: ^strings.Builder, instructions: []ir.Instruction, value_id: int) { - if value_id < 0 || value_id >= len(instructions) { - fmt.sbprintf(builder, "-6148914691236517206") +sentinel :: proc(value_type: types.Type) -> i64 { + switch value_type.bits { + case 8: return -86 + case 16: return -21846 + case 32: return -1431655766 + case: return -6148914691236517206 + } +} + +valid_instruction :: proc(instructions: []ir.Instruction, instruction_id: int) -> bool { + return instruction_id >= 0 && instruction_id < len(instructions) +} + +valid_value :: proc(instructions: []ir.Instruction, value_id: int, expected: types.Type) -> bool { + if !valid_instruction(instructions, value_id) || + !types.is_concrete_integer(expected) || + !types.equal(instructions[value_id].type, expected) { + return false + } + switch instructions[value_id].op { + case .Param, .Const, .Load_Global, .Load, .Widen, .Add_Checked, .Call: + return true + case .Alloca, .Store, .Trap, .Return, .Return_Void: + return false + } + return false +} + +write_operand :: proc(builder: ^strings.Builder, instructions: []ir.Instruction, value_id: int, expected: types.Type) { + if !valid_value(instructions, value_id, expected) { + fmt.sbprintf(builder, "%d", sentinel(expected)) return } value := instructions[value_id] @@ -95,13 +123,27 @@ emit_trap_call :: proc(emitter: ^Emitter, message_id: int) { ) } -emit_call_args :: proc(builder: ^strings.Builder, instructions: []ir.Instruction, args: []int) { +emit_recovery_value :: proc(emitter: ^Emitter, instruction_id: int, instruction: ir.Instruction, fallback: string) { + message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, fallback) + emit_trap_call(emitter, message) + if types.is_concrete_integer(instruction.type) { + fmt.sbprintf( + &emitter.builder, + " %%v%d = add %s 0, %d\n", + instruction_id, + llvm_type(instruction.type), + sentinel(instruction.type), + ) + } +} + +emit_call_args :: proc(builder: ^strings.Builder, instructions: []ir.Instruction, args: []int, param_types: []types.Type) { for arg, index in args { if index > 0 { strings.write_string(builder, ", ") } - fmt.sbprintf(builder, "%s ", llvm_type(instructions[arg].type)) - write_operand(builder, instructions, arg) + fmt.sbprintf(builder, "%s ", llvm_type(param_types[index])) + write_operand(builder, instructions, arg, param_types[index]) } } @@ -122,11 +164,14 @@ emit_instruction_stream :: proc( case .Param, .Const: case .Load_Global: if instruction.target < 0 || instruction.target >= len(emitter.module.globals) { - message := diagnostic_message(emitter, -1, instruction.span, "invalid global reference") - emit_trap_call(emitter, message) + emit_recovery_value(emitter, instruction_id, instruction, "invalid global reference") continue } global := emitter.module.globals[instruction.target] + if !types.equal(instruction.type, global.type) { + emit_recovery_value(emitter, instruction_id, instruction, "invalid global reference type") + continue + } if global.is_static { fmt.sbprintf( &emitter.builder, @@ -145,8 +190,18 @@ emit_instruction_stream :: proc( ) } case .Alloca: + if !types.is_concrete_integer(instruction.type) { + emit_recovery_value(emitter, instruction_id, instruction, "invalid allocation type") + continue + } fmt.sbprintf(&emitter.builder, " %%v%d = alloca %s\n", instruction_id, llvm_type(instruction.type)) case .Load: + if !valid_instruction(instructions, instruction.a) || + instructions[instruction.a].op != .Alloca || + !types.equal(instructions[instruction.a].type, instruction.type) { + emit_recovery_value(emitter, instruction_id, instruction, "invalid load slot") + continue + } fmt.sbprintf( &emitter.builder, " %%v%d = load %s, ptr %%v%d\n", @@ -155,22 +210,41 @@ emit_instruction_stream :: proc( instruction.a, ) case .Store: + if !valid_instruction(instructions, instruction.a) || + instructions[instruction.a].op != .Alloca || + !types.equal(instructions[instruction.a].type, instruction.type) || + !valid_value(instructions, instruction.b, instruction.type) { + emit_recovery_value(emitter, instruction_id, instruction, "invalid store operand") + continue + } fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(instruction.type)) - write_operand(&emitter.builder, instructions, instruction.b) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type) fmt.sbprintf(&emitter.builder, ", ptr %%v%d\n", instruction.a) case .Widen: + if !valid_instruction(instructions, instruction.a) || + !types.is_concrete_integer(instructions[instruction.a].type) || + !types.is_concrete_integer(instruction.type) || + instructions[instruction.a].type.bits >= instruction.type.bits { + emit_recovery_value(emitter, instruction_id, instruction, "invalid widening operand") + continue + } from_type := instructions[instruction.a].type fmt.sbprintf(&emitter.builder, " %%v%d = sext %s ", instruction_id, llvm_type(from_type)) - write_operand(&emitter.builder, instructions, instruction.a) + write_operand(&emitter.builder, instructions, instruction.a, from_type) fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type)) case .Add_Checked: + if !valid_value(instructions, instruction.a, instruction.type) || + !valid_value(instructions, instruction.b, instruction.type) { + emit_recovery_value(emitter, instruction_id, instruction, "invalid addition operand") + continue + } type_name := llvm_type(instruction.type) fmt.sbprintf(&emitter.builder, " %%pair%d = call ", instruction_id) strings.write_string(&emitter.builder, "{ ") fmt.sbprintf(&emitter.builder, "%s, i1 } @llvm.sadd.with.overflow.%s(%s ", type_name, type_name, type_name) - write_operand(&emitter.builder, instructions, instruction.a) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type) fmt.sbprintf(&emitter.builder, ", %s ", type_name) - write_operand(&emitter.builder, instructions, instruction.b) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type) fmt.sbprintf(&emitter.builder, ")\n") fmt.sbprintf(&emitter.builder, " %%v%d = extractvalue ", instruction_id) strings.write_string(&emitter.builder, "{ ") @@ -191,11 +265,27 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_id) case .Call: if instruction.target < 0 || instruction.target >= len(emitter.module.functions) { - message := diagnostic_message(emitter, -1, instruction.span, "invalid function specialization") - emit_trap_call(emitter, message) + emit_recovery_value(emitter, instruction_id, instruction, "invalid function specialization") continue } target := emitter.module.functions[instruction.target] + valid_args := len(instruction.args) == len(target.param_types) + if valid_args { + for arg, index in instruction.args { + if !valid_value(instructions, arg, target.param_types[index]) { + valid_args = false + break + } + } + } + target_result := target.result + if target.is_main { + target_result = types.I32 + } + if !valid_args || !types.equal(instruction.type, target_result) { + emit_recovery_value(emitter, instruction_id, instruction, "invalid function call operands") + continue + } if instruction.type.kind != .Void { fmt.sbprintf(&emitter.builder, " %%v%d = ", instruction_id) } else { @@ -206,7 +296,7 @@ emit_instruction_stream :: proc( strings.write_string(&emitter.builder, "fastcc ") } fmt.sbprintf(&emitter.builder, "%s @%s(", function_result_type(target), target.link_name) - emit_call_args(&emitter.builder, instructions, instruction.args) + emit_call_args(&emitter.builder, instructions, instruction.args, target.param_types) strings.write_string(&emitter.builder, ")\n") case .Trap: message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, "invalid recovered source") @@ -217,7 +307,7 @@ emit_instruction_stream :: proc( continue } fmt.sbprintf(&emitter.builder, " ret %s ", function_result_type(function)) - write_operand(&emitter.builder, instructions, instruction.a) + write_operand(&emitter.builder, instructions, instruction.a, function.result) strings.write_string(&emitter.builder, "\n") after_return = true case .Return_Void: @@ -286,11 +376,11 @@ emit_global_accessors :: proc(emitter: ^Emitter) { placeholder_function.result = global.type value := emit_instruction_stream(emitter, global.initializer, placeholder_function, true) fmt.sbprintf(&emitter.builder, " store %s ", type_name) - write_operand(&emitter.builder, global.initializer, value) + write_operand(&emitter.builder, global.initializer, value, global.type) fmt.sbprintf(&emitter.builder, ", ptr @bro.g.%d\n", global_id) fmt.sbprintf(&emitter.builder, " store i8 2, ptr @bro.gstate.%d\n", global_id) fmt.sbprintf(&emitter.builder, " ret %s ", type_name) - write_operand(&emitter.builder, global.initializer, value) + write_operand(&emitter.builder, global.initializer, value, global.type) strings.write_string(&emitter.builder, "\nready:\n") fmt.sbprintf(&emitter.builder, " %%value = load %s, ptr @bro.g.%d\n ret %s %%value\n}\n\n", type_name, global_id, type_name) } @@ -383,7 +473,7 @@ emit_declarations :: proc(emitter: ^Emitter) { } strings.write_string( &emitter.builder, - "\ndefine internal void @bro.trap(ptr %message, i64 %length) {\nentry:\n %written = call i64 @write(i32 2, ptr %message, i64 %length)\n call void @llvm.trap()\n unreachable\n}\n\n", + "\ndefine internal void @bro.trap(ptr %message, i64 %length) noreturn {\nentry:\n %written = call i64 @write(i32 2, ptr %message, i64 %length)\n call void @llvm.trap()\n unreachable\n}\n\n", ) } diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 5438bd9..d4c69f8 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -152,13 +152,12 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r } for file_info in files { - bytes, read_ok := os.read_entire_file(file_info.fullpath) + bytes, read_ok := os.read_entire_file(file_info.fullpath, state.sources.allocator) if !read_ok { state.root_failed = true continue } - source_id := source.add_source(state.sources, file_info.fullpath, string(bytes)) - delete(bytes) + source_id := source.add_source_owned(state.sources, file_info.fullpath, bytes) file_id := len(state.module.files) append(&state.module.files, ast.File{source=source_id, pkg=pkg_id}) stream := lexer.lex(&state.sources.items[source_id], state.diagnostics, state.symbols, state.token_allocator) diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 8c00600..bc41aa6 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -2,6 +2,7 @@ package lower import "../hir" import "../ir" +import "../source" import "../types" import "core:fmt" import "core:mem" @@ -11,6 +12,7 @@ State :: struct { instructions: [dynamic]ir.Instruction, local_values: []int, local_slots: []int, + expr_stack: [dynamic]Lower_Expr_Frame, allocator: mem.Allocator, } @@ -35,142 +37,25 @@ sentinel :: proc(value_type: types.Type) -> i64 { } } -lower_expr :: proc(state: ^State, expr_id: int) -> int { - if expr_id < 0 || expr_id >= len(state.hir_module.exprs) { - trap := append_instruction(state, ir.Instruction{ - op=.Trap, - type=types.VOID, - target=-1, - a=-1, - b=-1, - diagnostic=-1, - }) - _ = trap - return append_instruction(state, ir.Instruction{ - op=.Const, - type=types.I64, - integer=sentinel(types.I64), - target=-1, - a=-1, - b=-1, - diagnostic=-1, - }) - } - expr := state.hir_module.exprs[expr_id] - switch expr.kind { - case .Invalid: - append_instruction(state, ir.Instruction{ - op=.Trap, - span=expr.span, - type=types.VOID, - target=-1, - a=-1, - b=-1, - diagnostic=expr.diagnostic, - }) - fallback := expr.type - if !types.is_concrete_integer(fallback) { - fallback = types.I64 - } - return append_instruction(state, ir.Instruction{ - op=.Const, - span=expr.span, - type=fallback, - integer=sentinel(fallback), - target=-1, - a=-1, - b=-1, - diagnostic=-1, - }) - case .Integer: - return append_instruction(state, ir.Instruction{ - op=.Const, - span=expr.span, - type=expr.type, - integer=expr.integer, - target=-1, - a=-1, - b=-1, - diagnostic=-1, - }) - case .Local: - if expr.target >= 0 && expr.target < len(state.local_slots) && state.local_slots[expr.target] >= 0 { - return append_instruction(state, ir.Instruction{ - op=.Load, - span=expr.span, - type=expr.type, - target=-1, - a=state.local_slots[expr.target], - b=-1, - diagnostic=-1, - }) - } - if expr.target >= 0 && expr.target < len(state.local_values) { - return state.local_values[expr.target] - } - case .Global: - return append_instruction(state, ir.Instruction{ - op=.Load_Global, - span=expr.span, - type=expr.type, - target=expr.target, - a=-1, - b=-1, - diagnostic=-1, - }) - case .Widen: - value := lower_expr(state, expr.left) - return append_instruction(state, ir.Instruction{ - op=.Widen, - span=expr.span, - type=expr.type, - target=-1, - a=value, - b=-1, - diagnostic=-1, - }) - case .Add: - left := lower_expr(state, expr.left) - right := lower_expr(state, expr.right) - return append_instruction(state, ir.Instruction{ - op=.Add_Checked, - span=expr.span, - type=expr.type, - target=-1, - a=left, - b=right, - diagnostic=-1, - }) - case .Call: - args := make([]int, len(expr.args), state.allocator) - for arg, index in expr.args { - args[index] = lower_expr(state, arg) - } - return append_instruction(state, ir.Instruction{ - op=.Call, - span=expr.span, - type=expr.type, - target=expr.target, - a=-1, - b=-1, - args=args, - diagnostic=-1, - }) - } +append_recovery_value :: proc(state: ^State, span: source.Span, value_type: types.Type, diagnostic := -1) -> int { append_instruction(state, ir.Instruction{ op=.Trap, - span=expr.span, + span=span, type=types.VOID, target=-1, a=-1, b=-1, - diagnostic=expr.diagnostic, + diagnostic=diagnostic, }) + fallback := value_type + if !types.is_concrete_integer(fallback) { + fallback = types.I64 + } return append_instruction(state, ir.Instruction{ op=.Const, - span=expr.span, - type=types.I64, - integer=sentinel(types.I64), + span=span, + type=fallback, + integer=sentinel(fallback), target=-1, a=-1, b=-1, @@ -178,6 +63,133 @@ lower_expr :: proc(state: ^State, expr_id: int) -> int { }) } +Lower_Expr_Frame :: struct { + expr: int, + stage: u8, + left: int, + arg_index: int, + args: []int, +} + +lower_expr :: proc(state: ^State, expr_id: int) -> int { + stack := state.expr_stack + clear_dynamic_array(&stack) + defer { + for frame in stack { + delete(frame.args, state.allocator) + } + clear_dynamic_array(&stack) + state.expr_stack = stack + } + append(&stack, Lower_Expr_Frame{expr=expr_id}) + last := -1 + for len(stack) > 0 { + frame_index := len(stack)-1 + frame := stack[frame_index] + if frame.expr < 0 || frame.expr >= len(state.hir_module.exprs) { + last = append_recovery_value(state, source.Span{}, types.I64) + _ = pop(&stack) + continue + } + expr := state.hir_module.exprs[frame.expr] + if frame.stage == 0 { + switch expr.kind { + case .Invalid: + last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic) + _ = pop(&stack) + case .Integer: + last = append_instruction(state, ir.Instruction{ + op=.Const, span=expr.span, type=expr.type, integer=expr.integer, + target=-1, a=-1, b=-1, diagnostic=-1, + }) + _ = pop(&stack) + case .Local: + last = -1 + if expr.target >= 0 && expr.target < len(state.local_slots) && state.local_slots[expr.target] >= 0 { + last = append_instruction(state, ir.Instruction{ + op=.Load, span=expr.span, type=expr.type, target=-1, + a=state.local_slots[expr.target], b=-1, diagnostic=-1, + }) + } else if expr.target >= 0 && expr.target < len(state.local_values) && + state.local_values[expr.target] >= 0 { + last = state.local_values[expr.target] + } + if last < 0 { + last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic) + } + _ = pop(&stack) + case .Global: + if expr.target < 0 || expr.target >= len(state.hir_module.globals) { + last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic) + } else { + last = append_instruction(state, ir.Instruction{ + op=.Load_Global, span=expr.span, type=expr.type, target=expr.target, + a=-1, b=-1, diagnostic=-1, + }) + } + _ = pop(&stack) + case .Widen: + stack[frame_index].stage = 1 + append(&stack, Lower_Expr_Frame{expr=expr.left}) + case .Add: + stack[frame_index].stage = 2 + append(&stack, Lower_Expr_Frame{expr=expr.left}) + case .Call: + if expr.target < 0 || expr.target >= len(state.hir_module.functions) { + last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic) + _ = pop(&stack) + continue + } + stack[frame_index].args = make([]int, len(expr.args), state.allocator) + stack[frame_index].stage = 4 + if len(expr.args) > 0 { + append(&stack, Lower_Expr_Frame{expr=expr.args[0]}) + } + } + continue + } + if frame.stage == 1 { + last = append_instruction(state, ir.Instruction{ + op=.Widen, span=expr.span, type=expr.type, target=-1, + a=last, b=-1, diagnostic=-1, + }) + _ = pop(&stack) + continue + } + if frame.stage == 2 { + stack[frame_index].left = last + stack[frame_index].stage = 3 + append(&stack, Lower_Expr_Frame{expr=expr.right}) + continue + } + if frame.stage == 3 { + last = append_instruction(state, ir.Instruction{ + op=.Add_Checked, span=expr.span, type=expr.type, target=-1, + a=frame.left, b=last, diagnostic=-1, + }) + _ = pop(&stack) + continue + } + if frame.stage == 4 { + if frame.arg_index < len(expr.args) { + stack[frame_index].args[frame.arg_index] = last + stack[frame_index].arg_index += 1 + if frame.arg_index+1 < len(expr.args) { + append(&stack, Lower_Expr_Frame{expr=expr.args[frame.arg_index+1]}) + continue + } + } + last = append_instruction(state, ir.Instruction{ + op=.Call, span=expr.span, type=expr.type, target=expr.target, + a=-1, b=-1, args=stack[frame_index].args, diagnostic=-1, + }) + stack[frame_index].args = nil + _ = pop(&stack) + } + } + return last +} + lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: mem.Allocator) -> []ir.Instruction { state := State{ hir_module=hir_module, @@ -186,9 +198,11 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m local_slots=make([]int, len(function.locals), allocator), } state.instructions.allocator = allocator + state.expr_stack.allocator = allocator defer { delete(state.local_values, allocator) delete(state.local_slots, allocator) + delete(state.expr_stack) } for _, index in state.local_values { state.local_values[index] = -1 @@ -211,6 +225,13 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m switch statement.kind { case .Declaration: value := lower_expr(&state, statement.expr) + if statement.local < 0 || statement.local >= len(function.locals) { + append_instruction(&state, ir.Instruction{ + op=.Trap, span=statement.span, type=types.VOID, + target=-1, a=-1, b=-1, diagnostic=statement.diagnostic, + }) + continue + } local := function.locals[statement.local] if local.mutable { slot := append_instruction(&state, ir.Instruction{ @@ -241,6 +262,13 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m if statement.local >= 0 && statement.local < len(state.local_slots) { slot = state.local_slots[statement.local] } + if slot < 0 || statement.local < 0 || statement.local >= len(function.locals) { + append_instruction(&state, ir.Instruction{ + op=.Trap, span=statement.span, type=types.VOID, + target=-1, a=-1, b=-1, diagnostic=statement.diagnostic, + }) + continue + } append_instruction(&state, ir.Instruction{ op=.Store, span=statement.span, @@ -311,6 +339,8 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m lower_global_initializer :: proc(hir_module: ^hir.Module, global: hir.Global, allocator: mem.Allocator) -> []ir.Instruction { state := State{hir_module=hir_module, allocator=allocator} state.instructions.allocator = allocator + state.expr_stack.allocator = allocator + defer delete(state.expr_stack) value := lower_expr(&state, global.expr) append_instruction(&state, ir.Instruction{ op=.Return, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 5d964ab..161ab7e 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -19,6 +19,8 @@ Parser :: struct { delimiter_depth: int, } +MAX_EXPRESSION_NESTING :: 256 + token_text :: proc(parser: ^Parser, tok: token.Token) -> string { if tok.span.start < 0 || tok.span.end < tok.span.start || tok.span.end > len(parser.source_file.text) { return "" @@ -110,7 +112,30 @@ parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { return .Invalid } -parse_call :: proc(parser: ^Parser, qualifier: symbol.Id, first, name: token.Token) -> int { +skip_parenthesized :: proc(parser: ^Parser) -> source.Span { + start := current(parser) + depth := 0 + end := start + for current(parser).kind != .Eof { + tok := advance(parser) + end = tok + if tok.kind == .Left_Paren { + depth += 1 + } else if tok.kind == .Right_Paren { + depth -= 1 + if depth == 0 { + break + } + } + } + return span_from(start.span, end.span) +} + +parse_call :: proc(parser: ^Parser, qualifier: symbol.Id, first, name: token.Token, nesting: int) -> int { + if nesting >= MAX_EXPRESSION_NESTING { + span := skip_parenthesized(parser) + return invalid_expr(parser, span, "expression nesting exceeds 256 levels") + } left_paren := advance(parser) parser.delimiter_depth += 1 defer parser.delimiter_depth -= 1 @@ -118,7 +143,7 @@ parse_call :: proc(parser: ^Parser, qualifier: symbol.Id, first, name: token.Tok args.allocator = parser.module.allocator skip_newlines(parser) for current(parser).kind != .Right_Paren && current(parser).kind != .Eof { - append(&args, parse_expression(parser)) + append(&args, parse_expression_bp(parser, 0, nesting+1)) skip_newlines(parser) if _, ok := allow(parser, .Comma); ok { skip_newlines(parser) @@ -143,7 +168,7 @@ parse_call :: proc(parser: ^Parser, qualifier: symbol.Id, first, name: token.Tok }) } -parse_primary :: proc(parser: ^Parser) -> int { +parse_primary :: proc(parser: ^Parser, nesting: int) -> int { tok := current(parser) #partial switch tok.kind { case .Integer: @@ -172,7 +197,7 @@ parse_primary :: proc(parser: ^Parser) -> int { name = advance(parser) } if current(parser).kind == .Left_Paren { - return parse_call(parser, qualifier, first, name) + return parse_call(parser, qualifier, first, name, nesting) } return add_expr(parser, ast.Expr{ kind=.Name, @@ -187,11 +212,15 @@ parse_primary :: proc(parser: ^Parser) -> int { advance(parser) return invalid_expr(parser, tok.span, "'_' is a write-only sink and cannot be read") case .Left_Paren: + if nesting >= MAX_EXPRESSION_NESTING { + span := skip_parenthesized(parser) + return invalid_expr(parser, span, "expression nesting exceeds 256 levels") + } advance(parser) parser.delimiter_depth += 1 defer parser.delimiter_depth -= 1 skip_newlines(parser) - expr := parse_expression(parser) + expr := parse_expression_bp(parser, 0, nesting+1) skip_newlines(parser) if _, ok := allow(parser, .Right_Paren); !ok { source.add(parser.diagnostics, current(parser).span, "expected ')'") @@ -213,15 +242,34 @@ parse_primary :: proc(parser: ^Parser) -> int { return invalid_expr(parser, tok.span, "expected an expression") } -parse_expression :: proc(parser: ^Parser) -> int { - left := parse_primary(parser) +infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) { + #partial switch kind { + case .Plus: + return 10, 11, true + } + return 0, 0, false +} + +parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int) -> int { + if nesting > MAX_EXPRESSION_NESTING { + tok := current(parser) + if tok.kind != .Newline && tok.kind != .Right_Brace && tok.kind != .Eof { + advance(parser) + } + return invalid_expr(parser, tok.span, "expression nesting exceeds 256 levels") + } + left := parse_primary(parser, nesting) if parser.delimiter_depth > 0 { skip_newlines(parser) } - for current(parser).kind == .Plus { + for { + left_power, right_power, ok := infix_binding_power(current(parser).kind) + if !ok || left_power < minimum_binding_power { + break + } advance(parser) skip_newlines(parser) - right := parse_primary(parser) + right := parse_expression_bp(parser, right_power, nesting+1) left_expr := parser.module.exprs[left] right_expr := parser.module.exprs[right] left = add_expr(parser, ast.Expr{ @@ -238,6 +286,10 @@ parse_expression :: proc(parser: ^Parser) -> int { return left } +parse_expression :: proc(parser: ^Parser) -> int { + return parse_expression_bp(parser, 0, 0) +} + finish_statement :: proc(parser: ^Parser, allow_closing_brace := false) -> int { if current(parser).kind == .Newline { skip_newlines(parser) @@ -536,10 +588,17 @@ parse_top_level :: proc(parser: ^Parser) { skip_newlines(parser) c_abi := false - if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_C { + if operator.kind == .Colon_Colon && + current(parser).kind == .Identifier && + token_text(parser, current(parser)) == "c" { + saved := parser.cursor c_abi = true advance(parser) skip_newlines(parser) + if current(parser).kind != .Keyword_Func { + c_abi = false + parser.cursor = saved + } } if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Func { parse_function(parser, name, c_abi) diff --git a/compiler/source/source.odin b/compiler/source/source.odin index 2d8d70e..be7fc0d 100644 --- a/compiler/source/source.odin +++ b/compiler/source/source.odin @@ -10,9 +10,10 @@ Span :: struct { } Source :: struct { - id: int, - path: string, - text: string, + id: int, + path: string, + text: string, + line_starts: []int, } Store :: struct { @@ -29,9 +30,15 @@ Diagnostics :: struct { source: ^Source, store: ^Store, items: [dynamic]Diagnostic, + index: map[Diagnostic_Key]int, allocator: mem.Allocator, } +Diagnostic_Key :: struct { + span: Span, + message: string, +} + init_store :: proc(allocator := context.allocator) -> Store { result: Store result.allocator = allocator @@ -43,25 +50,47 @@ destroy_store :: proc(store: ^Store) { for item in store.items { delete(item.path, store.allocator) delete(item.text, store.allocator) + delete(item.line_starts, store.allocator) } delete(store.items) } -add_source :: proc(store: ^Store, path, text: string) -> int { +make_line_starts :: proc(text: string, allocator: mem.Allocator) -> []int { + result: [dynamic]int + result.allocator = allocator + append(&result, 0) + for value, offset in transmute([]byte)text { + if value == '\n' { + append(&result, offset+1) + } + } + return result[:] +} + +add_source_owned :: proc(store: ^Store, path: string, text: []byte) -> int { id := len(store.items) + owned_text := string(text) append(&store.items, Source{ id=id, path=fmt.aprintf("%s", path, allocator=store.allocator), - text=fmt.aprintf("%s", text, allocator=store.allocator), + text=owned_text, + line_starts=make_line_starts(owned_text, store.allocator), }) return id } +add_source :: proc(store: ^Store, path, text: string) -> int { + owned := make([]byte, len(text), store.allocator) + copy(owned, transmute([]byte)text) + return add_source_owned(store, path, owned) +} + init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -> Diagnostics { result: Diagnostics result.source = source_file result.allocator = allocator result.items.allocator = allocator + result.index.allocator = allocator return result } @@ -70,10 +99,12 @@ init_store_diagnostics :: proc(store: ^Store, allocator := context.allocator) -> result.store = store result.allocator = allocator result.items.allocator = allocator + result.index.allocator = allocator return result } destroy_diagnostics :: proc(diagnostics: ^Diagnostics) { + delete(diagnostics.index) for diagnostic in diagnostics.items { delete(diagnostic.message, diagnostics.allocator) } @@ -81,34 +112,50 @@ destroy_diagnostics :: proc(diagnostics: ^Diagnostics) { } add :: proc(diagnostics: ^Diagnostics, span: Span, message: string) -> int { - for diagnostic, id in diagnostics.items { - if diagnostic.span == span && diagnostic.message == message { - return id - } + key := Diagnostic_Key{span=span, message=message} + if id, ok := diagnostics.index[key]; ok { + return id } id := len(diagnostics.items) cloned := fmt.aprintf("%s", message, allocator=diagnostics.allocator) append(&diagnostics.items, Diagnostic{span=span, message=cloned}) + diagnostics.index[Diagnostic_Key{span=span, message=cloned}] = id return id } addf :: proc(diagnostics: ^Diagnostics, span: Span, format: string, args: ..any) -> int { message := fmt.aprintf(format, ..args, allocator=diagnostics.allocator) - for diagnostic, id in diagnostics.items { - if diagnostic.span == span && diagnostic.message == message { - delete(message, diagnostics.allocator) - return id - } + key := Diagnostic_Key{span=span, message=message} + if id, ok := diagnostics.index[key]; ok { + delete(message, diagnostics.allocator) + return id } id := len(diagnostics.items) append(&diagnostics.items, Diagnostic{span=span, message=message}) + diagnostics.index[Diagnostic_Key{span=span, message=message}] = id return id } line_and_column :: proc(source_file: ^Source, offset: int) -> (line, column: int) { + if len(source_file.line_starts) > 0 { + limit := min(max(offset, 0), len(source_file.text)) + low := 0 + high := len(source_file.line_starts) + for low < high { + middle := low + (high-low)/2 + if source_file.line_starts[middle] <= limit { + low = middle+1 + } else { + high = middle + } + } + line = max(low, 1) + column = limit-source_file.line_starts[line-1]+1 + return + } line = 1 column = 1 - limit := min(offset, len(source_file.text)) + limit := min(max(offset, 0), len(source_file.text)) for byte_value in transmute([]byte)source_file.text[:limit] { if byte_value == '\n' { line += 1 diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 5e218fd..cd9b39f 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -20,7 +20,6 @@ Kind :: enum { Left_Brace, Right_Brace, Comma, - Keyword_C, Keyword_Func, Keyword_Import, Keyword_Return, diff --git a/compiler_tests.odin b/compiler_tests.odin index 5714af3..50ab51f 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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.. (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..