compound assignment

This commit is contained in:
2026-06-22 21:20:15 +02:00
parent 663f4dc658
commit 6512ccd543
14 changed files with 995 additions and 99 deletions
+134 -39
View File
@@ -65,6 +65,7 @@ Constant_Kind :: enum {
Not_Constant,
Value,
Overflow,
Div_By_Zero,
}
Constant :: struct {
@@ -145,7 +146,8 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant {
continue
}
expr := checker.ast_module.exprs[frame.expr]
if expr.kind != .Add && expr.kind != .Negate {
if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul &&
expr.kind != .Div && expr.kind != .Negate {
result := Constant{kind = .Not_Constant}
if expr.kind == .Integer {
result = Constant{kind = .Value, value = i128(expr.integer)}
@@ -168,7 +170,9 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant {
operand = checker.constants[expr.left]
}
result := Constant{kind = .Not_Constant}
if operand.kind == .Overflow {
if operand.kind == .Div_By_Zero {
result = Constant{kind = .Div_By_Zero}
} else if operand.kind == .Overflow {
result = Constant{kind = .Overflow}
} else if operand.kind == .Value {
value, overflow := intrinsics.overflow_sub(i128(0), operand.value)
@@ -195,11 +199,30 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant {
right = checker.constants[expr.right]
}
result := Constant{kind = .Not_Constant}
if left.kind == .Overflow || right.kind == .Overflow {
if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero {
result = Constant{kind = .Div_By_Zero}
} else if left.kind == .Overflow || right.kind == .Overflow {
result = Constant{kind = .Overflow}
} else if left.kind == .Value && right.kind == .Value {
value, overflow := intrinsics.overflow_add(left.value, right.value)
result = Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value}
value: i128
overflow: bool
div_by_zero: bool
#partial switch expr.kind {
case .Sub: value, overflow = intrinsics.overflow_sub(left.value, right.value)
case .Mul: value, overflow = intrinsics.overflow_mul(left.value, right.value)
case .Div:
if right.value == 0 {
div_by_zero = true
} else {
value = left.value / right.value
}
case: value, overflow = intrinsics.overflow_add(left.value, right.value)
}
switch {
case div_by_zero: result = Constant{kind = .Div_By_Zero}
case overflow: result = Constant{kind = .Overflow}
case: result = Constant{kind = .Value, value = value}
}
}
checker.constants[frame.expr] = result
_ = pop(&stack)
@@ -611,7 +634,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
}
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Keyed:
append(&stack, expr.left)
case .Add, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name:
}
@@ -1163,7 +1186,8 @@ infer_expr :: proc(
expr := checker.ast_module.exprs[frame.expr]
if frame.stage == 0 {
constant := eval_constant(checker, frame.expr)
if constant.kind == .Overflow || (constant.kind == .Value && !fits_i64(constant.value)) {
if constant.kind == .Overflow || constant.kind == .Div_By_Zero ||
(constant.kind == .Value && !fits_i64(constant.value)) {
last = types.I64
_ = pop(&stack)
continue
@@ -1244,7 +1268,7 @@ infer_expr :: proc(
case .Negate:
stack[frame_index].stage = 5
append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION})
case .Add:
case .Add, .Sub, .Mul, .Div:
stack[frame_index].stage = 1
append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION})
case .Call:
@@ -1338,7 +1362,7 @@ infer_expr :: proc(
continue
}
if frame.stage == 2 {
if types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(last) {
if expr.kind == .Add && types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(last) {
last = frame.left
} else {
last = types.widest(frame.left, last)
@@ -1847,6 +1871,10 @@ build_constant_expr :: proc(
if types.is_concrete_integer(expected) {
recovery_type = expected
}
if constant.kind == .Div_By_Zero {
id := source.add(checker.diagnostics, expr.span, "division by zero in constant expression")
return invalid_hir_expr(checker, expr.span, id, recovery_type)
}
if constant.kind == .Overflow ||
(!types.is_concrete_integer(expected) && !fits_i64(constant.value)) {
id := source.add(
@@ -2485,6 +2513,45 @@ build_compound_expr :: proc(
}
}
// build_binary_arith constructs the HIR node for `left op right`, where `op` is
// an arithmetic AST kind (`Add`/`Sub`/`Mul`/`Div`). It models many-pointer `+`
// as `Pointer_Add`, coerces both operands to their common type, and emits the
// "arithmetic requires compatible numeric operands" diagnostic when they have no
// shared numeric type.
build_binary_arith :: proc(
checker: ^Checker,
op: ast.Expr_Kind,
left, right: hir.Expr_Id,
span: source.Span,
) -> hir.Expr_Id {
// Pointer arithmetic is only defined for `+` (many-pointer + usize).
if op == .Add &&
types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) &&
types.equal(checker.module.exprs[right].type, types.USIZE) {
return add_hir_expr(checker, hir.Expr{
kind=.Pointer_Add, span=span, type=checker.module.exprs[left].type,
left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type)
if !types.is_concrete_scalar(result) {
id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands")
return invalid_hir_expr(checker, span, id)
}
result_kind := hir.Expr_Kind.Add
#partial switch op {
case .Sub: result_kind = .Sub
case .Mul: result_kind = .Mul
case .Div: result_kind = .Div
}
coerced_left := coerce_expr(checker, left, result, checker.module.exprs[left].span)
coerced_right := coerce_expr(checker, right, result, checker.module.exprs[right].span)
return add_hir_expr(checker, hir.Expr{
kind=result_kind, span=span, type=result, left=coerced_left, right=coerced_right,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
build_expr :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
@@ -2520,7 +2587,7 @@ build_expr :: proc(
expr := checker.ast_module.exprs[frame.expr]
if frame.stage == 0 {
constant := eval_constant(checker, frame.expr)
if constant.kind == .Value || constant.kind == .Overflow {
if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero {
last = build_constant_expr(checker, expr, constant, frame.expected)
_ = pop(&stack)
continue
@@ -2609,7 +2676,7 @@ build_expr :: proc(
case .Negate:
stack[frame_index].stage = 5
append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION})
case .Add:
case .Add, .Sub, .Mul, .Div:
stack[frame_index].stage = 1
// Preserve assignment/return context for literal operands, e.g.
// assigning `i + 1` back into a `u32` local.
@@ -2768,29 +2835,7 @@ build_expr :: proc(
continue
}
if frame.stage == 2 {
left := frame.left
right := last
if types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) &&
types.equal(checker.module.exprs[right].type, types.USIZE) {
last = add_hir_expr(checker, hir.Expr{
kind=.Pointer_Add, span=expr.span, type=checker.module.exprs[left].type,
left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
_ = pop(&stack)
continue
}
result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type)
if !types.is_concrete_scalar(result) {
id := source.add(checker.diagnostics, expr.span, "addition requires compatible numeric operands")
last = invalid_hir_expr(checker, expr.span, id)
} else {
left = coerce_expr(checker, left, result, checker.module.exprs[left].span)
right = coerce_expr(checker, right, result, checker.module.exprs[right].span)
last = add_hir_expr(checker, hir.Expr{
kind=.Add, span=expr.span, type=result, left=left, right=right,
target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC,
})
}
last = build_binary_arith(checker, expr.kind, frame.left, last, expr.span)
_ = pop(&stack)
continue
}
@@ -3061,14 +3106,64 @@ build_block :: proc(
ctx.problematic^ = true
continue
}
value := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
target_type, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, target_type, statement.span)
value: hir.Expr_Id
assignment_op := hir.Assignment_Op.Set
if statement.assignment_op != .Set {
rhs_expected := target_type
if types.is_many_pointer(target_type, &checker.module.types) {
rhs_expected = types.USIZE if statement.assignment_op == .Add else types.INVALID
}
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
rhs_expected, ctx.pkg, ctx.file,
)
if types.is_many_pointer(target_type, &checker.module.types) {
assignment_op = .Pointer_Add
if statement.assignment_op != .Add {
id := source.add(
checker.diagnostics,
statement.span,
"many-item pointers only support '+=' compound assignment",
)
value = invalid_hir_expr(checker, statement.span, id, types.USIZE)
} else {
value = coerce_expr(checker, value, types.USIZE, statement.span)
}
} else {
#partial switch statement.assignment_op {
case .Add: assignment_op = .Add
case .Sub: assignment_op = .Sub
case .Mul: assignment_op = .Mul
case .Div: assignment_op = .Div
}
rhs_type := checker.module.exprs[value].type
result_type := types.widest(target_type, rhs_type)
if !types.is_concrete_scalar(result_type) ||
types.is_bool(result_type) {
id := source.add(
checker.diagnostics,
statement.span,
"arithmetic requires compatible numeric operands",
)
value = invalid_hir_expr(checker, statement.span, id, target_type)
} else {
// Compound assignment stores back into the original
// target type, so only an equal or widening RHS
// conversion is permitted.
value = coerce_expr(checker, value, target_type, statement.span)
}
}
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
target_type, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, target_type, statement.span)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL,
assignment_op=assignment_op,
target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid