feat: execute conditionals through typed IR
Use typed singleton arrays for braceless arm blocks: &[statement] compiled but trapped under the Bro bootstrap.
This commit is contained in:
+65
-7
@@ -1,11 +1,14 @@
|
||||
#! grammar:
|
||||
#! * type ..... -> identifier
|
||||
#! * assignment -> :: | :=
|
||||
#! * literal .. -> int | float | string
|
||||
#! * op ....... -> '+' | '-' | '*' | '/'
|
||||
#! * literal .. -> int | float | string | 'true' | 'false'
|
||||
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>='
|
||||
#! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')'
|
||||
#! * decl ..... -> identifier [type] assignment expr
|
||||
#! * stmt ..... -> decl | expr | 'return' [expr]
|
||||
#! * block .... -> '{' {stmt} '}'
|
||||
#! * arm ...... -> block | stmt (single statements become canonical blocks)
|
||||
#! * stmt ..... -> decl | expr | 'return' [expr] | 'if' expr arm ['else' arm]
|
||||
#! braceless guards require parentheses unless the condition is a call; else binds to the nearest if.
|
||||
|
||||
import "@std/debug"
|
||||
import "@std/mem"
|
||||
@@ -22,13 +25,14 @@ NodeId :: distinct u32
|
||||
no_node :: maxval!(NodeId)
|
||||
|
||||
LiteralKind :: enum {
|
||||
int, float, string
|
||||
int, float, string, bool
|
||||
}
|
||||
|
||||
ExprKind :: enum {
|
||||
literal_int
|
||||
literal_float
|
||||
literal_string
|
||||
literal_bool
|
||||
identifier
|
||||
unary
|
||||
binary
|
||||
@@ -41,7 +45,9 @@ DeclKind :: enum {
|
||||
}
|
||||
|
||||
StmtKind :: enum {
|
||||
declaration, expression, return_stmt, invalid
|
||||
declaration,
|
||||
expression,
|
||||
return_stmt, if_stmt, invalid
|
||||
}
|
||||
|
||||
Mutability :: enum {
|
||||
@@ -51,7 +57,11 @@ Mutability :: enum {
|
||||
UnaryOperator :: enum { negate }
|
||||
|
||||
BinaryOperator :: enum {
|
||||
add, subtract, multiply, divide
|
||||
add, subtract,
|
||||
multiply, divide
|
||||
equal, not_equal,
|
||||
less, less_equal,
|
||||
greater, greater_equal
|
||||
}
|
||||
|
||||
@hide:file
|
||||
@@ -59,6 +69,7 @@ NodeKind :: enum {
|
||||
literal_int,
|
||||
literal_float,
|
||||
literal_string,
|
||||
literal_bool,
|
||||
expr_identifier,
|
||||
expr_unary,
|
||||
expr_binary,
|
||||
@@ -66,6 +77,7 @@ NodeKind :: enum {
|
||||
stmt_expr,
|
||||
stmt_decl,
|
||||
stmt_return,
|
||||
stmt_if,
|
||||
func_decl,
|
||||
param_decl,
|
||||
block,
|
||||
@@ -157,6 +169,13 @@ ReturnView :: struct {
|
||||
value ?NodeId
|
||||
}
|
||||
|
||||
IfView :: struct {
|
||||
span Span
|
||||
condition NodeId
|
||||
then_block NodeId
|
||||
else_block ?NodeId
|
||||
}
|
||||
|
||||
InvalidView :: struct {
|
||||
span Span
|
||||
diagnostic DiagnosticId
|
||||
@@ -206,6 +225,7 @@ add_literal proc(builder @mut Builder, kind LiteralKind, span Span) NodeId ! mem
|
||||
.int: .literal_int
|
||||
.float: .literal_float
|
||||
.string: .literal_string
|
||||
.bool: .literal_bool
|
||||
}
|
||||
return try add_node(builder, Node{ kind = node_kind }, span)
|
||||
}
|
||||
@@ -273,6 +293,22 @@ add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! me
|
||||
}, span)
|
||||
}
|
||||
|
||||
add_if_stmt proc(
|
||||
builder @mut Builder,
|
||||
span Span,
|
||||
condition NodeId,
|
||||
then_block NodeId,
|
||||
else_block ?NodeId,
|
||||
) NodeId ! mem.AllocError {
|
||||
alternative :: if (else_block) |id| id else no_node
|
||||
return try add_node(builder, Node{
|
||||
kind = .stmt_if,
|
||||
main = NodeData{ node_id = condition },
|
||||
data0 = NodeData{ node_id = then_block },
|
||||
data1 = NodeData{ node_id = alternative },
|
||||
}, span)
|
||||
}
|
||||
|
||||
add_statement_decl proc(
|
||||
builder @mut Builder,
|
||||
span Span,
|
||||
@@ -408,6 +444,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
|
||||
.stmt_decl: .declaration
|
||||
.stmt_expr: .expression
|
||||
.stmt_return: .return_stmt
|
||||
.stmt_if: .if_stmt
|
||||
.invalid_stmt: .invalid
|
||||
else: unreachable
|
||||
}
|
||||
@@ -428,6 +465,17 @@ return_stmt proc(tree @Tree, id NodeId) ReturnView {
|
||||
}
|
||||
}
|
||||
|
||||
if_stmt proc(tree @Tree, id NodeId) IfView {
|
||||
node :: get_node(&tree.storage, id)
|
||||
debug.assert(node.kind == .stmt_if)
|
||||
return IfView{
|
||||
span = get_span(&tree.storage, id),
|
||||
condition = node.main.node_id,
|
||||
then_block = node.data0.node_id,
|
||||
else_block = maybe_node_id(u32(node.data1.node_id)),
|
||||
}
|
||||
}
|
||||
|
||||
statement_decl proc(tree @Tree, id NodeId) StatementDeclView {
|
||||
node :: get_node(&tree.storage, id)
|
||||
debug.assert(node.kind == .stmt_decl)
|
||||
@@ -491,10 +539,20 @@ block_statement proc(tree @Tree, id NodeId, index usize) NodeId {
|
||||
}
|
||||
|
||||
expr_kind proc(tree @Tree, id NodeId) ExprKind {
|
||||
return match get_node(&tree.storage, id).kind {
|
||||
return expression_kind(get_node(&tree.storage, id).kind)
|
||||
}
|
||||
|
||||
builder_expr_kind proc(builder @Builder, id NodeId) ExprKind {
|
||||
return expression_kind(get_node(&builder.storage, id).kind)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
expression_kind proc(kind NodeKind) ExprKind {
|
||||
return match kind {
|
||||
.literal_int: .literal_int
|
||||
.literal_float: .literal_float
|
||||
.literal_string: .literal_string
|
||||
.literal_bool: .literal_bool
|
||||
.expr_identifier: .identifier
|
||||
.expr_unary: .unary
|
||||
.expr_binary: .binary
|
||||
|
||||
@@ -41,25 +41,43 @@ render_function proc(
|
||||
try append_text(renderer, " function ")
|
||||
try append_text(renderer, ast.source_text(tree, function.name))
|
||||
try append_text(renderer, "\n")
|
||||
for 0..ast.block_statement_count(tree, function.body) |index| {
|
||||
statement :: ast.block_statement(tree, function.body, index)
|
||||
try render_block(renderer, tree, function.body, 2)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, depth usize) void ! mem.AllocError {
|
||||
for 0..ast.block_statement_count(tree, block) |index| {
|
||||
statement :: ast.block_statement(tree, block, index)
|
||||
for 0..depth |_| try append_text(renderer, " ")
|
||||
match ast.stmt_kind(tree, statement) {
|
||||
.declaration: try render_statement(renderer, tree, statement, " ")
|
||||
.declaration: try render_statement(renderer, tree, statement, "")
|
||||
.expression: {
|
||||
expression :: ast.statement_expr(tree, statement)
|
||||
try append_text(renderer, " expression ")
|
||||
try append_text(renderer, "expression ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression)))
|
||||
try append_text(renderer, "\n")
|
||||
}
|
||||
.return_stmt: {
|
||||
try append_text(renderer, " return")
|
||||
try append_text(renderer, "return")
|
||||
if (ast.return_stmt(tree, statement).value) |value| {
|
||||
try append_text(renderer, " ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value)))
|
||||
}
|
||||
try append_text(renderer, "\n")
|
||||
}
|
||||
.invalid: try append_text(renderer, " invalid statement\n")
|
||||
.if_stmt: {
|
||||
conditional :: ast.if_stmt(tree, statement)
|
||||
try append_text(renderer, "if ")
|
||||
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, conditional.condition)))
|
||||
try append_text(renderer, "\n")
|
||||
try render_block(renderer, tree, conditional.then_block, depth + 1)
|
||||
if (conditional.else_block) |alternative| {
|
||||
for 0..depth |_| try append_text(renderer, " ")
|
||||
try append_text(renderer, "else\n")
|
||||
try render_block(renderer, tree, alternative, depth + 1)
|
||||
}
|
||||
}
|
||||
.invalid: try append_text(renderer, "invalid statement\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ Code :: enum {
|
||||
cannot_infer_type,
|
||||
missing_return,
|
||||
return_type_mismatch,
|
||||
condition_not_bool,
|
||||
missing_entry,
|
||||
invalid_entry,
|
||||
literal_out_of_range,
|
||||
@@ -85,13 +86,14 @@ message proc(code Code) []u8 {
|
||||
.type_not_value: "type name cannot be used as a value"
|
||||
.invalid_void_type: "void cannot be stored or used as a parameter type"
|
||||
.type_mismatch: "incompatible value type"
|
||||
.invalid_operand: "arithmetic requires compatible numeric operands"
|
||||
.invalid_operand: "operator requires compatible operand types"
|
||||
.integer_division: "division requires float operands"
|
||||
.not_callable: "value is not callable"
|
||||
.argument_count_mismatch: "argument count does not match the function signature"
|
||||
.cannot_infer_type: "cannot infer a type without a type anchor"
|
||||
.missing_return: "nonvoid function can finish without returning a value"
|
||||
.return_type_mismatch: "return does not match the function result type"
|
||||
.condition_not_bool: "if condition must have type bool"
|
||||
.missing_entry: "program requires a main function"
|
||||
.invalid_entry: "main must be a function with no parameters"
|
||||
.literal_out_of_range: "numeric literal is outside its runtime type's range"
|
||||
|
||||
@@ -26,6 +26,7 @@ GlobalState :: struct {
|
||||
@hide:file
|
||||
Frame :: struct {
|
||||
function ir.FunctionId
|
||||
block ir.BlockId
|
||||
next_instruction usize = 0
|
||||
destination ir.ValueId = ir.no_value
|
||||
global ?usize = null
|
||||
@@ -60,11 +61,21 @@ run proc(
|
||||
while frames.items.len != 0 {
|
||||
frame_index :: frames.items.len - 1
|
||||
function :: &module.functions.items[usize(frames.items[frame_index].function)]
|
||||
block :: &function.blocks.items[usize(function.entry)]
|
||||
block :: &function.blocks.items[usize(frames.items[frame_index].block)]
|
||||
instruction_index :: frames.items[frame_index].next_instruction
|
||||
if instruction_index == block.instructions.items.len {
|
||||
terminator :: block.terminator
|
||||
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
|
||||
if (terminator.kind == .jump or terminator.kind == .branch) {
|
||||
target := terminator.target
|
||||
if (terminator.kind == .branch) {
|
||||
condition :: frames.items[frame_index].values[usize(terminator.value)]
|
||||
if (!condition.data.boolean) target = terminator.alternative
|
||||
}
|
||||
frames.items[frame_index].block = target
|
||||
frames.items[frame_index].next_instruction = 0
|
||||
continue
|
||||
}
|
||||
value := ir.Value{}
|
||||
if terminator.value != ir.no_value {
|
||||
value = frames.items[frame_index].values[usize(terminator.value)]
|
||||
@@ -169,6 +180,38 @@ run proc(
|
||||
}
|
||||
}
|
||||
}
|
||||
.equal, .not_equal, .less, .less_equal, .greater, .greater_equal: {
|
||||
left :: frames.items[frame_index].values[usize(instruction.left)]
|
||||
right :: frames.items[frame_index].values[usize(instruction.right)]
|
||||
result := false
|
||||
match left.type {
|
||||
.int: result = match instruction.op {
|
||||
.equal: left.data.integer == right.data.integer
|
||||
.not_equal: left.data.integer != right.data.integer
|
||||
.less: left.data.integer < right.data.integer
|
||||
.less_equal: left.data.integer <= right.data.integer
|
||||
.greater: left.data.integer > right.data.integer
|
||||
.greater_equal: left.data.integer >= right.data.integer
|
||||
else: unreachable
|
||||
}
|
||||
.float: result = match instruction.op {
|
||||
.equal: left.data.real == right.data.real
|
||||
.not_equal: left.data.real != right.data.real
|
||||
.less: left.data.real < right.data.real
|
||||
.less_equal: left.data.real <= right.data.real
|
||||
.greater: left.data.real > right.data.real
|
||||
.greater_equal: left.data.real >= right.data.real
|
||||
else: unreachable
|
||||
}
|
||||
.bool: result = match instruction.op {
|
||||
.equal: left.data.boolean == right.data.boolean
|
||||
.not_equal: left.data.boolean != right.data.boolean
|
||||
else: unreachable
|
||||
}
|
||||
else: unreachable
|
||||
}
|
||||
value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = result } }
|
||||
}
|
||||
}
|
||||
if instruction.destination != ir.no_value {
|
||||
frames.items[frame_index].values[usize(instruction.destination)] = value
|
||||
@@ -195,6 +238,7 @@ push_frame proc(
|
||||
# typed IR assigns each temporary before use; only local slots need initialization flags.
|
||||
try arraylist.append(frames, Frame{
|
||||
function = function,
|
||||
block = signature.entry,
|
||||
destination = destination,
|
||||
global = global,
|
||||
locals = locals,
|
||||
|
||||
@@ -391,8 +391,10 @@ recursive_invocations_use_heap_frames_and_free_on_allocation_failure test {
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
|
||||
module.entry = main
|
||||
try emit(&module, main, function_value(0, main))
|
||||
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||
recursive_block :: try add_block(&module, main)
|
||||
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = recursive_block }
|
||||
try emit_at(&module, main, recursive_block, function_value(0, main))
|
||||
try emit_at(&module, main, recursive_block, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
|
||||
state := AllocationState{ remaining = 2048 }
|
||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||
_ = run(allocator, &module, &store) catch |error| {
|
||||
@@ -414,13 +416,17 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.int]
|
||||
initializer :: try add_function(&module, types[..], 0, .int, 1)
|
||||
try emit(&module, initializer, integer(0, 42))
|
||||
return_value(&module, initializer, ir.ValueId(0))
|
||||
init_block :: try add_block(&module, initializer)
|
||||
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = init_block }
|
||||
try emit_at(&module, initializer, init_block, integer(0, 42))
|
||||
module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ value = ir.ValueId(0) }
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
main :: try add_function(&module, types[..], 0, .int, 1)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
return_value(&module, main, ir.ValueId(0))
|
||||
work :: try add_block(&module, main)
|
||||
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = work }
|
||||
try emit_at(&module, main, work, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
module.functions.items[usize(main)].blocks.items[usize(work)].terminator = ir.Terminator{ value = ir.ValueId(0) }
|
||||
succeeded := false
|
||||
failures usize := 0
|
||||
for (usize(0)..usize(16)) |limit| {
|
||||
@@ -443,7 +449,7 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
|
||||
try testing.expect(succeeded)
|
||||
try testing.expect(failures > 0)
|
||||
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
|
||||
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||
module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||
state := AllocationState{ remaining = 64 }
|
||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||
trapped :: try run(allocator, &module, &store)
|
||||
@@ -477,3 +483,185 @@ runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames t
|
||||
}
|
||||
return .expectation_failed
|
||||
}
|
||||
|
||||
@hide:file
|
||||
add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.AllocError {
|
||||
body := &module.functions.items[usize(function)]
|
||||
id :: ir.BlockId(body.blocks.items.len)
|
||||
try arraylist.append(&body.blocks, ir.init_block(module.allocator))
|
||||
return id
|
||||
}
|
||||
|
||||
@hide:file
|
||||
emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError {
|
||||
try arraylist.append(&module.functions.items[usize(function)].blocks.items[usize(block)].instructions, instruction)
|
||||
}
|
||||
|
||||
branching_recursive_calls_and_lazy_initializers_resume_the_current_block test {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
types [1]ir.Type :: [.int]
|
||||
count :: try add_function(&module, types[..], 1, .int, 9)
|
||||
done :: try add_block(&module, count)
|
||||
recurse :: try add_block(&module, count)
|
||||
try emit(&module, count, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
try emit(&module, count, integer(1, 0))
|
||||
try emit(&module, count, ir.Instruction{ op = .less_equal, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||
module.functions.items[usize(count)].blocks.items[0].terminator = ir.Terminator{
|
||||
kind = .branch, value = ir.ValueId(2), target = done, alternative = recurse,
|
||||
}
|
||||
module.functions.items[usize(count)].blocks.items[usize(done)].terminator = ir.Terminator{ value = ir.ValueId(1) }
|
||||
try emit_at(&module, count, recurse, function_value(3, count))
|
||||
try emit_at(&module, count, recurse, integer(4, 1))
|
||||
try emit_at(&module, count, recurse, ir.Instruction{ op = .subtract, type = .int, destination = ir.ValueId(5), left = ir.ValueId(0), right = ir.ValueId(4) })
|
||||
try arraylist.append(&module.functions.items[usize(count)].blocks.items[usize(recurse)].arguments, ir.ValueId(5))
|
||||
try emit_at(&module, count, recurse, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(6), left = ir.ValueId(3), arguments_count = 1 })
|
||||
try emit_at(&module, count, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(7), left = ir.ValueId(6), right = ir.ValueId(4) })
|
||||
module.functions.items[usize(count)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(7) }
|
||||
|
||||
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
|
||||
init_entry :: try add_block(&module, initializer)
|
||||
init_exit :: try add_block(&module, initializer)
|
||||
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
|
||||
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||
module.functions.items[usize(initializer)].entry = init_entry
|
||||
try emit_at(&module, initializer, init_entry, function_value(0, count))
|
||||
try emit_at(&module, initializer, init_entry, integer(1, 128))
|
||||
try arraylist.append(&module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].arguments, ir.ValueId(1))
|
||||
try emit_at(&module, initializer, init_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 })
|
||||
module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].terminator = ir.Terminator{ kind = .jump, target = init_exit }
|
||||
module.functions.items[usize(initializer)].blocks.items[usize(init_exit)].terminator = ir.Terminator{ value = ir.ValueId(2) }
|
||||
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
|
||||
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5)
|
||||
main_entry :: try add_block(&module, main)
|
||||
main_exit :: try add_block(&module, main)
|
||||
module.entry = main
|
||||
module.functions.items[usize(main)].entry = main_entry
|
||||
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||
try emit_at(&module, main, main_entry, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
|
||||
try emit_at(&module, main, main_entry, function_value(1, count))
|
||||
try emit_at(&module, main, main_entry, integer(2, 2))
|
||||
try arraylist.append(&module.functions.items[usize(main)].blocks.items[usize(main_entry)].arguments, ir.ValueId(2))
|
||||
try emit_at(&module, main, main_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(3), left = ir.ValueId(1), arguments_count = 1 })
|
||||
module.functions.items[usize(main)].blocks.items[usize(main_entry)].terminator = ir.Terminator{ kind = .jump, target = main_exit }
|
||||
try emit_at(&module, main, main_exit, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), right = ir.ValueId(3) })
|
||||
module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ value = ir.ValueId(4) }
|
||||
state := AllocationState{ remaining = 2048 }
|
||||
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
|
||||
outcome :: try run(allocator, &module, &store)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(130), outcome.value.data.integer)
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
try testing.expect_equal(usize(1), diagnostics.count(&store))
|
||||
# trapping after returning through many heap frames also releases every live frame.
|
||||
module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
|
||||
trapped :: try run(allocator, &module, &store)
|
||||
try testing.expect_equal(trap_id, trapped.trap?)
|
||||
try testing.expect_equal(usize(0), state.live)
|
||||
}
|
||||
|
||||
all_numeric_comparisons_dispatch_on_operand_type_not_bool_result_type test {
|
||||
operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal]
|
||||
lefts [3]i64 :: [-2, 2, 3]
|
||||
rights [3]i64 :: [3, 2, -2]
|
||||
operand_types [2]ir.Type :: [.int, .float]
|
||||
for (operand_types) |type| {
|
||||
for (lefts) |left, pair| {
|
||||
right :: rights[pair]
|
||||
for (operations) |op| {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
|
||||
module.entry = main
|
||||
left_value := ir.Value{ type = type, data = ir.ValueData{ integer = left } }
|
||||
right_value := ir.Value{ type = type, data = ir.ValueData{ integer = right } }
|
||||
if (type == .float) {
|
||||
left_value.data = ir.ValueData{ real = f64(left) }
|
||||
right_value.data = ir.ValueData{ real = f64(right) }
|
||||
}
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(0), constant = left_value })
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(1), constant = right_value })
|
||||
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||
return_value(&module, main, ir.ValueId(2))
|
||||
expected :: match op {
|
||||
.equal: left == right
|
||||
.not_equal: left != right
|
||||
.less: left < right
|
||||
.less_equal: left <= right
|
||||
.greater: left > right
|
||||
.greater_equal: left >= right
|
||||
else: unreachable
|
||||
}
|
||||
outcome :: try run(mem.c_allocator, &module, &store)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(ir.Type.bool, outcome.value.type)
|
||||
try testing.expect_equal(expected, outcome.value.data.boolean)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nan_comparisons_obey_ieee_rules_in_either_operand_position test {
|
||||
operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal]
|
||||
for (operations) |op| {
|
||||
for (usize(0)..usize(3)) |position| {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
|
||||
module.entry = main
|
||||
try emit(&module, main, ir.Instruction{
|
||||
op = .constant, type = .float, destination = ir.ValueId(0),
|
||||
constant = ir.Value{ type = .float, data = ir.ValueData{ real = 0 } },
|
||||
})
|
||||
try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(1), left = ir.ValueId(0), right = ir.ValueId(0) })
|
||||
left ir.ValueId :: if (position == 1) ir.ValueId(0) else ir.ValueId(1)
|
||||
right ir.ValueId :: if (position == 0) ir.ValueId(0) else ir.ValueId(1)
|
||||
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = left, right = right })
|
||||
return_value(&module, main, ir.ValueId(2))
|
||||
outcome :: try run(mem.c_allocator, &module, &store)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(ir.Type.bool, outcome.value.type)
|
||||
try testing.expect_equal(op == .not_equal, outcome.value.data.boolean)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean_equality_and_branching_use_boolean_payloads test {
|
||||
booleans [2]bool :: [false, true]
|
||||
operations [2]ir.Op :: [.equal, .not_equal]
|
||||
for (booleans) |left| {
|
||||
for (booleans) |right| {
|
||||
for (operations) |op| {
|
||||
module := ir.init(mem.c_allocator)
|
||||
defer ir.deinit(&module)
|
||||
store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&store)
|
||||
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5)
|
||||
module.entry = main
|
||||
yes :: try add_block(&module, main)
|
||||
no :: try add_block(&module, main)
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(0), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = left } } })
|
||||
try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(1), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = right } } })
|
||||
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
|
||||
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(2), target = yes, alternative = no }
|
||||
try emit_at(&module, main, yes, integer(3, 42))
|
||||
module.functions.items[usize(main)].blocks.items[usize(yes)].terminator = ir.Terminator{ value = ir.ValueId(3) }
|
||||
try emit_at(&module, main, no, integer(4, 7))
|
||||
module.functions.items[usize(main)].blocks.items[usize(no)].terminator = ir.Terminator{ value = ir.ValueId(4) }
|
||||
equal :: left == right
|
||||
expected_true :: if (op == .equal) equal else !equal
|
||||
expected i64 :: if (expected_true) 42 else 7
|
||||
outcome :: try run(mem.c_allocator, &module, &store)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(expected, outcome.value.data.integer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-2
@@ -9,10 +9,20 @@ ValueId :: distinct u32
|
||||
BlockId :: distinct u32
|
||||
no_value :: maxval!(ValueId)
|
||||
|
||||
Type :: enum { invalid, int, float, string, void, function }
|
||||
Type :: enum {
|
||||
invalid,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
string,
|
||||
void,
|
||||
function,
|
||||
}
|
||||
|
||||
ValueData :: union {
|
||||
integer i64
|
||||
real f64
|
||||
boolean bool
|
||||
string []u8
|
||||
function FunctionId
|
||||
}
|
||||
@@ -32,6 +42,12 @@ Op :: enum {
|
||||
multiply
|
||||
divide
|
||||
call
|
||||
equal
|
||||
not_equal
|
||||
less
|
||||
less_equal
|
||||
greater
|
||||
greater_equal
|
||||
}
|
||||
|
||||
Instruction :: struct {
|
||||
@@ -47,10 +63,18 @@ Instruction :: struct {
|
||||
arguments_count usize = 0
|
||||
}
|
||||
|
||||
TerminatorKind :: enum { return_value, trap }
|
||||
TerminatorKind :: enum {
|
||||
return_value,
|
||||
trap,
|
||||
jump,
|
||||
branch,
|
||||
}
|
||||
|
||||
Terminator :: struct {
|
||||
kind TerminatorKind = .return_value
|
||||
value ValueId = no_value
|
||||
target BlockId = BlockId(0)
|
||||
alternative BlockId = BlockId(0)
|
||||
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
||||
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
|
||||
{ "for", .for },
|
||||
{ "else", .else },
|
||||
{ "while", .while },
|
||||
{ "true", .true },
|
||||
{ "false", .false },
|
||||
])
|
||||
|
||||
#! owns token storage and retains its allocator; release with deinit.
|
||||
@@ -203,8 +205,28 @@ scan proc(
|
||||
continue
|
||||
}
|
||||
|
||||
if (cursor + 1 < program.len and program[cursor + 1] == '=') {
|
||||
comparison ?TokenKind :: match char {
|
||||
'=': .equal_equal
|
||||
'!': .not_equal
|
||||
'<': .less_equal
|
||||
'>': .greater_equal
|
||||
else: null
|
||||
}
|
||||
if (comparison) |value| {
|
||||
try add_token(state, Token{
|
||||
span = make_span(cursor, cursor + 2),
|
||||
kind = value,
|
||||
})
|
||||
cursor += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
kind ?TokenKind :: match char {
|
||||
'=': .equal
|
||||
'<': .less
|
||||
'>': .greater
|
||||
',': .comma
|
||||
'(': .open_paren
|
||||
')': .close_paren
|
||||
|
||||
@@ -30,3 +30,26 @@ reports_invalid_input_and_keeps_scanning test {
|
||||
try testing.expect_equal(u32(3), state.tokens.items[1].span.start)
|
||||
try testing.expect_equal(u32(7), state.tokens.items[1].span.end)
|
||||
}
|
||||
|
||||
reserves_boolean_keywords_and_scans_comparisons test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
state := init(mem.c_allocator)
|
||||
defer deinit(&state)
|
||||
source :: "true false bool trueish == != < <= > >="
|
||||
try scan(&state, source, &diagnostic_store)
|
||||
expected [11]TokenKind :: [
|
||||
TokenKind.true, .false, .ident, .ident,
|
||||
.equal_equal, .not_equal, .less, .less_equal, .greater, .greater_equal, .eof,
|
||||
]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
|
||||
try testing.expect_equal(u32(4), state.tokens.items[0].span.end)
|
||||
try testing.expect_equal(u32(5), state.tokens.items[1].span.start)
|
||||
try testing.expect_equal(u32(10), state.tokens.items[1].span.end)
|
||||
try testing.expect_equal("bool", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ TokenKind :: enum {
|
||||
if, else
|
||||
for, while
|
||||
proc, return
|
||||
true, false
|
||||
|
||||
# literals
|
||||
ident, string
|
||||
|
||||
+113
-14
@@ -25,6 +25,7 @@ State :: struct {
|
||||
module @mut ir.Module
|
||||
storage []mut Storage
|
||||
function usize = 0
|
||||
block ir.BlockId = ir.BlockId(0)
|
||||
terminated bool = false
|
||||
}
|
||||
|
||||
@@ -89,6 +90,7 @@ lower_type proc(type typecheck.Type) ir.Type {
|
||||
return match type.kind {
|
||||
.int: .int
|
||||
.float: .float
|
||||
.bool: .bool
|
||||
.string: .string
|
||||
.void: .void
|
||||
.function: .function
|
||||
@@ -136,7 +138,7 @@ find_entry proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityE
|
||||
|
||||
@hide:file
|
||||
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
|
||||
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
|
||||
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||
kind = .trap,
|
||||
diagnostic = diagnostic,
|
||||
span = span,
|
||||
@@ -162,7 +164,7 @@ trap_binding proc(state @mut State, id ast.NodeId) bool {
|
||||
|
||||
@hide:file
|
||||
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
|
||||
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
|
||||
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||
value = value,
|
||||
span = span,
|
||||
}
|
||||
@@ -177,7 +179,7 @@ emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocEr
|
||||
operation.destination = ir.ValueId(function.value_count)
|
||||
function.value_count += 1
|
||||
}
|
||||
try arraylist.append(&function.blocks.items[0].instructions, operation)
|
||||
try arraylist.append(&function.blocks.items[usize(state.block)].instructions, operation)
|
||||
return operation.destination
|
||||
}
|
||||
|
||||
@@ -190,6 +192,7 @@ constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueI
|
||||
lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
global :: state.module.globals.items[usize(state.storage[usize(id)].index)]
|
||||
state.function = usize(global.initializer)
|
||||
state.block = ir.BlockId(0)
|
||||
state.terminated = false
|
||||
state.module.functions.items[state.function].result = global.type
|
||||
if (trap_binding(state, id)) return
|
||||
@@ -205,6 +208,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
|
||||
tree :: state.tree
|
||||
declaration :: ast.function_decl(tree, id)
|
||||
state.function = usize(state.storage[usize(id)].index)
|
||||
state.block = ir.BlockId(0)
|
||||
state.terminated = false
|
||||
function := &state.module.functions.items[state.function]
|
||||
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type))
|
||||
@@ -215,30 +219,112 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
|
||||
try arraylist.append(&function.parameters, type)
|
||||
try arraylist.append(&function.locals, type)
|
||||
}
|
||||
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
|
||||
statement :: ast.block_statement(tree, declaration.body, i)
|
||||
if (ast.stmt_kind(tree, statement) != .declaration) continue
|
||||
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
|
||||
try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement)))
|
||||
}
|
||||
try predeclare_locals(state, declaration.body)
|
||||
# signature errors are entry traps; body errors are reached in statement order.
|
||||
if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return
|
||||
for (0..ast.function_param_count(tree, id)) |i| {
|
||||
if (trap_type(state, ast.function_param(tree, id, i))) return
|
||||
}
|
||||
if (trap_type(state, id)) return
|
||||
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
|
||||
if (state.terminated) return
|
||||
try lower_statement(state, ast.block_statement(tree, declaration.body, i))
|
||||
try lower_block(state, declaration.body)
|
||||
if (state.terminated) return
|
||||
# aggregate arm errors are not demanded by fallthrough; only missing_return is.
|
||||
body_type :: typecheck.type_of(state.checked, declaration.body)
|
||||
if (function.result != .void and body_type.kind == .invalid) {
|
||||
if (diagnostics.get(state.diagnostics, body_type.diagnostic)) |diagnostic| {
|
||||
if (diagnostic.code == .missing_return) {
|
||||
trap(state, body_type.diagnostic, ast.node_span(tree, declaration.body))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.terminated or trap_type(state, declaration.body)) return
|
||||
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
|
||||
}
|
||||
|
||||
@hide:file
|
||||
predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError {
|
||||
for (0..ast.block_statement_count(state.tree, block)) |i| {
|
||||
statement :: ast.block_statement(state.tree, block, i)
|
||||
match ast.stmt_kind(state.tree, statement) {
|
||||
.declaration: {
|
||||
function := &state.module.functions.items[state.function]
|
||||
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
|
||||
try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement)))
|
||||
}
|
||||
.if_stmt: {
|
||||
conditional :: ast.if_stmt(state.tree, statement)
|
||||
try predeclare_locals(state, conditional.then_block)
|
||||
if (conditional.else_block) |alternative| try predeclare_locals(state, alternative)
|
||||
}
|
||||
else: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
lower_block proc(state @mut State, block ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
for (0..ast.block_statement_count(state.tree, block)) |i| {
|
||||
if (state.terminated) return
|
||||
try lower_statement(state, ast.block_statement(state.tree, block, i))
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
add_block proc(state @mut State) ir.BlockId ! mem.AllocError {
|
||||
function := &state.module.functions.items[state.function]
|
||||
id :: ir.BlockId(function.blocks.items.len)
|
||||
try arraylist.append(&function.blocks, ir.init_block(state.module.allocator))
|
||||
return id
|
||||
}
|
||||
|
||||
@hide:file
|
||||
lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
conditional :: ast.if_stmt(state.tree, id)
|
||||
condition :: try lower_expression(state, conditional.condition)
|
||||
if (state.terminated) return
|
||||
origin :: state.block
|
||||
consequent :: try add_block(state)
|
||||
alternative :: try add_block(state)
|
||||
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
|
||||
kind = .branch,
|
||||
value = condition,
|
||||
target = consequent,
|
||||
alternative = alternative,
|
||||
span = ast.node_span(state.tree, conditional.condition),
|
||||
}
|
||||
state.block = consequent
|
||||
try lower_block(state, conditional.then_block)
|
||||
consequent_end :: state.block
|
||||
consequent_falls :: !state.terminated
|
||||
state.block = alternative
|
||||
state.terminated = false
|
||||
if (conditional.else_block) |block| try lower_block(state, block)
|
||||
alternative_end :: state.block
|
||||
alternative_falls :: !state.terminated
|
||||
if (!consequent_falls and !alternative_falls) {
|
||||
state.terminated = true
|
||||
return
|
||||
}
|
||||
join :: try add_block(state)
|
||||
if (consequent_falls) {
|
||||
state.module.functions.items[state.function].blocks.items[usize(consequent_end)].terminator = ir.Terminator{
|
||||
kind = .jump, target = join, span = conditional.span,
|
||||
}
|
||||
}
|
||||
if (alternative_falls) {
|
||||
state.module.functions.items[state.function].blocks.items[usize(alternative_end)].terminator = ir.Terminator{
|
||||
kind = .jump, target = join, span = conditional.span,
|
||||
}
|
||||
}
|
||||
state.block = join
|
||||
state.terminated = false
|
||||
}
|
||||
|
||||
@hide:file
|
||||
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
tree :: state.tree
|
||||
match ast.stmt_kind(tree, id) {
|
||||
.if_stmt: try lower_if(state, id)
|
||||
.invalid: {
|
||||
invalid :: ast.invalid(tree, id)
|
||||
trap(state, invalid.diagnostic, invalid.span)
|
||||
@@ -282,6 +368,13 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
||||
if (trap_type(state, id)) return ir.no_value
|
||||
return try lower_number(state, id)
|
||||
}
|
||||
.literal_bool: {
|
||||
if (trap_type(state, id)) return ir.no_value
|
||||
return try constant(state, ir.Value{
|
||||
type = .bool,
|
||||
data = ir.ValueData{ boolean = mem.eql(ast.source_text(tree, span), "true") },
|
||||
}, span)
|
||||
}
|
||||
.literal_string: {
|
||||
if (trap_type(state, id)) return ir.no_value
|
||||
return try lower_string(state, id)
|
||||
@@ -325,6 +418,12 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
||||
.subtract: .subtract
|
||||
.multiply: .multiply
|
||||
.divide: .divide
|
||||
.equal: .equal
|
||||
.not_equal: .not_equal
|
||||
.less: .less
|
||||
.less_equal: .less_equal
|
||||
.greater: .greater
|
||||
.greater_equal: .greater_equal
|
||||
}
|
||||
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
|
||||
}
|
||||
@@ -332,7 +431,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
||||
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
|
||||
if (state.terminated) return ir.no_value
|
||||
# reserve this call's packed slice before nested calls append theirs.
|
||||
block := &state.module.functions.items[state.function].blocks.items[0]
|
||||
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
|
||||
start :: block.arguments.items.len
|
||||
count :: ast.call_arg_count(tree, id)
|
||||
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
|
||||
|
||||
@@ -410,11 +410,23 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
||||
`text :: "owned\nstring"
|
||||
`factor float :: 3.5
|
||||
`identity proc(value string) string {
|
||||
` if false {
|
||||
` return value
|
||||
` }
|
||||
` local :: value
|
||||
` return local
|
||||
`}
|
||||
`main proc() string {
|
||||
` computed float :: factor * 2
|
||||
` if computed > 0 {
|
||||
` saved :: identity(text)
|
||||
` if computed < 10 {
|
||||
` return saved
|
||||
` }
|
||||
` } else {
|
||||
` fallback :: "fallback"
|
||||
` identity(fallback)
|
||||
` }
|
||||
` return identity(text)
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
@@ -450,3 +462,182 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
||||
try testing.expect(succeeded)
|
||||
try testing.expect(failures > 0)
|
||||
}
|
||||
|
||||
recursive_factorial_and_conditional_returns_execute_after_frontend_cleanup test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`factorial proc(n int) int {
|
||||
` if n <= 1 {
|
||||
` return 1
|
||||
` } else {
|
||||
` return n * factorial(n - 1)
|
||||
` }
|
||||
`}
|
||||
`main proc() int {
|
||||
` return factorial(5)
|
||||
`}
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(120), outcome.value.data.integer)
|
||||
}
|
||||
|
||||
nested_branches_joins_calls_and_lazy_globals_preserve_execution_position test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`seed :: classify(2)
|
||||
`enabled bool :: true
|
||||
`identity proc(value int) int { return value }
|
||||
`classify proc(n int) int {
|
||||
` if n < 0 {
|
||||
` return 3
|
||||
` } else if n == 0 {
|
||||
` return 5
|
||||
` } else {
|
||||
` if n > 1 {
|
||||
` local :: identity(7)
|
||||
` identity(local)
|
||||
` } else {
|
||||
` local :: identity(9)
|
||||
` identity(local)
|
||||
` }
|
||||
` return identity(11)
|
||||
` }
|
||||
`}
|
||||
`choose proc(flag bool) int {
|
||||
` if flag {
|
||||
` local :: identity(seed)
|
||||
` identity(local)
|
||||
` } else {
|
||||
` local :: identity(13)
|
||||
` identity(local)
|
||||
` }
|
||||
` return identity(seed) + classify(0) + classify(-1)
|
||||
`}
|
||||
`main proc() int {
|
||||
` return choose(enabled) + choose(false) + classify(1) - 7
|
||||
`}
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
}
|
||||
|
||||
bool_storage_results_and_numeric_comparisons_execute test {
|
||||
sources [16][]u8 :: [
|
||||
"main proc() bool { return 2 == 2 }\n",
|
||||
"main proc() bool { return 2 != 3 }\n",
|
||||
"main proc() bool { return -2 < 1 }\n",
|
||||
"main proc() bool { return 2 <= 2 }\n",
|
||||
"main proc() bool { return 3 > 2 }\n",
|
||||
"main proc() bool { return 2 >= 2 }\n",
|
||||
"main proc() bool { return 2 == 2.0 }\n",
|
||||
"main proc() bool { return 2 != 3.0 }\n",
|
||||
"main proc() bool { return 2 < 2.5 }\n",
|
||||
"main proc() bool { return 2 <= 2.0 }\n",
|
||||
"main proc() bool { return 3 > 2.5 }\n",
|
||||
"main proc() bool { return 2 >= 2.0 }\n",
|
||||
"main proc() bool { return true == true }\n",
|
||||
"main proc() bool { return false != true }\n",
|
||||
"flag bool :: false\nidentity proc(value bool) bool { return value }\nmain proc() bool {\n local bool :: identity(flag)\n return identity(local == false)\n}\n",
|
||||
"main proc() bool {\n local :: 2\n return local < 2.5\n}\n",
|
||||
]
|
||||
for (sources) |source| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(ir.Type.bool, outcome.value.type)
|
||||
try testing.expect(outcome.value.data.boolean)
|
||||
}
|
||||
}
|
||||
|
||||
conditional_errors_trap_only_when_demanded_and_preserve_original_identity test {
|
||||
sources [7][]u8 :: [
|
||||
"main proc() int {\n if true {\n return 42\n } else {\n return unknown\n }\n}\n",
|
||||
"main proc() int {\n if false {\n return 42\n } else {\n return unknown\n }\n}\n",
|
||||
"main proc() int {\n if true {\n return 42\n }\n}\n",
|
||||
"main proc() int {\n if false {\n return 42\n }\n}\n",
|
||||
"main proc() void {\n if false {\n unknown\n }\n}\n",
|
||||
"main proc() int {\n if 1 {\n return 42\n } else {\n return 0\n }\n}\n",
|
||||
"main proc() int {\n if unknown {\n return 42\n }\n}\n",
|
||||
]
|
||||
for (sources) |source, index| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
|
||||
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
if (index == 0 or index == 2 or index == 4) {
|
||||
try testing.expect(outcome.trap == null)
|
||||
if (index == 4) try testing.expect_equal(ir.Type.void, outcome.value.type)
|
||||
else try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
} else {
|
||||
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
|
||||
code := diagnostics.Code.unknown_name
|
||||
if (index == 3) code = .missing_return
|
||||
if (index == 5) code = .condition_not_bool
|
||||
try testing.expect_equal(code, original.code)
|
||||
if (index == 5) try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], "1"))
|
||||
}
|
||||
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
}
|
||||
|
||||
missing_return_does_not_replace_reached_arm_error test {
|
||||
sources [3][]u8 :: [
|
||||
"main proc() int {\n if true {\n return unknown\n }\n}\n",
|
||||
"main proc() int {\n if false {\n return unknown\n }\n}\n",
|
||||
"main proc() int {\n if false {\n return unknown\n }\n return 42\n}\n",
|
||||
]
|
||||
for (sources) |source, index| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
if (index == 2) {
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
} else {
|
||||
code diagnostics.Code :: if (index == 0) .unknown_name else .missing_return
|
||||
try expect_trap(outcome, &diagnostic_store, code)
|
||||
for (0..diagnostics.count(&diagnostic_store)) |i| {
|
||||
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(i))?
|
||||
if (diagnostic.code == code) try testing.expect_equal(diagnostics.DiagnosticId(i), outcome.trap?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nested_locals_are_predeclared_before_any_arm_executes test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`main proc() int {
|
||||
` if true {
|
||||
` if true {
|
||||
` before :: later
|
||||
` later :: 42
|
||||
` return before
|
||||
` }
|
||||
` }
|
||||
` return 0
|
||||
`}
|
||||
module := try compile_owned(source, &diagnostic_store)
|
||||
defer ir.deinit(&module)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
try expect_trap(outcome, &diagnostic_store, .read_before_initialization)
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ print_result proc(output io.Writer, value ir.Value) void ! io.WriteError {
|
||||
match value.type {
|
||||
.int: try io.print("{}\n", output, {value.data.integer})
|
||||
.float: try io.print("{d}\n", output, {value.data.real})
|
||||
.bool: try io.print("{}\n", output, {value.data.boolean})
|
||||
.string: try io.print("{s}\n", output, {value.data.string})
|
||||
.void: {}
|
||||
else: unreachable
|
||||
|
||||
+147
-10
@@ -143,7 +143,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
|
||||
skip_newlines(state)
|
||||
while !next_is(state, .close_curly) and !next_is(state, .eof) {
|
||||
start :: state.next_token
|
||||
statement :: parse_statement(state) catch |err| {
|
||||
statement :: parse_statement(state, false) catch |err| {
|
||||
match err {
|
||||
.unexpected_token: {
|
||||
recovered :: try recover_statement(state, start)
|
||||
@@ -167,7 +167,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_statement proc(state @mut State) ast.NodeId ! ParseError {
|
||||
parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
||||
if (
|
||||
next_is(state, .ident)
|
||||
and (
|
||||
@@ -179,8 +179,9 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
|
||||
return try parse_statement_decl(state)
|
||||
}
|
||||
match current(state).kind {
|
||||
.return: return try parse_return(state)
|
||||
.ident, .int, .float, .string, .minus, .open_paren: {
|
||||
.if: return try parse_if(state)
|
||||
.return: return try parse_return(state, allow_else)
|
||||
.ident, .int, .float, .string, .true, .false, .minus, .open_paren: {
|
||||
expression :: try parse_expr(state)
|
||||
return try ast.add_expression_stmt(&state.builder, expression)
|
||||
}
|
||||
@@ -189,13 +190,19 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_return proc(state @mut State) ast.NodeId ! ParseError {
|
||||
parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
||||
keyword :: try consume(state, .return)
|
||||
if (next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)) {
|
||||
if (
|
||||
next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)
|
||||
or (allow_else and next_is(state, .else))
|
||||
) {
|
||||
return try ast.add_return_stmt(&state.builder, keyword.span, null)
|
||||
}
|
||||
value :: try parse_expr(state)
|
||||
if (!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)) {
|
||||
if (
|
||||
!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)
|
||||
and !(allow_else and next_is(state, .else))
|
||||
) {
|
||||
return .unexpected_token
|
||||
}
|
||||
# include consumed grouping punctuation, which has no separate expression node.
|
||||
@@ -207,6 +214,116 @@ parse_return proc(state @mut State) ast.NodeId ! ParseError {
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_if proc(state @mut State) ast.NodeId ! ParseError {
|
||||
keyword :: try consume(state, .if)
|
||||
condition_start :: state.next_token
|
||||
condition := parse_expr(state) catch |err| {
|
||||
match err {
|
||||
.unexpected_token: {}
|
||||
else: return err
|
||||
}
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||
yield try ast.add_invalid_expr(&state.builder, current(state).span, diagnostic)
|
||||
}
|
||||
if (ast.builder_expr_kind(&state.builder, condition) == .invalid) {
|
||||
# discard a malformed guard's tail, but leave its arm and enclosing block intact.
|
||||
while (
|
||||
!next_is(state, .open_curly) and !next_is(state, .close_curly)
|
||||
and !next_is(state, .newline) and !next_is(state, .eof)
|
||||
and !next_is(state, .else) and !next_is(state, .return) and !next_is(state, .if)
|
||||
) {
|
||||
state.next_token += 1
|
||||
}
|
||||
}
|
||||
if (
|
||||
!next_is(state, .open_curly)
|
||||
and !parenthesized_guard(state, condition_start)
|
||||
and ast.builder_expr_kind(&state.builder, condition) != .call
|
||||
and ast.builder_expr_kind(&state.builder, condition) != .invalid
|
||||
) {
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||
condition = try ast.add_invalid_expr(
|
||||
&state.builder,
|
||||
ast.builder_span(&state.builder, condition),
|
||||
diagnostic,
|
||||
)
|
||||
}
|
||||
braced :: next_is(state, .open_curly)
|
||||
then_block :: try parse_arm(state)
|
||||
# only a braced arm permits its else on a later line.
|
||||
after_then :: state.next_token
|
||||
if (braced) skip_newlines(state)
|
||||
alternative ?ast.NodeId := null
|
||||
if next_is(state, .else) {
|
||||
state.next_token += 1
|
||||
alternative = try parse_arm(state)
|
||||
} else {
|
||||
state.next_token = after_then
|
||||
}
|
||||
end :: if (alternative) |block|
|
||||
ast.builder_span(&state.builder, block).end
|
||||
else
|
||||
ast.builder_span(&state.builder, then_block).end
|
||||
return try ast.add_if_stmt(
|
||||
&state.builder,
|
||||
ast.Span{ start = keyword.span.start, end = end },
|
||||
condition,
|
||||
then_block,
|
||||
alternative,
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parenthesized_guard proc(state @State, start usize) bool {
|
||||
if (state.tokens[start].kind != .open_paren) return false
|
||||
depth usize := 0
|
||||
for start..state.next_token |index| {
|
||||
kind :: state.tokens[index].kind
|
||||
if (kind == .open_paren) depth += 1
|
||||
if (kind == .close_paren) {
|
||||
depth -= 1
|
||||
if (depth == 0) return index + 1 == state.next_token
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_arm proc(state @mut State) ast.NodeId ! ParseError {
|
||||
if next_is(state, .open_curly) return try parse_block(state)
|
||||
start :: state.next_token
|
||||
statement :: parse_statement(state, true) catch |err| {
|
||||
match err {
|
||||
.unexpected_token: {}
|
||||
else: return err
|
||||
}
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||
span :: synchronize_arm(state, start)
|
||||
yield try ast.add_invalid_stmt(&state.builder, span, diagnostic)
|
||||
}
|
||||
statements [1]ast.NodeId :: [statement]
|
||||
return try ast.add_block(
|
||||
&state.builder,
|
||||
ast.builder_span(&state.builder, statement),
|
||||
statements[..],
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
synchronize_arm proc(state @mut State, start usize) ast.Span {
|
||||
first :: state.tokens[start].span
|
||||
end := first.end
|
||||
while (
|
||||
!next_is(state, .eof) and !next_is(state, .newline)
|
||||
and !next_is(state, .close_curly) and !next_is(state, .else)
|
||||
) {
|
||||
end = current(state).span.end
|
||||
state.next_token += 1
|
||||
}
|
||||
return ast.Span{ start = first.start, end = end }
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
|
||||
name :: try consume(state, .ident)
|
||||
@@ -295,6 +412,10 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
|
||||
!next_is(state, .close_paren)
|
||||
and !next_is(state, .newline)
|
||||
and !next_is(state, .close_curly)
|
||||
and !next_is(state, .open_curly)
|
||||
and !next_is(state, .else)
|
||||
and !next_is(state, .return)
|
||||
and !next_is(state, .if)
|
||||
and !next_is(state, .eof)
|
||||
) {
|
||||
argument :: try parse_expr(state)
|
||||
@@ -347,6 +468,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
||||
state.next_token += 1
|
||||
return try ast.add_literal(&state.builder, .string, token.span)
|
||||
}
|
||||
.true, .false: {
|
||||
state.next_token += 1
|
||||
return try ast.add_literal(&state.builder, .bool, token.span)
|
||||
}
|
||||
.ident: {
|
||||
state.next_token += 1
|
||||
return try ast.add_identifier(&state.builder, token.span)
|
||||
@@ -362,6 +487,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
||||
and token.kind != .comma
|
||||
and token.kind != .close_paren
|
||||
and token.kind != .close_curly
|
||||
and token.kind != .open_curly
|
||||
and token.kind != .else
|
||||
and token.kind != .return
|
||||
and token.kind != .if
|
||||
and token.kind != .eof
|
||||
) {
|
||||
state.next_token += 1
|
||||
@@ -451,7 +580,7 @@ consume_either proc(state @mut State, kinds []lexer.TokenKind) lexer.Token ! Syn
|
||||
@hide:file
|
||||
prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
|
||||
return match kind {
|
||||
.minus: BindingPowerScalar(30)
|
||||
.minus: BindingPowerScalar(50)
|
||||
else: null
|
||||
}
|
||||
}
|
||||
@@ -459,8 +588,10 @@ prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
|
||||
@hide:file
|
||||
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
|
||||
return match kind {
|
||||
.plus, .minus: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
|
||||
.star, .slash: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
|
||||
.equal_equal, .not_equal: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
|
||||
.less, .less_equal, .greater, .greater_equal: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
|
||||
.plus, .minus: BindingPower{ left = BindingPowerScalar(30), right = BindingPowerScalar(31) }
|
||||
.star, .slash: BindingPower{ left = BindingPowerScalar(40), right = BindingPowerScalar(41) }
|
||||
else: null
|
||||
}
|
||||
}
|
||||
@@ -472,6 +603,12 @@ binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
|
||||
.minus: .subtract
|
||||
.star: .multiply
|
||||
.slash: .divide
|
||||
.equal_equal: .equal
|
||||
.not_equal: .not_equal
|
||||
.less: .less
|
||||
.less_equal: .less_equal
|
||||
.greater: .greater
|
||||
.greater_equal: .greater_equal
|
||||
else: unreachable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,3 +330,183 @@ parses_return_boundaries_and_recovers_trailing_tokens test {
|
||||
try testing.expect_equal("return", ast.source_text(&tree, last.span))
|
||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
parses_boolean_comparison_precedence_and_associativity test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`value :: -f() * 2 + 3 < 4 <= 5 > 6 >= 7 == true != false
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
different :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer)
|
||||
try testing.expect_equal(ast.BinaryOperator.not_equal, different.operator)
|
||||
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, different.right))
|
||||
try testing.expect_equal("false", ast.source_text(&tree, ast.node_span(&tree, different.right)))
|
||||
equal :: ast.binary_expr(&tree, different.left)
|
||||
try testing.expect_equal(ast.BinaryOperator.equal, equal.operator)
|
||||
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, equal.right))
|
||||
relation := equal.left
|
||||
operators [4]ast.BinaryOperator :: [.greater_equal, .greater, .less_equal, .less]
|
||||
for (operators) |operator| {
|
||||
binary :: ast.binary_expr(&tree, relation)
|
||||
try testing.expect_equal(operator, binary.operator)
|
||||
relation = binary.left
|
||||
}
|
||||
addition :: ast.binary_expr(&tree, relation)
|
||||
try testing.expect_equal(ast.BinaryOperator.add, addition.operator)
|
||||
product :: ast.binary_expr(&tree, addition.left)
|
||||
try testing.expect_equal(ast.BinaryOperator.multiply, product.operator)
|
||||
negate :: ast.unary_expr(&tree, product.left)
|
||||
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, negate.operand))
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
parses_factorial_and_canonical_conditional_arms test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`factorial proc(n int) int {
|
||||
` if (n <= 1) return 1 else return n * factorial(n - 1)
|
||||
`}
|
||||
`main proc() int {
|
||||
` if true {
|
||||
` local :: factorial(5)
|
||||
` if (false) return 0 else return local
|
||||
` }
|
||||
` else if ready() return 2 else { return 3 }
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
factorial :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, factorial.body))
|
||||
guard :: ast.if_stmt(&tree, ast.block_statement(&tree, factorial.body, 0))
|
||||
try testing.expect_equal("n <= 1", ast.source_text(&tree, ast.node_span(&tree, guard.condition)))
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.then_block))
|
||||
try testing.expect_equal("return 1", ast.source_text(&tree, ast.return_stmt(&tree, ast.block_statement(&tree, guard.then_block, 0)).span))
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.else_block?))
|
||||
try testing.expect_equal(ast.StmtKind.return_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, guard.else_block?, 0)))
|
||||
main :: ast.function_decl(&tree, ast.package_decl(&tree, 1))
|
||||
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, main.body, 0))
|
||||
try testing.expect_equal(2, ast.block_statement_count(&tree, outer.then_block))
|
||||
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, outer.then_block, 0)))
|
||||
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 1))
|
||||
try testing.expect(nested.else_block != null)
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, outer.else_block?))
|
||||
chain :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
|
||||
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, chain.condition))
|
||||
try testing.expect(chain.else_block != null)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
attaches_dangling_else_to_nearest_if test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source :: "main proc() int { if (true) if (false) return 1 else return 2 else return 3 }"
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||
inner :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 0))
|
||||
inner_return :: ast.return_stmt(&tree, ast.block_statement(&tree, inner.else_block?, 0))
|
||||
outer_return :: ast.return_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
|
||||
try testing.expect_equal("2", ast.source_text(&tree, ast.node_span(&tree, inner_return.value?)))
|
||||
try testing.expect_equal("3", ast.source_text(&tree, ast.node_span(&tree, outer_return.value?)))
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
recovers_conditional_conditions_and_arms_without_losing_siblings test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`main proc() void {
|
||||
` if { good() } else { other() }
|
||||
` if (true) else return
|
||||
` if broken( { kept() }
|
||||
` after :: 7
|
||||
` return 1 else return 2
|
||||
` final :: 8
|
||||
`}
|
||||
`tail :: 9
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(2, ast.package_decl_count(&tree))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
try testing.expect_equal(6, ast.block_statement_count(&tree, body))
|
||||
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
|
||||
condition_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
|
||||
try testing.expect_equal("{", ast.source_text(&tree, condition_diagnostic.span))
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, first.then_block))
|
||||
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||
bad_arm :: ast.block_statement(&tree, second.then_block, 0)
|
||||
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, bad_arm))
|
||||
arm_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, bad_arm).diagnostic)?
|
||||
try testing.expect_equal("else", ast.source_text(&tree, arm_diagnostic.span))
|
||||
try testing.expect(second.else_block != null)
|
||||
third :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 2))
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, third.then_block))
|
||||
after :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3))
|
||||
try testing.expect_equal("after", ast.source_text(&tree, after.name))
|
||||
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 4)))
|
||||
final :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 5))
|
||||
try testing.expect_equal("final", ast.source_text(&tree, final.name))
|
||||
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
recovers_invalid_guard_syntax_and_missing_arm_at_enclosing_brace test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`main proc() int {
|
||||
` if true return 1 else return 2
|
||||
` if (true junk) { kept() } else { other() }
|
||||
` sibling :: 3
|
||||
` if (false) }
|
||||
`tail :: 4
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(2, ast.package_decl_count(&tree))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
try testing.expect_equal(4, ast.block_statement_count(&tree, body))
|
||||
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
|
||||
guard_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
|
||||
try testing.expect_equal("return", ast.source_text(&tree, guard_error.span))
|
||||
try testing.expect(first.else_block != null)
|
||||
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, second.then_block))
|
||||
try testing.expect(second.else_block != null)
|
||||
sibling :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 2))
|
||||
try testing.expect_equal("sibling", ast.source_text(&tree, sibling.name))
|
||||
last :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 3))
|
||||
missing :: ast.block_statement(&tree, last.then_block, 0)
|
||||
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, missing))
|
||||
arm_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, missing).diagnostic)?
|
||||
try testing.expect_equal("}", ast.source_text(&tree, arm_error.span))
|
||||
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
+121
-60
@@ -5,7 +5,7 @@ import "@source/ast"
|
||||
import "@source/diagnostics"
|
||||
sema :: import "@source/sema"
|
||||
|
||||
TypeKind :: enum { none, invalid, int, float, string, void, function }
|
||||
TypeKind :: enum { none, invalid, int, float, bool, string, void, function }
|
||||
|
||||
Type :: struct {
|
||||
kind TypeKind = .none
|
||||
@@ -27,7 +27,7 @@ Result :: struct { storage Storage }
|
||||
no_index :: maxval!(usize)
|
||||
|
||||
@hide:file
|
||||
Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body, return_stmt }
|
||||
Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body, return_stmt, if_stmt }
|
||||
|
||||
@hide:file
|
||||
Cell :: struct {
|
||||
@@ -161,6 +161,7 @@ check proc(
|
||||
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
|
||||
.binary: {
|
||||
expression :: ast.binary_expr(tree, id)
|
||||
if (bool_equality(&state, id)) continue
|
||||
try require_numeric(&state, id, expression.left)
|
||||
try require_numeric(&state, id, expression.right)
|
||||
}
|
||||
@@ -178,10 +179,14 @@ check proc(
|
||||
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
|
||||
.binary: {
|
||||
expression :: ast.binary_expr(tree, id)
|
||||
try numeric_operand(&state, id, expression.left)
|
||||
try numeric_operand(&state, id, expression.right)
|
||||
if (expression.operator == .divide) {
|
||||
try impose(&state, id, .float, .integer_division, expression.span)
|
||||
if (comparison(expression.operator)) {
|
||||
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
|
||||
} else {
|
||||
try numeric_operand(&state, id, expression.left)
|
||||
try numeric_operand(&state, id, expression.right)
|
||||
if (expression.operator == .divide) {
|
||||
try impose(&state, id, .float, .integer_division, expression.span)
|
||||
}
|
||||
}
|
||||
}
|
||||
else: {}
|
||||
@@ -246,6 +251,28 @@ check proc(
|
||||
# errors travel only from dependencies to users. A rejected use never poisons
|
||||
# an explicitly typed declaration, and body errors do not erase signatures.
|
||||
propagate(&state, queue)
|
||||
# validate conditions after operand errors have reached their users.
|
||||
for (0..count) |i| {
|
||||
if (cells[i].role != .if_stmt) continue
|
||||
condition :: ast.if_stmt(tree, ast.NodeId(i)).condition
|
||||
actual :: types[usize(condition)]
|
||||
if (actual.kind != .invalid and actual.kind != .bool) {
|
||||
try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition))
|
||||
}
|
||||
}
|
||||
propagate(&state, queue)
|
||||
for (0..ast.package_decl_count(tree)) |i| {
|
||||
id :: ast.package_decl(tree, i)
|
||||
if (ast.decl_kind(tree, id) != .function) continue
|
||||
function :: ast.function_decl(tree, id)
|
||||
result :: types[usize(function.return_type)]
|
||||
if (result.kind == .void or result.kind == .invalid or types[usize(id)].kind == .invalid) continue
|
||||
if (block_falls_through(&state, function.body)) {
|
||||
# lowering must find this diagnostic even when another arm failed.
|
||||
diagnostic :: try diagnostics.add(diagnostic_store, .missing_return, ast.node_span(tree, function.body))
|
||||
types[usize(function.body)] = invalid_type(diagnostic)
|
||||
}
|
||||
}
|
||||
return Result{ storage = Storage{ types = types, allocator = allocator } }
|
||||
}
|
||||
|
||||
@@ -371,6 +398,7 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
|
||||
type.kind = match binding.data.builtin {
|
||||
.int: .int
|
||||
.float: .float
|
||||
.bool: .bool
|
||||
.string: .string
|
||||
.void: .void
|
||||
}
|
||||
@@ -427,25 +455,44 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
|
||||
state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
|
||||
state.cells[usize(id)].type.function = id
|
||||
}
|
||||
state.cells[usize(function.body)].role = .body
|
||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
||||
statement :: ast.block_statement(tree, function.body, i)
|
||||
try collect_block(state, function.body, function.return_type)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
tree :: state.tree
|
||||
state.cells[usize(id)].role = .body
|
||||
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||
statement :: ast.block_statement(tree, id, i)
|
||||
match ast.stmt_kind(tree, statement) {
|
||||
.declaration: try collect_declaration(state, statement)
|
||||
.expression: try collect_expression(state, ast.statement_expr(tree, statement))
|
||||
.return_stmt: {
|
||||
state.cells[usize(statement)].role = .return_stmt
|
||||
state.cells[usize(statement)].type = Type{ kind = .void }
|
||||
state.cells[usize(statement)].return_annotation = function.return_type
|
||||
state.cells[usize(statement)].return_annotation = return_annotation
|
||||
if (ast.return_stmt(tree, statement).value) |value| {
|
||||
try collect_expression(state, value)
|
||||
try depend(state, value, statement)
|
||||
}
|
||||
}
|
||||
.if_stmt: {
|
||||
conditional :: ast.if_stmt(tree, statement)
|
||||
state.cells[usize(statement)].role = .if_stmt
|
||||
state.cells[usize(statement)].type = Type{ kind = .void }
|
||||
try collect_expression(state, conditional.condition)
|
||||
try depend(state, conditional.condition, statement)
|
||||
try collect_block(state, conditional.then_block, return_annotation)
|
||||
try depend(state, conditional.then_block, id)
|
||||
if (conditional.else_block) |other| {
|
||||
try collect_block(state, other, return_annotation)
|
||||
try depend(state, other, id)
|
||||
}
|
||||
}
|
||||
.invalid: collect_invalid(state, statement)
|
||||
}
|
||||
source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement
|
||||
try depend(state, source, function.body)
|
||||
try depend(state, source, id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +507,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
||||
state.cells[usize(id)].anchored = true
|
||||
}
|
||||
.literal_float: state.cells[usize(id)].type = Type{ kind = .float }
|
||||
.literal_bool: state.cells[usize(id)].type = Type{ kind = .bool }
|
||||
.literal_string: state.cells[usize(id)].type = Type{ kind = .string }
|
||||
.identifier: {
|
||||
binding :: sema.binding(state.resolution, id)
|
||||
@@ -476,8 +524,12 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
||||
try depend(state, operand, id)
|
||||
}
|
||||
.binary: {
|
||||
state.cells[usize(id)].numeric = true
|
||||
expression :: ast.binary_expr(tree, id)
|
||||
if (comparison(expression.operator)) {
|
||||
state.cells[usize(id)].type = Type{ kind = .bool }
|
||||
} else {
|
||||
state.cells[usize(id)].numeric = true
|
||||
}
|
||||
try collect_expression(state, expression.left)
|
||||
try collect_expression(state, expression.right)
|
||||
try depend(state, expression.left, id)
|
||||
@@ -502,6 +554,40 @@ numeric_kind proc(kind TypeKind) bool {
|
||||
return kind == .int or kind == .float
|
||||
}
|
||||
|
||||
@hide:file
|
||||
comparison proc(operator ast.BinaryOperator) bool {
|
||||
return (
|
||||
operator == .equal or operator == .not_equal or operator == .less
|
||||
or operator == .less_equal or operator == .greater or operator == .greater_equal
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
bool_equality proc(state @mut State, id ast.NodeId) bool {
|
||||
expression :: ast.binary_expr(state.tree, id)
|
||||
return (
|
||||
(expression.operator == .equal or expression.operator == .not_equal)
|
||||
and group_type(state, expression.left).kind == .bool
|
||||
and group_type(state, expression.right).kind == .bool
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
comparison_operands proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
if (state.types[usize(id)].kind == .invalid) return
|
||||
expression :: ast.binary_expr(state.tree, id)
|
||||
left :: root(state, usize(expression.left))
|
||||
right :: root(state, usize(expression.right))
|
||||
# only operand groups receive numeric demands; the result stays bool.
|
||||
if (state.cells[left].numeric or state.cells[left].type.kind == .none) {
|
||||
state.cells[left].numeric = true
|
||||
try numeric_constraint(state, expression.left, expression.right, id)
|
||||
} else if (state.cells[right].numeric or state.cells[right].type.kind == .none) {
|
||||
state.cells[right].numeric = true
|
||||
try numeric_constraint(state, expression.right, expression.left, id)
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
index :: root(state, usize(id))
|
||||
@@ -516,6 +602,11 @@ impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Cod
|
||||
|
||||
@hide:file
|
||||
numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
try numeric_constraint(state, expression, operand, expression)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
numeric_constraint proc(state @mut State, expression, operand, operation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
index :: root(state, usize(operand))
|
||||
type :: state.cells[index].type
|
||||
if (state.types[usize(expression)].kind == .invalid or state.types[usize(operand)].kind == .invalid) return
|
||||
@@ -525,13 +616,13 @@ numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (m
|
||||
operand_demand :: state.cells[index].demand
|
||||
join(state, expression, operand)
|
||||
if (expression_demand != .none) {
|
||||
try impose(state, expression, expression_demand, .invalid_operand, ast.node_span(state.tree, expression))
|
||||
try impose(state, expression, expression_demand, .invalid_operand, ast.node_span(state.tree, operation))
|
||||
}
|
||||
if (operand_demand != .none) {
|
||||
try impose(state, expression, operand_demand, .invalid_operand, ast.node_span(state.tree, expression))
|
||||
try impose(state, expression, operand_demand, .invalid_operand, ast.node_span(state.tree, operation))
|
||||
}
|
||||
} else if (numeric_kind(type.kind)) {
|
||||
try impose(state, expression, type.kind, .invalid_operand, ast.node_span(state.tree, expression))
|
||||
try impose(state, expression, type.kind, .invalid_operand, ast.node_span(state.tree, operation))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,20 +659,6 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
|
||||
}
|
||||
if (declaration.type) |type| try compatible(state, declaration.initializer, state.types[usize(type)])
|
||||
}
|
||||
.function: {
|
||||
function :: ast.function_decl(tree, id)
|
||||
result :: state.types[usize(function.return_type)]
|
||||
if (result.kind == .void or result.kind == .invalid or state.types[usize(id)].kind == .invalid) return
|
||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
||||
if (ast.stmt_kind(tree, ast.block_statement(tree, function.body, i)) == .return_stmt) return
|
||||
}
|
||||
if (syntactically_valid_body(tree, function.body)) {
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .missing_return, ast.node_span(tree, function.body))
|
||||
if (state.types[usize(function.body)].kind != .invalid) {
|
||||
state.types[usize(function.body)] = invalid_type(diagnostic)
|
||||
}
|
||||
}
|
||||
}
|
||||
.return_stmt: {
|
||||
statement :: ast.return_stmt(tree, id)
|
||||
expected :: state.types[usize(state.cells[usize(id)].return_annotation)]
|
||||
@@ -611,6 +688,8 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
|
||||
left :: state.types[usize(expression.left)]
|
||||
right :: state.types[usize(expression.right)]
|
||||
if (left.kind == .invalid or right.kind == .invalid) return
|
||||
if ((expression.operator == .equal or expression.operator == .not_equal)
|
||||
and left.kind == .bool and right.kind == .bool) return
|
||||
if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or left.kind != right.kind) {
|
||||
try fail(state, id, .invalid_operand, expression.span)
|
||||
} else if (expression.operator == .divide and left.kind == .int) {
|
||||
@@ -649,43 +728,25 @@ validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
syntactically_valid_expression proc(tree @ast.Tree, id ast.NodeId) bool {
|
||||
match ast.expr_kind(tree, id) {
|
||||
.invalid: return false
|
||||
.unary: return syntactically_valid_expression(tree, ast.unary_expr(tree, id).operand)
|
||||
.binary: {
|
||||
expression :: ast.binary_expr(tree, id)
|
||||
return syntactically_valid_expression(tree, expression.left) and syntactically_valid_expression(tree, expression.right)
|
||||
}
|
||||
.call: {
|
||||
if (!syntactically_valid_expression(tree, ast.call_expr(tree, id).callee)) return false
|
||||
for (0..ast.call_arg_count(tree, id)) |i| {
|
||||
if (!syntactically_valid_expression(tree, ast.call_arg(tree, id, i))) return false
|
||||
}
|
||||
}
|
||||
else: {}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@hide:file
|
||||
syntactically_valid_body proc(tree @ast.Tree, id ast.NodeId) bool {
|
||||
block_falls_through proc(state @State, id ast.NodeId) bool {
|
||||
tree :: state.tree
|
||||
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||
statement :: ast.block_statement(tree, id, i)
|
||||
match ast.stmt_kind(tree, statement) {
|
||||
.invalid: return false
|
||||
.expression: {
|
||||
if (!syntactically_valid_expression(tree, ast.statement_expr(tree, statement))) return false
|
||||
}
|
||||
.return_stmt: {
|
||||
if (ast.return_stmt(tree, statement).value) |value| {
|
||||
if (!syntactically_valid_expression(tree, value)) return false
|
||||
kind :: ast.stmt_kind(tree, statement)
|
||||
source :: if (kind == .expression) ast.statement_expr(tree, statement) else statement
|
||||
if (state.types[usize(source)].kind == .invalid) return false
|
||||
match kind {
|
||||
.return_stmt, .invalid: return false
|
||||
.if_stmt: {
|
||||
conditional :: ast.if_stmt(tree, statement)
|
||||
then_falls :: block_falls_through(state, conditional.then_block)
|
||||
if (conditional.else_block) |other| {
|
||||
if (!then_falls and !block_falls_through(state, other)) return false
|
||||
}
|
||||
}
|
||||
.declaration: {
|
||||
if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false
|
||||
}
|
||||
else: {}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -731,3 +731,240 @@ return_errors_preserve_identity_check_unreachable_code_and_suppress_missing_retu
|
||||
try testing.expect_equal(2, code_count(&diagnostic_store, .unexpected_token))
|
||||
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
checks_bool_comparisons_recursive_returns_and_nested_numeric_context test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`flag bool :: true
|
||||
`same :: flag == false
|
||||
`different :: true != flag
|
||||
`less :: alias < 4.0
|
||||
`less_equal :: seed <= 5
|
||||
`greater :: 6 > seed
|
||||
`greater_equal :: seed >= 1
|
||||
`equal :: seed == 3
|
||||
`not_equal :: seed != 2
|
||||
`alias :: seed
|
||||
`seed := 3
|
||||
`identity proc(value bool) bool { return value }
|
||||
`factorial proc(n int) int {
|
||||
` if n <= 1 { return 1 }
|
||||
` return n * factorial(n - 1)
|
||||
`}
|
||||
`fraction proc(value bool) float {
|
||||
` if value {
|
||||
` before :: later
|
||||
` later := 3
|
||||
` return -before / 2
|
||||
` } else if false {
|
||||
` later :: 4
|
||||
` return later
|
||||
` } else {
|
||||
` return 5
|
||||
` }
|
||||
`}
|
||||
`main proc() bool { return identity(flag) }
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||
defer deinit(&result)
|
||||
|
||||
for (0..usize(9)) |i| {
|
||||
declaration :: ast.package_decl(&tree, i)
|
||||
try expect_kind(&result, declaration, .bool)
|
||||
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .bool)
|
||||
if (i < 3) continue
|
||||
comparison_view :: ast.binary_expr(&tree, ast.statement_decl(&tree, declaration).initializer)
|
||||
try expect_kind(&result, comparison_view.left, .float)
|
||||
try expect_kind(&result, comparison_view.right, .float)
|
||||
}
|
||||
for (usize(9)..usize(11)) |i| try expect_kind(&result, ast.package_decl(&tree, i), .float)
|
||||
identity :: ast.package_decl(&tree, 11)
|
||||
try expect_kind(&result, ast.param(&tree, ast.function_param(&tree, identity, 0)).type, .bool)
|
||||
try expect_kind(&result, ast.function_decl(&tree, identity).return_type, .bool)
|
||||
factorial :: ast.package_decl(&tree, 12)
|
||||
factorial_view :: ast.function_decl(&tree, factorial)
|
||||
try expect_kind(&result, factorial, .function)
|
||||
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, factorial_view.body, 0))
|
||||
try expect_kind(&result, conditional.condition, .bool)
|
||||
operands :: ast.binary_expr(&tree, conditional.condition)
|
||||
try expect_kind(&result, operands.left, .int)
|
||||
try expect_kind(&result, operands.right, .int)
|
||||
fraction :: ast.function_decl(&tree, ast.package_decl(&tree, 13))
|
||||
arms :: ast.if_stmt(&tree, ast.block_statement(&tree, fraction.body, 0))
|
||||
for (0..usize(2)) |i| {
|
||||
declaration :: ast.block_statement(&tree, arms.then_block, i)
|
||||
try expect_kind(&result, declaration, .float)
|
||||
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .float)
|
||||
}
|
||||
if (arms.else_block) |other| {
|
||||
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, other, 0))
|
||||
try expect_kind(&result, ast.block_statement(&tree, nested.then_block, 0), .float)
|
||||
} else {
|
||||
return .expectation_failed
|
||||
}
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
rejects_bool_numeric_mixing_and_preserves_invalid_condition_identity test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`sum :: true + false
|
||||
`order :: true < false
|
||||
`mixed :: true == 1
|
||||
`strings :: "x" == "x"
|
||||
`wrong_bool bool :: 1
|
||||
`wrong_int int :: false
|
||||
`main proc() void {
|
||||
` if (1) { okay :: true }
|
||||
` if "text" { okay :: false }
|
||||
` if absent { okay :: true }
|
||||
` if true + false { okay :: false }
|
||||
` if true { return } else { -"bad" }
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||
defer deinit(&result)
|
||||
|
||||
for (0..usize(4)) |i| {
|
||||
expression :: ast.statement_decl(&tree, ast.package_decl(&tree, i)).initializer
|
||||
_ = try expect_error(&result, &diagnostic_store, expression, .invalid_operand, ast.node_span(&tree, expression))
|
||||
}
|
||||
for (usize(4)..usize(6)) |i| {
|
||||
expression :: ast.statement_decl(&tree, ast.package_decl(&tree, i)).initializer
|
||||
_ = try expect_error(&result, &diagnostic_store, expression, .type_mismatch, ast.node_span(&tree, expression))
|
||||
}
|
||||
function :: ast.package_decl(&tree, 6)
|
||||
body :: ast.function_decl(&tree, function).body
|
||||
for (0..usize(2)) |i| {
|
||||
statement :: ast.block_statement(&tree, body, i)
|
||||
condition :: ast.if_stmt(&tree, statement).condition
|
||||
diagnostic :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition))
|
||||
try testing.expect_equal(diagnostic, type_of(&result, statement).diagnostic)
|
||||
}
|
||||
unknown :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 2)).condition
|
||||
original :: sema.binding(&resolution, unknown).data.diagnostic
|
||||
try testing.expect_equal(original, type_of(&result, unknown).diagnostic)
|
||||
failed :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 3)).condition
|
||||
_ = try expect_error(&result, &diagnostic_store, failed, .invalid_operand, ast.node_span(&tree, failed))
|
||||
final_if :: ast.block_statement(&tree, body, 4)
|
||||
try expect_kind(&result, final_if, .void)
|
||||
final_view :: ast.if_stmt(&tree, final_if)
|
||||
if (final_view.else_block) |other| {
|
||||
error_expr :: ast.statement_expr(&tree, ast.block_statement(&tree, other, 0))
|
||||
_ = try expect_error(&result, &diagnostic_store, error_expr, .invalid_operand, ast.node_span(&tree, error_expr))
|
||||
} else {
|
||||
return .expectation_failed
|
||||
}
|
||||
try expect_kind(&result, function, .function)
|
||||
try testing.expect_equal(2, code_count(&diagnostic_store, .condition_not_bool))
|
||||
try testing.expect_equal(6, code_count(&diagnostic_store, .invalid_operand))
|
||||
try testing.expect_equal(2, code_count(&diagnostic_store, .type_mismatch))
|
||||
try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name))
|
||||
try testing.expect_equal(11, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
analyzes_return_paths_after_trap_propagation_without_poisoning_signatures test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`both proc(flag bool) int {
|
||||
` if flag { return 1 } else { return 2 }
|
||||
`}
|
||||
`missing proc() int { if true { return 1 } }
|
||||
`bad_arm proc(flag bool) int { if flag { -"bad" } }
|
||||
`trap_and_return proc(flag bool) int {
|
||||
` if flag { return 1 } else { bad int :: false }
|
||||
`}
|
||||
`invalid_condition proc() int { if 1 {} }
|
||||
`invalid_expression proc() int { -"bad" }
|
||||
`invalid_declaration proc() int { bad bool :: 1 }
|
||||
`syntax_trap proc() int {
|
||||
` return 1 stray
|
||||
`}
|
||||
`unreachable proc() int {
|
||||
` return 1
|
||||
` -"bad"
|
||||
`}
|
||||
`nested proc(a bool, b bool) int {
|
||||
` if a { if b { return 1 } else { return 2 } } else { return 3 }
|
||||
`}
|
||||
`join proc(flag bool) int {
|
||||
` if flag {} else { return 1 }
|
||||
` return 2
|
||||
`}
|
||||
`answer :: bad_arm(false)
|
||||
`wrong_return proc(flag bool) int {
|
||||
` if flag { return false } else { return 1 }
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
defer sema.deinit(&resolution)
|
||||
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||
defer deinit(&result)
|
||||
|
||||
for (0..usize(11)) |i| try expect_kind(&result, ast.package_decl(&tree, i), .function)
|
||||
for (usize(1)..usize(3)) |i| {
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, i)).body
|
||||
_ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body))
|
||||
}
|
||||
bad_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body
|
||||
conditional :: ast.block_statement(&tree, bad_body, 0)
|
||||
try expect_kind(&result, conditional, .void)
|
||||
arm :: ast.if_stmt(&tree, conditional).then_block
|
||||
operand :: ast.statement_expr(&tree, ast.block_statement(&tree, arm, 0))
|
||||
original :: try expect_error(&result, &diagnostic_store, operand, .invalid_operand, ast.node_span(&tree, operand))
|
||||
try testing.expect_equal(original, type_of(&result, arm).diagnostic)
|
||||
try testing.expect(type_of(&result, bad_body).diagnostic != original)
|
||||
condition_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
|
||||
condition :: ast.if_stmt(&tree, ast.block_statement(&tree, condition_body, 0)).condition
|
||||
condition_diag :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition))
|
||||
try testing.expect_equal(condition_diag, type_of(&result, condition_body).diagnostic)
|
||||
recovered :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body
|
||||
syntax :: ast.block_statement(&tree, recovered, 0)
|
||||
try testing.expect_equal(ast.invalid(&tree, syntax).diagnostic, type_of(&result, recovered).diagnostic)
|
||||
unreachable_body :: ast.function_decl(&tree, ast.package_decl(&tree, 8)).body
|
||||
checked_after_return :: ast.statement_expr(&tree, ast.block_statement(&tree, unreachable_body, 1))
|
||||
_ = try expect_error(&result, &diagnostic_store, checked_after_return, .invalid_operand, ast.node_span(&tree, checked_after_return))
|
||||
try expect_kind(&result, ast.package_decl(&tree, 11), .int)
|
||||
wrong_function :: ast.package_decl(&tree, 12)
|
||||
try expect_kind(&result, wrong_function, .function)
|
||||
wrong_body :: ast.function_decl(&tree, wrong_function).body
|
||||
wrong_if :: ast.block_statement(&tree, wrong_body, 0)
|
||||
try expect_kind(&result, wrong_if, .void)
|
||||
wrong_arm :: ast.if_stmt(&tree, wrong_if).then_block
|
||||
wrong_statement :: ast.block_statement(&tree, wrong_arm, 0)
|
||||
if (ast.return_stmt(&tree, wrong_statement).value) |value| {
|
||||
mismatch :: try expect_error(&result, &diagnostic_store, value, .return_type_mismatch, ast.node_span(&tree, value))
|
||||
try testing.expect_equal(mismatch, type_of(&result, wrong_statement).diagnostic)
|
||||
try testing.expect_equal(mismatch, type_of(&result, wrong_body).diagnostic)
|
||||
} else {
|
||||
return .expectation_failed
|
||||
}
|
||||
try testing.expect_equal(1, code_count(&diagnostic_store, .return_type_mismatch))
|
||||
try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return))
|
||||
try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool))
|
||||
}
|
||||
|
||||
+30
-11
@@ -6,12 +6,13 @@ import "@std/strmap"
|
||||
import "@source/ast"
|
||||
import "@source/diagnostics"
|
||||
|
||||
BuiltinType :: enum { int, float, string, void }
|
||||
BuiltinType :: enum { int, float, bool, string, void }
|
||||
|
||||
@hide:file
|
||||
builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([
|
||||
{ "int", .int },
|
||||
{ "float", .float },
|
||||
{ "bool", .bool },
|
||||
{ "string", .string },
|
||||
{ "void", .void },
|
||||
])
|
||||
@@ -175,20 +176,38 @@ resolve_function proc(
|
||||
parameter :: ast.function_param(tree, id, i)
|
||||
try predeclare(state, &scope, parameter, ast.param(tree, parameter).name)
|
||||
}
|
||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
||||
statement :: ast.block_statement(tree, function.body, i)
|
||||
try resolve_block(state, &scope, function.body)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_arm proc(state @mut State, parent @Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
scope := Scope{ names = hashmap.init(state.allocator), parent = parent }
|
||||
defer hashmap.deinit(&scope.names)
|
||||
try resolve_block(state, &scope, id)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
tree :: state.tree
|
||||
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||
statement :: ast.block_statement(tree, id, i)
|
||||
if ast.stmt_kind(tree, statement) == .declaration {
|
||||
try predeclare(state, &scope, statement, ast.statement_decl(tree, statement).name)
|
||||
try predeclare(state, scope, statement, ast.statement_decl(tree, statement).name)
|
||||
}
|
||||
}
|
||||
|
||||
for (0..ast.block_statement_count(tree, function.body)) |i| {
|
||||
statement :: ast.block_statement(tree, function.body, i)
|
||||
for (0..ast.block_statement_count(tree, id)) |i| {
|
||||
statement :: ast.block_statement(tree, id, i)
|
||||
match ast.stmt_kind(tree, statement) {
|
||||
.declaration: try resolve_statement_decl(state, &scope, statement)
|
||||
.expression: try resolve_expr(state, &scope, ast.statement_expr(tree, statement))
|
||||
.declaration: try resolve_statement_decl(state, scope, statement)
|
||||
.expression: try resolve_expr(state, scope, ast.statement_expr(tree, statement))
|
||||
.return_stmt: {
|
||||
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, &scope, value)
|
||||
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
|
||||
}
|
||||
.if_stmt: {
|
||||
conditional :: ast.if_stmt(tree, statement)
|
||||
try resolve_expr(state, scope, conditional.condition)
|
||||
try resolve_arm(state, scope, conditional.then_block)
|
||||
if (conditional.else_block) |other| try resolve_arm(state, scope, other)
|
||||
}
|
||||
.invalid: resolve_invalid(state, statement)
|
||||
}
|
||||
@@ -225,7 +244,7 @@ resolve_expr proc(
|
||||
if (state.bindings[usize(id)].kind != .none) return
|
||||
tree :: state.tree
|
||||
match ast.expr_kind(tree, id) {
|
||||
.literal_int, .literal_float, .literal_string: {}
|
||||
.literal_int, .literal_float, .literal_bool, .literal_string: {}
|
||||
.identifier: {
|
||||
span :: ast.node_span(tree, id)
|
||||
name :: ast.source_text(tree, span)
|
||||
|
||||
@@ -298,3 +298,71 @@ preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
|
||||
try testing.expect_equal(later, binding(&resolution, valid_ref).data.declaration)
|
||||
try testing.expect_equal(syntax_count + 3, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
resolves_conditional_arm_scopes_without_leaks_or_shadowing test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`choose proc(flag bool, value int) void {
|
||||
` if flag {
|
||||
` before :: later
|
||||
` later :: value
|
||||
` if before == value { nested :: later }
|
||||
` } else {
|
||||
` later :: value
|
||||
` }
|
||||
` later
|
||||
` if later { later :: 1 }
|
||||
` if flag {
|
||||
` value :: 2
|
||||
` bool :: false
|
||||
` if flag { value }
|
||||
` }
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||
defer deinit(&resolution)
|
||||
|
||||
function :: ast.package_decl(&tree, 0)
|
||||
view :: ast.function_decl(&tree, function)
|
||||
flag :: ast.function_param(&tree, function, 0)
|
||||
value :: ast.function_param(&tree, function, 1)
|
||||
try testing.expect_equal(BuiltinType.bool, binding(&resolution, ast.param(&tree, flag).type).data.builtin)
|
||||
first :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 0))
|
||||
try testing.expect_equal(flag, binding(&resolution, first.condition).data.declaration)
|
||||
before :: ast.block_statement(&tree, first.then_block, 0)
|
||||
later :: ast.block_statement(&tree, first.then_block, 1)
|
||||
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, before).initializer).data.declaration)
|
||||
try testing.expect_equal(value, binding(&resolution, ast.statement_decl(&tree, later).initializer).data.declaration)
|
||||
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, first.then_block, 2))
|
||||
nested_decl :: ast.block_statement(&tree, nested.then_block, 0)
|
||||
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, nested_decl).initializer).data.declaration)
|
||||
if (first.else_block) |other| {
|
||||
sibling :: ast.block_statement(&tree, other, 0)
|
||||
try testing.expect_equal(BindingKind.declaration, binding(&resolution, sibling).kind)
|
||||
try testing.expect_equal(sibling, binding(&resolution, sibling).data.declaration)
|
||||
} else {
|
||||
return .expectation_failed
|
||||
}
|
||||
outside :: ast.statement_expr(&tree, ast.block_statement(&tree, view.body, 1))
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, outside).kind)
|
||||
independent :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 2))
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, independent.condition).kind)
|
||||
try testing.expect_equal(BindingKind.declaration, binding(&resolution, ast.block_statement(&tree, independent.then_block, 0)).kind)
|
||||
forbidden :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 3))
|
||||
shadow :: ast.block_statement(&tree, forbidden.then_block, 0)
|
||||
builtin :: ast.block_statement(&tree, forbidden.then_block, 1)
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, shadow).kind)
|
||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind)
|
||||
poisoned :: ast.if_stmt(&tree, ast.block_statement(&tree, forbidden.then_block, 2))
|
||||
use :: ast.statement_expr(&tree, ast.block_statement(&tree, poisoned.then_block, 0))
|
||||
try testing.expect_equal(binding(&resolution, shadow).data.diagnostic, binding(&resolution, use).data.diagnostic)
|
||||
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user