feat: execute short-circuit boolean expressions

This commit is contained in:
2026-10-10 00:36:07 +02:00
parent 7551ce5f6d
commit 5f845b9520
15 changed files with 952 additions and 30 deletions
+4 -2
View File
@@ -43,6 +43,8 @@ function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. ca
`true` and `false` are boolean literals; `bool` is available for parameters, storage and results. `==`, `!=`, `<`, `<=`, `>` and `>=` compare compatible numeric operands and produce `bool`; `==` and `!=` also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion.
`!`, `and` and `or` require boolean operands and produce `bool`. prefix `!` and `-` bind tighter than arithmetic; equality binds tighter than `and`, which binds tighter than `or`. binary operators associate left to right, while prefixes nest right to left. operands evaluate left to right: `false and rhs` and `true or rhs` skip the right operand, including calls, lazy global initialization and runtime traps. both operands are still parsed, resolved and type-checked; errors in a skipped operand remain source diagnostics without forcing a runtime trap. this applies in initializers, assignments, returns, call arguments and conditions. [programs/boolean_conditions.hon](programs/boolean_conditions.hon) demonstrates combined predicates; `./build/honey run programs/boolean_conditions.hon` prints `true`.
`if`/`else` supports braced arms, single statements and `else if` chains. conditions must have type `bool`. a braceless guard requires parentheses unless it is a function call: `if (n <= 1) return 1` or `if ready() return`. `else` binds to the nearest `if`; after a braced arm it may start on the next line. branches are represented as AST blocks even when braceless.
`name = expression` updates a local `:=` binding. `::` bindings, parameters, named functions, builtin types and globals are not assignment targets. the established binding type is preserved: assignment inputs do not widen or infer that type, though numeric literals can adopt its float context. function-valued locals accept replacement functions with the same parameter and result types.
@@ -59,8 +61,8 @@ numeric typing follows contextual rules within Honey's current `int` and `float`
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed.
`lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter.
`lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. short-circuit expressions use branches and synthetic boolean local slots to merge results; call arguments are packed in the block reached after evaluating them. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter.
`interpreter.run` consumes only IR and returns a value or a diagnostic trap. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. integers use signed 64-bit storage with checked arithmetic; floats use 64-bit IEEE arithmetic and comparisons, including NaN behavior. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation, `for`, `break`/`continue`, compound assignment and boolean logical operators remain unimplemented.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation, `for`, `break`/`continue` and compound assignment remain unimplemented.
+11
View File
@@ -0,0 +1,11 @@
in_range proc(value, low, high int) bool {
return value >= low and value < high
}
main proc() bool {
return (
in_range(5, 0, 10)
and !in_range(12, 0, 10)
or in_range(20, 0, 10)
)
}
+6 -3
View File
@@ -2,13 +2,15 @@
#! * type ..... -> identifier
#! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>='
#! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
#! * expr ..... -> identifier | literal | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')'
#! * decl ..... -> identifier [type] binding expr
#! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'if' expr arm ['else' arm] | 'while' expr arm
#! braceless guards require parentheses unless the condition is a call; else binds to the nearest if.
#! precedence (tightest first): calls, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or.
#! binary operators associate left; unary operators associate right.
import "@std/debug"
import "@std/mem"
@@ -58,7 +60,7 @@ Mutability :: enum {
constant, variable
}
UnaryOperator :: enum { negate }
UnaryOperator :: enum { negate, logical_not }
BinaryOperator :: enum {
add, subtract,
@@ -66,6 +68,7 @@ BinaryOperator :: enum {
equal, not_equal,
less, less_equal,
greater, greater_equal
logical_and, logical_or
}
@hide:file
+4
View File
@@ -170,6 +170,10 @@ run proc(
value = ir.Value{ type = .int, data = ir.ValueData{ integer = -operand.data.integer } }
}
}
.logical_not: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = !operand.data.boolean } }
}
.add, .subtract, .multiply, .divide: {
left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)]
+31
View File
@@ -779,3 +779,34 @@ assignment_diagnostic_allocation_failure_releases_runtime_storage test {
}
return .expectation_failed
}
logical_not_inverts_boolean_payload_and_composes_in_typed_ir test {
operands [2]bool :: [false, true]
expected [2]bool :: [true, false]
for (operands) |operand, index| {
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 = .bool,
destination = ir.ValueId(0),
constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = operand } },
})
try emit(&module, main, ir.Instruction{ op = .logical_not, type = .bool, destination = ir.ValueId(1), left = ir.ValueId(0) })
try emit(&module, main, ir.Instruction{ op = .logical_not, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(1) })
return_value(&module, main, ir.ValueId(1))
inverted :: try run(mem.c_allocator, &module, &store)
try testing.expect(inverted.trap == null)
try testing.expect_equal(ir.Type.bool, inverted.value.type)
try testing.expect_equal(expected[index], inverted.value.data.boolean)
return_value(&module, main, ir.ValueId(2))
composed :: try run(mem.c_allocator, &module, &store)
try testing.expect(composed.trap == null)
try testing.expect_equal(operand, composed.value.data.boolean)
try testing.expect_equal(usize(0), diagnostics.count(&store))
}
}
+1
View File
@@ -40,6 +40,7 @@ Op :: enum {
reset_local
load_global
negate
logical_not
add
subtract
multiply
+3
View File
@@ -28,6 +28,8 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "while", .while },
{ "true", .true },
{ "false", .false },
{ "and", .and },
{ "or", .or },
])
#! owns token storage and retains its allocator; release with deinit.
@@ -225,6 +227,7 @@ scan proc(
kind ?TokenKind :: match char {
'=': .equal
'!': .bang
'<': .less
'>': .greater
',': .comma
+26
View File
@@ -53,3 +53,29 @@ reserves_boolean_keywords_and_scans_comparisons test {
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))
}
scans_logical_operators_without_splitting_identifiers 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 :: "and or android orbit and_1 or2 ! != !! !true"
try scan(&state, source, &diagnostic_store)
expected [13]TokenKind :: [
TokenKind.and, .or, .ident, .ident, .ident, .ident,
.bang, .not_equal, .bang, .bang, .bang, .true, .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("android", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
try testing.expect_equal("orbit", strpool.get_str(&strpool.strings, state.tokens.items[3].data.str_id)?)
try testing.expect_equal("and_1", strpool.get_str(&strpool.strings, state.tokens.items[4].data.str_id)?)
try testing.expect_equal("or2", strpool.get_str(&strpool.strings, state.tokens.items[5].data.str_id)?)
try testing.expect_equal(u32(31), state.tokens.items[6].span.start)
try testing.expect_equal(u32(32), state.tokens.items[6].span.end)
try testing.expect_equal(u32(33), state.tokens.items[7].span.start)
try testing.expect_equal(u32(35), state.tokens.items[7].span.end)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
+4
View File
@@ -22,6 +22,7 @@ TokenKind :: enum {
for, while
proc, return
true, false
and, or
# literals
ident, string
@@ -40,6 +41,9 @@ TokenKind :: enum {
plus, minus
star, slash
# logical
bang
# assignment & arithmetic
plus_equal, minus_equal
star_equal, slash_equal
+68 -8
View File
@@ -24,6 +24,7 @@ State :: struct {
diagnostics @mut diagnostics.Store
module @mut ir.Module
storage []mut Storage
arguments arraylist.ArrayList(ir.ValueId)
function usize = 0
block ir.BlockId = ir.BlockId(0)
terminated bool = false
@@ -50,7 +51,9 @@ lower proc(
diagnostics = diagnostic_store,
module = &module,
storage = storage,
arguments = arraylist.init(ir.ValueId, allocator),
}
defer arraylist.deinit(&state.arguments)
# allocate every package identity before lowering any initializer or body.
for (0..ast.package_decl_count(tree)) |i| {
@@ -344,9 +347,10 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn
state.block = guard
condition :: try lower_expression(state, loop.condition)
if (state.terminated) return
guard_end :: state.block
body :: try add_block(state)
exit :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(guard)].terminator = ir.Terminator{
state.module.functions.items[state.function].blocks.items[usize(guard_end)].terminator = ir.Terminator{
kind = .branch,
value = condition,
target = body,
@@ -413,6 +417,48 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
}
}
@hide:file
lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
expression :: ast.binary_expr(state.tree, id)
span :: ast.node_span(state.tree, id)
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
origin :: state.block
slot :: u32(state.module.functions.items[state.function].locals.items.len)
try arraylist.append(&state.module.functions.items[state.function].locals, ir.Type.bool)
right_block :: try add_block(state)
skip :: try add_block(state)
join :: try add_block(state)
conjunction :: expression.operator == .logical_and
target :: if (conjunction) right_block else skip
alternative :: if (conjunction) skip else right_block
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .branch,
value = left,
target = target,
alternative = alternative,
span = ast.node_span(state.tree, expression.left),
}
state.block = skip
_ = try emit(state, ir.Instruction{ op = .store_local, type = .bool, left = left, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = right_block
right :: try lower_expression(state, expression.right)
if (!state.terminated) {
_ = try emit(state, ir.Instruction{ op = .store_local, type = .bool, left = right, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
}
# the skip edge reaches the join even when the demanded right edge traps.
state.block = join
state.terminated = false
if (trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{ op = .load_local, type = .bool, slot = slot, span = span })
}
@hide:file
lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
@@ -454,7 +500,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
}
.unary: {
expression :: ast.unary_expr(tree, id)
if (type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) {
if (expression.operator == .negate and type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) {
text :: ast.source_text(tree, ast.node_span(tree, expression.operand))
if (integer_magnitude(text, u64(maxval!(i64)) + 1)) |magnitude| {
if (magnitude == u64(maxval!(i64)) + 1) {
@@ -464,10 +510,17 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
}
operand :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{ op = .negate, type = type, left = operand, span = span })
op ir.Op :: match expression.operator {
.negate: .negate
.logical_not: .logical_not
}
return try emit(state, ir.Instruction{ op = op, type = type, left = operand, span = span })
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (expression.operator == .logical_and or expression.operator == .logical_or) {
return try lower_logical(state, id)
}
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
right :: try lower_expression(state, expression.right)
@@ -483,23 +536,30 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
.less_equal: .less_equal
.greater: .greater
.greater_equal: .greater_equal
.logical_and, .logical_or: unreachable
}
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
}
.call: {
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[usize(state.block)]
start :: block.arguments.items.len
# recursion borrows later scratch slots; no items pointer survives a child.
scratch_start :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch_start]
count :: ast.call_arg_count(tree, id)
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
try arraylist.reserve(&state.arguments, scratch_start + count)
for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..count) |i| {
argument :: try lower_expression(state, ast.call_arg(tree, id, i))
if (state.terminated) return ir.no_value
block.arguments.items[start + i] = argument
state.arguments.items[scratch_start + i] = argument
}
if (trap_type(state, id)) return ir.no_value
# branchful arguments end in a different block from the callee.
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
try arraylist.reserve(&block.arguments, start + count)
for (0..count) |i| try arraylist.append(&block.arguments, state.arguments.items[scratch_start + i])
return try emit(state, ir.Instruction{
op = .call,
type = type,
+272 -5
View File
@@ -422,6 +422,13 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
source ::
`text :: "owned\nstring"
`factor float :: 3.5
`enabled :: true or false and false
`compose proc(left bool, right bool) bool {
` return left and right
`}
`broken proc() bool {
` return compose(true and absent, false or true)
`}
`identity proc(value string) string {
` if false {
` return value
@@ -432,12 +439,12 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
`main proc() string {
` computed float :: factor * 2
` remaining := 2
` while remaining > 0 {
` while compose(remaining > 0 and enabled, compose(!false, compose(true or false, compose(true, compose(true, true))))) {
` pass :: identity(text)
` identity(pass)
` remaining = remaining - 1
` }
` if computed > 0 {
` if computed > 0 and (enabled or false) {
` saved :: identity(text)
` if computed < 10 {
` return saved
@@ -459,7 +466,7 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
defer typecheck.deinit(&checked)
succeeded := false
failures := usize(0)
for (usize(0)..usize(128)) |limit| {
for (usize(0)..usize(512)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
module := lower(allocator, &tree, &resolution, &checked, &diagnostic_store) catch |error| {
@@ -856,8 +863,8 @@ loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_over
bounded_pure_loops_do_not_allocate_per_iteration_and_release_runtime_storage test {
sources [2][]u8 :: [
"main proc() int {\n n := 0\n while n < 1 {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while n < 10000 {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while (n < 1 and !(n == 1)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while (n < 10000 and !(n == 10000)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n",
]
expected [2]i64 :: [1, 10000]
allocations := usize(0)
@@ -877,3 +884,263 @@ bounded_pure_loops_do_not_allocate_per_iteration_and_release_runtime_storage tes
else try testing.expect_equal(allocations, usize(64) - state.remaining)
}
}
boolean_truth_tables_nesting_negation_and_precedence_execute test {
sources [18][]u8 :: [
"main proc() bool { return false and false }\n",
"main proc() bool { return false and true }\n",
"main proc() bool { return true and false }\n",
"main proc() bool { return true and true }\n",
"main proc() bool { return false or false }\n",
"main proc() bool { return false or true }\n",
"main proc() bool { return true or false }\n",
"main proc() bool { return true or true }\n",
"main proc() bool { return !false }\n",
"main proc() bool { return !true }\n",
"main proc() bool { return !!true }\n",
"main proc() bool { return !!!true }\n",
"main proc() bool { return true or false and false }\n",
"main proc() bool { return (true or false) and false }\n",
"main proc() bool { return !(1 + 2 < 3) and 4 == 4 or false }\n",
"main proc() bool { return false or (true and (false or !false)) }\n",
"main proc() bool { return true and false or true and true }\n",
"main proc() bool { return !(-9223372036854775808 < 0) }\n",
]
expected [18]bool :: [
false, false, false, true, false, true, true, true,
true, false, true, false, true, false, true, true, true, false,
]
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(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_equal(expected[index], outcome.value.data.boolean)
}
}
branchful_calls_preserve_earlier_temps_nested_arguments_and_function_callees test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`integer proc(value int) int { return value }
`boolean proc(value bool) bool { return value }
`score proc(first int, left bool, right bool, last int) int {
` if left and right { return first + last }
` return 0
`}
`invoke :: score
`main proc() int {
` local :: invoke
` return local(integer(20), boolean(false or true), boolean(true and (false or true)), local(integer(0), false or boolean(true), true and !false, local(integer(5), true or false, true and true, integer(17))))
`}
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)
}
short_circuit_source_errors_remain_checked_but_skipped_in_every_consumer test {
sources [10][]u8 :: [
"main proc() bool { return false and absent or true }\n",
"main proc() bool { return true or 1 }\n",
"main proc() bool { return !(false and absent) }\n",
"main proc() bool { return (false and absent) == false }\n",
"flag :: true or absent\nmain proc() bool { return flag }\n",
"main proc() bool {\n flag :: true or absent\n return flag\n}\n",
"main proc() bool {\n flag := false\n flag = true or absent\n return flag\n}\n",
"identity proc(value bool) bool { return value }\nmain proc() bool { return identity(true or absent) }\n",
"main proc() bool {\n if true or absent { return true }\n return false\n}\n",
"main proc() bool {\n true or absent\n while false and absent {}\n return true\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)
count :: diagnostics.count(&diagnostic_store)
try testing.expect(count > 0)
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)
try testing.expect_equal(count, diagnostics.count(&diagnostic_store))
}
}
demanded_boolean_source_errors_keep_the_original_diagnostic_and_operand_span test {
sources [8][]u8 :: [
"main proc() bool { return true and absent }\n",
"main proc() bool { return false or absent }\n",
"main proc() bool { return true and 1 }\n",
"main proc() bool { return false or 1 }\n",
"main proc() bool { return !1 }\n",
"main proc() bool { return !absent }\n",
"main proc() bool { return absent and false }\n",
"main proc() bool { return absent or true }\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))?
code diagnostics.Code :: if (index >= 2 and index <= 4) .invalid_operand else .unknown_name
operand []u8 :: if (index >= 2 and index <= 4) "1" else "absent"
try testing.expect_equal(code, original.code)
try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], operand))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
branchful_call_arguments_evaluate_left_to_right_before_later_errors test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`both proc(left bool, right bool) bool { return left and right }
`main proc() bool {
` return both(false or (true and first_absent), true and second_absent)
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
original :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect_equal(diagnostics.Code.unknown_name, original.code)
try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], "first_absent"))
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
}
short_circuit_protects_uninitialized_reads_global_cycles_and_overflow test {
sources [6][]u8 :: [
"main proc() bool {\n before :: false and later\n later :: true\n return !before\n}\n",
"main proc() bool {\n before :: true or later\n later :: false\n return before\n}\n",
"flag bool :: true or flag\nmain proc() bool { return flag }\n",
"left bool :: false and right\nright bool :: left\nmain proc() bool { return !left }\n",
"main proc() bool { return !(false and 9223372036854775807 + 1 == 0) }\n",
"main proc() bool { return true or -(-9223372036854775808) == 0 }\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(outcome.value.data.boolean)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
}
demanded_boolean_runtime_errors_keep_the_instruction_span test {
sources [4][]u8 :: [
"main proc() bool {\n before :: true and later\n later :: true\n return before\n}\n",
"flag bool :: false or flag\nmain proc() bool { return flag }\n",
"main proc() bool { return true and 9223372036854775807 + 1 == 0 }\n",
"main proc() bool {\n minimum :: -9223372036854775808\n return false or -minimum == 0\n}\n",
]
codes [4]diagnostics.Code :: [.read_before_initialization, .initialization_cycle, .integer_overflow, .integer_overflow]
spans [4][]u8 :: ["later", "flag", "9223372036854775807 + 1", "-minimum"]
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(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try expect_trap(outcome, &diagnostic_store, codes[index])
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], spans[index]))
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
composed_loop_guards_recompute_and_overwrite_synthetic_results test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`enabled :: false or true and !false
`positive proc(n int) bool { return n > 0 }
`main proc() int {
` n := 4
` total := 0
` while (enabled and positive(n)) or n > 4 {
` if (n == 4 or n == 2) and !(n == 3) {
` total = total + 21
` }
` n = n - 1
` }
` return total + n
`}
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)
}
lazy_branchful_global_initializers_and_calls_release_every_runtime_allocation test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`first :: true and later
`later :: false or !false
`both proc(left bool, right bool) bool { return left and right }
`main proc() bool {
` return both(first and first, both(false or later, !false and later))
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
failures := usize(0)
succeeded := false
for (usize(0)..usize(64)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome :: interpreter.run(allocator, &module, &diagnostic_store) catch |error| {
try testing.expect(error == .out_of_memory)
failures += 1
try testing.expect_equal(usize(0), state.live)
continue
}
try testing.expect(outcome.trap == null)
try testing.expect(outcome.value.data.boolean)
try testing.expect_equal(usize(0), state.live)
succeeded = true
break
}
try testing.expect(succeeded)
try testing.expect(failures > 0)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
skipped_rhs_does_not_hide_a_demanded_outer_type_mismatch test {
sources [3][]u8 :: [
"main proc() int { return true or absent }\n",
"main proc() void {\n number := 0\n number = true or absent\n}\n",
"integer proc(value int) int { return value }\nmain proc() int { return integer(true or absent) }\n",
]
codes [3]diagnostics.Code :: [.return_type_mismatch, .type_mismatch, .type_mismatch]
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)
count :: diagnostics.count(&diagnostic_store)
try testing.expect_equal(2, count)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try expect_trap(outcome, &diagnostic_store, codes[index])
try testing.expect_equal(count, diagnostics.count(&diagnostic_store))
}
}
+19 -4
View File
@@ -185,7 +185,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.if: return try parse_if(state)
.while: return try parse_while(state, allow_else)
.return: return try parse_return(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .open_paren: {
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren: {
expression :: try parse_expr(state)
return try ast.add_expression_stmt(&state.builder, expression)
}
@@ -418,10 +418,11 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
state.next_token += 1
operand :: try parse_expr_bp(state, right_bp)
operand_span :: ast.builder_span(&state.builder, operand)
operator ast.UnaryOperator :: if (token.kind == .bang) .logical_not else .negate
yield try ast.add_unary(
&state.builder,
ast.Span{ start = token.span.start, end = operand_span.end },
.negate,
operator,
operand,
)
} else {
@@ -525,7 +526,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
else
try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
if token.kind != .comma and !is_expression_boundary(token.kind) {
if !is_expression_recovery_boundary(token.kind) {
state.next_token += 1
}
@@ -542,6 +543,16 @@ is_expression_boundary proc(kind lexer.TokenKind) bool {
}
}
@hide:file
is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
return (
kind == .comma
or kind == .and
or kind == .or
or is_expression_boundary(kind)
)
}
@hide:file
recover_decl proc(
state @mut State,
@@ -625,7 +636,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(50)
.minus, .bang: BindingPowerScalar(50)
else: null
}
}
@@ -633,6 +644,8 @@ prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
@hide:file
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
return match kind {
.or: BindingPower{ left = BindingPowerScalar(2), right = BindingPowerScalar(3) }
.and: BindingPower{ left = BindingPowerScalar(4), right = BindingPowerScalar(5) }
.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) }
@@ -644,6 +657,8 @@ infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
@hide:file
binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
return match kind {
.and: .logical_and
.or: .logical_or
.plus: .add
.minus: .subtract
.star: .multiply
+160
View File
@@ -739,3 +739,163 @@ recovers_nested_braces_in_malformed_loop_body_and_assignment test {
tail :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal("tail", ast.source_text(&tree, tail.name))
}
parses_logical_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 :: !probe() == a + b * c < d and e and f or g or h\n"
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(0, diagnostics.count(&diagnostic_store))
value :: ast.statement_decl(&tree, ast.package_decl(&tree, 0))
outer_or :: ast.binary_expr(&tree, value.initializer)
try testing.expect_equal(ast.BinaryOperator.logical_or, outer_or.operator)
try testing.expect_equal("h", ast.source_text(&tree, ast.node_span(&tree, outer_or.right)))
inner_or :: ast.binary_expr(&tree, outer_or.left)
try testing.expect_equal(ast.BinaryOperator.logical_or, inner_or.operator)
outer_and :: ast.binary_expr(&tree, inner_or.left)
try testing.expect_equal(ast.BinaryOperator.logical_and, outer_and.operator)
try testing.expect_equal("f", ast.source_text(&tree, ast.node_span(&tree, outer_and.right)))
inner_and :: ast.binary_expr(&tree, outer_and.left)
try testing.expect_equal(ast.BinaryOperator.logical_and, inner_and.operator)
equality :: ast.binary_expr(&tree, inner_and.left)
try testing.expect_equal(ast.BinaryOperator.equal, equality.operator)
logical_not :: ast.unary_expr(&tree, equality.left)
try testing.expect_equal(ast.UnaryOperator.logical_not, logical_not.operator)
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, logical_not.operand))
try testing.expect_equal("!probe()", ast.source_text(&tree, logical_not.span))
comparison :: ast.binary_expr(&tree, equality.right)
try testing.expect_equal(ast.BinaryOperator.less, comparison.operator)
addition :: ast.binary_expr(&tree, comparison.left)
try testing.expect_equal(ast.BinaryOperator.add, addition.operator)
multiply :: ast.binary_expr(&tree, addition.right)
try testing.expect_equal(ast.BinaryOperator.multiply, multiply.operator)
}
parses_right_associative_mixed_prefix_operators 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 :: !-!probe()\nother :: -!false\n"
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(0, diagnostics.count(&diagnostic_store))
value :: ast.statement_decl(&tree, ast.package_decl(&tree, 0))
first :: ast.unary_expr(&tree, value.initializer)
second :: ast.unary_expr(&tree, first.operand)
third :: ast.unary_expr(&tree, second.operand)
try testing.expect_equal(ast.UnaryOperator.logical_not, first.operator)
try testing.expect_equal(ast.UnaryOperator.negate, second.operator)
try testing.expect_equal(ast.UnaryOperator.logical_not, third.operator)
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, third.operand))
try testing.expect_equal("!-!probe()", ast.source_text(&tree, first.span))
try testing.expect_equal("-!probe()", ast.source_text(&tree, second.span))
other :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
negate :: ast.unary_expr(&tree, other.initializer)
try testing.expect_equal(ast.UnaryOperator.negate, negate.operator)
try testing.expect_equal(ast.UnaryOperator.logical_not, ast.unary_expr(&tree, negate.operand).operator)
}
parses_logical_expressions_in_all_expression_positions 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 ::
`global :: false and absent
`main proc() bool {
` value := true or absent
` value = !value and ready()
` !value
` if !value or ready() { value = false }
` while value and !done() { value = false }
` return choose(value or false, nested(!value and true))
`}
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(0, diagnostics.count(&diagnostic_store))
global :: ast.statement_decl(&tree, ast.package_decl(&tree, 0))
global_and :: ast.binary_expr(&tree, global.initializer)
try testing.expect_equal(ast.BinaryOperator.logical_and, global_and.operator)
try testing.expect_equal("absent", ast.source_text(&tree, ast.node_span(&tree, global_and.right)))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal(6, ast.block_statement_count(&tree, function.body))
local :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0))
try testing.expect_equal(ast.BinaryOperator.logical_or, ast.binary_expr(&tree, local.initializer).operator)
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, function.body, 1))
try testing.expect_equal(ast.BinaryOperator.logical_and, ast.binary_expr(&tree, assignment.value).operator)
expression :: ast.statement_expr(&tree, ast.block_statement(&tree, function.body, 2))
try testing.expect_equal(ast.UnaryOperator.logical_not, ast.unary_expr(&tree, expression).operator)
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, function.body, 3))
try testing.expect_equal(ast.BinaryOperator.logical_or, ast.binary_expr(&tree, conditional.condition).operator)
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, function.body, 4))
try testing.expect_equal(ast.BinaryOperator.logical_and, ast.binary_expr(&tree, loop.condition).operator)
returned :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 5)).value?
try testing.expect_equal(2, ast.call_arg_count(&tree, returned))
first_argument :: ast.call_arg(&tree, returned, 0)
try testing.expect_equal(ast.BinaryOperator.logical_or, ast.binary_expr(&tree, first_argument).operator)
nested :: ast.call_arg(&tree, returned, 1)
nested_argument :: ast.call_arg(&tree, nested, 0)
try testing.expect_equal(ast.BinaryOperator.logical_and, ast.binary_expr(&tree, nested_argument).operator)
}
recovers_missing_logical_operands_and_preserves_enclosing_nodes 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() bool {
` first :: true and
` second :: true and or false
` third :: choose(!, false or)
` if ! { return false }
` return true or }
`after :: true
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(6, diagnostics.count(&diagnostic_store))
try testing.expect_equal(2, ast.package_decl_count(&tree))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
try testing.expect_equal(5, ast.block_statement_count(&tree, function.body))
first :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0))
first_and :: ast.binary_expr(&tree, first.initializer)
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first_and.right))
second :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 1))
second_or :: ast.binary_expr(&tree, second.initializer)
try testing.expect_equal(ast.BinaryOperator.logical_or, second_or.operator)
second_and :: ast.binary_expr(&tree, second_or.left)
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, second_and.right))
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, second_or.right))
third :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 2))
try testing.expect_equal(2, ast.call_arg_count(&tree, third.initializer))
missing_prefix :: ast.unary_expr(&tree, ast.call_arg(&tree, third.initializer, 0))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing_prefix.operand))
missing_rhs :: ast.binary_expr(&tree, ast.call_arg(&tree, third.initializer, 1))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing_rhs.right))
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, function.body, 3))
try testing.expect_equal(1, ast.block_statement_count(&tree, conditional.then_block))
returned :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 4)).value?
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, ast.binary_expr(&tree, returned).right))
after :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal("after", ast.source_text(&tree, after.name))
}
+43 -7
View File
@@ -199,10 +199,13 @@ check proc(
id :: ast.NodeId(i)
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
match ast.expr_kind(tree, id) {
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) try require_numeric(&state, id, expression.operand)
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (bool_equality(&state, id)) continue
if (logical(expression.operator) or bool_equality(&state, id)) continue
try require_numeric(&state, id, expression.left)
try require_numeric(&state, id, expression.right)
}
@@ -216,9 +219,13 @@ check proc(
id :: ast.NodeId(i)
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
match ast.expr_kind(tree, id) {
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) try numeric_operand(&state, id, expression.operand)
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (logical(expression.operator)) continue
if (comparison(expression.operator)) {
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
} else {
@@ -599,14 +606,19 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
}
}
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .logical_not) {
state.cells[usize(id)].type = Type{ kind = .bool }
} else {
state.cells[usize(id)].numeric = true
operand :: ast.unary_expr(tree, id).operand
}
operand :: expression.operand
try collect_expression(state, operand)
try depend(state, operand, id)
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (comparison(expression.operator)) {
if (comparison(expression.operator) or logical(expression.operator)) {
state.cells[usize(id)].type = Type{ kind = .bool }
} else {
state.cells[usize(id)].numeric = true
@@ -614,7 +626,8 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
try collect_expression(state, expression.left)
try collect_expression(state, expression.right)
try depend(state, expression.left, id)
try depend(state, expression.right, id)
# the RHS may be skipped, so its errors do not erase the bool result.
if (!logical(expression.operator)) try depend(state, expression.right, id)
}
.call: {
callee :: ast.call_expr(tree, id).callee
@@ -643,6 +656,11 @@ comparison proc(operator ast.BinaryOperator) bool {
)
}
@hide:file
logical proc(operator ast.BinaryOperator) bool {
return operator == .logical_and or operator == .logical_or
}
@hide:file
bool_equality proc(state @mut State, id ast.NodeId) bool {
expression :: ast.binary_expr(state.tree, id)
@@ -722,6 +740,14 @@ fail proc(state @mut State, id ast.NodeId, code diagnostics.Code, span ast.Span)
state.types[usize(id)] = invalid_type(try diagnostics.add(state.diagnostics, code, span))
}
@hide:file
require_bool proc(state @mut State, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
type :: state.types[usize(operand)]
if (type.kind != .invalid and type.kind != .bool) {
try fail(state, operand, .invalid_operand, ast.node_span(state.tree, operand))
}
}
@hide:file
mutable_target proc(state @State, id ast.NodeId) bool {
binding :: sema.binding(state.resolution, id)
@@ -793,13 +819,23 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
.expression: {
match ast.expr_kind(tree, id) {
.unary: {
operand :: state.types[usize(ast.unary_expr(tree, id).operand)]
expression :: ast.unary_expr(tree, id)
if (expression.operator == .logical_not) {
try require_bool(state, expression.operand)
return
}
operand :: state.types[usize(expression.operand)]
if (operand.kind != .invalid and !numeric_kind(operand.kind)) {
try fail(state, id, .invalid_operand, ast.node_span(tree, id))
}
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (logical(expression.operator)) {
try require_bool(state, expression.left)
try require_bool(state, expression.right)
return
}
left :: state.types[usize(expression.left)]
right :: state.types[usize(expression.right)]
if (left.kind == .invalid or right.kind == .invalid) return
+299
View File
@@ -1249,3 +1249,302 @@ while_checks_nested_bodies_without_claiming_a_zero_iteration_return test {
try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool))
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand))
}
logical_types_preserve_comparison_context_and_assignment_isolation 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 ::
`seed :: 3
`forward :: seed
`composed :: !(forward < 4.0) and true or false
`equal :: !(true and false) == (true or false)
`nested :: !!!true
`identity proc(value bool) bool { return value }
`main proc() bool {
` count := 1
` flag := true
` flag = !(count < 2) and identity(true) or false
` return 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 (usize(0)..usize(2)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .float)
for usize(2)..usize(5) |index| {
declaration :: ast.package_decl(&tree, index)
try expect_kind(&result, declaration, .bool)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .bool)
}
composed :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer)
try testing.expect_equal(ast.BinaryOperator.logical_or, composed.operator)
conjunction :: ast.binary_expr(&tree, composed.left)
try testing.expect_equal(ast.BinaryOperator.logical_and, conjunction.operator)
inverted :: ast.unary_expr(&tree, conjunction.left)
try expect_kind(&result, conjunction.left, .bool)
comparison_view :: ast.binary_expr(&tree, inverted.operand)
try expect_kind(&result, comparison_view.left, .float)
try expect_kind(&result, comparison_view.right, .float)
nested := ast.statement_decl(&tree, ast.package_decl(&tree, 4)).initializer
for 0..3 |_| {
try expect_kind(&result, nested, .bool)
view :: ast.unary_expr(&tree, nested)
try testing.expect_equal(ast.UnaryOperator.logical_not, view.operator)
nested = view.operand
}
try expect_kind(&result, nested, .bool)
body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
count :: ast.block_statement(&tree, body, 0)
try expect_kind(&result, count, .int)
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 2))
try expect_kind(&result, assignment.value, .bool)
disjunction :: ast.binary_expr(&tree, assignment.value)
assignment_conjunction :: ast.binary_expr(&tree, disjunction.left)
assignment_not :: ast.unary_expr(&tree, assignment_conjunction.left)
assignment_comparison :: ast.binary_expr(&tree, assignment_not.operand)
try expect_kind(&result, assignment_comparison.left, .int)
try expect_kind(&result, assignment_comparison.right, .int)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
logical_operand_errors_are_independent_and_skipped_rhs_preserves_bool 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 ::
`number :: 1
`number_alias :: number
`skipped_unknown :: false and absent
`skipped_kind :: true or number
`both_bad :: 1 and "wrong"
`demanded_unknown :: missing or true
`demanded_not :: !number_alias
`nested :: !(false and absent_nested) == true
`independent :: left_missing and 1
`identity proc(value bool) bool { return value }
`skips proc() bool {
` local :: true or 1
` value := false
` value = false and absent_assignment
` identity(true or absent_argument)
` if true or 1 { value = true }
` while false and 1 { value = false }
` return false and absent_return
`}
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 usize(0)..usize(2) |index| {
declaration :: ast.package_decl(&tree, index)
try expect_kind(&result, declaration, .int)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .int)
}
skipped_indices [3]usize :: [2, 3, 7]
for skipped_indices |index| {
declaration :: ast.package_decl(&tree, index)
try expect_kind(&result, declaration, .bool)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .bool)
}
unknown :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer)
try testing.expect_equal(sema.binding(&resolution, unknown.right).data.diagnostic, type_of(&result, unknown.right).diagnostic)
_ = try expect_error(&result, &diagnostic_store, unknown.right, .unknown_name, ast.node_span(&tree, unknown.right))
wrong :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 3)).initializer)
_ = try expect_error(&result, &diagnostic_store, wrong.right, .invalid_operand, ast.node_span(&tree, wrong.right))
both_declaration :: ast.package_decl(&tree, 4)
both_id :: ast.statement_decl(&tree, both_declaration).initializer
both :: ast.binary_expr(&tree, both_id)
left_error :: try expect_error(&result, &diagnostic_store, both.left, .invalid_operand, ast.node_span(&tree, both.left))
right_error :: try expect_error(&result, &diagnostic_store, both.right, .invalid_operand, ast.node_span(&tree, both.right))
try testing.expect(left_error != right_error)
try testing.expect_equal(left_error, type_of(&result, both_id).diagnostic)
try testing.expect_equal(left_error, type_of(&result, both_declaration).diagnostic)
demanded_id :: ast.statement_decl(&tree, ast.package_decl(&tree, 5)).initializer
demanded :: ast.binary_expr(&tree, demanded_id)
try testing.expect_equal(sema.binding(&resolution, demanded.left).data.diagnostic, type_of(&result, demanded_id).diagnostic)
not_id :: ast.statement_decl(&tree, ast.package_decl(&tree, 6)).initializer
not_view :: ast.unary_expr(&tree, not_id)
not_error :: try expect_error(&result, &diagnostic_store, not_view.operand, .invalid_operand, ast.node_span(&tree, not_view.operand))
try testing.expect_equal(not_error, type_of(&result, not_id).diagnostic)
independent_id :: ast.statement_decl(&tree, ast.package_decl(&tree, 8)).initializer
independent :: ast.binary_expr(&tree, independent_id)
try testing.expect_equal(sema.binding(&resolution, independent.left).data.diagnostic, type_of(&result, independent_id).diagnostic)
_ = try expect_error(&result, &diagnostic_store, independent.right, .invalid_operand, ast.node_span(&tree, independent.right))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 10)).body
try expect_kind(&result, body, .none)
try expect_kind(&result, ast.block_statement(&tree, body, 0), .bool)
assignment_id :: ast.block_statement(&tree, body, 2)
assignment :: ast.assignment_stmt(&tree, assignment_id)
try expect_kind(&result, assignment_id, .void)
try expect_kind(&result, assignment.value, .bool)
call :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 3))
try expect_kind(&result, call, .bool)
try expect_kind(&result, ast.call_arg(&tree, call, 0), .bool)
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 4))
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 5))
try expect_kind(&result, conditional.condition, .bool)
try expect_kind(&result, loop.condition, .bool)
returned :: ast.block_statement(&tree, body, 6)
try expect_kind(&result, returned, .void)
if (ast.return_stmt(&tree, returned).value) |value| {
try expect_kind(&result, value, .bool)
} else {
return .expectation_failed
}
try testing.expect_equal(7, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(8, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(15, diagnostics.count(&diagnostic_store))
}
logical_external_mismatches_and_always_demanded_not_preserve_errors 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 ::
`accept_bool proc(value bool) bool { return value }
`accept_int proc(value int) bool { return true }
`noop proc() void {}
`wrong_decl int :: true or 1
`wrong_return proc() int { return false and absent_return }
`wrong_argument proc() bool { return accept_int(false and absent_argument) }
`wrong_assignment proc() void {
` count := 1
` count = true or 1
`}
`unary_bad proc() void {
` !1.0
` !"text"
` !accept_bool
` !noop()
`}
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)
declaration :: ast.package_decl(&tree, 3)
initializer :: ast.statement_decl(&tree, declaration).initializer
declaration_error :: try expect_error(&result, &diagnostic_store, initializer, .type_mismatch, ast.node_span(&tree, initializer))
try testing.expect_equal(declaration_error, type_of(&result, declaration).diagnostic)
return_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
return_id :: ast.block_statement(&tree, return_body, 0)
if (ast.return_stmt(&tree, return_id).value) |value| {
return_error :: try expect_error(&result, &diagnostic_store, value, .return_type_mismatch, ast.node_span(&tree, value))
try testing.expect_equal(return_error, type_of(&result, return_id).diagnostic)
operand :: ast.binary_expr(&tree, value).right
try testing.expect_equal(sema.binding(&resolution, operand).data.diagnostic, type_of(&result, operand).diagnostic)
try testing.expect(return_error != type_of(&result, operand).diagnostic)
} else {
return .expectation_failed
}
argument_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body
argument_return :: ast.block_statement(&tree, argument_body, 0)
if (ast.return_stmt(&tree, argument_return).value) |call| {
argument :: ast.call_arg(&tree, call, 0)
argument_error :: try expect_error(&result, &diagnostic_store, argument, .type_mismatch, ast.node_span(&tree, argument))
try testing.expect_equal(argument_error, type_of(&result, call).diagnostic)
try testing.expect_equal(argument_error, type_of(&result, argument_return).diagnostic)
} else {
return .expectation_failed
}
assignment_body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
assignment_id :: ast.block_statement(&tree, assignment_body, 1)
assignment :: ast.assignment_stmt(&tree, assignment_id)
assignment_error :: try expect_error(&result, &diagnostic_store, assignment.value, .type_mismatch, ast.node_span(&tree, assignment.value))
try testing.expect_equal(assignment_error, type_of(&result, assignment_id).diagnostic)
try expect_kind(&result, assignment.target, .int)
try expect_kind(&result, ast.block_statement(&tree, assignment_body, 0), .int)
unary_body :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body
for usize(0)..usize(4) |index| {
expression :: ast.statement_expr(&tree, ast.block_statement(&tree, unary_body, index))
operand :: ast.unary_expr(&tree, expression).operand
original :: try expect_error(&result, &diagnostic_store, operand, .invalid_operand, ast.node_span(&tree, operand))
try testing.expect_equal(original, type_of(&result, expression).diagnostic)
}
for (usize(0)..usize(3)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .function)
try testing.expect_equal(6, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(2, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(3, code_count(&diagnostic_store, .type_mismatch))
try testing.expect_equal(1, code_count(&diagnostic_store, .return_type_mismatch))
try testing.expect_equal(12, diagnostics.count(&diagnostic_store))
}
short_circuit_skip_paths_do_not_guarantee_a_return 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 ::
`expression proc() int { false and missing_expression }
`declaration proc() int { local :: true or 1 }
`assignment proc() int {
` value := false
` value = false and missing_assignment
`}
`call proc() int { take(true or 1) }
`conditional proc() int { if false and missing_condition { return 1 } }
`loop proc() int { while true or missing_guard { return 1 } }
`loop_body proc() int { while true { true or 1 } }
`arms proc(flag bool) int {
` if flag { false and missing_arm } else { return 1 }
`}
`demanded proc() int { missing_left or true }
`successful proc() bool { return true or missing_return }
`take proc(value bool) void {}
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 usize(0)..usize(8) |index| {
function :: ast.package_decl(&tree, index)
try expect_kind(&result, function, .function)
body :: ast.function_decl(&tree, function).body
_ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body))
}
expression_body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
expression :: ast.statement_expr(&tree, ast.block_statement(&tree, expression_body, 0))
try expect_kind(&result, expression, .bool)
loop_body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
loop_id :: ast.block_statement(&tree, loop_body, 0)
try expect_kind(&result, loop_id, .void)
demanded_body :: ast.function_decl(&tree, ast.package_decl(&tree, 8)).body
demanded :: ast.statement_expr(&tree, ast.block_statement(&tree, demanded_body, 0))
left :: ast.binary_expr(&tree, demanded).left
original :: sema.binding(&resolution, left).data.diagnostic
try testing.expect_equal(original, type_of(&result, demanded).diagnostic)
try testing.expect_equal(original, type_of(&result, demanded_body).diagnostic)
successful_body :: ast.function_decl(&tree, ast.package_decl(&tree, 9)).body
try expect_kind(&result, successful_body, .none)
try expect_kind(&result, ast.block_statement(&tree, successful_body, 0), .void)
try testing.expect_equal(8, code_count(&diagnostic_store, .missing_return))
try testing.expect_equal(7, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(18, diagnostics.count(&diagnostic_store))
}