diff --git a/README.md b/README.md index 89d1eba..b52156f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/programs/boolean_conditions.hon b/programs/boolean_conditions.hon new file mode 100644 index 0000000..2fa1fff --- /dev/null +++ b/programs/boolean_conditions.hon @@ -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) + ) +} diff --git a/source/ast/ast.hon b/source/ast/ast.hon index dea482c..6be84a3 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -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 diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 9804cd6..99c516e 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -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)] diff --git a/source/interpreter/interpreter.test.hon b/source/interpreter/interpreter.test.hon index a5c07fa..d2af5a3 100644 --- a/source/interpreter/interpreter.test.hon +++ b/source/interpreter/interpreter.test.hon @@ -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)) + } +} diff --git a/source/ir/ir.hon b/source/ir/ir.hon index 7e12d31..0382d04 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -40,6 +40,7 @@ Op :: enum { reset_local load_global negate + logical_not add subtract multiply diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index 5ae3d21..be0a046 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -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 diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index b86b545..87abd38 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -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)) +} diff --git a/source/lexer/token.hon b/source/lexer/token.hon index b33d642..27dd4ae 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -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 diff --git a/source/lower/lower.hon b/source/lower/lower.hon index a0a99e4..8dca4c9 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -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, diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index ab7a29e..0e2a322 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -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)) + } +} diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 2c41ac3..2affedb 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -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 diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 9a96cfb..df5230e 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -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)) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index f318036..c6a3552 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -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 diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index 1b11490..6df1e7f 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -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)) +}