diff --git a/LANGUAGE.md b/LANGUAGE.md index cf7c9af..0a87de4 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -13,8 +13,8 @@ ### types and expressions - `i8`, `i16`, `i32`, `i64`, `void`, and inferred integer-constrained `int` -- contextual integer literals and constant folding of addition trees -- checked signed integer addition +- contextual integer literals and constant folding of addition and negation trees +- checked signed integer addition and unary negation - function calls, assignments, and returns ### functions and packages diff --git a/README.md b/README.md index 60a64be..f2d4698 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,13 @@ Current prototype features: - `#` comments - Immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks - `i8`, `i16`, `i32`, `i64`, and loose integer-constrained `int` -- Contextual integer constants and compile-time folding of literal addition trees +- Contextual integer constants and compile-time folding of addition and unary negation trees - Directory packages with merged declarations and file-local relative imports - Qualified imported globals and functions with package-aware symbol mangling - Demand-monomorphized Brolang and C-ABI functions - Bodyless concrete C function declarations with exact external symbol names - Ordered linking of additional C sources, objects, and libraries -- Checked signed addition +- Checked signed addition and unary negation - Static, eager runtime, and deferred problematic globals - Runtime diagnostics followed by `llvm.trap` diff --git a/TODO.md b/TODO.md index 70969d3..458e9b5 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,6 @@ # compiler hardening follow-ups -- support unary minus, including the signed i64 minimum literal boundary - move ignored example binaries into a dedicated build directory and remove `.review_tmp` # milestones diff --git a/benchmarks/symbols/README.md b/benchmarks/symbols/README.md index c95561a..9304b5c 100644 --- a/benchmarks/symbols/README.md +++ b/benchmarks/symbols/README.md @@ -40,3 +40,8 @@ After pruning stale function specializations on 2026-06-12, peak memory remained 8,651,659 bytes and allocations fell to 30,121. Lookup-only specialization resolution avoids constructing temporary signatures for already discovered calls. + +After adding checked unary negation on 2026-06-12, peak memory remained +8,651,659 bytes and allocations remained 30,121. `Span`, `Token`, AST +expressions, HIR expressions, and IR instructions remain 12, 24, 64, 88, and +88 bytes respectively. diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 3b4fb95..3bd9dad 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -74,13 +74,14 @@ Expr_Kind :: enum u8 { Invalid, Integer, Name, + Negate, Add, Call, } Expr :: struct { span: source.Span, - integer: i64, + integer: u64, args: []Expr_Id, qualifier: symbol.Id, name: symbol.Id, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index aaaa2a4..8ad95ef 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -125,7 +125,7 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { continue } expr := checker.ast_module.exprs[frame.expr] - if expr.kind != .Add { + if expr.kind != .Add && expr.kind != .Negate { result := Constant{kind = .Not_Constant} if expr.kind == .Integer { result = Constant{kind = .Value, value = i128(expr.integer)} @@ -142,6 +142,22 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { } continue } + if frame.stage == 1 && expr.kind == .Negate { + operand := Constant{kind = .Not_Constant} + if expr.left != ast.INVALID_EXPR && int(expr.left) < len(checker.constants) { + operand = checker.constants[expr.left] + } + result := Constant{kind = .Not_Constant} + if operand.kind == .Overflow { + result = Constant{kind = .Overflow} + } else if operand.kind == .Value { + value, overflow := intrinsics.overflow_sub(i128(0), operand.value) + result = Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} + } + checker.constants[frame.expr] = result + _ = pop(&stack) + continue + } if frame.stage == 1 { stack[frame_index].stage = 2 if expr.right != ast.INVALID_EXPR && int(expr.right) < len(checker.ast_module.exprs) && @@ -407,6 +423,8 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as switch expr.kind { case .Call: append(&stack, ..expr.args) + case .Negate: + append(&stack, expr.left) case .Add: append(&stack, expr.left, expr.right) case .Invalid, .Integer, .Name: @@ -644,7 +662,10 @@ infer_expr :: proc( last = types.INVALID _ = pop(&stack) case .Integer: - last = types.smallest_signed_for_literal(expr.integer) + last = types.I64 + if expr.integer <= 0x7fff_ffff_ffff_ffff { + last = types.smallest_signed_for_literal(i64(expr.integer)) + } _ = pop(&stack) case .Name: last = types.INVALID @@ -661,6 +682,9 @@ infer_expr :: proc( } } _ = pop(&stack) + case .Negate: + stack[frame_index].stage = 5 + append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION}) case .Add: stack[frame_index].stage = 1 append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION}) @@ -690,6 +714,13 @@ infer_expr :: proc( } continue } + if frame.stage == 5 { + if !types.is_signed(last) { + last = types.INVALID + } + _ = pop(&stack) + continue + } if frame.stage == 1 { stack[frame_index].left = last stack[frame_index].stage = 2 @@ -1097,6 +1128,9 @@ build_expr :: proc( } } _ = pop(&stack) + case .Negate: + stack[frame_index].stage = 5 + append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) case .Add: stack[frame_index].stage = 1 append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) @@ -1147,6 +1181,24 @@ build_expr :: proc( } continue } + if frame.stage == 5 { + operand := last + operand_type := checker.module.exprs[operand].type + if !types.is_signed(operand_type) { + id := source.add(checker.diagnostics, expr.span, "negation requires a signed integer") + last = invalid_hir_expr(checker, expr.span, id) + } else { + last = add_hir_expr(checker, hir.Expr{ + kind=.Negate, span=expr.span, type=operand_type, left=operand, + target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, + }) + if types.is_signed(frame.expected) { + last = coerce_expr(checker, last, frame.expected, expr.span) + } + } + _ = pop(&stack) + continue + } if frame.stage == 1 { stack[frame_index].left = last stack[frame_index].stage = 2 diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index bb6df3a..d4c1730 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -77,6 +77,7 @@ Expr_Kind :: enum u8 { Local, Global, Widen, + Negate, Add, Call, } diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index 0f39ad4..de736e9 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -72,6 +72,7 @@ Opcode :: enum u8 { Load, Store, Widen, + Neg_Checked, Add_Checked, Call, Trap, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 6f2b564..f88088a 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -83,6 +83,9 @@ lex :: proc( case '+': append_token(&stream, source_file, .Plus, cursor, cursor+1) cursor += 1 + case '-': + append_token(&stream, source_file, .Minus, cursor, cursor+1) + cursor += 1 case '.': append_token(&stream, source_file, .Dot, cursor, cursor+1) cursor += 1 diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 6400600..a929d30 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -60,7 +60,7 @@ valid_value :: proc(instructions: []ir.Instruction, value_id: ir.Instruction_Id, return false } switch instructions[value_id].op { - case .Param, .Const, .Load_Global, .Load, .Widen, .Add_Checked, .Call: + case .Param, .Const, .Load_Global, .Load, .Widen, .Neg_Checked, .Add_Checked, .Call: return true case .Alloca, .Store, .Trap, .Return, .Return_Void: return false @@ -234,6 +234,34 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " %%v%d = sext %s ", instruction_index, llvm_type(from_type)) write_operand(&emitter.builder, instructions, instruction.a, from_type) fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type)) + case .Neg_Checked: + if !valid_value(instructions, instruction.a, instruction.type) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid negation operand") + continue + } + type_name := llvm_type(instruction.type) + fmt.sbprintf(&emitter.builder, " %%pair%d = call ", instruction_index) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } @llvm.ssub.with.overflow.%s(%s 0, %s ", type_name, type_name, type_name, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type) + fmt.sbprintf(&emitter.builder, ")\n") + fmt.sbprintf(&emitter.builder, " %%v%d = extractvalue ", instruction_index) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 0\n", type_name, instruction_index) + fmt.sbprintf(&emitter.builder, " %%overflow%d = extractvalue ", instruction_index) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 1\n", type_name, instruction_index) + fmt.sbprintf( + &emitter.builder, + " br i1 %%overflow%d, label %%overflow_trap%d, label %%overflow_continue%d\n", + instruction_index, + instruction_index, + instruction_index, + ) + fmt.sbprintf(&emitter.builder, "overflow_trap%d:\n", instruction_index) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "signed integer negation overflow") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_index) case .Add_Checked: if !valid_value(instructions, instruction.a, instruction.type) || !valid_value(instructions, instruction.b, instruction.type) { @@ -473,6 +501,10 @@ emit_declarations :: proc(emitter: ^Emitter) { fmt.sbprintf(&emitter.builder, "%d", bits) strings.write_string(&emitter.builder, ", i1 } @llvm.sadd.with.overflow.i") fmt.sbprintf(&emitter.builder, "%d(i%d, i%d)\n", bits, bits, bits) + strings.write_string(&emitter.builder, "declare { i") + fmt.sbprintf(&emitter.builder, "%d", bits) + strings.write_string(&emitter.builder, ", i1 } @llvm.ssub.with.overflow.i") + fmt.sbprintf(&emitter.builder, "%d(i%d, i%d)\n", bits, bits, bits) } strings.write_string( &emitter.builder, diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 2022e86..d1bc0b8 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -138,6 +138,9 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Widen: stack[frame_index].stage = 1 append(&stack, Lower_Expr_Frame{expr=expr.left}) + case .Negate: + stack[frame_index].stage = 5 + append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Add: stack[frame_index].stage = 2 append(&stack, Lower_Expr_Frame{expr=expr.left}) @@ -156,6 +159,14 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { } continue } + if frame.stage == 5 { + last = append_instruction(state, ir.Instruction{ + op=.Neg_Checked, span=expr.span, type=expr.type, target=ir.INVALID_REF, + a=last, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + _ = pop(&stack) + continue + } if frame.stage == 1 { last = append_instruction(state, ir.Instruction{ op=.Widen, span=expr.span, type=expr.type, target=ir.INVALID_REF, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index aa12fe3..5b145a9 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -4,8 +4,8 @@ import "../ast" import "../source" import "../symbol" import "../token" +import "base:intrinsics" import "core:fmt" -import "core:strconv" import "core:strings" Parser :: struct { @@ -168,14 +168,32 @@ parse_call :: proc(parser: ^Parser, qualifier: symbol.Id, first, name: token.Tok }) } +parse_integer_magnitude :: proc(text: string) -> (u64, bool) { + value: u64 + for byte in transmute([]byte)text { + if byte < '0' || byte > '9' { + return 0, false + } + next, overflow := intrinsics.overflow_mul(value, u64(10)) + if overflow { + return 0, false + } + value, overflow = intrinsics.overflow_add(next, u64(byte-'0')) + if overflow { + return 0, false + } + } + return value, len(text) > 0 +} + parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { tok := current(parser) #partial switch tok.kind { case .Integer: advance(parser) - value, ok := strconv.parse_i64(token_text(parser, tok)) + value, ok := parse_integer_magnitude(token_text(parser, tok)) if !ok { - return invalid_expr(parser, tok.span, "integer literal does not fit in i64") + return invalid_expr(parser, tok.span, "integer literal magnitude does not fit in u64") } return add_expr(parser, ast.Expr{ kind=.Integer, @@ -250,6 +268,14 @@ infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) { return 0, 0, false } +prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) { + #partial switch kind { + case .Minus: + return 20, true + } + return 0, false +} + parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int) -> ast.Expr_Id { if nesting > MAX_EXPRESSION_NESTING { tok := current(parser) @@ -258,7 +284,24 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int } return invalid_expr(parser, tok.span, "expression nesting exceeds 256 levels") } - left := parse_primary(parser, nesting) + left := ast.INVALID_EXPR + if right_power, ok := prefix_binding_power(current(parser).kind); ok { + operator := advance(parser) + if parser.delimiter_depth > 0 { + skip_newlines(parser) + } + operand := parse_expression_bp(parser, right_power, nesting+1) + operand_expr := parser.module.exprs[operand] + left = add_expr(parser, ast.Expr{ + kind=.Negate, + span=span_from(operator.span, operand_expr.span), + left=operand, + right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } else { + left = parse_primary(parser, nesting) + } if parser.delimiter_depth > 0 { skip_newlines(parser) } diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 065be4f..41f008a 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -14,6 +14,7 @@ Kind :: enum u8 { Colon_Colon, Equal, Plus, + Minus, Dot, Left_Paren, Right_Paren, diff --git a/compiler_tests.odin b/compiler_tests.odin index b557394..c698000 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -78,7 +78,7 @@ main :: func() void { _ = value } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, value_count, 2) testing.expect_value(t, module.imports[0].path, "../math") - testing.expect_value(t, module.exprs[module.globals[0].expr].integer, i64(42)) + testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(42)) testing.expect_value(t, module.statements[module.functions[0].body[0]].name, sink_symbol) } @@ -86,6 +86,9 @@ main :: func() void { _ = value } compact_ids_reserve_invalid_values_and_preserve_layout :: proc(t: ^testing.T) { testing.expect_value(t, size_of(source.Span), 12) testing.expect_value(t, size_of(token.Token), 24) + testing.expect_value(t, size_of(ast.Expr), 64) + testing.expect_value(t, size_of(hir.Expr), 88) + testing.expect_value(t, size_of(ir.Instruction), 88) source_index, source_ok := source.source_index(source.Source_Id(0), 1) testing.expect_value(t, source_index, 0) @@ -271,7 +274,41 @@ pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) { testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, root.kind, ast.Expr_Kind.Add) testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Add) - testing.expect_value(t, module.exprs[root.right].integer, i64(3)) + testing.expect_value(t, module.exprs[root.right].integer, u64(3)) +} + +@(test) +pratt_parser_handles_prefix_negation_precedence :: proc(t: ^testing.T) { + text := `identity :: func(value i8) i8 { return value } +loose :: -1 + 2 +grouped :: -(1 + 2) +called :: -identity(1) +chained :: --1 +main :: func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&module) + + loose := module.exprs[module.globals[0].expr] + grouped := module.exprs[module.globals[1].expr] + called := module.exprs[module.globals[2].expr] + chained := module.exprs[module.globals[3].expr] + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, loose.kind, ast.Expr_Kind.Add) + testing.expect_value(t, module.exprs[loose.left].kind, ast.Expr_Kind.Negate) + testing.expect_value(t, grouped.kind, ast.Expr_Kind.Negate) + testing.expect_value(t, module.exprs[grouped.left].kind, ast.Expr_Kind.Add) + testing.expect_value(t, called.kind, ast.Expr_Kind.Negate) + testing.expect_value(t, module.exprs[called.left].kind, ast.Expr_Kind.Call) + testing.expect_value(t, chained.kind, ast.Expr_Kind.Negate) + testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate) } nested_expression_source :: proc(call: bool, depth: int) -> string { @@ -296,6 +333,17 @@ nested_expression_source :: proc(call: bool, depth: int) -> string { return strings.clone(strings.to_string(builder)) } +nested_negation_source :: proc(depth: int) -> string { + builder := strings.builder_make() + defer strings.builder_destroy(&builder) + strings.write_string(&builder, "value :: ") + for _ in 0.. (count: int, found_budget: bool) { source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) @@ -327,6 +375,17 @@ parser_enforces_explicit_expression_nesting_budget :: proc(t: ^testing.T) { _, found = parse_nesting_result(over_limit) testing.expect(t, found) } + + at_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING) + defer delete(at_limit) + count, found := parse_nesting_result(at_limit) + testing.expect_value(t, count, 0) + testing.expect(t, !found) + + over_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING+1) + defer delete(over_limit) + _, found = parse_nesting_result(over_limit) + testing.expect(t, found) } @(test) @@ -1267,6 +1326,49 @@ checked_addition_traps_on_overflow :: proc(t: ^testing.T) { testing.expect(t, !state.success) } +@(test) +checked_runtime_negation_traps_for_every_signed_width :: proc(t: ^testing.T) { + Case :: struct { + type_name: string, + magnitude: string, + } + cases := [?]Case{ + {type_name="i8", magnitude="128"}, + {type_name="i16", magnitude="32768"}, + {type_name="i32", magnitude="2147483648"}, + {type_name="i64", magnitude="9223372036854775808"}, + } + for test_case in cases { + directory := fmt.aprintf("/tmp/brolang-test-negate-overflow-%s", test_case.type_name) + main_path := fmt.aprintf("%s/main.bro", directory) + output := fmt.aprintf("/tmp/brolang-test-negate-overflow-output-%s", test_case.type_name) + builder := strings.builder_make() + fmt.sbprintf( + &builder, + "negate :: func(value %s) %s {{ return -value }}\nmain :: func() void {{ _ = negate(-%s) }}\n", + test_case.type_name, + test_case.type_name, + test_case.magnitude, + ) + text := strings.clone(strings.to_string(builder)) + strings.builder_destroy(&builder) + _ = os2.remove_all(directory) + _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + status := compiler_core.compile_package(directory, output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect(t, !state.success) + _ = os.remove(output) + _ = os2.remove_all(directory) + delete(text) + delete(output) + delete(main_path) + delete(directory) + } +} + @(test) constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) { output := "/tmp/brolang-test-constant-context-error" @@ -1393,7 +1495,114 @@ maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) { defer ast.destroy_module(&module) testing.expect_value(t, len(diagnostics.items), 0) - testing.expect_value(t, module.exprs[module.globals[0].expr].integer, i64(9223372036854775807)) + testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(9223372036854775807)) +} + +@(test) +negative_constants_fold_and_accept_signed_i64_minimum :: proc(t: ^testing.T) { + text := `minimum :: -9223372036854775808 +grouped i64 :: -(9223372036854775808) +folded :: -(1 + 2) +main :: func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, types.equal(hir_module.globals[0].type, types.I64)) + testing.expect(t, types.equal(hir_module.globals[1].type, types.I64)) + testing.expect(t, types.equal(hir_module.globals[2].type, types.I8)) + testing.expect_value(t, hir_module.globals[0].static_value, i64(-9223372036854775807-1)) + testing.expect_value(t, hir_module.globals[1].static_value, i64(-9223372036854775807-1)) + testing.expect_value(t, hir_module.globals[2].static_value, i64(-3)) +} + +@(test) +out_of_range_negative_constants_are_diagnosed :: proc(t: ^testing.T) { + text := `positive :: 9223372036854775808 +below_minimum :: -9223372036854775809 +double_minimum :: --9223372036854775808 +maximum_u64 :: 18446744073709551615 +beyond_u64 :: 18446744073709551616 +main :: func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found_signed_range := 0 + found_u64_range := false + for diagnostic in diagnostics.items { + found_signed_range += 1 if strings.contains(diagnostic.message, "exceeds signed i64 range") else 0 + found_u64_range = found_u64_range || strings.contains(diagnostic.message, "magnitude does not fit in u64") + } + testing.expect_value(t, found_signed_range, 4) + testing.expect(t, found_u64_range) +} + +@(test) +runtime_negation_preserves_operand_type_before_result_widening :: proc(t: ^testing.T) { + text := `negate_i8 :: func(value i8) i8 { + return -value +} +widen_after_negate :: func(value i8) i16 { + return -value +} +main :: func() void { + _ = negate_i8(1) + _ = widen_after_negate(1) +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i8")) + found_widen := false + for function in hir_module.functions { + if symbol.resolve(&symbols, function.name) != "widen_after_negate" { + continue + } + statement := hir_module.statements[function.body[0]] + widen := hir_module.exprs[statement.expr] + negate := hir_module.exprs[widen.left] + testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen) + testing.expect(t, types.equal(widen.type, types.I16)) + testing.expect_value(t, negate.kind, hir.Expr_Kind.Negate) + testing.expect(t, types.equal(negate.type, types.I8)) + found_widen = true + } + testing.expect(t, found_widen) } @(test) @@ -1445,10 +1654,11 @@ malformed_hir_references_lower_to_valid_trapped_llvm :: proc(t: ^testing.T) { malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) { module := ir.init_module() defer ir.destroy_module(&module) - instructions := make([]ir.Instruction, 3) + instructions := make([]ir.Instruction, 4) instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} - instructions[2] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} + instructions[2] = ir.Instruction{op=.Neg_Checked, type=types.I16, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} + instructions[3] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} append(&module.functions, ir.Function{ link_name=strings.clone("main"), calling_convention=.C, @@ -1469,6 +1679,7 @@ malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) { testing.expect(t, strings.contains(text, "call void @bro.trap")) testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86")) testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766")) + testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846")) testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn")) testing.expect(t, !strings.contains(text, "%v-1")) llvm_path := "/tmp/brolang-test-malformed-recovery.ll"