From 8ab5b71a0c5ce41306d0ceb4f253955ca43d2576 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 23 Oct 2026 11:18:08 +0200 Subject: [PATCH] feat: support checked explicit sentinel slices --- LANGUAGE.md | 24 ++- compiler/checker/checker.odin | 52 ++++++- compiler/checker/comptime.odin | 120 +++++++++++++-- compiler/llvm/llvm.odin | 19 ++- compiler/lower/lower.odin | 6 +- compiler/parser/parser.odin | 12 +- compiler_tests.odin | 263 +++++++++++++++++++++++++++++++-- 7 files changed, 465 insertions(+), 31 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index f7798d9..ff6fb94 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -34,7 +34,7 @@ roadmap and milestone history. - compile-time `minval!(T)` and `maxval!(T)` bounds for concrete native, C, and scalar-backed distinct integer types; the result retains `T` - arrays `[N]T`, inferred-count arrays `[_]T`, sentinel arrays `[N;S]T`, compile-time expression array counts, slices `[]T` / `[;S]T`, single-item pointers `@T`, many-item pointers `*T`, and sentinel many-item pointers `[*;S]T` - pointer mutability via `mut`, optional pointers as nullable pointers, pointer arithmetic for many-item pointers, postfix dereference `^`, and trapping optional unwrap `?` -- pointer-to-array `.len`, indexing, slicing, `.ptr` on slices and pointers-to-arrays, implicit address-taking for array-variable slices, and pointer/slice sentinel weakening +- pointer-to-array `.len`, indexing, slicing, `.ptr` on slices and pointers-to-arrays, implicit address-taking for array-variable slices, pointer/slice sentinel weakening, and checked explicit sentinel slicing `base[start..end;S]` - `ptrcast!(ptr)` infers its complete pointer result from context; `ptrcast!(P, ptr)` supplies `P` explicitly; both change the child type, then permit only ordinary implicit pointer conversions to the requested target - unsafe `constcast!(value)` for restoring mutability to pointers, optional pointers, and slices without changing their child type or shape - UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, supporting `\\`, `\"`, `\n`, `\r`, `\t`, `\0`, and `\xNN` escapes, plus raw backtick multiline strings @@ -48,6 +48,28 @@ roadmap and milestone history. - fallible channel types `T ! E`, where `E` is a native enum, native struct, tagged union, or supported sum composition; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield` - bodyful local functions and root `main` may write `T!` to infer a specialization-local error channel from propagated `try` expressions and concretely typed error returns; inference composes only existing named error types, never synthesizes variants, and requires at least one inferred error +#### checked sentinel slicing + +`base[start..end;S]` constructs a sentinel slice `[;S]T` without copying elements. +The sentinel `S` must be a compile-time integer representable by the source element type `T`; +the result retains the source's element mutability. Bounds use `usize`, as in ordinary slicing. +`start` defaults to zero; `end` may be omitted for arrays, pointers-to-arrays, and slices, where +it defaults to the logical length. Many-item pointers, including sentinel pointers, always +require an explicit end: `pointer[..count;0]`. + +The result contains elements `[start, end)` and excludes the sentinel at `end`. +Before constructing it, runtime checks require ordered in-range bounds, a readable endpoint, +and `base[end] == S`; failure traps. For a bounded source without a sentinel, `end` must be +strictly less than its length. A sentinel source permits `end == len`, reading its stored +sentinel. A many-item pointer has no length to check: the caller must provide valid memory +through the endpoint, and the endpoint value is still checked. Comptime evaluation enforces +the same endpoint and sentinel requirements and diagnoses failures. + +Ordinary `base[start..end]` slicing keeps its existing bounds rules and preserves a source +sentinel only when the end bound is omitted. Explicit sentinel slicing can assert a different +sentinel only when the actual endpoint matches it; it never adds a terminator or restores +mutability. + #### distinct types `Name :: distinct T` creates a nominal identity and reuses `T`'s runtime representation. When diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 7bb56ca..3b3a29a 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -400,6 +400,9 @@ block_reads_name :: proc(checker: ^Checker, statements: []ast.Stmt_Id, name: sym case .Call, .Array, .Struct_Literal, .Slice: append(&expr_stack, ..expr.args) append(&expr_stack, expr.left) + if expr.kind == .Slice { + append(&expr_stack, expr.right) + } case .Negate, .Not, .Bit_Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast: append(&expr_stack, expr.left) case .Comptime: @@ -743,12 +746,18 @@ comptime_slice_bytes :: proc( pkg: ast.Package_Id, file: ast.File_Id, ) -> (string, bool) { + if expr.right != ast.INVALID_EXPR { + return "", false + } state := ct_state_make(checker, pkg, file, diagnose=false) defer ct_state_destroy(&state) value, flow, ok := ct_eval_slice_expr(&state, expr, 0) if !ok || flow.kind != .Normal { return "", false } + if item, item_ok := types.container(state.values[value].type, &checker.module.types); item_ok && item.has_sentinel { + return "", false + } return ct_value_bytes(&state, value) } @@ -2753,6 +2762,15 @@ match_inferred_type_pattern :: proc( values, bound, span, diagnose, depth+1, ) } + if actual_ok && actual_item.kind == .Slice { + if pattern_item.has_sentinel && (!actual_item.has_sentinel || pattern_item.sentinel != actual_item.sentinel) { + return false + } + return match_inferred_type_pattern( + checker, function, prefix, pattern_item.child, actual_item.child, + values, bound, span, diagnose, depth+1, + ) + } } if !actual_ok || pattern_item.kind != actual_item.kind { resolved := type_from_syntax(checker, pattern, function.pkg, function.file) @@ -3786,6 +3804,9 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as if expr.left != ast.INVALID_EXPR { append(&stack, expr.left) } + if expr.kind == .Slice && expr.right != ast.INVALID_EXPR { + append(&stack, expr.right) + } case .Negate, .Not, .Bit_Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast: append(&stack, expr.left) case .Comptime: @@ -5168,6 +5189,15 @@ infer_compound_expr :: proc( _ = record_demand(checker, bound, types.USIZE, locals, local_types, pkg, file) } } + if expr.right != ast.INVALID_EXPR { + state := ct_state_make(checker, pkg, file, diagnose=false) + defer ct_state_destroy(&state) + sentinel, sentinel_ok := checked_slice_sentinel(&state, expr.right, item.child) + if !sentinel_ok { + return types.INVALID + } + return types.slice(store, item.child, item.mutable, true, u64(sentinel)) + } preserve := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR return types.slice(store, item.child, item.mutable, preserve, item.sentinel) case .Field: @@ -8773,6 +8803,22 @@ build_compound_expr :: proc( id := source.add(checker.diagnostics, expr.span, "many-item pointer slicing requires an explicit end bound") return invalid_hir_expr(checker, expr.span, id) } + sentinel_expr := hir.INVALID_EXPR + sentinel := item.sentinel + if expr.right != ast.INVALID_EXPR { + state := ct_state_make(checker, pkg, file) + defer ct_state_destroy(&state) + constant, sentinel_ok := checked_slice_sentinel(&state, expr.right, item.child) + if !sentinel_ok { + return invalid_hir_expr(checker, expr.span, state.diagnostic) + } + sentinel = u64(constant) + sentinel_expr = add_hir_expr(checker, hir.Expr{ + kind=.Integer, span=checker.ast_module.exprs[expr.right].span, type=item.child, integer=i64(sentinel), + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } bounds := make([]hir.Expr_Id, 2, checker.allocator) bounds[0] = hir.INVALID_EXPR bounds[1] = hir.INVALID_EXPR @@ -8791,11 +8837,11 @@ build_compound_expr :: proc( } } } - preserve_sentinel := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR - result_type := types.slice(store, item.child, item.mutable, preserve_sentinel, item.sentinel) + preserve_sentinel := expr.right != ast.INVALID_EXPR || item.has_sentinel && expr.args[1] == ast.INVALID_EXPR + result_type := types.slice(store, item.child, item.mutable, preserve_sentinel, sentinel) return add_hir_expr(checker, hir.Expr{ kind=.Slice, span=expr.span, type=result_type, args=bounds, left=container, - target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, + target=hir.INVALID_REF, right=sentinel_expr, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Field: if enum_type, enum_ok := enum_type_from_field_expr(checker, expr, pkg, file, true); enum_ok { diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index de37cd2..86b32b0 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -792,6 +792,17 @@ ct_place_get :: proc(state: ^Ct_State, place_id: Ct_Place_Id) -> (Ct_Value_Id, b #partial switch elem.kind { case .Index: index := int(elem.index) + if value.kind == .Array { + item, ok := types.node(&state.checker.module.types, value.type) + if ok && item.has_sentinel && u64(index) == item.count { + integer := i128(item.sentinel) + if types.is_signed(types.resolve_alias(item.child, &state.checker.module.types), state.checker.target) { + integer = i128(i64(item.sentinel)) + } + current = ct_add_value(state, Ct_Value{kind=.Integer, type=item.child, integer=integer}) + continue + } + } if value.kind != .Array || index < 0 || index >= len(children) { return INVALID_CT_VALUE, false } @@ -963,7 +974,7 @@ ct_value_can_materialize :: proc(state: ^Ct_State, id: Ct_Value_Id, depth := 0) store := &state.checker.module.types slice, slice_ok := types.node(store, value.type) place_id := Ct_Place_Id(value.index) - if !slice_ok || slice.kind != .Slice || slice.mutable || value.start != 0 || + if !slice_ok || slice.kind != .Slice || slice.mutable || place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) { return false } @@ -977,7 +988,7 @@ ct_value_can_materialize :: proc(state: ^Ct_State, id: Ct_Value_Id, depth := 0) } root := state.values[root_id] array, array_ok := types.node(store, root.type) - return array_ok && array.kind == .Array && u64(value.count) == array.count && + return array_ok && array.kind == .Array && u64(value.start)+u64(value.count) <= array.count && types.equal(slice.child, array.child) && ct_value_can_materialize(state, root_id, depth+1) case .Range, .Array, .Struct, .Optional_Some, .Fallible: for child in ct_child_slice(state, value) { @@ -1065,7 +1076,7 @@ ct_materialize_array_slice :: proc( store := &state.checker.module.types slice, slice_ok := types.node(store, value.type) place_id := Ct_Place_Id(value.index) - if !slice_ok || slice.kind != .Slice || slice.mutable || value.start != 0 || + if !slice_ok || slice.kind != .Slice || slice.mutable || place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) { return hir.INVALID_EXPR, false } @@ -1079,7 +1090,7 @@ ct_materialize_array_slice :: proc( } root := state.values[root_id] array, array_ok := types.node(store, root.type) - if !array_ok || array.kind != .Array || u64(value.count) != array.count || + if !array_ok || array.kind != .Array || u64(value.start)+u64(value.count) > array.count || !types.equal(slice.child, array.child) { return hir.INVALID_EXPR, false } @@ -1093,7 +1104,28 @@ ct_materialize_array_slice :: proc( if !ok { return hir.INVALID_EXPR, false } - return coerce_expr(state.checker, address, value.type, span), true + checker := state.checker + bounds := make([]hir.Expr_Id, 2, checker.allocator) + endpoints := [2]u32{value.start, value.start+value.count} + for integer, index in endpoints { + bounds[index] = add_hir_expr(checker, hir.Expr{ + kind=.Integer, span=span, type=types.USIZE, integer=i64(integer), + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + sentinel_expr := hir.INVALID_EXPR + if slice.has_sentinel { + sentinel_expr = add_hir_expr(checker, hir.Expr{ + kind=.Integer, span=span, type=slice.child, integer=i64(slice.sentinel), + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + return add_hir_expr(checker, hir.Expr{ + kind=.Slice, span=span, type=value.type, args=bounds, left=address, right=sentinel_expr, + target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, + }), true } ct_materialize_value :: proc( @@ -1223,7 +1255,7 @@ ct_materialize_value :: proc( return result } if state.diagnostic == source.INVALID_DIAGNOSTIC { - state.diagnostic = source.add(checker.diagnostics, span, "only immutable full-array comptime slices can materialize as runtime memory") + state.diagnostic = source.add(checker.diagnostics, span, "only immutable array-backed comptime slices can materialize as runtime memory") } return invalid_hir_expr(checker, span, state.diagnostic, value.type) case .Function: @@ -1948,6 +1980,22 @@ ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Ty return ct_add_value(state, Ct_Value{kind=.Struct, type=struct_type, start=start, count=u32(len(values)), active=active_field}), ct_flow(.Normal), true } +checked_slice_sentinel :: proc(state: ^Ct_State, id: ast.Expr_Id, child: types.Type) -> (i128, bool) { + checker := state.checker + span := checker.ast_module.exprs[id].span + previous_error := state.error + constant := eval_integer_constant_in_state(state, id, allow_bool=false) + state.error = previous_error + if constant.kind != .Value { + return 0, ct_fail(state, .Not_Comptime, span, "slice sentinel must be a compile-time integer") + } + representation := types.resolve_alias(child, &checker.module.types) + if !types.is_concrete_integer(representation) || !fits_integer_type(constant.value, representation, checker.target) { + return 0, ct_failf(state, .Not_Comptime, span, "slice sentinel value does not fit element type %s", type_label(checker, child)) + } + return constant.value, true +} + ct_eval_slice_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { checker := state.checker store := &checker.module.types @@ -1958,6 +2006,7 @@ ct_eval_slice_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_ container_place := INVALID_CT_PLACE container_start := 0 container_len := 0 + bounded := true item: types.Node item_ok := false if base_value != INVALID_CT_VALUE && int(base_value) < len(state.values) { @@ -1967,6 +2016,25 @@ ct_eval_slice_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_ container_start = int(base.start) container_len = int(base.count) item, item_ok = types.node(store, base.type) + } else if base.kind == .Array && checker.ast_module.exprs[expr.left].kind == .Array && expr.right != ast.INVALID_EXPR { + item, item_ok = types.node(store, base.type) + container_place = ct_add_place(state, ct_add_cell(state, base_value, false), base.type, false) + container_len = int(base.count) + item.mutable = false + } else if base.kind == .String && expr.right != ast.INVALID_EXPR && base.index < u64(len(checker.ast_module.strings)) { + text := checker.ast_module.strings[base.index] + array_type := types.array(store, types.U8, u64(len(text)), false, true, 0) + children := make([]Ct_Value_Id, len(text), checker.allocator) + defer delete(children, checker.allocator) + for byte, index in transmute([]u8)text { + children[index] = ct_add_value(state, Ct_Value{kind=.Integer, type=types.U8, integer=i128(byte)}) + } + child_start := u32(len(state.children)) + append(&state.children, ..children) + array_value := ct_add_value(state, Ct_Value{kind=.Array, type=array_type, start=child_start, count=u32(len(text))}) + container_place = ct_add_place(state, ct_add_cell(state, array_value, false), array_type, false) + container_len = len(text) + item, item_ok = types.container(base.type, store) } else if base.kind == .Pointer { pointer_item, pointer_ok := types.node(store, base.type) array_item: types.Node @@ -1980,6 +2048,15 @@ ct_eval_slice_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_ item = array_item item.mutable = pointer_item.mutable && array_item.mutable item_ok = true + } else if pointer_ok && pointer_item.many { + if expr.args[1] == ast.INVALID_EXPR { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "many-item pointer slicing requires an explicit end bound") + } + container_place = Ct_Place_Id(base.index) + container_start = int(base.active) + item = pointer_item + item_ok = true + bounded = false } } } @@ -2031,11 +2108,31 @@ ct_eval_slice_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_ end = int(integer) } } - if start < 0 || end < start || end > container_len { + if start < 0 || end < start || bounded && end > container_len || container_start < 0 { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime slice bounds out of range") } - preserve_sentinel := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR - result_type := types.slice(store, item.child, item.mutable, preserve_sentinel, item.sentinel) + sentinel := item.sentinel + if expr.right != ast.INVALID_EXPR { + constant, sentinel_ok := checked_slice_sentinel(state, expr.right, item.child) + if !sentinel_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + if bounded && (end == container_len && !item.has_sentinel) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime sentinel slice endpoint out of range") + } + endpoint := ct_extend_place(state, container_place, Ct_Path_Elem{kind=.Index, index=u32(container_start+end)}, item.child, false) + value, value_ok := ct_place_get(state, endpoint) + if !value_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime sentinel slice endpoint out of range") + } + actual, actual_ok := ct_integer_value(state, value) + if !actual_ok || actual != constant { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime slice sentinel mismatch") + } + sentinel = u64(constant) + } + preserve_sentinel := expr.right != ast.INVALID_EXPR || item.has_sentinel && expr.args[1] == ast.INVALID_EXPR + result_type := types.slice(store, item.child, item.mutable, preserve_sentinel, sentinel) return ct_add_value(state, Ct_Value{ kind=.Slice, type=result_type, index=u64(container_place), start=u32(container_start+start), count=u32(end-start), @@ -5973,7 +6070,7 @@ eval_integer_constant_in_context :: proc( return eval_integer_constant_in_state(&state, expr_id, depth) } -eval_integer_constant_in_state :: proc(state: ^Ct_State, expr_id: ast.Expr_Id, depth := 0) -> Constant { +eval_integer_constant_in_state :: proc(state: ^Ct_State, expr_id: ast.Expr_Id, depth := 0, allow_bool := true) -> Constant { if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(state.checker.ast_module.exprs) { return Constant{kind=.Not_Constant} } @@ -5994,6 +6091,9 @@ eval_integer_constant_in_state :: proc(state: ^Ct_State, expr_id: ast.Expr_Id, d } return Constant{kind=.Not_Constant} } + if !allow_bool && state.values[value].kind != .Integer { + return Constant{kind=.Not_Constant} + } integer, integer_ok := ct_integer_value(state, value) if !integer_ok { return Constant{kind=.Not_Constant} diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index a6628fe..b6f6652 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -1556,6 +1556,11 @@ emit_instruction_stream :: proc( emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container") continue } + check_sentinel := instruction.b != ir.INVALID_INSTRUCTION + if check_sentinel && !valid_value(instructions, instruction.b, item.child, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid slice sentinel") + continue + } pointer_name := fmt.tprintf("%%v%d", instruction.a) length_name := fmt.tprintf("%d", item.count) if item.kind == .Array { @@ -1597,13 +1602,25 @@ emit_instruction_stream :: proc( if item.kind == .Pointer { fmt.sbprintf(&emitter.builder, " %%slice_ok%d = or i1 false, %%slice_order%d\n", instruction_index, instruction_index) } else { - fmt.sbprintf(&emitter.builder, " %%slice_end_ok%d = icmp ule i64 %s, %s\n", instruction_index, end_name, length_name) + comparison := "ult" if check_sentinel && !item.has_sentinel else "ule" + fmt.sbprintf(&emitter.builder, " %%slice_end_ok%d = icmp %s i64 %s, %s\n", instruction_index, comparison, end_name, length_name) fmt.sbprintf(&emitter.builder, " %%slice_ok%d = and i1 %%slice_order%d, %%slice_end_ok%d\n", instruction_index, instruction_index, instruction_index) } fmt.sbprintf(&emitter.builder, " br i1 %%slice_ok%d, label %%slice_continue%d, label %%slice_trap%d\nslice_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "slice bounds out of range") emit_trap_call(emitter, message) fmt.sbprintf(&emitter.builder, " unreachable\nslice_continue%d:\n", instruction_index) + if check_sentinel { + element_type := llvm_type(item.child, &emitter.module.types) + fmt.sbprintf(&emitter.builder, " %%slice_sentinel_ptr%d = getelementptr %s, ptr %s, i64 %s\n", instruction_index, element_type, pointer_name, end_name) + fmt.sbprintf(&emitter.builder, " %%slice_sentinel_value%d = load %s, ptr %%slice_sentinel_ptr%d\n", instruction_index, element_type, instruction_index) + fmt.sbprintf(&emitter.builder, " %%slice_sentinel_ok%d = icmp eq %s %%slice_sentinel_value%d, ", instruction_index, element_type, instruction_index) + write_operand(&emitter.builder, instructions, instruction.b, item.child, &emitter.module.types) + fmt.sbprintf(&emitter.builder, "\n br i1 %%slice_sentinel_ok%d, label %%slice_sentinel_continue%d, label %%slice_sentinel_trap%d\nslice_sentinel_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + sentinel_message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "slice sentinel mismatch") + emit_trap_call(emitter, sentinel_message) + fmt.sbprintf(&emitter.builder, " unreachable\nslice_sentinel_continue%d:\n", instruction_index) + } fmt.sbprintf(&emitter.builder, " %%slice_start%d = getelementptr %s, ptr %s, i64 %s\n", instruction_index, llvm_type(item.child, &emitter.module.types), pointer_name, start_name) fmt.sbprintf(&emitter.builder, " %%slice_result%d = insertvalue %s poison, ptr %%slice_start%d, 0\n", instruction_index, llvm_type(instruction.type, &emitter.module.types), instruction_index) fmt.sbprintf(&emitter.builder, " %%slice_result_len%d = sub i64 %s, %s\n", instruction_index, end_name, start_name) diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 4c26c8a..472feed 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -306,9 +306,13 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi args[index] = lower_nested_expr(state, arg) } } + checked_sentinel := ir.INVALID_INSTRUCTION + if expr.right != hir.INVALID_EXPR { + checked_sentinel = lower_nested_expr(state, expr.right) + } return append_instruction(state, ir.Instruction{ op=.Slice, span=expr.span, type=expr.type, args=args, - target=ir.INVALID_REF, a=container, b=ir.INVALID_INSTRUCTION, + target=ir.INVALID_REF, a=container, b=checked_sentinel, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Unwrap: diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 89de3c2..decf0ee 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1450,6 +1450,7 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int skip_newlines(parser) start_expr := ast.INVALID_EXPR end_expr := ast.INVALID_EXPR + sentinel_expr := ast.INVALID_EXPR slicing := false if _, ok := allow(parser, .Range); ok { slicing = true @@ -1463,12 +1464,19 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int } } skip_newlines(parser) - if slicing && current(parser).kind != .Right_Bracket { + if slicing && current(parser).kind != .Right_Bracket && current(parser).kind != .Semicolon { parser.range_disabled += 1 end_expr = parse_expression_bp(parser, 0, nesting+1) parser.range_disabled -= 1 skip_newlines(parser) } + if slicing { + if _, ok := allow(parser, .Semicolon); ok { + skip_newlines(parser) + sentinel_expr = parse_expression_bp(parser, 0, nesting+1) + skip_newlines(parser) + } + } end_token, ok := allow(parser, .Right_Bracket) if !ok { source.add(parser.diagnostics, current(parser).span, "expected ']' after index or slice") @@ -1485,7 +1493,7 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int span=span_from(left_expr.span, end_token.span), args=args, left=left, - right=ast.INVALID_EXPR, + right=sentinel_expr, diagnostic=source.INVALID_DIAGNOSTIC, }) } else { diff --git a/compiler_tests.odin b/compiler_tests.odin index a74cbd3..3216dff 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2252,6 +2252,256 @@ main func() i32 { testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr")) } +@(test) +explicit_sentinel_slices_execute_without_copying :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-explicit-sentinel-slices" + main_path := "/tmp/brolang-test-explicit-sentinel-slices/main.bro" + output := "/tmp/brolang-test-explicit-sentinel-slices-output" + text := `pointer func(value *mut u8, count usize) [;0]mut u8 { return value[..count;0] } +bounded func(value []u8, count usize) [;0]u8 { return value[..count;0] } +array func(value @mut [3]mut u8, start usize, count usize) [;0]mut u8 { return value[start..count;0] } +terminated func(value [;0]u8) [;0]u8 { return value[..;0] } +nonzero func(value [*;9]u8, count usize) [;9]u8 { return value[1..count;9] } +signed func(value *i16, count usize) [;-7]i16 { return value[..count;-7] } +argument func(value [*;0]u8, present bool) ?[;0]u8 { + if !present { return null } + length usize := 0 + while value[length] != 0 { length += 1 } + return value[..length;0] +} +static_text func() [;0]u8 { return "hello"[..;0] } +static_partial func() [;7]u16 { + values [4]u16 := [11, 22, 7, 44] + return values[1..2;7] +} +comptime_text [;0]u8 :: $static_text() +comptime_partial [;7]u16 :: $static_partial() +equal func($T type, left, right []T) bool { + if left.len != right.len { return false } + for 0..left.len |index| { + if left[index] != right[index] { return false } + } + return true +} +main func() i32 { + values [3]mut u8 := [11, 22, 0] + view :: pointer((&values).ptr, 2) + if view.len != 2 { return 1 } + view[0] = 33 + if values[0] != 33 { return 2 } + all :: values[..] + if all.len != 3 { return 3 } + part :: bounded(all, 2) + if part.len != 2 or part[1] != 22 { return 4 } + suffix :: array(&values, 1, 2) + suffix[0] = 44 + if suffix.len != 1 or values[1] != 44 { return 5 } + empty :: pointer((&values).ptr + 2, 0) + if empty.len != 0 { return 6 } + zeros [2;0]mut u8 := [1, 2] + full :: terminated(zeros[..]) + if full.len != 2 or full[1] != 2 { return 7 } + nines [2;9]u8 := [3, 4] + nine :: nonzero((&nines).ptr, 2) + if nine.len != 1 or nine[0] != 4 { return 8 } + wide [2]i16 := [123, -7] + negative :: signed((&wide).ptr, 1) + if negative.len != 1 or negative[0] != 123 { return 9 } + direct :: zeros[..;0] + if direct.len != 2 { return 10 } + if argument((&zeros).ptr, true) |arg| { + zeros[0] = 55 + if arg.len != 2 or arg[0] != 55 { return 11 } + } else { return 12 } + if argument((&zeros).ptr, false) |_| { return 13 } + if comptime_text.len != 5 or comptime_text[0] != 'h' or comptime_text[4] != 'o' { return 14 } + if comptime_partial.len != 1 or comptime_partial[0] != 22 { return 15 } + if !equal(comptime_text, "hello") or equal(comptime_text, "hellx") { return 16 } + if !equal(direct, "7\x02") { return 17 } + return 0 +} +` + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + testing.expect_value(t, compiler_core.compile_package(directory, output), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +explicit_sentinel_slice_runtime_guards_trap :: proc(t: ^testing.T) { + cases := [?]struct { + text, message: string, + }{ + { + `check func(value *u8, count usize) void { _ = value[..count;0] } +main func() void { + values [2]u8 := [1, 2] + check((&values).ptr, 1) +} +`, "slice sentinel mismatch", + }, + { + `check func(value @[2]u8, count usize) void { _ = value[..count;0] } +main func() void { + values [2]u8 := [1, 0] + check(&values, 2) +} +`, "slice bounds out of range", + }, + { + `check func(value []u8, count usize) void { _ = value[..count;0] } +main func() void { + values [2;0]u8 := [1, 2] + check(values[..2], 2) +} +`, "slice bounds out of range", + }, + { + `check func(value [;0]u8, count usize) void { _ = value[..count;0] } +main func() void { + values [2;0]u8 := [1, 2] + check(values[..], 3) +} +`, "slice bounds out of range", + }, + { + `check func(value *u8, start usize, count usize) void { _ = value[start..count;0] } +main func() void { + values [2]u8 := [1, 0] + check((&values).ptr, 2, 1) +} +`, "slice bounds out of range", + }, + { + `check func(value [;0]u8, count usize) void { _ = value[..count;9] } +main func() void { + values [2;0]u8 := [1, 2] + check(values[..], 2) +} +`, "slice sentinel mismatch", + }, + } + for test_case, index in cases { + directory := fmt.tprintf("/tmp/brolang-test-sentinel-slice-trap-%d", index) + main_path := fmt.tprintf("%s/main.bro", directory) + output := fmt.tprintf("%s-output", directory) + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)test_case.text)) + testing.expect_value(t, compiler_core.compile_package(directory, output), 0) + state, stdout, stderr, err := os2.process_exec( + os2.Process_Desc{command=[]string{output}}, context.allocator, + ) + defer delete(stdout) + defer delete(stderr) + testing.expect(t, err == nil) + testing.expect(t, !state.success) + testing.expect(t, strings.contains(string(stderr), test_case.message)) + } +} + +@(test) +explicit_sentinel_slice_semantic_errors_are_diagnosed :: proc(t: ^testing.T) { + cases := [?]struct { + text, message: string, + }{ + { + `bad func(value *u8, count usize, mark u8) void { _ = value[..count;mark] } +main func() void { + values [2]u8 := [1, 0] + bad((&values).ptr, 1, 0) +} +`, "slice sentinel must be a compile-time integer", + }, + { + `bad func(value *u8, count usize) void { _ = value[..count;true] } +main func() void { + values [2]u8 := [1, 0] + bad((&values).ptr, 1) +} +`, "slice sentinel must be a compile-time integer", + }, + { + `bad func(value *u8, count usize) void { _ = value[..count;256] } +main func() void { + values [2]u8 := [1, 0] + bad((&values).ptr, 1) +} +`, "slice sentinel value does not fit element type", + }, + { + `bad func(value *u8, count usize) void { _ = value[..count;-1] } +main func() void { + values [2]u8 := [1, 0] + bad((&values).ptr, 1) +} +`, "slice sentinel value does not fit element type", + }, + { + `bad func(value [*;0]u8) void { _ = value[..;0] } +main func() void { bad("text") } +`, "many-item pointer slicing requires an explicit end bound", + }, + { + `bad func(value *u8, count usize) [;0]mut u8 { return value[..count;0] } +main func() void { + values [2]u8 := [1, 0] + _ = bad((&values).ptr, 1) +} +`, "cannot implicitly convert", + }, + { + `bad func(count usize) usize { + values [2]u8 := [1, 0] + return values[..count;0].len +} +result :: $bad(2) +main func() void { _ = result } +`, "comptime sentinel slice endpoint out of range", + }, + { + `bad func(count usize) usize { + values [2]u8 := [1, 2] + return values[..count;0].len +} +result :: $bad(1) +main func() void { _ = result } +`, "comptime slice sentinel mismatch", + }, + { + `bad func(value []u8) usize { return value[..;0].len } +result :: $bad("text") +main func() void { _ = result } +`, "comptime sentinel slice endpoint out of range", + }, + } + for test_case in cases { + source_file := source.Source{path="invalid_sentinel_slice.bro", text=test_case.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 := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, test_case.message) + } + testing.expect(t, found, test_case.message) + } +} + @(test) slicing_an_array_rvalue_materializes_a_temporary :: proc(t: ^testing.T) { // The checker accepts slicing a non-location array (here, a by-value array @@ -5848,16 +6098,6 @@ main func() void { values [2]mut i32 := [1, 2] yield &values[0] } - _ = ${ - values [2]mut i32 := [1, 2] - view []mut i32 := values[..] - yield view - } - _ = ${ - values [2]mut i32 := [1, 2] - view []i32 := values[1..] - yield view - } _ = $spin() _ = $missing() _ = ${ @@ -5880,7 +6120,6 @@ main func() void { found_runtime := false runtime_only_count := 0 pointer_errors := 0 - slice_errors := 0 found_quota := false found_missing := false found_yield := false @@ -5889,7 +6128,6 @@ main func() void { found_runtime = found_runtime || strings.contains(message, "unresolved comptime value 'runtime'") runtime_only_count += 1 if strings.contains(message, "runtime-only") else 0 pointer_errors += 1 if strings.contains(message, "only immutable pointers to whole comptime arrays can materialize as runtime memory") else 0 - slice_errors += 1 if strings.contains(message, "only immutable full-array comptime slices can materialize as runtime memory") else 0 found_quota = found_quota || strings.contains(message, "comptime evaluation exceeded the step quota") found_missing = found_missing || strings.contains(message, "did not return a value") found_yield = found_yield || strings.contains(message, "a value block must end with an explicit 'yield'") @@ -5897,7 +6135,6 @@ main func() void { testing.expect(t, found_runtime) testing.expect(t, runtime_only_count >= 2) testing.expect(t, pointer_errors >= 4) - testing.expect(t, slice_errors >= 2) testing.expect(t, found_quota) testing.expect(t, found_missing) testing.expect(t, found_yield)