diff --git a/TODO.md b/TODO.md index b31ca76..05257da 100644 --- a/TODO.md +++ b/TODO.md @@ -242,7 +242,10 @@ 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: ``` excl_range range :: 0..10 @@ -252,9 +255,6 @@ 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) 19. multi-line strings (see below) diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 5874a7f..0597ac9 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -1510,10 +1510,17 @@ infer_expr :: proc( expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) { last = right } 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) { + // 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) last = frame.left } 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) { _ = record_demand(checker, expr.left, right, locals, local_types, pkg, file) last = right @@ -1540,6 +1547,17 @@ infer_expr :: proc( for arg_index in 0.. 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( checker: ^Checker, expr_id: ast.Expr_Id, @@ -2162,17 +2280,12 @@ infer_all :: proc(checker: ^Checker) { checker.global_types[index] = checker.global_demands[index] changed = true } - } else if checker.global_open_const[index] { - resolved := types.smallest_signed_for_literal(i64(checker.global_const_value[index])) - if !types.equal(checker.global_types[index], resolved) { - checker.global_types[index] = resolved - changed = true - } - } 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 if checker.global_open_const[index] || checker.global_open_float[index] { + // Defer defaulting: an undemanded open constant stays typeless during the + // fixpoint so its provisional smallest-signed default never leaks as a + // demand and poisons a sibling open constant used in the same arithmetic + // (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 { changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed } @@ -2195,6 +2308,19 @@ infer_all :: proc(checker: ^Checker) { 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) { diff --git a/compiler_tests.odin b/compiler_tests.odin index ac2a786..c71ed67 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -6630,6 +6630,47 @@ contextual_inference_flows_through_compound_assignment :: proc(t: ^testing.T) { 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) contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) { text := `main :: func() void {