feat: contextually type empty mutable slice literals
This commit is contained in:
+11
@@ -50,6 +50,17 @@ 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`
|
- 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
|
- 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
|
||||||
|
|
||||||
|
#### contextual empty mutable slices
|
||||||
|
|
||||||
|
A fresh empty array literal address `&[]` may initialize an expected `[]mut T`,
|
||||||
|
including local declarations, call arguments, and record fields. The context supplies
|
||||||
|
the element type `T`; the resulting slice has length zero and no writable elements.
|
||||||
|
This is literal-only contextual typing, not a readonly-to-mutable conversion:
|
||||||
|
named readonly zero-length arrays, existing readonly empty slices, and nonempty
|
||||||
|
readonly literals still cannot initialize mutable slices. Sentinel slices `[;S]T`
|
||||||
|
and `[;S]mut T` still require backing storage containing the sentinel; `&[]` does
|
||||||
|
not provide it.
|
||||||
|
|
||||||
#### checked sentinel slicing
|
#### checked sentinel slicing
|
||||||
|
|
||||||
`base[start..end;S]` constructs a sentinel slice `[;S]T` without copying elements.
|
`base[start..end;S]` constructs a sentinel slice `[;S]T` without copying elements.
|
||||||
|
|||||||
@@ -1488,6 +1488,15 @@ is_numeric_constant_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> boo
|
|||||||
return eval_constant(checker, expr_id).kind == .Value || is_float_constant_expr(checker, expr_id)
|
return eval_constant(checker, expr_id).kind == .Value || is_float_constant_expr(checker, expr_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_empty_array_address :: proc(checker: ^Checker, expr: ast.Expr) -> bool {
|
||||||
|
if expr.kind != .Address || expr.left == ast.INVALID_EXPR ||
|
||||||
|
int(expr.left) >= len(checker.ast_module.exprs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
operand := checker.ast_module.exprs[expr.left]
|
||||||
|
return operand.kind == .Array && len(operand.args) == 0
|
||||||
|
}
|
||||||
|
|
||||||
is_typed_integer_fold_candidate :: proc(checker: ^Checker, expr_id: ast.Expr_Id, depth := 0) -> bool {
|
is_typed_integer_fold_candidate :: proc(checker: ^Checker, expr_id: ast.Expr_Id, depth := 0) -> bool {
|
||||||
if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
||||||
return false
|
return false
|
||||||
@@ -3152,8 +3161,9 @@ infer_call_comptime_values :: proc(
|
|||||||
}
|
}
|
||||||
arg_expr := checker.ast_module.exprs[arg_id]
|
arg_expr := checker.ast_module.exprs[arg_id]
|
||||||
is_null := arg_expr.kind == .Null
|
is_null := arg_expr.kind == .Null
|
||||||
|
// An empty literal supplies no element evidence ahead of concrete arguments.
|
||||||
is_weak := is_numeric_constant_expr(checker, arg_id) ||
|
is_weak := is_numeric_constant_expr(checker, arg_id) ||
|
||||||
arg_expr.kind == .String || is_null
|
arg_expr.kind == .String || is_null || is_empty_array_address(checker, arg_expr)
|
||||||
contains_type_call := type_pattern_contains_type_call(
|
contains_type_call := type_pattern_contains_type_call(
|
||||||
checker, function.params[param_index].type,
|
checker, function.params[param_index].type,
|
||||||
)
|
)
|
||||||
@@ -5220,6 +5230,13 @@ infer_compound_expr :: proc(
|
|||||||
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
return type_from_syntax(checker, expr.type, pkg, file)
|
return type_from_syntax(checker, expr.type, pkg, file)
|
||||||
case .Address:
|
case .Address:
|
||||||
|
if is_empty_array_address(checker, expr) {
|
||||||
|
if node, ok := types.node(store, expected);
|
||||||
|
ok && node.kind == .Slice && node.mutable && !node.has_sentinel {
|
||||||
|
child := types.array(store, node.child, 0, true)
|
||||||
|
return types.pointer(store, child, true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
return types.pointer(store, child, false, false)
|
return types.pointer(store, child, false, false)
|
||||||
case .Deref:
|
case .Deref:
|
||||||
@@ -8758,11 +8775,9 @@ build_compound_expr :: proc(
|
|||||||
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
|
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
|
||||||
return build_scalar_cast(checker, value, target, expr.span)
|
return build_scalar_cast(checker, value, target, expr.span)
|
||||||
case .Address:
|
case .Address:
|
||||||
// `&<array literal>` (Zig's `&.{...}`): the operand is an rvalue with no
|
// Promote array literals to anonymous globals so their addresses never
|
||||||
// address, so promote it to an anonymous global constant and take *its*
|
// dangle. Only a fresh empty literal may acquire mutable slice context;
|
||||||
// address. Reuses the existing non-scalar-global storage path; only the
|
// ordinary literals retain readonly storage and the usual decay rules.
|
||||||
// stable global address enters the expression, so it never dangles. The
|
|
||||||
// resulting `*[N]T` then decays to a slice via the usual coercion.
|
|
||||||
if expr.left != ast.INVALID_EXPR && checker.ast_module.exprs[expr.left].kind == .Array {
|
if expr.left != ast.INVALID_EXPR && checker.ast_module.exprs[expr.left].kind == .Array {
|
||||||
operand := checker.ast_module.exprs[expr.left]
|
operand := checker.ast_module.exprs[expr.left]
|
||||||
// Propagate an element-expected type through `&` so literal elements
|
// Propagate an element-expected type through `&` so literal elements
|
||||||
@@ -8770,12 +8785,15 @@ build_compound_expr :: proc(
|
|||||||
// Without this, `&["x"]` infers `*[1]*[N:0]u8`, which won't decay to
|
// Without this, `&["x"]` infers `*[1]*[N:0]u8`, which won't decay to
|
||||||
// `[][]u8` because can_decay_array_pointer requires child equality.
|
// `[][]u8` because can_decay_array_pointer requires child equality.
|
||||||
element := types.INVALID
|
element := types.INVALID
|
||||||
|
mutable := false
|
||||||
if node, ok := types.node(store, expected); ok && (node.kind == .Slice || node.kind == .Array) {
|
if node, ok := types.node(store, expected); ok && (node.kind == .Slice || node.kind == .Array) {
|
||||||
element = node.child
|
element = node.child
|
||||||
|
mutable = len(operand.args) == 0 && node.kind == .Slice &&
|
||||||
|
node.mutable && !node.has_sentinel
|
||||||
}
|
}
|
||||||
synth_expected := types.INVALID
|
synth_expected := types.INVALID
|
||||||
if types.is_valid(element) {
|
if types.is_valid(element) {
|
||||||
synth_expected = types.array(store, element, u64(len(operand.args)), false)
|
synth_expected = types.array(store, element, u64(len(operand.args)), mutable)
|
||||||
}
|
}
|
||||||
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, synth_expected, pkg, file)
|
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, synth_expected, pkg, file)
|
||||||
array_type := checker.module.exprs[value].type
|
array_type := checker.module.exprs[value].type
|
||||||
@@ -8784,7 +8802,7 @@ build_compound_expr :: proc(
|
|||||||
name = symbol.intern(checker.symbols, "__anon.array"),
|
name = symbol.intern(checker.symbols, "__anon.array"),
|
||||||
type = array_type,
|
type = array_type,
|
||||||
expr = value,
|
expr = value,
|
||||||
writable = false,
|
writable = mutable,
|
||||||
external = false,
|
external = false,
|
||||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
@@ -8794,7 +8812,7 @@ build_compound_expr :: proc(
|
|||||||
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
|
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
return add_hir_expr(checker, hir.Expr{
|
return add_hir_expr(checker, hir.Expr{
|
||||||
kind=.Address, span=expr.span, type=types.pointer(store, array_type, false, false),
|
kind=.Address, span=expr.span, type=types.pointer(store, array_type, mutable, false),
|
||||||
left=global_ref, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
|
left=global_ref, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -10389,14 +10407,21 @@ build_expr :: proc(
|
|||||||
checker.current_comptime_values = comptime_values
|
checker.current_comptime_values = comptime_values
|
||||||
for source_index in 0..<len(expr.args) {
|
for source_index in 0..<len(expr.args) {
|
||||||
param_index := call_param_index(frame.mapping, source_index)
|
param_index := call_param_index(frame.mapping, source_index)
|
||||||
|
empty_array := is_empty_array_address(checker, checker.ast_module.exprs[expr.args[source_index]])
|
||||||
if param_index >= len(function.params) || function.params[param_index].comptime_value ||
|
if param_index >= len(function.params) || function.params[param_index].comptime_value ||
|
||||||
!is_numeric_constant_expr(checker, expr.args[source_index]) {
|
(!is_numeric_constant_expr(checker, expr.args[source_index]) && !empty_array) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
expected_arg := call_arg_expected(checker, function, param_index)
|
expected_arg := call_arg_expected(checker, function, param_index)
|
||||||
if !is_runtime_type(checker, expected_arg) {
|
if !is_runtime_type(checker, expected_arg) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if empty_array && types.can_decay_array_pointer(
|
||||||
|
checker.module.exprs[stack[frame_index].built_args[source_index]].type,
|
||||||
|
expected_arg, &checker.module.types,
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
rebuilt := build_nested_expr(
|
rebuilt := build_nested_expr(
|
||||||
checker, expr.args[source_index], locals, global_reads, calls,
|
checker, expr.args[source_index], locals, global_reads, calls,
|
||||||
expected_arg, pkg, file,
|
expected_arg, pkg, file,
|
||||||
|
|||||||
@@ -1828,6 +1828,102 @@ immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testi
|
|||||||
testing.expect_value(t, len(diagnostics.items), 0)
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
fresh_empty_literals_initialize_mutable_slices_and_grow :: proc(t: ^testing.T) {
|
||||||
|
directory := "/tmp/brolang-test-contextual-empty-slices"
|
||||||
|
main_path := "/tmp/brolang-test-contextual-empty-slices/main.bro"
|
||||||
|
output := "/tmp/brolang-test-contextual-empty-slices-output"
|
||||||
|
text := `mem :: import "@std/mem"
|
||||||
|
Item :: struct { value i32 }
|
||||||
|
Views :: struct { integers []mut i32, records []mut Item }
|
||||||
|
integers func(values []mut i32) usize { return values.len }
|
||||||
|
records func(values []mut Item) usize { return values.len }
|
||||||
|
generic func($T type, values []mut T, _ T) usize { return values.len }
|
||||||
|
main func() i32 {
|
||||||
|
numbers []mut i32 := &[]
|
||||||
|
items []mut Item := &[]
|
||||||
|
views Views := {integers = &[], records = &[]}
|
||||||
|
if numbers.len != 0 or items.len != 0 { return 1 }
|
||||||
|
if integers(&[]) != 0 or records(&[]) != 0 { return 2 }
|
||||||
|
if views.integers.len != 0 or views.records.len != 0 { return 3 }
|
||||||
|
integer i32 := 7
|
||||||
|
item Item := {value = 9}
|
||||||
|
if generic(&[], integer) != 0 or generic(&[], item) != 0 { return 7 }
|
||||||
|
numbers = mem.realloc(mem.c_allocator, numbers, 2) catch |_| { return 4 }
|
||||||
|
defer mem.free(mem.c_allocator, numbers)
|
||||||
|
items = mem.realloc(mem.c_allocator, items, 1) catch |_| { return 5 }
|
||||||
|
defer mem.free(mem.c_allocator, items)
|
||||||
|
numbers[0] = 17
|
||||||
|
numbers[1] = 25
|
||||||
|
items[0] = {value = numbers[0] + numbers[1]}
|
||||||
|
if numbers.len != 2 or items.len != 1 or items[0].value != 42 { return 6 }
|
||||||
|
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, nil, target.DEFAULT, cimport.Options{}, ".",
|
||||||
|
), 0)
|
||||||
|
state := run_executable(output)
|
||||||
|
testing.expect_value(t, state.exit_code, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
empty_literal_mutability_does_not_weaken_readonly_or_sentinel_rules :: proc(t: ^testing.T) {
|
||||||
|
cases := [?]string {
|
||||||
|
`main func() void {
|
||||||
|
values [0]i32 := []
|
||||||
|
view []mut i32 := &values
|
||||||
|
_ = view
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
`main func() void {
|
||||||
|
view []mut i32 := &[1]
|
||||||
|
_ = view
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
`main func() void {
|
||||||
|
readonly []i32 := &[]
|
||||||
|
view []mut i32 := readonly
|
||||||
|
_ = view
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
`main func() void {
|
||||||
|
view [;0]i32 := &[]
|
||||||
|
_ = view
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
`main func() void {
|
||||||
|
view [;0]mut i32 := &[]
|
||||||
|
_ = view
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
for text in cases {
|
||||||
|
source_file := source.Source{path="invalid_empty_slice.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_conversion := false
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
found_conversion = found_conversion ||
|
||||||
|
strings.contains(diagnostic.message, "cannot implicitly convert")
|
||||||
|
}
|
||||||
|
testing.expect(t, found_conversion, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
many_item_pointer_slices_compile_and_run :: proc(t: ^testing.T) {
|
many_item_pointer_slices_compile_and_run :: proc(t: ^testing.T) {
|
||||||
directory := "/tmp/brolang-test-pointer-slices"
|
directory := "/tmp/brolang-test-pointer-slices"
|
||||||
|
|||||||
Reference in New Issue
Block a user