From c2343b54bb6c4b88d01852d5986d2dedb5a2252c Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Tue, 11 Aug 2026 20:12:12 +0200 Subject: [PATCH] usize indexing only --- LANGUAGE.md | 7 +- README.md | 2 +- compiler/checker/checker.odin | 64 ++++++----- compiler/checker/comptime.odin | 100 +++++++++++++++--- compiler_tests.odin | 88 ++++++++++++++- examples/programs/distinct_types/main.bro | 4 +- .../main.bro | 2 +- examples/programs/milestone_24/main.bro | 2 +- 8 files changed, 216 insertions(+), 53 deletions(-) rename examples/programs/{index_int_constraint_error => index_unsigned_error}/main.bro (86%) diff --git a/LANGUAGE.md b/LANGUAGE.md index 2ca386b..991bdfc 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -63,8 +63,9 @@ nominal result type: checked integer `+`, `-`, `*`, unary `-`, bitwise operators comparisons, and compound assignments; float arithmetic, unary `-`, comparisons, and compound assignments; and boolean equality/inequality. Integer literals and float literals are contextual. Typed backing values and separate distinct identities remain incompatible in ordinary operations; -an explicit constructor is required to cross that boundary. Distinct integers also work as indices and slice bounds; -`minval!` / `maxval!` return the distinct type. Runtime and comptime behavior match. +an explicit constructor is required to cross that boundary. Distinct integers require an explicit +`usize` cast for indices and slice bounds; `minval!` / `maxval!` return the distinct type. +Runtime and comptime behavior match. `typeinfo!(Distinct).backing` reports the immediate declared backing. Standard formatting peels distinct layers recursively, so all scalar format verbs behave like the final scalar backing. @@ -117,7 +118,7 @@ fields. `_` is not a keyword member name. - checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or` - Zig-style integer bitwise complement `~`, binary `&`, `|`, `xor`, shifts `<<` / `>>`, and saturating left shift `<<|`; postfix `^` remains pointer dereference - assignments and compound assignments `+= -= *= /= &= |= xor= <<= >>= <<|=` with single evaluation of complex lvalues; `/=` is float-only and `xor=` is contiguous -- field access through struct values and pointers, index/slice bounds contextually coerced to `usize`, and unsigned narrower index support +- field access through struct values and pointers, exact `usize` indices and slice bounds, and contextual integer constants in those positions - boolean `if` / `else if` / `else` and `for` loops with braceless single-statement bodies when the preceding expression is parenthesized or a function call - `while` loops with conditional unwrap captures and guards plus optional post-iteration update clauses - `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures; `inline for` specializes a comptime aggregate into one checked body per element diff --git a/README.md b/README.md index 5207914..9620c07 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ Current prototype features: - Pointer-preserving `.ptr`/`.len`, pointer-to-array indexing and slicing, postfix pointer dereference and optional unwrap, and keyed struct literals - Contextual integer constants and typed compile-time evaluation of arithmetic and Zig-style bitwise expressions - Integer `~`, `&`, `|`, `xor`, guarded `<<` / `>>`, saturating `<<|`, and their compound assignments; postfix `^` remains pointer dereference -- Scalar-backed nominal `distinct` types with same-identity runtime/comptime operators, explicit backing extraction casts, integer bounds/indexing, reflection, and recursive standard formatting +- Scalar-backed nominal `distinct` types with same-identity runtime/comptime operators, explicit backing extraction casts, integer bounds, reflection, and recursive standard formatting - Directory packages with merged declarations and file-local relative imports - Relative C header imports as synthetic package namespaces - Plain imported C structs/unions, fixed arrays, and C function pointer typedefs, including keyed literals, field access, callbacks, and Apple Silicon by-value ABI lowering diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index ca829af..95b3562 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -5108,7 +5108,8 @@ infer_compound_expr :: proc( return types.child_type(value, store) if types.is_pointer(value, store) else types.INVALID case .Index: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) - _ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types) + _ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types, types.USIZE) + _ = record_demand(checker, expr.right, types.USIZE, locals, local_types, pkg, file) item, ok := types.container(value, store) return item.child if ok else types.INVALID case .Slice: @@ -5124,7 +5125,8 @@ infer_compound_expr :: proc( } for bound in expr.args { if bound != ast.INVALID_EXPR { - _ = infer_nested_expr(checker, bound, locals, pkg, file, demanded, local_types) + _ = infer_nested_expr(checker, bound, locals, pkg, file, demanded, local_types, types.USIZE) + _ = record_demand(checker, bound, types.USIZE, locals, local_types, pkg, file) } } preserve := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR @@ -8100,36 +8102,35 @@ build_scalar_cast :: proc( diagnostic=source.INVALID_DIAGNOSTIC, }) } -normalize_index_expr :: proc(checker: ^Checker, value: hir.Expr_Id, span: source.Span) -> hir.Expr_Id { - store := &checker.module.types - current := value - current_type := checker.module.exprs[current].type - representation, distinct_ok := types.distinct_scalar_backing(current_type, store) - if distinct_ok && types.is_concrete_integer(representation) { - for { - backing, ok := types.distinct_backing(current_type, store) - if !ok { - break - } - current = add_hir_expr(checker, hir.Expr{ - kind=.Retype, - span=span, - type=backing, - left=current, - target=hir.INVALID_REF, - right=hir.INVALID_EXPR, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - current_type = backing - } +normalize_index_expr :: proc( + checker: ^Checker, + value: hir.Expr_Id, + span: source.Span, + label: string, +) -> hir.Expr_Id { + if value == hir.INVALID_EXPR { + return value } - current_expr := &checker.module.exprs[current] + if _, invalid := invalid_expr_diagnostic(checker, value); invalid { + return value + } + current_expr := &checker.module.exprs[value] if current_expr.kind == .Integer && fits_integer_type(i128(current_expr.integer), types.USIZE, checker.target) { current_expr.type = types.USIZE - return current + return value } - return coerce_expr(checker, current, types.USIZE, span) + if !types.equal(current_expr.type, types.USIZE) { + id := source.addf( + checker.diagnostics, + span, + "%s must have type usize, got %s", + label, + type_label(checker, current_expr.type), + ) + return invalid_hir_expr(checker, span, id, types.USIZE) + } + return value } @@ -8690,7 +8691,7 @@ build_compound_expr :: proc( case .Index: container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) index := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) - index = normalize_index_expr(checker, index, expr.span) + index = normalize_index_expr(checker, index, expr.span, "index") if invalid, propagated := propagate_invalid_expr(checker, expr.span, container, index); propagated { return invalid } @@ -8734,7 +8735,12 @@ build_compound_expr :: proc( for bound, index in expr.args { if bound != ast.INVALID_EXPR { bounds[index] = build_nested_expr(checker, bound, locals, global_reads, calls, types.INVALID, pkg, file) - bounds[index] = normalize_index_expr(checker, bounds[index], checker.ast_module.exprs[bound].span) + bounds[index] = normalize_index_expr( + checker, + bounds[index], + checker.ast_module.exprs[bound].span, + "slice bound", + ) if invalid, propagated := propagate_invalid_expr(checker, expr.span, bounds[index]); propagated { delete(bounds, checker.allocator) return invalid diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 254a0f3..de37cd2 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -291,6 +291,7 @@ Ct_Binding :: struct { value: Ct_Value_Id, cell: Ct_Cell_Id, mutable: bool, + open_integer: bool, } Ct_Error_Refinement :: struct { @@ -473,9 +474,23 @@ ct_extend_place :: proc( return ct_add_place(state, base.cell, value_type, writable, extended[:]) } -ct_bind_value :: proc(state: ^Ct_State, name: symbol.Id, value_type: types.Type, value: Ct_Value_Id, mutable: bool) { +ct_bind_value :: proc( + state: ^Ct_State, + name: symbol.Id, + value_type: types.Type, + value: Ct_Value_Id, + mutable: bool, + open_integer := false, +) { cell := ct_add_cell(state, value, mutable) - append(&state.bindings, Ct_Binding{name=name, type=value_type, value=value, cell=cell, mutable=mutable}) + append(&state.bindings, Ct_Binding{ + name=name, + type=value_type, + value=value, + cell=cell, + mutable=mutable, + open_integer=open_integer, + }) } ct_pop_bindings :: proc(state: ^Ct_State, start: int) { @@ -581,6 +596,32 @@ ct_binding_value :: proc(state: ^Ct_State, index: int) -> Ct_Value_Id { return binding.value } +ct_contextualize_open_integer_binding :: proc(state: ^Ct_State, index: int, expected: types.Type) { + if expected != types.USIZE || index < 0 || index >= len(state.bindings) || + !state.bindings[index].open_integer { + return + } + value_id := ct_binding_value(state, index) + if value_id == INVALID_CT_VALUE || int(value_id) >= len(state.values) { + return + } + value := state.values[value_id] + if value.kind != .Integer || + !fits_integer_type(value.integer, expected, state.checker.target) { + return + } + value.type = expected + contextual := ct_add_value(state, value) + binding := &state.bindings[index] + binding.type = expected + binding.value = contextual + binding.open_integer = false + if binding.cell != INVALID_CT_CELL && int(binding.cell) < len(state.cells) && + state.cells[binding.cell].live { + state.cells[binding.cell].value = contextual + } +} + ct_binding_place :: proc(state: ^Ct_State, index: int) -> Ct_Place_Id { if index < 0 || index >= len(state.bindings) { return INVALID_CT_PLACE @@ -1315,6 +1356,7 @@ ct_eval_expr :: proc( case .Name: if !symbol.is_valid(expr.qualifier) { if index, ok := ct_find_binding_index(state, expr.name); ok { + ct_contextualize_open_integer_binding(state, index, expected) return ct_observe_value(state, ct_binding_value(state, index), expr.span) } if value, ok := current_comptime_value(checker, expr.name); ok { @@ -1452,16 +1494,19 @@ ct_eval_expr :: proc( } return ct_eval_field_value(state, base_id, expr.name, expr.span) case .Index: - index_id, index_flow, index_ok := ct_eval_expr(state, expr.right, types.INVALID, depth+1) + index_id, index_flow, index_ok := ct_eval_expr(state, expr.right, types.USIZE, depth+1) if !index_ok || index_flow.kind != .Normal { return INVALID_CT_VALUE, index_flow, index_ok } index_type := state.values[index_id].type - index_representation := types.runtime_representation(index_type, store) - index_literal := is_numeric_constant_expr(checker, expr.right) - if !types.is_concrete_integer(index_representation) || - !index_literal && !can_implicitly_convert_type(checker, index_representation, types.USIZE) { - return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime index must be coercible to usize") + if !types.equal(index_type, types.USIZE) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( + state, + .Not_Comptime, + checker.ast_module.exprs[expr.right].span, + "index must have type usize, got %s", + type_label(checker, index_type), + ) } index_value, index_is_int := ct_integer_value(state, index_id) if !index_is_int || index_value < 0 || index_value > i128(0x7fff_ffff) { @@ -1962,16 +2007,19 @@ ct_eval_slice_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_ if bound == ast.INVALID_EXPR { continue } - value, bound_flow, bound_ok := ct_eval_expr(state, bound, types.INVALID, depth+1) + value, bound_flow, bound_ok := ct_eval_expr(state, bound, types.USIZE, depth+1) if !bound_ok || bound_flow.kind != .Normal { return INVALID_CT_VALUE, bound_flow, bound_ok } bound_type := state.values[value].type - bound_representation := types.runtime_representation(bound_type, store) - bound_literal := is_numeric_constant_expr(checker, bound) - if !types.is_concrete_integer(bound_representation) || - !bound_literal && !can_implicitly_convert_type(checker, bound_representation, types.USIZE) { - return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "slice bounds must be coercible to usize") + if !types.equal(bound_type, types.USIZE) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( + state, + .Not_Comptime, + checker.ast_module.exprs[bound].span, + "slice bound must have type usize, got %s", + type_label(checker, bound_type), + ) } integer, integer_ok := ct_integer_value(state, value) if !integer_ok || integer < 0 || integer > i128(0x7fff_ffff) { @@ -2352,6 +2400,16 @@ ct_eval_place :: proc( if !index_ok || index_flow.kind != .Normal { return INVALID_CT_PLACE, types.INVALID, false, index_flow, index_ok } + index_type := state.values[index_value].type + if !types.equal(index_type, types.USIZE) { + return INVALID_CT_PLACE, types.INVALID, false, ct_flow(.Normal), ct_failf( + state, + .Not_Comptime, + checker.ast_module.exprs[expr.right].span, + "index must have type usize, got %s", + type_label(checker, index_type), + ) + } index_int, int_ok := ct_integer_value(state, index_value) if !int_ok || index_int < 0 || index_int > i128(0x7fff_ffff) { return INVALID_CT_PLACE, types.INVALID, false, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime index must be a non-negative integer") @@ -4983,6 +5041,11 @@ ct_exec_statements :: proc( } else { declared := type_from_syntax(checker, statement.type, state.pkg, state.file, active_state=state) expected := declared if types.is_valid(declared) && !types.is_void(declared) else types.INVALID + open_integer := false + if !is_runtime_type(checker, declared) { + constant := eval_integer_constant_in_context(checker, statement.expr, state.pkg, state.file) + open_integer = constant.kind == .Value + } value, expr_flow, expr_ok := ct_eval_expr(state, statement.expr, expected, depth+1) ok = expr_ok flow = expr_flow @@ -4991,7 +5054,14 @@ ct_exec_statements :: proc( value, ok = ct_coerce_value(state, value, expected, statement.span) } if ok && statement.name != checker.sink_symbol { - ct_bind_value(state, statement.name, state.values[value].type, value, !statement.immutable) + ct_bind_value( + state, + statement.name, + state.values[value].type, + value, + !statement.immutable, + open_integer=open_integer, + ) } } } diff --git a/compiler_tests.odin b/compiler_tests.odin index efe87a4..bc27921 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -1867,6 +1867,92 @@ many_item_pointer_slices_require_end_bound :: proc(t: ^testing.T) { testing.expect_value(t, end_bound_errors, 2) } +@(test) +indices_and_slice_bounds_require_usize :: proc(t: ^testing.T) { + text := `Index :: distinct u32 +main func() void { + values [2]i32 := [1, 2] + index := 1 + start := 0 + end := 1 + small u32 := 0 + id Index := Index(0) + _ = values[index] + _ = values[start..end] + _ = values[small] + _ = values[id] + _ = values[small..] + _ = values[..id] +} +` + 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) + + index_errors, bound_errors := 0, 0 + for diagnostic in diagnostics.items { + index_errors += 1 if strings.contains(diagnostic.message, "index must have type usize") else 0 + bound_errors += 1 if strings.contains(diagnostic.message, "slice bound must have type usize") else 0 + } + testing.expect_value(t, len(diagnostics.items), 4) + testing.expect_value(t, index_errors, 2) + testing.expect_value(t, bound_errors, 2) +} + +@(test) +comptime_indices_infer_usize_and_reject_explicit_integer_types :: proc(t: ^testing.T) { + text := `good func($items [2]u8) u8 { + start := 0 + end := 1 + part :: items[start..end] + return part[start] +} +bad_index func($items [2]u8) u8 { + cursor u32 := 0 + return items[cursor] +} +bad_bound func($items [2]u8) u8 { + start u32 := 0 + return items[start..][0] +} +GOOD :: $good([7, 8]) +BAD_INDEX :: $bad_index([7, 8]) +BAD_BOUND :: $bad_bound([7, 8]) +main func() void { + _ = GOOD + _ = BAD_INDEX + _ = BAD_BOUND +} +` + 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) + + testing.expect_value(t, len(diagnostics.items), 2) + index_error, bound_error := false, false + for diagnostic in diagnostics.items { + index_error = index_error || strings.contains(diagnostic.message, "index must have type usize, got u32") + bound_error = bound_error || strings.contains(diagnostic.message, "slice bound must have type usize, got u32") + } + testing.expect(t, index_error && bound_error) +} + @(test) layout_builtins_compile_and_run :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-layout-builtins" @@ -6107,7 +6193,7 @@ milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) { } cases := [?]Case{ {"examples/programs/index_signed_error", "/tmp/brolang-test-index-signed-error"}, - {"examples/programs/index_int_constraint_error", "/tmp/brolang-test-index-int-constraint-error"}, + {"examples/programs/index_unsigned_error", "/tmp/brolang-test-index-unsigned-error"}, {"examples/programs/scalar_cast_error", "/tmp/brolang-test-scalar-cast-error"}, {"examples/programs/array_const_size_error", "/tmp/brolang-test-array-const-size-error"}, } diff --git a/examples/programs/distinct_types/main.bro b/examples/programs/distinct_types/main.bro index 4707213..10ac14c 100644 --- a/examples/programs/distinct_types/main.bro +++ b/examples/programs/distinct_types/main.bro @@ -82,8 +82,8 @@ main func() i32 { if bits != 255 { return 10 } values [10]u8 := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - if values[LocalID(4)] != 4 { return 11 } - section []u8 := values[LocalID(2)..LocalID(5)] + if values[usize(LocalID(4))] != 4 { return 11 } + section []u8 := values[usize(LocalID(2))..usize(LocalID(5))] if section.len != 3 or section[usize(0)] != 2 or section[usize(2)] != 4 { return 12 } if u32(id) != 7 or usize(id) != 7 or f64(id) != 7.0 { return 13 } diff --git a/examples/programs/index_int_constraint_error/main.bro b/examples/programs/index_unsigned_error/main.bro similarity index 86% rename from examples/programs/index_int_constraint_error/main.bro rename to examples/programs/index_unsigned_error/main.bro index 156191e..db7ef02 100644 --- a/examples/programs/index_int_constraint_error/main.bro +++ b/examples/programs/index_unsigned_error/main.bro @@ -1,6 +1,6 @@ main func() i32 { items [3]mut i32 := undefined - i int := 1 + i u32 := 1 items[i] = 42 return 0 } diff --git a/examples/programs/milestone_24/main.bro b/examples/programs/milestone_24/main.bro index 9e70e10..04caf09 100644 --- a/examples/programs/milestone_24/main.bro +++ b/examples/programs/milestone_24/main.bro @@ -52,7 +52,7 @@ main func() i32 { items[0] = 10 items[1] = 20 - idx u8 := 2 + idx usize := 2 items[idx] = items[0] + items[1] if (items[2] != 30) return 1