feat: support checked explicit sentinel slices

This commit is contained in:
2026-10-23 11:18:08 +02:00
parent 7cb4ace0fa
commit 8ab5b71a0c
7 changed files with 465 additions and 31 deletions
+250 -13
View File
@@ -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)