enforce integer division via explicit builtins

This commit is contained in:
2026-07-13 11:39:06 +02:00
parent a4d0fb1e26
commit 2ed333c70d
13 changed files with 1004 additions and 98 deletions
+183 -2
View File
@@ -399,6 +399,31 @@ Type_Builtin :: enum u8 {
Max_Value,
}
Division_Builtin :: enum u8 {
None,
Trunc,
Floor,
Exact,
Ceil,
Rem,
Mod,
}
division_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Division_Builtin {
if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None
}
switch symbol_text(checker, expr.name) {
case "div_trunc": return .Trunc
case "div_floor": return .Floor
case "div_exact": return .Exact
case "div_ceil": return .Ceil
case "rem": return .Rem
case "mod": return .Mod
}
return .None
}
type_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Type_Builtin {
if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None
@@ -598,6 +623,10 @@ type_from_syntax :: proc(
changed = true
}
} else {
if constant.kind == .Integer_Division {
source.add(checker.diagnostics, span, "integer '/' is not allowed; use div_trunc, div_floor, div_exact, or div_ceil")
return types.INVALID
}
source.add(checker.diagnostics, span, "array count must be a compile-time integer expression")
return types.INVALID
}
@@ -2568,6 +2597,35 @@ infer_nested_expr :: proc(
return result
}
infer_division_builtin :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id,
local_types: []types.Type,
expected: types.Type,
) -> types.Type {
if len(expr.args) != 2 {
return types.INVALID
}
hint := expected if types.is_concrete_scalar(expected) && !types.is_bool(expected) else types.INVALID
left_const := is_numeric_constant_expr(checker, expr.args[0])
right_const := is_numeric_constant_expr(checker, expr.args[1])
left, right := types.INVALID, types.INVALID
if left_const && !right_const && !types.is_valid(hint) {
right = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types)
left = infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types, right)
} else {
left = infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types, hint)
right_hint := hint if types.is_valid(hint) else left
right = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types, right_hint)
}
result := types.widest(left, right)
return result if types.is_concrete_scalar(result) && !types.is_bool(result) else types.INVALID
}
infer_compound_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
@@ -2938,6 +2996,11 @@ infer_expr :: proc(
_ = pop(&stack)
continue
}
if division_builtin_call(checker, expr) != .None {
last = infer_division_builtin(checker, expr, locals, pkg, file, demanded, local_types, frame.expected)
_ = pop(&stack)
continue
}
if is_ptr_cast_call(checker, expr) {
if len(expr.args) != 2 {
last = types.INVALID
@@ -3952,6 +4015,12 @@ record_demand :: proc(
right := record_demand(checker, expr.right, demand, locals, local_types, pkg, file)
return left || right
}
case .Call:
if division_builtin_call(checker, expr) != .None && len(expr.args) == 2 && is_numeric_demand(demand, checker.target) {
left := record_demand(checker, expr.args[0], demand, locals, local_types, pkg, file)
right := record_demand(checker, expr.args[1], demand, locals, local_types, pkg, file)
return left || right
}
}
return false
}
@@ -4435,6 +4504,14 @@ build_constant_expr :: proc(
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 == .Non_Exact {
id := source.add(checker.diagnostics, expr.span, "exact division has a remainder")
return invalid_hir_expr(checker, expr.span, id, recovery_type)
}
if constant.kind == .Integer_Division {
id := source.add(checker.diagnostics, expr.span, "integer '/' is not allowed; use div_trunc, div_floor, div_exact, or div_ceil")
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(
@@ -4868,6 +4945,89 @@ build_nested_expr :: proc(
return result
}
try_build_comptime_division :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Division_Builtin,
expected: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (hir.Expr_Id, bool) {
state := ct_state_make(checker, pkg, file, diagnose=false)
defer ct_state_destroy(&state)
value, flow, ok := ct_eval_division_call(&state, expr, kind, expected, 0)
if ok && flow.kind == .Normal && value != INVALID_CT_VALUE {
return ct_materialize_value(&state, value, expr.span, expected), true
}
message := ""
#partial switch state.error {
case .Div_By_Zero: message = "division builtin denominator is zero"
case .Overflow: message = "signed integer division overflow"
case .Non_Exact: message = "exact division has a remainder"
}
if len(message) == 0 {
return hir.INVALID_EXPR, false
}
id := source.add(checker.diagnostics, expr.span, message)
recovery := expected if types.is_concrete_scalar(expected) else types.I64
return invalid_hir_expr(checker, expr.span, id, recovery), true
}
build_division_builtin :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Division_Builtin,
locals: []Build_Local,
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
expected: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
if len(expr.args) != 2 {
id := source.addf(
checker.diagnostics, expr.span, "%s expects 2 arguments, got %d",
symbol_text(checker, expr.name), len(expr.args),
)
return invalid_hir_expr(checker, expr.span, id)
}
if value, handled := try_build_comptime_division(checker, expr, kind, expected, pkg, file); handled {
return value
}
hint := expected if types.is_concrete_scalar(expected) && !types.is_bool(expected) else types.INVALID
left_const := is_numeric_constant_expr(checker, expr.args[0])
right_const := is_numeric_constant_expr(checker, expr.args[1])
left, right := hir.INVALID_EXPR, hir.INVALID_EXPR
if left_const && !right_const && !types.is_valid(hint) {
right = build_nested_expr(checker, expr.args[1], locals, global_reads, calls, types.INVALID, pkg, file)
left = build_nested_expr(checker, expr.args[0], locals, global_reads, calls, checker.module.exprs[right].type, pkg, file)
} else {
left = build_nested_expr(checker, expr.args[0], locals, global_reads, calls, hint, pkg, file)
right_hint := hint if types.is_valid(hint) else checker.module.exprs[left].type
right = build_nested_expr(checker, expr.args[1], locals, global_reads, calls, right_hint, pkg, file)
}
result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type)
if !types.is_concrete_scalar(result) || types.is_bool(result) {
id := source.add(checker.diagnostics, expr.span, "division builtins require compatible numeric operands")
return invalid_hir_expr(checker, expr.span, id)
}
left = coerce_expr(checker, left, result, checker.module.exprs[left].span)
right = coerce_expr(checker, right, result, checker.module.exprs[right].span)
result_kind := hir.Expr_Kind.Div_Trunc
#partial switch kind {
case .Floor: result_kind = .Div_Floor
case .Exact: result_kind = .Div_Exact
case .Ceil: result_kind = .Div_Ceil
case .Rem: result_kind = .Rem
case .Mod: result_kind = .Mod
case:
}
return add_hir_expr(checker, hir.Expr{
kind=result_kind, span=expr.span, type=result, left=left, right=right,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
fallible_aggregate :: proc(
checker: ^Checker,
span: source.Span,
@@ -5578,6 +5738,13 @@ build_binary_arith :: proc(
id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands")
return invalid_hir_expr(checker, span, id)
}
if op == .Div && !types.is_float(result, checker.target) {
id := source.add(
checker.diagnostics, span,
"integer '/' is not allowed; use div_trunc, div_floor, div_exact, or div_ceil",
)
return invalid_hir_expr(checker, span, id, result)
}
result_kind := hir.Expr_Kind.Add
#partial switch op {
case .Sub: result_kind = .Sub
@@ -5627,7 +5794,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 || constant.kind == .Div_By_Zero {
if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero || constant.kind == .Non_Exact {
last = build_constant_expr(checker, expr, constant, frame.expected)
_ = pop(&stack)
continue
@@ -5807,6 +5974,13 @@ build_expr :: proc(
_ = pop(&stack)
continue
}
if builtin := division_builtin_call(checker, expr); builtin != .None {
last = build_division_builtin(
checker, expr, builtin, locals, global_reads, calls, frame.expected, pkg, file,
)
_ = pop(&stack)
continue
}
if is_ptr_cast_call(checker, expr) {
if len(expr.args) != 2 {
id := source.addf(checker.diagnostics, expr.span, "ptr_cast expects 2 arguments, got %d", len(expr.args))
@@ -6653,7 +6827,14 @@ build_block :: proc(
}
rhs_type := checker.module.exprs[value].type
result_type := types.widest(target_type, rhs_type)
if !types.is_concrete_scalar(result_type) ||
if statement.assignment_op == .Div && types.is_concrete_integer(result_type) {
id := source.add(
checker.diagnostics,
statement.span,
"integer '/=' is not allowed; assign through an explicit division builtin",
)
value = invalid_hir_expr(checker, statement.span, id, target_type)
} else if !types.is_concrete_scalar(result_type) ||
types.is_bool(result_type) {
id := source.add(
checker.diagnostics,