feat: execute short-circuit boolean expressions
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ Op :: enum {
|
||||
reset_local
|
||||
load_global
|
||||
negate
|
||||
logical_not
|
||||
add
|
||||
subtract
|
||||
multiply
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
state.cells[usize(id)].numeric = true
|
||||
operand :: ast.unary_expr(tree, id).operand
|
||||
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 :: 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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user