diff --git a/TODO.md b/TODO.md index 880df41..a0379de 100644 --- a/TODO.md +++ b/TODO.md @@ -181,19 +181,26 @@ - 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. 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 +13. introduce `float` and `range` type constraints (the `int` family generalized) (implemented) + - `float` resolves a local binding to any float scalar (`f32`/`f64`) via static analysis; + widens `f32` -> `f64` across assignments, mirroring how `int` picks the smallest 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 + - `range` is now a spellable type/constraint: `r range :: 0..10` resolves to the inferred + range (element type preserved), and `func(start, end int) range { return start..end }` + monomorphizes the result per call. this replaces the prior `int`-as-passthrough hack that + was the only way to forward a range through a function + - `int`/`float`/`range` constraints now gate by family in every position (previously + params/results were unchecked generic passthroughs): + - a local initializer out of family errors instead of silently taking the natural type + - a param rejects an out-of-family argument (`cannot pass f64 to 'int' parameter 'x'`); + a `float` param accepts an integer-literal argument as f64 (e.g. `f(3)`) + - a function result is narrowed to the constraint's family -14. add slice-by-range +14. broaden type inference to surrounding context + +15. add slice-by-range - allow the use of a range in slice expressions: ``` excl_range range :: 0..10 @@ -203,8 +210,6 @@ some_arr[incl_range] # slice by named inclusive range ``` -15. broaden type inference from surrounding context - ## A word on multi-unwrap Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated. diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index f8b7c80..6916faf 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -538,7 +538,14 @@ call_arg_expected :: proc(function: ast.Function, index: int) -> types.Type { if index < 0 || index >= len(function.params) { return types.INVALID } - return type_from_syntax(function.params[index].type) + declared := type_from_syntax(function.params[index].type) + // A `float` param defaults to f64 so an integer-literal argument builds as a + // float constant (e.g. `f(3)` -> 3.0), mirroring `pi float = 3` for locals. + // `int`/`range` constraints have no single default and keep building naturally. + if declared == types.FLOAT { + return types.F64 + } + return declared } callable_arg_expected :: proc(function_type: types.Type, function_item: types.Node, store: ^types.Store, index: int) -> types.Type { @@ -994,7 +1001,7 @@ find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []t if param_index < len(actual_args) { actual = actual_args[param_index] } - if !types.equal(spec.args[param_index], specialized_param_type(param.type, actual)) { + if !types.equal(spec.args[param_index], specialized_param_type(checker, param.type, actual)) { matches = false break } @@ -1006,10 +1013,14 @@ find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []t return INVALID_SPEC } -specialized_param_type :: proc(syntax: ast.Type_Syntax, actual: types.Type) -> types.Type { +// specialized_param_type maps a parameter's declared type to its monomorphized +// type for a given actual argument. A constraint param (`int`/`float`/`range`) +// resolves to the actual's family member (INVALID if out of family), so a call +// passing an out-of-family argument fails to specialize and is rejected. +specialized_param_type :: proc(checker: ^Checker, syntax: ast.Type_Syntax, actual: types.Type) -> types.Type { declared := type_from_syntax(syntax) if types.is_constraint(declared) { - return actual + return types.constraint_target(declared, actual, &checker.module.types) } return declared } @@ -1020,7 +1031,7 @@ can_specialize :: proc(checker: ^Checker, function: ast.Function, actual_args: [ if index < len(actual_args) { actual = actual_args[index] } - if !is_runtime_type(checker, specialized_param_type(param.type, actual)) { + if !is_runtime_type(checker, specialized_param_type(checker, param.type, actual)) { return false } } @@ -1039,7 +1050,7 @@ ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: [ if index < len(actual_args) { actual = actual_args[index] } - append(&signature, specialized_param_type(param.type, actual)) + append(&signature, specialized_param_type(checker, param.type, actual)) } result := type_from_syntax(function.result) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { @@ -1541,7 +1552,7 @@ merge_infer_local_type :: proc( return false } if types.is_constraint(local.declared) { - if !types.constraint_accepts(local.declared, inferred) { + if !types.constraint_accepts(local.declared, inferred, &checker.module.types) { return false } if !is_runtime_type(checker, local.type) { @@ -1553,7 +1564,7 @@ merge_infer_local_type :: proc( return false } merged := types.widest(local.type, inferred) - if types.constraint_accepts(local.declared, merged) { + if types.constraint_accepts(local.declared, merged, &checker.module.types) { local.type = merged record_infer_local_type(local^, local_types) return true @@ -1608,7 +1619,7 @@ infer_statements :: proc( } 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) + value_type = types.constraint_target(declared_local, value_type, &checker.module.types) } local := Infer_Local{ name=statement.name, @@ -1753,10 +1764,10 @@ 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 + // Narrow the inferred result to the constraint's family; an out-of-family + // result (e.g. returning a non-integer from an `int` function) yields + // INVALID and is rejected downstream. + return local_types, types.constraint_target(declared, result, &checker.module.types) } return local_types, declared } @@ -3230,9 +3241,37 @@ build_expr :: proc( continue } } + arg_violation := source.INVALID_DIAGNOSTIC + { + params := checker.ast_module.functions[frame.template].params + for index in 0..= len(stack[frame_index].arg_types) { + break + } + declared := type_from_syntax(params[index].type) + actual := stack[frame_index].arg_types[index] + if types.is_constraint(declared) && types.is_valid(actual) && + !types.is_valid(types.constraint_target(declared, actual, &checker.module.types)) { + arg_violation = source.addf( + checker.diagnostics, expr.span, + "cannot pass %s to '%s' parameter '%s'", + types.name(actual), types.name(declared), + symbol_text(checker, params[index].name), + ) + break + } + } + } spec := find_spec(checker, frame.template, stack[frame_index].arg_types) delete(stack[frame_index].arg_types, checker.allocator) stack[frame_index].arg_types = nil + if arg_violation != source.INVALID_DIAGNOSTIC { + delete(stack[frame_index].built_args, checker.allocator) + stack[frame_index].built_args = nil + last = invalid_hir_expr(checker, expr.span, arg_violation) + _ = pop(&stack) + continue + } if spec == INVALID_SPEC { id := source.addf( checker.diagnostics, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index fc9071f..7c7de79 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -39,6 +39,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "bool": return .Keyword_Bool case "int": return .Keyword_Int case "float": return .Keyword_Float + case "range": return .Keyword_Range 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 30c4f3b..63d544a 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_Float, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64, + case .Keyword_Int, .Keyword_Float, .Keyword_Range, .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, @@ -219,6 +219,9 @@ parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { case .Keyword_Float: advance(parser) return types.FLOAT + case .Keyword_Range: + advance(parser) + return types.RANGE case .Keyword_I8: advance(parser) return types.I8 diff --git a/compiler/token/token.odin b/compiler/token/token.odin index f79b78a..2d48bbc 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -73,6 +73,7 @@ Kind :: enum u8 { Keyword_Bool, Keyword_Int, Keyword_Float, + Keyword_Range, Keyword_I8, Keyword_I16, Keyword_I32, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index f264e8b..f98a813 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -42,6 +42,7 @@ C_LONGDOUBLE :: Type(28) BOOL :: Type(29) FLOAT :: Type(30) +RANGE :: Type(31) DYNAMIC_START :: Type(64) @@ -57,6 +58,7 @@ Kind :: enum u8 { Void, Int_Constraint, Float_Constraint, + Range_Constraint, Scalar, Array, Pointer, @@ -299,6 +301,8 @@ kind :: proc(value: Type, store: ^Store = nil) -> Kind { return .Int_Constraint case FLOAT: return .Float_Constraint + case RANGE: + return .Range_Constraint case BOOL: return .Scalar } @@ -338,15 +342,17 @@ is_bool :: proc(value: Type) -> bool { } is_constraint :: proc(value: Type) -> bool { - return value == INT || value == FLOAT + return value == INT || value == FLOAT || value == RANGE } -// 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 { +// constraint_target reports the concrete type a constraint binding (local, or a +// param/result monomorphized per call site) takes for an inferred value, or +// INVALID if the value's 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). RANGE accepts any range +// type, keeping its inferred element type. +constraint_target :: proc(constraint, inferred: Type, store: ^Store = nil) -> Type { switch constraint { case INT: return inferred if is_concrete_integer(inferred) else INVALID @@ -355,18 +361,22 @@ constraint_target :: proc(constraint, inferred: Type) -> Type { return inferred } return F64 if is_concrete_integer(inferred) else INVALID + case RANGE: + return inferred if is_range(inferred, store) 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 { +// constraint_accepts reports strict family membership, used when widening a +// constraint binding across assignments (no integer-to-float defaulting here). +constraint_accepts :: proc(constraint, concrete: Type, store: ^Store = nil) -> bool { switch constraint { case INT: return is_concrete_integer(concrete) case FLOAT: return is_float(concrete) + case RANGE: + return is_range(concrete, store) } return false } @@ -1175,6 +1185,7 @@ name :: proc(value: Type) -> string { case BOOL: return "bool" case INT: return "int" case FLOAT: return "float" + case RANGE: return "range" case I8: return "i8" case I16: return "i16" case I32: return "i32" diff --git a/compiler_tests.odin b/compiler_tests.odin index 86f3640..cfa3ab7 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -4841,7 +4841,7 @@ main :: func() i32 { @(test) equal_range_returns_infer_a_usable_result_type :: proc(t: ^testing.T) { - text := `choose :: func(first bool) int { + text := `choose :: func(first bool) range { if first { return 0..1 } @@ -4886,10 +4886,10 @@ main :: func() i32 { @(test) for_loop_lowering_evaluates_once_and_avoids_index_bounds_checks :: proc(t: ^testing.T) { - text := `make_range :: func() int { + text := `make_range :: func() range { return 0..2 } -make_array :: func() int { +make_array :: func() [2]i32 { return [1, 2] } main :: func() i32 { @@ -5339,6 +5339,128 @@ main :: func() void { testing.expect(t, found_convert_error) } +@(test) +range_constraint_local_resolves_to_inferred_range :: proc(t: ^testing.T) { + text := `make :: func() range { + r range :: 0..10 + return r +} +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) + + r_type, found := float_local_type(&hir_module, &symbols, "make", "r") + result_type, _ := float_result_type(&hir_module, &symbols, "make") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found) + testing.expect(t, types.is_range(r_type, &hir_module.types)) + testing.expect_value(t, types.child_type(r_type, &hir_module.types), types.I8) + testing.expect(t, types.is_range(result_type, &hir_module.types)) +} + +@(test) +range_constraint_param_and_result_monomorphize :: proc(t: ^testing.T) { + text := `pass :: func(r range) range { + return r +} +main :: func() void { + once :: 0..5 + for pass(once) |v| { + _ = v + } +} +` + 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, "pass") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found) + testing.expect(t, types.is_range(result_type, &hir_module.types)) +} + +@(test) +int_param_rejects_float_argument :: proc(t: ^testing.T) { + text := `take :: func(x int) int { + return x +} +main :: func() void { + _ = take(1.5) +} +` + 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_reject := false + for diagnostic in diagnostics.items { + found_reject = found_reject || + strings.contains(diagnostic.message, "cannot pass f64 to 'int' parameter 'x'") + } + + testing.expect(t, found_reject) +} + +@(test) +float_param_accepts_integer_literal_argument :: proc(t: ^testing.T) { + text := `take :: func(x float) float { + return x +} +main :: func() void { + y f64 = take(3) + _ = y +} +` + 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, "take") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found) + testing.expect_value(t, result_type, types.F64) +} + @(test) undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) { text := `Point :: struct { diff --git a/examples/programs/for_loop/main.bro b/examples/programs/for_loop/main.bro index 2e91106..2c0a632 100644 --- a/examples/programs/for_loop/main.bro +++ b/examples/programs/for_loop/main.bro @@ -1,6 +1,6 @@ # Milestone 5: ranges and sequence for loops. -pass :: func(value int) int { +pass :: func(value range) range { return value } diff --git a/examples/programs/for_loop_edges/main.bro b/examples/programs/for_loop_edges/main.bro index e616e43..1b78ac7 100644 --- a/examples/programs/for_loop_edges/main.bro +++ b/examples/programs/for_loop_edges/main.bro @@ -1,4 +1,4 @@ -make_range :: func(calls @mut i32, end usize) int { +make_range :: func(calls @mut i32, end usize) range { calls^ = calls^ + 1 return 0..end }