From c3208fdb611325cf984e8e3a332ef5447f2e4545 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 25 Jun 2026 22:09:08 +0200 Subject: [PATCH] add float constraint type --- TODO.md | 24 +++- compiler/checker/checker.odin | 48 +++++++- compiler/lexer/lexer.odin | 1 + compiler/parser/parser.odin | 5 +- compiler/token/token.odin | 1 + compiler/types/types.odin | 37 +++++- compiler_tests.odin | 213 +++++++++++++++++++++++++++++++++- 7 files changed, 319 insertions(+), 10 deletions(-) diff --git a/TODO.md b/TODO.md index 24eaef1..880df41 100644 --- a/TODO.md +++ b/TODO.md @@ -181,7 +181,29 @@ - disallow: `b :: undefined` since assigning undefined to something that can't change defeats the purpose - disallow assigning `undefined` after declaration; use optionals and `none` for values that intentionally move back to an empty state -13. broaden type inference from surrounding context +13. introduce a `float` type constraint (similar to `int`) (implemented) + - resolves a local binding to any float scalar (`f32`/`f64`) via static analysis; widens + `f32` -> `f64` across assignments, mirroring how `int` picks the smallest fitting integer + - on a local declaration, integer literals satisfy `float` and default to `f64` + (`pi float = 3` is `3.0`); a runtime integer (`x float = some_i32`) stays a + `cannot implicitly convert` error + - a local initializer whose numeric family doesn't satisfy the constraint now errors for + both `int` and `float` instead of silently taking the initializer's natural type + - as with `int`, a constraint in a param/result position is a generic passthrough (it + forwards the inferred type unchanged, e.g. an identity `func(v int) int` over a range), + so the literal-as-float and family checks apply to local bindings, not passthroughs + +14. add slice-by-range + - allow the use of a range in slice expressions: + ``` + excl_range range :: 0..10 + some_arr[excl_range] # slice by named exclusive range + + incl_range range :: 0..=10 + some_arr[incl_range] # slice by named inclusive range + ``` + +15. broaden type inference from surrounding context ## A word on multi-unwrap diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index c9e9a37..f8b7c80 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -1541,7 +1541,7 @@ merge_infer_local_type :: proc( return false } if types.is_constraint(local.declared) { - if !types.is_concrete_integer(inferred) { + if !types.constraint_accepts(local.declared, inferred) { return false } if !is_runtime_type(checker, local.type) { @@ -1553,7 +1553,7 @@ merge_infer_local_type :: proc( return false } merged := types.widest(local.type, inferred) - if types.is_concrete_integer(merged) { + if types.constraint_accepts(local.declared, merged) { local.type = merged record_infer_local_type(local^, local_types) return true @@ -1605,6 +1605,10 @@ infer_statements :: proc( } if is_runtime_type(checker, declared_local) { value_type = declared_local + } else if types.is_constraint(declared_local) { + // Seed the binding in-family (INVALID on mismatch, which + // build_block reports). FLOAT defaults integers to f64. + value_type = types.constraint_target(declared_local, value_type) } local := Infer_Local{ name=statement.name, @@ -1749,6 +1753,9 @@ infer_spec_locals_and_result :: proc( result := types.INVALID infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint) if types.is_constraint(declared) { + // Params/results use a constraint as a generic passthrough (e.g. an + // identity `func(v int) int` forwarding a range), so the result keeps + // the inferred type as-is rather than being narrowed to the family. return local_types, result } return local_types, declared @@ -2067,6 +2074,25 @@ build_constant_expr :: proc( if types.is_concrete_integer(expected) { recovery_type = expected } + // An integer constant in a float context (e.g. `pi float = 3`) folds to a + // float literal, mirroring build_float_expr's bit packing. + if constant.kind == .Value && types.is_float(expected, checker.target) { + fval := f64(constant.value) // ponytail: silent precision loss past 2^53, like C int->double + bits := transmute(i64)fval + if types.bits(expected, checker.target) == 32 { + bits = i64(transmute(u32)f32(fval)) + } + return add_hir_expr(checker, hir.Expr{ + kind = .Float, + span = expr.span, + type = expected, + integer = bits, + target = hir.INVALID_REF, + left = hir.INVALID_EXPR, + right = hir.INVALID_EXPR, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + } 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) @@ -3427,6 +3453,24 @@ build_block :: proc( (types.is_constraint(declared) || is_undefined_expr(checker, statement.expr)) { declared = ctx.local_types[statement_id] } + // A still-unresolved constraint means the initializer's numeric + // family did not satisfy `int`/`float` (`undefined` reports its own). + if types.is_constraint(declared) && !is_undefined_expr(checker, statement.expr) { + id := source.addf( + checker.diagnostics, + statement.span, + "could not resolve the '%s' constraint for local '%s'", + types.name(declared), + symbol_text(checker, statement.name), + ) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } expected := types.INVALID value := hir.INVALID_EXPR value_type := types.INVALID diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 733c330..fc9071f 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -38,6 +38,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "void": return .Keyword_Void case "bool": return .Keyword_Bool case "int": return .Keyword_Int + case "float": return .Keyword_Float case "i8": return .Keyword_I8 case "i16": return .Keyword_I16 case "i32": return .Keyword_I32 diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index b8796b4..30c4f3b 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -89,7 +89,7 @@ invalid_expr :: proc(parser: ^Parser, span: source.Span, message: string) -> ast is_type_token :: proc(kind: token.Kind) -> bool { #partial switch kind { - case .Keyword_Int, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64, + case .Keyword_Int, .Keyword_Float, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64, .Keyword_U8, .Keyword_U16, .Keyword_U32, .Keyword_U64, .Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64, .Keyword_C_Char, .Keyword_C_Schar, .Keyword_C_Uchar, @@ -216,6 +216,9 @@ parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { case .Keyword_Int: advance(parser) return types.INT + case .Keyword_Float: + advance(parser) + return types.FLOAT case .Keyword_I8: advance(parser) return types.I8 diff --git a/compiler/token/token.odin b/compiler/token/token.odin index b059ad8..f79b78a 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -72,6 +72,7 @@ Kind :: enum u8 { Keyword_Void, Keyword_Bool, Keyword_Int, + Keyword_Float, Keyword_I8, Keyword_I16, Keyword_I32, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 9a5e2e6..f264e8b 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -41,6 +41,7 @@ C_DOUBLE :: Type(27) C_LONGDOUBLE :: Type(28) BOOL :: Type(29) +FLOAT :: Type(30) DYNAMIC_START :: Type(64) @@ -55,6 +56,7 @@ Kind :: enum u8 { Invalid, Void, Int_Constraint, + Float_Constraint, Scalar, Array, Pointer, @@ -295,6 +297,8 @@ kind :: proc(value: Type, store: ^Store = nil) -> Kind { return .Void case INT: return .Int_Constraint + case FLOAT: + return .Float_Constraint case BOOL: return .Scalar } @@ -334,7 +338,37 @@ is_bool :: proc(value: Type) -> bool { } is_constraint :: proc(value: Type) -> bool { - return value == INT + return value == INT || value == FLOAT +} + +// constraint_target reports the concrete type a constraint binding takes for an +// inferred value, or INVALID if the value's numeric family is incompatible. +// FLOAT accepts integers by defaulting them to f64: a constant integer becomes a +// float literal in build_constant_expr, while a runtime integer then fails the +// cross-family f64 coercion in coerce_expr (the intended mismatch error). +constraint_target :: proc(constraint, inferred: Type) -> Type { + switch constraint { + case INT: + return inferred if is_concrete_integer(inferred) else INVALID + case FLOAT: + if is_float(inferred) { + return inferred + } + return F64 if is_concrete_integer(inferred) else INVALID + } + return INVALID +} + +// constraint_accepts reports strict numeric-family membership, used when widening +// a constraint binding across assignments (no integer-to-float defaulting here). +constraint_accepts :: proc(constraint, concrete: Type) -> bool { + switch constraint { + case INT: + return is_concrete_integer(concrete) + case FLOAT: + return is_float(concrete) + } + return false } is_c :: proc(value: Type) -> bool { @@ -1140,6 +1174,7 @@ name :: proc(value: Type) -> string { case VOID: return "void" case BOOL: return "bool" case INT: return "int" + case FLOAT: return "float" case I8: return "i8" case I16: return "i16" case I32: return "i32" diff --git a/compiler_tests.odin b/compiler_tests.odin index 861c516..86f3640 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -5136,6 +5136,209 @@ main :: func() void { testing.expect(t, found_value_i16) } +float_local_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name, local_name: string) -> (types.Type, bool) { + fn_symbol := symbol.intern(symbols, function_name) + loc_symbol := symbol.intern(symbols, local_name) + for function in hir_module.functions { + if function.name != fn_symbol { + continue + } + for local in function.locals { + if local.name == loc_symbol { + return local.type, true + } + } + } + return types.INVALID, false +} + +float_result_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name: string) -> (types.Type, bool) { + fn_symbol := symbol.intern(symbols, function_name) + for function in hir_module.functions { + if function.name == fn_symbol { + return function.result, true + } + } + return types.INVALID, false +} + +@(test) +float_constraint_resolves_to_f64 :: proc(t: ^testing.T) { + text := `make :: func() float { + pi float = 3.14 + return pi +} +main :: func() void { + _ = make() +} +` + 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) + + pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi") + result_type, _ := float_result_type(&hir_module, &symbols, "make") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found) + testing.expect_value(t, pi_type, types.F64) + testing.expect_value(t, result_type, types.F64) +} + +@(test) +float_constraint_accepts_integer_literal :: proc(t: ^testing.T) { + text := `make :: func() float { + pi float = 3 + return pi +} +main :: func() void { + _ = make() +} +` + 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) + + pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found) + testing.expect_value(t, pi_type, types.F64) +} + +@(test) +float_constraint_result_resolves_to_f64 :: proc(t: ^testing.T) { + text := `make :: func() float { + return 3.0 +} +main :: func() void { + _ = make() +} +` + 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) + + result_type, found := float_result_type(&hir_module, &symbols, "make") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found) + testing.expect_value(t, result_type, types.F64) +} + +@(test) +float_constraint_widens_f32_to_f64 :: proc(t: ^testing.T) { + text := `wide :: func(a f32, b f64) float { + x float = a + x = b + return x +} +main :: func() void { + _ = wide(1.0, 2.0) +} +` + 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) + + x_type, found := float_local_type(&hir_module, &symbols, "wide", "x") + result_type, _ := float_result_type(&hir_module, &symbols, "wide") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found) + testing.expect_value(t, x_type, types.F64) + testing.expect_value(t, result_type, types.F64) +} + +@(test) +int_constraint_rejects_float_initializer :: proc(t: ^testing.T) { + text := `main :: func() void { + x int = 1.0 +} +` + 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_constraint_error := false + for diagnostic in diagnostics.items { + found_constraint_error = found_constraint_error || + strings.contains(diagnostic.message, "could not resolve the 'int' constraint for local 'x'") + } + + testing.expect(t, found_constraint_error) +} + +@(test) +float_constraint_rejects_runtime_integer :: proc(t: ^testing.T) { + text := `take :: func(n i32) void { + x float = n +} +main :: func() void { + take(7) +} +` + 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_convert_error := false + for diagnostic in diagnostics.items { + found_convert_error = found_convert_error || + strings.contains(diagnostic.message, "cannot implicitly convert i32 to f64") + } + + testing.expect(t, found_convert_error) +} + @(test) undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) { text := `Point :: struct { @@ -5423,11 +5626,11 @@ compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) unsigned -= 2 unsigned *= 3 unsigned /= 4 - float f64 = 24.0 - float += 6.0 - float -= 2.0 - float *= 3.0 - float /= 4.0 + real f64 = 24.0 + real += 6.0 + real -= 2.0 + real *= 3.0 + real /= 4.0 return signed } `