unary minus
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -77,6 +77,7 @@ Expr_Kind :: enum u8 {
|
||||
Local,
|
||||
Global,
|
||||
Widen,
|
||||
Negate,
|
||||
Add,
|
||||
Call,
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ Opcode :: enum u8 {
|
||||
Load,
|
||||
Store,
|
||||
Widen,
|
||||
Neg_Checked,
|
||||
Add_Checked,
|
||||
Call,
|
||||
Trap,
|
||||
|
||||
@@ -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
|
||||
|
||||
+33
-1
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ Kind :: enum u8 {
|
||||
Colon_Colon,
|
||||
Equal,
|
||||
Plus,
|
||||
Minus,
|
||||
Dot,
|
||||
Left_Paren,
|
||||
Right_Paren,
|
||||
|
||||
+216
-5
@@ -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..<depth {
|
||||
strings.write_byte(&builder, '-')
|
||||
}
|
||||
strings.write_string(&builder, "1\nmain :: func() void {}\n")
|
||||
return strings.clone(strings.to_string(builder))
|
||||
}
|
||||
|
||||
parse_nesting_result :: proc(text: string) -> (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"
|
||||
|
||||
Reference in New Issue
Block a user