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:
+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