harden compiler parsing, recovery, and deep-expression handling

This commit is contained in:
2026-06-12 00:28:45 +02:00
parent a5ceb727c1
commit 99ba907f59
13 changed files with 1159 additions and 503 deletions
+7
View File
@@ -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
+4
View File
@@ -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.
-2
View File
@@ -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)
+490 -319
View File
@@ -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 {
+4 -4
View File
@@ -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)
-1
View File
@@ -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
+109 -19
View File
@@ -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",
)
}
+2 -3
View File
@@ -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)
+157 -127
View File
@@ -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,
+69 -10
View File
@@ -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)
+62 -15
View File
@@ -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
-1
View File
@@ -20,7 +20,6 @@ Kind :: enum {
Left_Brace,
Right_Brace,
Comma,
Keyword_C,
Keyword_Func,
Keyword_Import,
Keyword_Return,
+255 -2
View File
@@ -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"