transform: avoid folding array text after storage escapes
This commit is contained in:
1 parent
efa3d1ce5e
commit
c98d45fdf9
6 files changed
+304
-5
No files matched your search
@@ -1730,6 +1730,9 @@ the parent array or the resulting slice is mutable, V inserts an implicit
|
||||
affecting the other. If both arrays are immutable, the slice safely reuses the
|
||||
parent array's memory without cloning. The shared-memory examples below
|
||||
therefore use `unsafe {}` intentionally.
|
||||
Printing or interpolating the parent array shows changes made through such a shared slice.
|
||||
Formatting such a mutated array with `-b wasm` currently reports an unsupported
|
||||
runtime-formatting error.
|
||||
|
||||
```v
|
||||
mut a := [0, 1, 2, 3, 4, 5]
|
||||
|
||||
@@ -21907,6 +21907,11 @@ fn unsupported_backend_node_error(a &flat.FlatAst, tc &types.TypeChecker, id fla
|
||||
if callee.kind == .ident && callee.value == 'new_map' {
|
||||
unsupported_type = tc.resolve_type(id).source_type_id()
|
||||
}
|
||||
// Runtime auto-formatters need a recursion guard with no wasm body.
|
||||
// Constant array text is folded before this post-transform check.
|
||||
if callee.kind == .ident && callee.value == 'autostr_addr_type_push' {
|
||||
return '${fallback_location}error: the V wasm backend does not support runtime value formatting yet'
|
||||
}
|
||||
}
|
||||
if unsupported_type != 0 {
|
||||
return '${fallback_location}error: the V wasm backend does not support type `${flat.source_type_name(unsupported_type)}` yet'
|
||||
|
||||
@@ -401,3 +401,37 @@ fn test_fold_print_wasm_matches_native_text() {
|
||||
}
|
||||
assert code == 0, 'the host reported a non-zero exit: ${out}'
|
||||
}
|
||||
|
||||
fn test_shared_array_text_requires_runtime_formatting() {
|
||||
mutations := [
|
||||
'mut view := unsafe { values[..] }; view[0] = 9',
|
||||
'change(values)',
|
||||
'alias := values; change(alias)',
|
||||
'ArrayTextInts(values).change()',
|
||||
]
|
||||
for i, mutation in mutations {
|
||||
for j, print_expr in ['println("\${values}")', 'println(values.str())'] {
|
||||
source := '
|
||||
fn change(values []int) {
|
||||
mut view := unsafe { values[..] }
|
||||
view[0] = 9
|
||||
}
|
||||
type ArrayTextInts = []int
|
||||
fn (values ArrayTextInts) change() {
|
||||
change(values)
|
||||
}
|
||||
fn main() {
|
||||
values := [1, 2]
|
||||
${mutation}
|
||||
${print_expr}
|
||||
}
|
||||
'
|
||||
tour_compile('shared_array_text_${i}_${j}', source) or {
|
||||
assert err.msg().contains('error: the V wasm backend does not support runtime value formatting yet'), err.msg()
|
||||
assert !err.msg().contains('panic'), err.msg()
|
||||
continue
|
||||
}
|
||||
assert false, 'shared array text unexpectedly compiled; check that runtime formatting supports [9, 2]'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
fn test_array_text_reads_updates_through_an_unsafe_shared_slice() {
|
||||
values := [1, 2]
|
||||
// The slice deliberately shares the immutable array's element storage.
|
||||
mut view := unsafe { values[..] }
|
||||
view[0] = 9
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn test_split_array_text_reads_updates_through_an_unsafe_shared_slice() {
|
||||
values := 'a,b'.split(',')
|
||||
// The slice deliberately shares the immutable array's element storage.
|
||||
mut view := unsafe { values[..] }
|
||||
view[0] = 'z'
|
||||
assert values[0] == 'z'
|
||||
assert '${values}' == "['z', 'b']"
|
||||
assert values.str() == "['z', 'b']"
|
||||
}
|
||||
|
||||
fn test_bool_array_text_reads_updates_through_an_unsafe_shared_slice() {
|
||||
values := [true, false]
|
||||
// The slice deliberately shares the immutable array's element storage.
|
||||
mut view := unsafe { values[..] }
|
||||
view[0] = false
|
||||
assert !values[0]
|
||||
assert '${values}' == '[false, false]'
|
||||
assert values.str() == '[false, false]'
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_a_data_pointer() {
|
||||
values := [1, 2]
|
||||
unsafe {
|
||||
// The pointer deliberately aliases the immutable array's element storage.
|
||||
mut data := &int(values.data)
|
||||
data[0] = 9
|
||||
}
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_a_captured_shared_slice() {
|
||||
values := [1, 2]
|
||||
mutate := fn [values] () {
|
||||
// The captured header retains the original array's element storage.
|
||||
mut view := unsafe { values[..] }
|
||||
view[0] = 9
|
||||
}
|
||||
mutate()
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_a_header_pointer() {
|
||||
values := [1, 2]
|
||||
// The pointer deliberately retains the original array header and storage.
|
||||
mut alias := unsafe { &values }
|
||||
unsafe {
|
||||
(*alias)[0] = 9
|
||||
}
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn mutate_array_text_storage(values []int) {
|
||||
// The callee deliberately changes storage shared with an immutable argument.
|
||||
mut view := unsafe { values[..] }
|
||||
view[0] = 9
|
||||
}
|
||||
|
||||
type ArrayTextInts = []int
|
||||
|
||||
fn (values ArrayTextInts) mutate_array_text_receiver() {
|
||||
mutate_array_text_storage(values)
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_an_immutable_argument() {
|
||||
values := [1, 2]
|
||||
mutate_array_text_storage(values)
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_an_alias_argument() {
|
||||
values := [1, 2]
|
||||
alias := values
|
||||
mutate_array_text_storage(alias)
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_a_slice_argument() {
|
||||
values := [1, 2]
|
||||
mutate_array_text_storage(values[..])
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_an_immutable_receiver() {
|
||||
values := [1, 2]
|
||||
ArrayTextInts(values).mutate_array_text_receiver()
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
|
||||
fn test_array_text_reads_updates_through_a_slice_alias_argument() {
|
||||
values := [1, 2]
|
||||
view := values[..]
|
||||
mutate_array_text_storage(view)
|
||||
assert values[0] == 9
|
||||
assert '${values}' == '[9, 2]'
|
||||
assert values.str() == '[9, 2]'
|
||||
}
|
||||
@@ -419,8 +419,8 @@ fn comptime_array_elem_text(element ComptimeStringScalar, elem_type u32) ?string
|
||||
}
|
||||
|
||||
// later_stmts_write_array_local reports whether a statement after the declaration
|
||||
// writes through the local. An element write or a mutable borrow changes the array
|
||||
// a print would render, so a compile-time array is only trusted when none follows.
|
||||
// writes through the local or exposes its storage. Aliases and unknown callees
|
||||
// may change the elements a print renders, even when the binding is immutable.
|
||||
fn (t &Transformer) later_stmts_write_array_local(decl_id flat.NodeId, name string) bool {
|
||||
ids := t.cur_stmt_list
|
||||
mut first := 0
|
||||
@@ -443,14 +443,63 @@ fn (t &Transformer) node_writes_array_local(id flat.NodeId, name string) bool {
|
||||
return false
|
||||
}
|
||||
node := t.a.nodes[int(id)]
|
||||
// A function literal has its own scope, so a name declared there is another
|
||||
// binding and a captured array is a copy that belongs to the closure.
|
||||
if node.kind == .fn_literal {
|
||||
// A captured array header can still share its element storage. Only inspect
|
||||
// closures that capture this binding; other closures have their own locals.
|
||||
if node.kind == .fn_literal && !t.fn_literal_captures_name(id, name) {
|
||||
return false
|
||||
}
|
||||
if node.kind == .ident && node.is_mut && node.value == name {
|
||||
return true
|
||||
}
|
||||
// Unsafe slices can share the original array's elements. An address or data
|
||||
// pointer can expose that storage too, even when the binding is immutable.
|
||||
if ((node.kind == .block && node.value == 'unsafe')
|
||||
|| (node.kind == .prefix && node.op == .amp)
|
||||
|| (node.kind == .selector && node.value == 'data'))
|
||||
&& t.array_map_expr_references_ident(id, name) {
|
||||
return true
|
||||
}
|
||||
if node.kind in [.decl_assign, .assign, .selector_assign, .index_assign, .return_stmt] {
|
||||
first := if node.kind == .decl_assign { 1 } else { 0 }
|
||||
for i in first .. node.children_count {
|
||||
if t.expr_exposes_array_local(t.a.child(&node, i), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.kind == .infix && node.op == .left_shift && node.children_count == 2
|
||||
&& t.expr_exposes_array_local(t.a.child(&node, 1), name) {
|
||||
return true
|
||||
}
|
||||
if node.kind == .for_in_stmt && node.children_count >= 3 {
|
||||
container := t.a.child(&node, 2)
|
||||
// Iterating the array itself reads scalars; an aggregate may yield its header.
|
||||
if t.a.node(container).kind != .ident && t.expr_exposes_array_local(container, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if node.kind == .call && node.children_count > 0 {
|
||||
callee := t.a.child_node(&node, 0)
|
||||
// These compiler intrinsics render their argument without exposing storage.
|
||||
is_print := callee.kind == .ident
|
||||
&& callee.value in ['print', 'println', 'eprint', 'eprintln']
|
||||
if !is_print {
|
||||
for i in 1 .. node.children_count {
|
||||
if t.expr_exposes_array_local(t.a.child(&node, i), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if callee.kind == .selector && callee.children_count > 0 {
|
||||
receiver := t.a.child(callee, 0)
|
||||
receiver_node := t.a.node(receiver)
|
||||
is_array_str := callee.value == 'str' && node.children_count == 1
|
||||
&& receiver_node.kind == .ident && receiver_node.value == name
|
||||
if !is_array_str && t.expr_exposes_array_local(receiver, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.kind in [.index_assign, .selector_assign] {
|
||||
if t.node_writes_array_local(t.a.child(&node, 0), name) {
|
||||
return true
|
||||
@@ -464,6 +513,35 @@ fn (t &Transformer) node_writes_array_local(id flat.NodeId, name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// expr_exposes_array_local recognizes expressions that can retain the array's
|
||||
// storage. Scalar reads and rendered strings do not carry that storage to callees.
|
||||
fn (t &Transformer) expr_exposes_array_local(id flat.NodeId, name string) bool {
|
||||
if int(id) < 0 || int(id) >= t.a.nodes.len {
|
||||
return false
|
||||
}
|
||||
node := t.a.nodes[int(id)]
|
||||
match node.kind {
|
||||
.ident { return node.value == name }
|
||||
.fn_literal { return t.fn_literal_captures_name(id, name) }
|
||||
.index {
|
||||
if node.value != 'range' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
.paren, .cast_expr, .as_expr, .array_literal, .array_init, .struct_init,
|
||||
.field_init, .map_init, .if_expr, .match_stmt, .match_branch, .or_expr,
|
||||
.assoc, .block, .expr_stmt {
|
||||
}
|
||||
else { return false }
|
||||
}
|
||||
for i in 0 .. node.children_count {
|
||||
if t.expr_exposes_array_local(t.a.child(&node, i), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// later_stmts_assign_local reports whether a statement that follows the declaration
|
||||
// `decl_id` in its statement list assigns to the declared local `name`, increments it, or
|
||||
// passes it as a mutable argument or exposes its address. Only those statements can see the binding: the same name
|
||||
|
||||
@@ -266,3 +266,62 @@ fn test_comptime_array_bindings_restore_after_a_nested_statement_scope() {
|
||||
}
|
||||
assert saw_inner
|
||||
}
|
||||
|
||||
fn test_comptime_array_text_preserves_exposed_storage() {
|
||||
for source in [
|
||||
'fn main() { values := [1, 2]; mut view := unsafe { values[..] }; view[0] = 9 }',
|
||||
'fn main() { values := [1, 2]; address := &values }',
|
||||
'fn main() { values := [1, 2]; change(values) }',
|
||||
'fn main() { values := [1, 2]; alias := values; change(alias) }',
|
||||
'fn main() { values := [1, 2]; change(values[..]) }',
|
||||
'fn main() { values := [1, 2]; values.change() }',
|
||||
'type ArrayAlias = []int\nfn main() { values := [1, 2]; ArrayAlias(values).str() }',
|
||||
'fn main() { values := [1, 2]; holder.values = values; change(holder.values) }',
|
||||
'fn main() { values := [1, 2]; rows[0] = values; change(rows[0]) }',
|
||||
'fn main() { values := [1, 2]; alias := if cond { side_effect(); values } else { []int{} }; change(alias) }',
|
||||
'fn main() { values := [1, 2]; for alias in [values] { change(alias) } }',
|
||||
'fn main() { values := [1, 2]; mut rows := [][]int{}; rows << values; change(rows[0]) }',
|
||||
'fn main() { values := [1, 2]; data := values.data }',
|
||||
'fn main() { values := [1, 2]; f := fn [values] () { mut view := unsafe { values[..] }; view[0] = 9 }; f() }',
|
||||
] {
|
||||
mut p := parser.Parser.new(pref.new_preferences())
|
||||
mut a := p.parse_text('array_storage_escape.v', source)
|
||||
assert p.diagnostics.len == 0, p.diagnostics.str()
|
||||
mut tc := types.TypeChecker.new(a)
|
||||
mut t := new_transformer(mut a, &tc, map[string]bool{})
|
||||
mut found := false
|
||||
for i, node in a.nodes {
|
||||
if node.kind == .fn_decl && node.value == 'main' {
|
||||
found = true
|
||||
assert t.node_writes_array_local(flat.NodeId(i), 'values'), source
|
||||
}
|
||||
}
|
||||
assert found
|
||||
}
|
||||
}
|
||||
|
||||
fn test_comptime_array_text_can_fold_read_only_storage() {
|
||||
for source in [
|
||||
'fn main() { values := [1, 2]; println(values) }',
|
||||
'fn main() { values := [1, 2]; println(values.str()); eprintln(values) }',
|
||||
'fn main() { values := [1, 2]; consume("\${values}"); consume(values[0]) }',
|
||||
'fn main() { values := [1, 2]; text := values.str(); number := values[0] }',
|
||||
'fn main() { values := [1, 2]; for number in values { consume(number) }; println(values) }',
|
||||
'fn main() { values := [1, 2]; f := fn () { values := [3]; mut view := unsafe { values[..] }; view[0] = 9 }; f(); println(values) }',
|
||||
'fn main() { values := [1, 2]; other := [3]; mut view := unsafe { other[..] }; view[0] = 9; println(values) }',
|
||||
] {
|
||||
mut p := parser.Parser.new(pref.new_preferences())
|
||||
mut a := p.parse_text('array_storage_read.v', source)
|
||||
assert p.diagnostics.len == 0, p.diagnostics.str()
|
||||
mut tc := types.TypeChecker.new(a)
|
||||
mut t := new_transformer(mut a, &tc, map[string]bool{})
|
||||
mut found := false
|
||||
for i, node in a.nodes {
|
||||
if node.kind == .fn_decl && node.value == 'main' {
|
||||
found = true
|
||||
assert !t.node_writes_array_local(flat.NodeId(i), 'values'), source
|
||||
}
|
||||
}
|
||||
assert found
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user