deferred defaulting typing
This commit is contained in:
@@ -242,7 +242,10 @@
|
|||||||
c :: b + 3 # c is constrained to `i32`
|
c :: b + 3 # c is constrained to `i32`
|
||||||
```
|
```
|
||||||
|
|
||||||
16. add slice-by-range
|
16. for if statements, allow `if (cond) one-line statement` or `if some_func(some_arg) one-line statement` (instead of forcing either `if (cond) { block }` or `if cond { block }`)
|
||||||
|
- if statements without a bracketed body must wrap the condition in parentheses UNLESS it's a function call
|
||||||
|
|
||||||
|
17. add slice-by-range
|
||||||
- allow the use of a range in slice expressions:
|
- allow the use of a range in slice expressions:
|
||||||
```
|
```
|
||||||
excl_range range :: 0..10
|
excl_range range :: 0..10
|
||||||
@@ -252,9 +255,6 @@
|
|||||||
some_arr[incl_range] # slice by named inclusive range
|
some_arr[incl_range] # slice by named inclusive range
|
||||||
```
|
```
|
||||||
|
|
||||||
17. for if statements, allow `if (cond) one-line statement` (instead of forcing either `if (cond) { block }` or `if cond { block }`)
|
|
||||||
- if statements without a bracketed body must wrap the condition in parentheses UNLESS it's a function call
|
|
||||||
|
|
||||||
18. add `defer` statement (inspired by zig)
|
18. add `defer` statement (inspired by zig)
|
||||||
|
|
||||||
19. multi-line strings (see below)
|
19. multi-line strings (see below)
|
||||||
|
|||||||
+144
-18
@@ -1510,10 +1510,17 @@ infer_expr :: proc(
|
|||||||
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
|
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
|
||||||
last = right
|
last = right
|
||||||
} else if is_numeric_demand(frame.left, checker.target) &&
|
} else if is_numeric_demand(frame.left, checker.target) &&
|
||||||
|
!numeric_operand_is_open(checker, expr.left, locals, pkg, file) &&
|
||||||
expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) {
|
expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) {
|
||||||
|
// Propagate only from an authoritative (fixed-type) left operand. A left
|
||||||
|
// operand that is still a provisional open constant carries only its
|
||||||
|
// smallest-signed default, which must not poison the sibling's family;
|
||||||
|
// two provisional operands are resolved together by the backward demand
|
||||||
|
// from the declaration/use.
|
||||||
_ = record_demand(checker, expr.right, frame.left, locals, local_types, pkg, file)
|
_ = record_demand(checker, expr.right, frame.left, locals, local_types, pkg, file)
|
||||||
last = frame.left
|
last = frame.left
|
||||||
} else if is_numeric_demand(right, checker.target) &&
|
} else if is_numeric_demand(right, checker.target) &&
|
||||||
|
!numeric_operand_is_open(checker, expr.right, locals, pkg, file) &&
|
||||||
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
|
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
|
||||||
_ = record_demand(checker, expr.left, right, locals, local_types, pkg, file)
|
_ = record_demand(checker, expr.left, right, locals, local_types, pkg, file)
|
||||||
last = right
|
last = right
|
||||||
@@ -1540,6 +1547,17 @@ infer_expr :: proc(
|
|||||||
for arg_index in 0..<len(expr.args) {
|
for arg_index in 0..<len(expr.args) {
|
||||||
record_demand(checker, expr.args[arg_index], call_arg_expected(function, arg_index), locals, local_types, pkg, file)
|
record_demand(checker, expr.args[arg_index], call_arg_expected(function, arg_index), locals, local_types, pkg, file)
|
||||||
}
|
}
|
||||||
|
// Deferred defaulting leaves an undemanded open constant typeless; give such an
|
||||||
|
// argument its default so the call can still monomorphize (the default feeds only
|
||||||
|
// the spec arg vector, not a demand).
|
||||||
|
for arg_index in 0..<len(expr.args) {
|
||||||
|
if !is_runtime_type(checker, stack[frame_index].args[arg_index]) {
|
||||||
|
fallback := open_const_default_type(checker, expr.args[arg_index], locals, pkg, file)
|
||||||
|
if is_runtime_type(checker, fallback) {
|
||||||
|
stack[frame_index].args[arg_index] = fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if valid_call_arity(function, len(expr.args)) &&
|
if valid_call_arity(function, len(expr.args)) &&
|
||||||
can_specialize(checker, function, stack[frame_index].args) {
|
can_specialize(checker, function, stack[frame_index].args) {
|
||||||
spec := INVALID_SPEC
|
spec := INVALID_SPEC
|
||||||
@@ -1723,9 +1741,23 @@ infer_statements :: proc(
|
|||||||
record_infer_local_type(local, local_types)
|
record_infer_local_type(local, local_types)
|
||||||
// A typed/constraint declaration initialized by a bare name pushes its resolved
|
// A typed/constraint declaration initialized by a bare name pushes its resolved
|
||||||
// type backward onto that name (mirrors the `Y int :: X; Z i32 :: Y` global chain).
|
// type backward onto that name (mirrors the `Y int :: X; Z i32 :: Y` global chain).
|
||||||
record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
|
// Skip this for a constraint/untyped declaration whose initializer is arithmetic:
|
||||||
|
// value_type is then only a provisional default (e.g. `x int = a - b` resolving to
|
||||||
|
// i16 before the operands' real uses are seen) and would poison the open-constant
|
||||||
|
// operands' family. The operands resolve from their own authoritative uses, and the
|
||||||
|
// local adopts their resolved type forward. Concrete-declared arithmetic (e.g.
|
||||||
|
// `b u16 :: a + 2`) still pushes, since value_type is the concrete declared type.
|
||||||
|
init_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
|
||||||
|
if is_runtime_type(checker, declared_local) || !init_is_arith {
|
||||||
|
record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
|
||||||
|
}
|
||||||
case .Assignment:
|
case .Assignment:
|
||||||
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
|
// Only push the target's type back onto a bare-name RHS (e.g. `x += speed`):
|
||||||
|
// pushing through an arithmetic RHS would feed the target's (often provisional)
|
||||||
|
// type onto open-constant operands and poison their family. Operands of an
|
||||||
|
// arithmetic RHS resolve from their own authoritative uses.
|
||||||
|
rhs_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
|
||||||
if statement.target != ast.INVALID_EXPR {
|
if statement.target != ast.INVALID_EXPR {
|
||||||
target_type := infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types)
|
target_type := infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types)
|
||||||
target_expr := checker.ast_module.exprs[statement.target]
|
target_expr := checker.ast_module.exprs[statement.target]
|
||||||
@@ -1735,16 +1767,16 @@ infer_statements :: proc(
|
|||||||
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
|
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Push the target's concrete type backward onto the RHS so a const used
|
if !rhs_is_arith {
|
||||||
// only in an assignment (e.g. `x += speed`) resolves, mirroring how
|
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
|
||||||
// declarations and returns demand their context (record_demand self-gates
|
}
|
||||||
// on a concrete demand and only touches fitting open slots).
|
|
||||||
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
|
|
||||||
} else if statement.name != checker.sink_symbol {
|
} else if statement.name != checker.sink_symbol {
|
||||||
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok &&
|
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok &&
|
||||||
locals^[local_index].mutable {
|
locals^[local_index].mutable {
|
||||||
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
|
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
|
||||||
_ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file)
|
if !rhs_is_arith {
|
||||||
|
_ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .Expression:
|
case .Expression:
|
||||||
@@ -2001,6 +2033,92 @@ merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_arith_kind :: proc(k: ast.Expr_Kind) -> bool {
|
||||||
|
return k == .Add || k == .Sub || k == .Mul || k == .Div || k == .Negate
|
||||||
|
}
|
||||||
|
|
||||||
|
// open_const_default_type returns the fallback type a bare open-constant reference
|
||||||
|
// (local or global) would take if no use ever demands it: the smallest signed type
|
||||||
|
// that holds an integer constant, or f64 for a float constant. Used to give a call's
|
||||||
|
// argument a concrete type for monomorphization when deferred defaulting has left the
|
||||||
|
// open constant typeless (its param is a constraint, so the call records no demand on
|
||||||
|
// it). The fallback feeds only the specialization's arg vector, never a demand, so it
|
||||||
|
// cannot leak back onto the constant or a sibling.
|
||||||
|
open_const_default_type :: proc(
|
||||||
|
checker: ^Checker,
|
||||||
|
expr_id: ast.Expr_Id,
|
||||||
|
locals: []Infer_Local,
|
||||||
|
pkg: ast.Package_Id,
|
||||||
|
file: ast.File_Id,
|
||||||
|
) -> types.Type {
|
||||||
|
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
||||||
|
return types.INVALID
|
||||||
|
}
|
||||||
|
expr := checker.ast_module.exprs[expr_id]
|
||||||
|
if expr.kind != .Name {
|
||||||
|
return types.INVALID
|
||||||
|
}
|
||||||
|
if !symbol.is_valid(expr.qualifier) {
|
||||||
|
if index, ok := find_infer_local_index(locals, expr.name); ok {
|
||||||
|
local := locals[index]
|
||||||
|
if local.open_const {
|
||||||
|
return types.smallest_signed_for_literal(i64(local.const_value))
|
||||||
|
}
|
||||||
|
if local.open_float {
|
||||||
|
return types.F64
|
||||||
|
}
|
||||||
|
return types.INVALID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target_pkg, available := expr_package(checker, expr, pkg, file)
|
||||||
|
if !available {
|
||||||
|
return types.INVALID
|
||||||
|
}
|
||||||
|
global := find_global(checker, expr.name, target_pkg)
|
||||||
|
index := int(global)
|
||||||
|
if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) {
|
||||||
|
return types.INVALID
|
||||||
|
}
|
||||||
|
if checker.global_open_const[index] {
|
||||||
|
return types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
|
||||||
|
}
|
||||||
|
if checker.global_open_float[index] {
|
||||||
|
return types.F64
|
||||||
|
}
|
||||||
|
return types.INVALID
|
||||||
|
}
|
||||||
|
|
||||||
|
// numeric_operand_is_open reports whether an arithmetic operand still carries a
|
||||||
|
// provisional type (an open constant at its smallest-signed default, or a bare
|
||||||
|
// numeric literal) rather than an authoritative one. A provisional operand must not
|
||||||
|
// propagate its type onto a sibling open constant: doing so locks the sibling into a
|
||||||
|
// default family and blocks the real backward demand from the declaration/use. Two
|
||||||
|
// provisional operands are instead resolved together by that backward demand.
|
||||||
|
numeric_operand_is_open :: proc(
|
||||||
|
checker: ^Checker,
|
||||||
|
expr_id: ast.Expr_Id,
|
||||||
|
locals: []Infer_Local,
|
||||||
|
pkg: ast.Package_Id,
|
||||||
|
file: ast.File_Id,
|
||||||
|
) -> bool {
|
||||||
|
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expr := checker.ast_module.exprs[expr_id]
|
||||||
|
#partial switch expr.kind {
|
||||||
|
case .Name:
|
||||||
|
// A bare open-constant name is open iff it has a deferred default to assign.
|
||||||
|
return is_runtime_type(checker, open_const_default_type(checker, expr_id, locals, pkg, file))
|
||||||
|
case .Negate:
|
||||||
|
return numeric_operand_is_open(checker, expr.left, locals, pkg, file)
|
||||||
|
case .Add, .Sub, .Mul, .Div:
|
||||||
|
return numeric_operand_is_open(checker, expr.left, locals, pkg, file) ||
|
||||||
|
numeric_operand_is_open(checker, expr.right, locals, pkg, file)
|
||||||
|
}
|
||||||
|
// A bare integer/float literal adapts freely, so it too is provisional.
|
||||||
|
return is_numeric_constant_expr(checker, expr_id)
|
||||||
|
}
|
||||||
|
|
||||||
expr_accepts_numeric_demand :: proc(
|
expr_accepts_numeric_demand :: proc(
|
||||||
checker: ^Checker,
|
checker: ^Checker,
|
||||||
expr_id: ast.Expr_Id,
|
expr_id: ast.Expr_Id,
|
||||||
@@ -2162,17 +2280,12 @@ infer_all :: proc(checker: ^Checker) {
|
|||||||
checker.global_types[index] = checker.global_demands[index]
|
checker.global_types[index] = checker.global_demands[index]
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
} else if checker.global_open_const[index] {
|
} else if checker.global_open_const[index] || checker.global_open_float[index] {
|
||||||
resolved := types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
|
// Defer defaulting: an undemanded open constant stays typeless during the
|
||||||
if !types.equal(checker.global_types[index], resolved) {
|
// fixpoint so its provisional smallest-signed default never leaks as a
|
||||||
checker.global_types[index] = resolved
|
// demand and poisons a sibling open constant used in the same arithmetic
|
||||||
changed = true
|
// (e.g. the lagging type in `x += speed`). The pass after this loop assigns
|
||||||
}
|
// the default once the fixpoint settles and no further demand can arrive.
|
||||||
} else if checker.global_open_float[index] {
|
|
||||||
if !types.equal(checker.global_types[index], types.F64) {
|
|
||||||
checker.global_types[index] = types.F64
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
|
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
|
||||||
}
|
}
|
||||||
@@ -2195,6 +2308,19 @@ infer_all :: proc(checker: ^Checker) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any open constant that no use ever demanded now takes its default: an integer
|
||||||
|
// constant the smallest signed type that holds its value, a float constant f64.
|
||||||
|
for global, index in checker.ast_module.globals {
|
||||||
|
if global.external || is_runtime_type(checker, checker.global_types[index]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if checker.global_open_const[index] {
|
||||||
|
checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
|
||||||
|
} else if checker.global_open_float[index] {
|
||||||
|
checker.global_types[index] = types.F64
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prune_specs :: proc(checker: ^Checker) {
|
prune_specs :: proc(checker: ^Checker) {
|
||||||
|
|||||||
@@ -6630,6 +6630,47 @@ contextual_inference_flows_through_compound_assignment :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, len(diagnostics.items), 0)
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_resolves_open_global_arithmetic_across_uses :: proc(t: ^testing.T) {
|
||||||
|
text := `take_ci :: func(v c_int) void {}
|
||||||
|
W :: 800
|
||||||
|
Z :: 40
|
||||||
|
STEP :: 5
|
||||||
|
main :: func() void {
|
||||||
|
take_ci(W)
|
||||||
|
x int = W - Z
|
||||||
|
take_ci(Z)
|
||||||
|
x += STEP
|
||||||
|
take_ci(x)
|
||||||
|
_ = x
|
||||||
|
}
|
||||||
|
`
|
||||||
|
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)
|
||||||
|
|
||||||
|
// `W - Z` mixes two open-constant globals while `Z`'s c_int use comes only after the
|
||||||
|
// arithmetic, and `STEP` is used only in a compound assignment. With deferred defaulting
|
||||||
|
// an undemanded open global stays typeless during the fixpoint instead of leaking a
|
||||||
|
// provisional i16 default, so W/Z/STEP all resolve to c_int. Previously this reported
|
||||||
|
// "arithmetic requires compatible numeric operands" / "cannot implicitly convert i16 to c_int".
|
||||||
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
|
for global in hir_module.globals {
|
||||||
|
name := symbol.resolve(&symbols, global.name)
|
||||||
|
if name == "W" || name == "Z" || name == "STEP" {
|
||||||
|
testing.expect(t, types.equal(global.type, types.C_INT))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) {
|
contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) {
|
||||||
text := `main :: func() void {
|
text := `main :: func() void {
|
||||||
|
|||||||
Reference in New Issue
Block a user