compiler: avoid repeated self-compilation lookups and copies (#31197)

* compiler: reduce repeated self-compilation lookups and copies

Reuse immutable type and call metadata, avoid unnecessary name work and
node copies, and accelerate standard map, string and builder operations.
Add focused coverage for cache invalidation and borrowed storage lifetime.

* docs: record paired self-compilation results
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-26 16:01:18 +03:00
1 parent 3cb0db1bf7
commit 6cce632507
88 files changed
+7062 -491

No files matched your search

+97
View File
@@ -47,6 +47,103 @@ fn string_index_byte(data &u8, size int, needle u8) int {
}
}
// string_index_pair searches adjacent bytes without reading outside the span.
@[inline]
fn string_index_pair(data &u8, size int, first u8, second u8) int {
$if prod && !tinyc && !msvc && !native && little_endian {
if size >= 8 && size <= 32 {
low_bits := u64(0x7f7f7f7f7f7f7f7f)
repeated_a := u64(first) * u64(0x0101010101010101)
repeated_b := u64(second) * u64(0x0101010101010101)
mut offset := 0
for {
mut word := u64(0)
// Seven adjacent pairs fit in this bounded, unaligned window.
unsafe { C.memcpy(&word, data + offset, 8) }
da := word ^ repeated_a
// Exact masks prevent a borrow from creating a false adjacent match.
za := ~(((da & low_bits) + low_bits) | da | low_bits)
mut zb := za
if first != second {
db := word ^ repeated_b
zb = ~(((db & low_bits) + low_bits) | db | low_bits)
}
mask := za & (zb >> 8)
if mask != 0 {
return offset + int(C.__builtin_ctzll(mask) >> 3)
}
if offset + 8 == size { return -1 }
offset = if offset + 15 > size { size - 8 } else { offset + 7 }
}
}
}
last_start := size - 2
for i := 0; i <= last_start; i++ {
if unsafe { data[i] } != first {
next := string_index_byte(unsafe { data + i + 1 }, last_start - i, first)
if next < 0 { return -1 }
i += next + 1
}
if unsafe { data[i + 1] } == second { return i }
}
return -1
}
// string_contains_byte checks readable bytes without computing a match position.
@[inline]
fn string_contains_byte(data &u8, size int, needle u8) bool {
if size <= 0 {
return false
}
$if freestanding || vinix || native || v2_native_windows_pe_minimal ? {
return string_index_byte(data, size, needle) >= 0
} $else {
$if prod && !tinyc && !msvc && !native {
if size <= 32 {
if size >= 8 {
ones := u64(0x0101010101010101)
repeated := u64(needle) * ones
mut head := u64(0)
mut tail := u64(0)
// Every unaligned load stays inside the readable span.
unsafe {
C.memcpy(&head, data, 8)
C.memcpy(&tail, data + size - 8, 8)
}
head ^= repeated
tail ^= repeated
mut mask := ((head - ones) & ~head) | ((tail - ones) & ~tail)
if size > 16 {
unsafe {
C.memcpy(&head, data + 8, 8)
C.memcpy(&tail, data + size - 16, 8)
}
head ^= repeated
tail ^= repeated
mask |= ((head - ones) & ~head) | ((tail - ones) & ~tail)
}
return mask & u64(0x8080808080808080) != 0
}
if size >= 4 {
ones := u32(0x01010101)
repeated := u32(needle) * ones
mut head := u32(0)
mut tail := u32(0)
unsafe {
C.memcpy(&head, data, 4)
C.memcpy(&tail, data + size - 4, 4)
}
head ^= repeated
tail ^= repeated
return (((head - ones) & ~head) | ((tail - ones) & ~tail)) & u32(0x80808080) != 0
}
return unsafe { data[0] == needle || data[size >> 1] == needle || data[size - 1] == needle }
}
}
return !isnil(unsafe { C.memchr(data, i32(needle), uint(size)) })
}
}
// string_last_index_byte searches complete words without reading outside the span.
@[inline]
fn string_last_index_byte(data &u8, size int, needle u8) int {
+2
View File
@@ -124,10 +124,12 @@ fn map_hash_int_2(pkey voidptr) u64 {
return C.wyhash64(*unsafe { &u16(pkey) }, 0)
}
@[inline]
fn map_hash_int_4(pkey voidptr) u64 {
return C.wyhash64(*unsafe { &u32(pkey) }, 0)
}
@[inline]
fn map_hash_int_8(pkey voidptr) u64 {
return C.wyhash64(*unsafe { &u64(pkey) }, 0)
}
+2 -1
View File
@@ -479,7 +479,8 @@ fn (m &map) key_to_index(pkey voidptr) (u32, u32) {
m.hash_fn(pkey)
}
index := hash & m.even_index
meta := ((hash >> m.shift) & hash_mask) | probe_inc
// Valid table growth cannot exhaust the 64-bit hash before the u32 index.
meta := ((hash >> (m.shift & 63)) & hash_mask) | probe_inc
return u32(index), u32(meta)
}
+133
View File
@@ -0,0 +1,133 @@
@[has_globals]
module builtin
__global (
callback_kind_hash_calls int
callback_kind_eq_calls int
)
fn callback_kind_custom_hash(key voidptr) u64 {
callback_kind_hash_calls++
return map_hash_int_8(key) ^ u64(0xa0761d6478bd642f)
}
fn callback_kind_custom_eq(a voidptr, b voidptr) bool {
callback_kind_eq_calls++
return map_eq_int_8(a, b)
}
fn check_callback_kind_tags(values &map, hash_kind u8, eq_kind u8) {
$if v3_backend ? {
assert values.data.hash_kind == hash_kind
assert values.data.eq_kind == eq_kind
}
}
fn check_callback_kind_reads(values &map, count int) {
for i in 0 .. count + 1 {
key := u64(i) << 40
assert values.exists(&key) == (i < count)
found := values.get_check(&key)
zero := -1
if i < count {
assert found != unsafe { nil }
assert unsafe { *&int(found) } == i + 10
assert unsafe { *&int(values.get(&key, &zero)) } == i + 10
} else {
assert found == unsafe { nil }
assert values.get(&key, &zero) == voidptr(&zero)
}
}
}
fn test_map_callback_kinds_preserve_independent_custom_callbacks_and_clones() {
for mode in 0 .. 4 {
hash_fn := if mode & 1 != 0 { callback_kind_custom_hash } else { map_hash_int_8 }
eq_fn := if mode & 2 != 0 { callback_kind_custom_eq } else { map_eq_int_8 }
mut values := new_map(int(sizeof(u64)), int(sizeof(int)), hash_fn, eq_fn,
map_clone_int_8, map_free_nop)
hash_kind := if mode & 1 != 0 { u8(0) } else { u8(8) }
eq_kind := if mode & 2 != 0 { u8(0) } else { u8(8) }
check_callback_kind_tags(&values, hash_kind, eq_kind)
mut empty := unsafe { values.clone() }
check_callback_kind_tags(&empty, hash_kind, eq_kind)
check_callback_kind_reads(&empty, 0)
for i in 0 .. 32 {
key := u64(i) << 40
value := i + 10
values.set(&key, &value)
check_callback_kind_reads(&values, i + 1)
}
callback_kind_hash_calls = 0
callback_kind_eq_calls = 0
check_callback_kind_reads(&values, 32)
assert (callback_kind_hash_calls > 0) == (mode & 1 != 0)
assert (callback_kind_eq_calls > 0) == (mode & 2 != 0)
mut dense := unsafe { values.clone() }
check_callback_kind_reads(&dense, 32)
check_callback_kind_tags(&dense, hash_kind, eq_kind)
values.reserve(4096)
mut sparse := unsafe { values.clone() }
check_callback_kind_reads(&sparse, 32)
check_callback_kind_tags(&sparse, hash_kind, eq_kind)
values.clear()
check_callback_kind_tags(&values, hash_kind, eq_kind)
mut moved := sparse.move()
check_callback_kind_reads(&moved, 32)
check_callback_kind_reads(&sparse, 0)
check_callback_kind_tags(&sparse, hash_kind, eq_kind)
unsafe {
empty.free()
dense.free()
moved.free()
sparse.free()
values.free()
}
}
}
fn test_map_callback_kinds_cover_standard_constructors_and_header_reset() {
mut strings := map[string]int{}
strings['one'] = 1
strings['two'] = 2
strings['three'] = 3
assert strings['three'] == 3
mut small := map[u32]int{}
for i in 0 .. 32 {
small[u32(i) << 24] = i
}
for i in 0 .. 32 {
assert small[u32(i) << 24] == i
}
$if v3_backend ? {
assert strings.data.hash_kind == 1
assert strings.data.eq_kind & 15 == 1
assert small.data.hash_kind == 4
assert small.data.eq_kind == 4
}
key := u64(1) << 40
value := 42
$if v3_backend ? {
mut data := new_map_data(int(sizeof(u64)), int(sizeof(int)), map_hash_int_8,
map_eq_int_8, map_clone_int_8, map_free_nop)
assert data.hash_kind == 8
assert data.eq_kind == 8
data.set(&key, &value)
unsafe { data.free() }
assert data.hash_kind == 0
assert data.eq_kind == 0
unsafe { free(data) }
}
mut dense := new_map_with_dense_array(int(sizeof(u64)), int(sizeof(int)),
map_hash_int_8, callback_kind_custom_eq, map_clone_int_8, map_free_nop,
DenseArray{ key_bytes: int(sizeof(u64)), value_bytes: int(sizeof(int)) }, unsafe { nil })
check_callback_kind_tags(&dense, 8, 0)
dense.set(&key, &value)
assert dense.exists(&key)
assert unsafe { *&int(dense.get_check(&key)) } == value
unsafe {
strings.free()
small.free()
dense.free()
}
}
+160 -48
View File
@@ -207,6 +207,9 @@ mut:
cached_hashbits u8
// Used for right-shifting out used hashbits
shift u8
// Callback tags occupy existing padding and preserve custom hash/equality independently.
hash_kind u8
eq_kind u8
// Array storing key-values (ordered)
key_values DenseArray
// Pointer to meta-data:
@@ -215,14 +218,16 @@ mut:
metas &u32
// Extra metas that allows for no ranging when incrementing
// index in the hashmap
extra_metas u32
has_string_keys bool
hash_fn MapHashFn
key_eq_fn MapEqFn
clone_fn MapCloneFn
free_fn MapFreeFn
extra_metas u32
hash_fn MapHashFn
key_eq_fn MapEqFn
clone_fn MapCloneFn
free_fn MapFreeFn
// Number of key-values currently in the hashmap
count int
// Conservative residues of cloned standard string keys; deletions retain a superset.
string_length_bits u64
string_first_bits u64
}
// map is the pointer-sized internal representation of a V `map` type.
@@ -350,10 +355,80 @@ fn map_free_string(pkey voidptr) {
fn map_free_nop(_ voidptr) {
}
// Classify once when constructing the map; zero preserves custom callbacks.
@[inline]
fn map_hash_kind(callback MapHashFn) u8 {
return if voidptr(callback) == voidptr(map_hash_string) {
u8(1)
} else if voidptr(callback) == voidptr(map_hash_int_4) {
u8(4)
} else if voidptr(callback) == voidptr(map_hash_int_8) {
u8(8)
} else {
u8(0)
}
}
@[inline]
fn map_eq_kind(callback MapEqFn) u8 {
return if voidptr(callback) == voidptr(map_eq_string) {
u8(1)
} else if voidptr(callback) == voidptr(map_eq_int_4) {
u8(4)
} else if voidptr(callback) == voidptr(map_eq_int_8) {
u8(8)
} else {
u8(0)
}
}
// The extra bit enables the filter without repeating callback comparisons on reads.
// A custom clone is excluded because it can change the stored string bytes.
@[inline]
fn map_length_filter_eq_kind(hash_fn MapHashFn, key_eq_fn MapEqFn, clone_fn MapCloneFn) u8 {
kind := map_eq_kind(key_eq_fn)
if kind == 1 && voidptr(hash_fn) == voidptr(map_hash_string)
&& voidptr(clone_fn) == voidptr(map_clone_string) {
return kind | u8(16)
}
return kind
}
@[inline]
fn map_string_length_bit(key voidptr) u64 {
return u64(1) << (u32(unsafe { (&string(key)).len }) & u32(63))
}
@[inline]
fn map_string_first_bit(key voidptr) u64 {
value := unsafe { &string(key) }
first := if value.len == 0 { u8(0) } else { unsafe { value.str[0] } }
return u64(1) << (u32(first) & u32(63))
}
@[inline]
fn (m &VMapData) rejects_string_key(key voidptr) bool {
return m.eq_kind & 16 != 0
&& (m.string_length_bits & map_string_length_bit(key) == 0
|| m.string_first_bits & map_string_first_bit(key) == 0)
}
@[inline]
fn (mut m VMapData) remember_string_key(key voidptr) {
if m.eq_kind & 16 == 0 {
return
}
m.string_length_bits |= map_string_length_bit(key)
m.string_first_bits |= map_string_first_bit(key)
if m.string_length_bits == u64(0xffffffffffffffff)
&& m.string_first_bits == u64(0xffffffffffffffff) {
// Neither saturated filter can reject another lookup until clear.
m.eq_kind &= u8(0xef)
}
}
fn new_map_data(key_bytes int, value_bytes int, hash_fn MapHashFn, key_eq_fn MapEqFn, clone_fn MapCloneFn,
free_fn MapFreeFn) &VMapData {
// for now assume anything bigger than a pointer is a string
has_string_keys := key_bytes > int(sizeof(voidptr))
// Arenas retain old metadata slabs, so reserve a modest probe tail before
// insertion instead of repeatedly copying the slab to add four entries.
mut initial_extra_metas := u32(extra_metas_inc)
@@ -366,11 +441,12 @@ fn new_map_data(key_bytes int, value_bytes int, hash_fn MapHashFn, key_eq_fn Map
even_index: init_even_index
cached_hashbits: max_cached_hashbits
shift: init_log_capicity
hash_kind: map_hash_kind(hash_fn)
eq_kind: map_length_filter_eq_kind(hash_fn, key_eq_fn, clone_fn)
key_values: DenseArray{ key_bytes: key_bytes, value_bytes: value_bytes }
metas: unsafe { nil }
extra_metas: initial_extra_metas
count: 0
has_string_keys: has_string_keys
hash_fn: hash_fn
key_eq_fn: key_eq_fn
clone_fn: clone_fn
@@ -380,25 +456,31 @@ fn new_map_data(key_bytes int, value_bytes int, hash_fn MapHashFn, key_eq_fn Map
fn new_map_with_dense_array(key_bytes int, value_bytes int, hash_fn MapHashFn, key_eq_fn MapEqFn,
clone_fn MapCloneFn, free_fn MapFreeFn, key_values DenseArray, metas &u32) map {
has_string_keys := key_bytes > int(sizeof(voidptr))
return map{
data: &VMapData{
key_bytes: key_bytes
value_bytes: value_bytes
even_index: init_even_index
cached_hashbits: max_cached_hashbits
shift: init_log_capicity
key_values: key_values
metas: unsafe { metas }
extra_metas: extra_metas_inc
count: 0
has_string_keys: has_string_keys
hash_fn: hash_fn
key_eq_fn: key_eq_fn
clone_fn: clone_fn
free_fn: free_fn
mut data := &VMapData{
key_bytes: key_bytes
value_bytes: value_bytes
even_index: init_even_index
cached_hashbits: max_cached_hashbits
shift: init_log_capicity
hash_kind: map_hash_kind(hash_fn)
eq_kind: map_length_filter_eq_kind(hash_fn, key_eq_fn, clone_fn)
key_values: key_values
metas: unsafe { metas }
extra_metas: extra_metas_inc
count: 0
hash_fn: hash_fn
key_eq_fn: key_eq_fn
clone_fn: clone_fn
free_fn: free_fn
}
if data.eq_kind & 16 != 0 {
for i in 0 .. key_values.len {
if key_values.has_index(i) {
data.remember_string_key(key_values.key(i))
}
}
}
return map{ data: data }
}
fn new_map(key_bytes int, value_bytes int, hash_fn MapHashFn, key_eq_fn MapEqFn, clone_fn MapCloneFn, free_fn MapFreeFn) map {
@@ -470,6 +552,9 @@ fn (mut m VMapData) clear() {
if m.metas == unsafe { nil } {
return
}
m.string_length_bits = 0
m.string_first_bits = 0
m.eq_kind = map_length_filter_eq_kind(m.hash_fn, m.key_eq_fn, m.clone_fn)
unsafe {
if m.key_values.all_deleted != 0 {
free(m.key_values.all_deleted)
@@ -503,24 +588,35 @@ fn (m &VMapData) panic_nil_map_hash_fn() {
// the callback for all other key representations and custom comparators.
@[inline]
fn (m &VMapData) keys_equal(a voidptr, b voidptr) bool {
if voidptr(m.key_eq_fn) == voidptr(map_eq_string) {
if m.eq_kind & 1 != 0 {
return map_eq_string(a, b)
}
if m.eq_kind == 4 {
return map_eq_int_4(a, b)
}
if m.eq_kind == 8 {
return map_eq_int_8(a, b)
}
return m.key_eq_fn(a, b)
}
@[inline]
fn (m &VMapData) key_to_index(pkey voidptr) (u32, u32) {
if voidptr(m.hash_fn) == unsafe { nil } {
m.panic_nil_map_hash_fn()
}
hash := if voidptr(m.hash_fn) == voidptr(map_hash_string) {
hash := if m.hash_kind & 1 != 0 {
map_hash_string(pkey)
} else if m.hash_kind == 4 {
map_hash_int_4(pkey)
} else if m.hash_kind == 8 {
map_hash_int_8(pkey)
} else {
if voidptr(m.hash_fn) == unsafe { nil } {
m.panic_nil_map_hash_fn()
}
m.hash_fn(pkey)
}
index := hash & m.even_index
meta := ((hash >> m.shift) & hash_mask) | probe_inc
// Valid table growth cannot exhaust the 64-bit hash before the u32 index.
meta := ((hash >> (m.shift & 63)) & hash_mask) | probe_inc
return u32(index), u32(meta)
}
@@ -652,6 +748,7 @@ fn (mut m VMapData) set_general(key voidptr, value voidptr) {
pkey := m.key_values.key(kv_index)
pvalue := m.key_values.value(kv_index)
m.clone_fn(pkey, key)
m.remember_string_key(pkey)
vmemcpy(pvalue, value, m.value_bytes)
}
m.meta_greater(index, meta, u32(kv_index))
@@ -845,6 +942,9 @@ fn (m &VMapData) get_nonempty(key voidptr, zero voidptr) voidptr {
}
return zero
}
if m.rejects_string_key(key) {
return zero
}
mut index, mut meta := m.key_to_index(key)
for {
stored_meta := unsafe { m.metas[index] }
@@ -895,6 +995,9 @@ fn (m &VMapData) get_check_nonempty(key voidptr) voidptr {
}
return 0
}
if m.rejects_string_key(key) {
return 0
}
mut index, mut meta := m.key_to_index(key)
for {
stored_meta := unsafe { m.metas[index] }
@@ -990,6 +1093,9 @@ fn (m &VMapData) exists_nonempty(key voidptr) bool {
}
return false
}
if m.rejects_string_key(key) {
return false
}
mut index, mut meta := m.key_to_index(key)
for {
stored_meta := unsafe { m.metas[index] }
@@ -1256,23 +1362,26 @@ fn (m &VMapData) clone() &VMapData {
}
metasize := int(sizeof(u32) * (m.even_index + 2 + m.extra_metas))
res := &VMapData{
key_bytes: m.key_bytes
value_bytes: m.value_bytes
even_index: m.even_index
cached_hashbits: m.cached_hashbits
shift: m.shift
key_values: unsafe { m.key_values.clone() }
metas: unsafe { &u32(malloc_noscan(metasize)) }
extra_metas: m.extra_metas
count: m.count
has_string_keys: m.has_string_keys
hash_fn: m.hash_fn
key_eq_fn: m.key_eq_fn
clone_fn: m.clone_fn
free_fn: m.free_fn
key_bytes: m.key_bytes
value_bytes: m.value_bytes
even_index: m.even_index
cached_hashbits: m.cached_hashbits
shift: m.shift
hash_kind: m.hash_kind
eq_kind: m.eq_kind
key_values: unsafe { m.key_values.clone() }
metas: unsafe { &u32(malloc_noscan(metasize)) }
extra_metas: m.extra_metas
count: m.count
string_length_bits: m.string_length_bits
string_first_bits: m.string_first_bits
hash_fn: m.hash_fn
key_eq_fn: m.key_eq_fn
clone_fn: m.clone_fn
free_fn: m.free_fn
}
unsafe { vmemcpy(res.metas, m.metas, metasize) }
if !m.has_string_keys {
if m.key_bytes <= int(sizeof(voidptr)) {
return res
}
// clone keys
@@ -1350,6 +1459,8 @@ fn (m &VMapData) free() {
}
// TODO: the next lines assume that callback functions are static and independent from each particular
// map instance. Closures may invalidate that assumption, so revisit when RC for closures works.
m.hash_kind = 0
m.eq_kind = 0
m.hash_fn = nil
m.key_eq_fn = nil
m.clone_fn = nil
@@ -1361,7 +1472,8 @@ fn (m &VMapData) free() {
m.cached_hashbits = 0
m.shift = 0
m.extra_metas = 0
m.has_string_keys = false
m.string_length_bits = 0
m.string_first_bits = 0
m.count = 0
}
}
+60
View File
@@ -0,0 +1,60 @@
module builtin
fn map_shift_test_hash(key voidptr) u64 {
return unsafe { *&u64(key) }
}
fn test_map_hash_shift_mask_preserves_all_in_range_shifts() {
$if v3_backend ? {
mut state := VMapData{
even_index: 0x7ffffffe
hash_fn: map_shift_test_hash
metas: unsafe { nil }
}
for shift in 0 .. 64 {
state.shift = u8(shift)
for key in [u64(0), 1, 0x8000000000000000, 0xffffffffffffffff, 0x123456789abcdef0] {
index, meta := state.key_to_index(&key)
assert index == u32(key & 0x7ffffffe)
assert meta == u32(((key >> shift) & hash_mask) | probe_inc)
}
mut key := u64(0x9e3779b97f4a7c15)
for _ in 0 .. 1024 {
// Xorshift visits mixed high and low bits without overflowing arithmetic.
key ^= key << 13
key ^= key >> 7
key ^= key << 17
index, meta := state.key_to_index(&key)
assert index == u32(key & 0x7ffffffe)
assert meta == u32(((key >> shift) & hash_mask) | probe_inc)
}
}
}
}
fn test_map_hash_shift_stays_within_hash_until_index_exhausted() {
// Model the growth recurrence without allocating exponentially large tables.
mut even_index := u64(init_even_index)
mut cached := max_cached_hashbits
mut shift := init_log_capicity
mut expansions := 0
for {
assert even_index <= 0xfffffffe
assert shift == init_log_capicity || shift == init_log_capicity + max_cached_hashbits
assert shift < 64
next := ((even_index + 2) << 1) - 2
if next > 0xfffffffe {
break
}
even_index = next
if cached == 0 {
shift += max_cached_hashbits
cached = max_cached_hashbits
} else {
cached--
}
expansions++
}
assert expansions == 27
assert shift == 21
}
@@ -0,0 +1,423 @@
@[has_globals]
module builtin
__global (
length_filter_hash_calls int
length_filter_eq_calls int
length_filter_clone_calls int
)
fn length_filter_custom_hash(key voidptr) u64 {
length_filter_hash_calls++
return map_hash_string(key)
}
fn length_filter_custom_eq(a voidptr, b voidptr) bool {
length_filter_eq_calls++
return map_eq_string(a, b)
}
fn length_filter_custom_clone(destination voidptr, source voidptr) {
length_filter_clone_calls++
map_clone_string(destination, source)
}
fn length_filter_shortening_clone(destination voidptr, source voidptr) {
length_filter_clone_calls++
map_clone_string(destination, source)
unsafe { (&string(destination)).len-- }
}
fn length_filter_first_byte_clone(destination voidptr, source voidptr) {
length_filter_clone_calls++
map_clone_string(destination, source)
unsafe { (&string(destination)).str[0] = u8(255) }
}
fn assert_length_filter_read(values &map, key string, expected int) {
assert values.exists(&key) == (expected >= 0)
found := values.get_check(&key)
if expected >= 0 {
assert found != unsafe { nil }
assert unsafe { *&int(found) } == expected
} else {
assert found == unsafe { nil }
}
zero := -1
assert unsafe { *&int(values.get(&key, &zero)) } == expected
}
fn new_length_filter_map() map {
return new_map(int(sizeof(string)), int(sizeof(int)), map_hash_string, map_eq_string,
map_clone_string, map_free_string)
}
fn test_map_string_length_filter_handles_length_residues_and_clones() {
$if v3_backend ? {
mut values := new_length_filter_map()
keys := ['', 'a'.repeat(31), 'b'.repeat(32), 'c'.repeat(63), 'd'.repeat(64), 'e'.repeat(129),
'a\x00b', '\x00z', 'é', '界', 'f'.repeat(127), 'g'.repeat(128)]
for i, key in keys {
value := i + 10
values.set(&key, &value)
}
assert values.data.eq_kind == 17
expected_bits := u64(1) | (u64(1) << 31) | (u64(1) << 32) | (u64(1) << 63) |
(u64(1) << 1) | (u64(1) << 2) | (u64(1) << 3)
assert values.data.string_length_bits == expected_bits
expected_first := u64(1) | (u64(1) << 33) | (u64(1) << 34) | (u64(1) << 35) |
(u64(1) << 36) | (u64(1) << 37) | (u64(1) << 38) | (u64(1) << 3) |
(u64(1) << 39)
assert values.data.string_first_bits == expected_first
missing := 'xxxx'
assert values.data.rejects_string_key(&missing)
mut direct := unsafe { values.clone() }
assert direct.data.string_length_bits == values.data.string_length_bits
assert direct.data.string_first_bits == values.data.string_first_bits
values.reserve(4096)
mut sparse := unsafe { values.clone() }
assert sparse.data.string_length_bits == values.data.string_length_bits
assert sparse.data.string_first_bits == values.data.string_first_bits
for i, key in keys {
assert_length_filter_read(&values, key, i + 10)
assert_length_filter_read(&direct, key, i + 10)
assert_length_filter_read(&sparse, key, i + 10)
}
assert_length_filter_read(&values, 'xx', -1)
assert_length_filter_read(&values, 'z'.repeat(31), -1)
key := keys[1]
updated := 77
bits := values.data.string_length_bits
first_bits := values.data.string_first_bits
values.set(&key, &updated)
assert values.data.string_length_bits == bits
assert values.data.string_first_bits == first_bits
assert_length_filter_read(&values, key, updated)
unsafe {
direct.free()
sparse.free()
values.free()
}
}
}
fn test_map_string_length_filter_survives_deletion_compaction_and_insert_on_read() {
$if v3_backend ? {
mut values := new_length_filter_map()
for i in 0 .. 64 {
key := 'entry-${100 + i}'
values.set(&key, &i)
}
bits := values.data.string_length_bits
first_bits := values.data.string_first_bits
for i in 0 .. 32 {
key := 'entry-${100 + i}'
unsafe { values.delete(&key) }
}
assert values.data.key_values.len == 32
assert values.data.key_values.deletes == 0
assert values.data.string_length_bits == bits
assert values.data.string_first_bits == first_bits
values.reserve(4096)
for i in 0 .. 64 {
assert_length_filter_read(&values, 'entry-${100 + i}', if i >= 32 { i } else { -1 })
}
key := 'new-key-longer'
zero := 99
assert unsafe { *&int(values.get_and_set(&key, &zero)) } == zero
assert !values.data.rejects_string_key(&key)
assert_length_filter_read(&values, key, zero)
unsafe { values.free() }
}
}
fn test_map_string_length_filter_saturation_clear_empty_clone_and_move() {
$if v3_backend ? {
mut values := new_length_filter_map()
values.clear()
assert values.data.eq_kind == 17
assert values.data.string_length_bits == 0
assert values.data.string_first_bits == 0
for i in 0 .. 64 {
key := 's'.repeat(i)
values.set(&key, &i)
}
assert values.data.string_length_bits == u64(0xffffffffffffffff)
assert values.data.string_first_bits == u64(1) | (u64(1) << 51)
assert values.data.eq_kind == 17
missing := 'z'
assert values.data.rejects_string_key(&missing)
assert_length_filter_read(&values, missing, -1)
for i in 0 .. 64 {
key := [u8(i), u8(120)].bytestr()
values.set(&key, &i)
}
assert values.data.string_first_bits == u64(0xffffffffffffffff)
assert values.data.eq_kind == 1
mut direct := unsafe { values.clone() }
assert direct.data.string_length_bits == u64(0xffffffffffffffff)
assert direct.data.string_first_bits == u64(0xffffffffffffffff)
assert direct.data.eq_kind == 1
values.reserve(4096)
mut sparse := unsafe { values.clone() }
assert sparse.data.string_length_bits == u64(0xffffffffffffffff)
assert sparse.data.string_first_bits == u64(0xffffffffffffffff)
assert sparse.data.eq_kind == 1
mut moved := sparse.move()
assert sparse.data.eq_kind == 17
assert sparse.data.string_length_bits == 0
assert sparse.data.string_first_bits == 0
for i in 0 .. 64 {
key := 's'.repeat(i)
assert_length_filter_read(&direct, key, i)
assert_length_filter_read(&moved, key, i)
byte_key := [u8(i), u8(120)].bytestr()
assert_length_filter_read(&direct, byte_key, i)
assert_length_filter_read(&moved, byte_key, i)
}
values.clear()
assert values.data.eq_kind == 17
assert values.data.string_length_bits == 0
assert values.data.string_first_bits == 0
mut empty := unsafe { values.clone() }
assert empty.data.eq_kind == 17
assert empty.data.string_length_bits == 0
assert empty.data.string_first_bits == 0
for i in 1 .. 4 {
key := 'r'.repeat(i * 64)
values.set(&key, &i)
}
assert values.data.eq_kind == 17
assert values.data.string_length_bits == 1
assert values.data.string_first_bits == u64(1) << 50
assert_length_filter_read(&values, 'r'.repeat(128), 2)
assert_length_filter_read(&values, 's'.repeat(31), -1)
unsafe {
direct.free()
moved.free()
sparse.free()
empty.free()
values.free()
}
}
}
fn test_map_string_length_filter_keeps_length_filter_when_first_is_saturated() {
$if v3_backend ? {
mut values := new_length_filter_map()
for i in 0 .. 64 {
key := [u8(i), u8(120)].bytestr()
values.set(&key, &i)
}
assert values.data.string_length_bits == u64(1) << 2
assert values.data.string_first_bits == u64(0xffffffffffffffff)
assert values.data.eq_kind == 17
missing := 'abc'
assert values.data.rejects_string_key(&missing)
assert_length_filter_read(&values, missing, -1)
for i in 0 .. 64 {
key := [u8(i), u8(120)].bytestr()
assert_length_filter_read(&values, key, i)
}
unsafe { values.free() }
}
}
fn test_map_string_length_filter_handles_unsigned_first_byte_residues() {
$if v3_backend ? {
mut values := new_length_filter_map()
first_bytes := [u8(0), 63, 64, 127, 128, 255]
for i, first in first_bytes {
key := [first, u8(i), u8(120)].bytestr()
values.set(&key, &i)
}
assert values.data.string_length_bits == u64(1) << 3
assert values.data.string_first_bits == u64(1) | (u64(1) << 63)
missing := [u8(1), u8(0), u8(120)].bytestr()
assert values.data.rejects_string_key(&missing)
assert_length_filter_read(&values, missing, -1)
for i, first in first_bytes {
key := [first, u8(i), u8(120)].bytestr()
assert_length_filter_read(&values, key, i)
}
unsafe { values.free() }
}
}
fn test_map_string_length_filter_compact_clone_rebuilds_saturated_history() {
$if v3_backend ? {
mut values := new_length_filter_map()
for i in 0 .. 64 {
key := 's'.repeat(i)
values.set(&key, &i)
byte_key := [u8(i), u8(120)].bytestr()
values.set(&byte_key, &i)
}
assert values.data.eq_kind == 1
for i in 0 .. 64 {
byte_key := [u8(i), u8(120)].bytestr()
unsafe { values.delete(&byte_key) }
if i < 61 {
key := 's'.repeat(i)
unsafe { values.delete(&key) }
}
}
assert values.data.count == 3
assert values.data.string_length_bits == u64(0xffffffffffffffff)
assert values.data.string_first_bits == u64(0xffffffffffffffff)
values.reserve(4096)
mut compact := unsafe { values.clone() }
assert compact.data.eq_kind == 17
assert compact.data.string_length_bits == (u64(1) << 61) | (u64(1) << 62) |
(u64(1) << 63)
assert compact.data.string_first_bits == u64(1) << 51
for i in 61 .. 64 {
assert_length_filter_read(&compact, 's'.repeat(i), i)
}
missing := 'x'.repeat(61)
assert compact.data.rejects_string_key(&missing)
assert_length_filter_read(&compact, missing, -1)
unsafe {
compact.free()
values.free()
}
}
}
fn test_map_string_length_filter_keeps_custom_callbacks_and_clone_lengths() {
$if v3_backend ? {
for mode in 1 .. 8 {
hash_fn := if mode & 1 != 0 { length_filter_custom_hash } else { map_hash_string }
eq_fn := if mode & 2 != 0 { length_filter_custom_eq } else { map_eq_string }
clone_fn := if mode & 4 != 0 { length_filter_custom_clone } else { map_clone_string }
mut values := new_map(int(sizeof(string)), int(sizeof(int)), hash_fn, eq_fn,
clone_fn, map_free_string)
length_filter_clone_calls = 0
for i in 1 .. 5 {
key := 'k'.repeat(i)
values.set(&key, &i)
}
assert values.data.eq_kind & 16 == 0
assert values.data.string_length_bits == 0
assert values.data.string_first_bits == 0
assert length_filter_clone_calls == if mode & 4 != 0 { 4 } else { 0 }
length_filter_hash_calls = 0
length_filter_eq_calls = 0
assert_length_filter_read(&values, 'kkkk', 4)
assert length_filter_hash_calls == if mode & 1 != 0 { 3 } else { 0 }
assert length_filter_eq_calls == if mode & 2 != 0 { 3 } else { 0 }
assert_length_filter_read(&values, 'missing-length', -1)
assert length_filter_hash_calls == if mode & 1 != 0 { 6 } else { 0 }
values.clear()
assert values.data.eq_kind & 16 == 0
unsafe { values.free() }
}
mut shortened := new_map(int(sizeof(string)), int(sizeof(int)), map_hash_string,
map_eq_string, length_filter_shortening_clone, map_free_string)
length_filter_clone_calls = 0
for i in 1 .. 5 {
key := 'x'.repeat(i + 1)
shortened.set(&key, &i)
}
assert length_filter_clone_calls == 4
assert shortened.data.eq_kind == 1
assert shortened.data.string_length_bits == 0
assert shortened.data.string_first_bits == 0
for i in 0 .. 4 {
assert unsafe { (&string(shortened.data.key_values.key(i))).len } == i + 1
}
// This raw callback changes key identity; original keys must retain ordinary misses.
assert_length_filter_read(&shortened, 'xxxxx', -1)
unsafe { shortened.free() }
mut changed_first := new_map(int(sizeof(string)), int(sizeof(int)), map_hash_string,
map_eq_string, length_filter_first_byte_clone, map_free_string)
length_filter_clone_calls = 0
for i in 1 .. 5 {
key := 'x'.repeat(i + 1)
changed_first.set(&key, &i)
}
assert length_filter_clone_calls == 4
assert changed_first.data.eq_kind == 1
assert changed_first.data.string_length_bits == 0
assert changed_first.data.string_first_bits == 0
for i in 0 .. 4 {
assert unsafe { (&string(changed_first.data.key_values.key(i))).str[0] } == u8(255)
}
assert_length_filter_read(&changed_first, 'xxxxx', -1)
unsafe { changed_first.free() }
}
}
fn test_map_string_length_filter_seeds_only_live_provided_dense_keys() {
$if v3_backend ? {
mut dense := new_dense_array(int(sizeof(string)), int(sizeof(int)), 0, 0)
keys := ['', 'discard', 'x'.repeat(31), 'y'.repeat(32)]
for i, key in keys {
index := dense.expand()
map_clone_string(dense.key(index), &key)
unsafe { vmemcpy(dense.value(index), &i, int(sizeof(int))) }
}
map_free_string(dense.key(1))
dense.delete(1)
metas := unsafe { &u32(vcalloc_noscan(int(sizeof(u32) * (init_capicity + extra_metas_inc)))) }
mut values := new_map_with_dense_array(int(sizeof(string)), int(sizeof(int)),
map_hash_string, map_eq_string, map_clone_string, map_free_string, dense, metas)
expected_bits := u64(1) | (u64(1) << 31) | (u64(1) << 32)
assert values.data.string_length_bits == expected_bits
assert values.data.string_first_bits == u64(1) | (u64(1) << 56) | (u64(1) << 57)
assert values.data.rejects_string_key(&keys[1])
values.data.count = 3
values.data.rehash()
for i, key in keys {
assert_length_filter_read(&values, key, if i == 1 { -1 } else { i })
}
unsafe { values.free() }
}
}
fn length_filter_read_worker(values &map, keys []string, worker_id int) int {
$if v3_backend ? {
missing := 'absent-length'
zero := -1
for iteration in 0 .. 256 {
index := (iteration + worker_id) % keys.len
key := keys[index]
found := values.get_check(&key)
if found == unsafe { nil } || unsafe { *&int(found) } != index + 1
|| !values.exists(&key)
|| unsafe { *&int(values.get(&key, &zero)) } != index + 1 {
return -1
}
if values.get_check(&missing) != unsafe { nil } || values.exists(&missing)
|| values.get(&missing, &zero) != voidptr(&zero) {
return -1
}
}
}
return worker_id
}
fn test_map_string_length_filter_allows_concurrent_readonly_mixed_keys() {
$if v3_backend ? {
mut values := new_length_filter_map()
keys := ['alpha', 'b'.repeat(31), 'c'.repeat(32), '\x00'.repeat(64)]
for i, key in keys {
value := i + 1
values.set(&key, &value)
}
bits := values.data.string_length_bits
first_bits := values.data.string_first_bits
mut threads := []thread int{}
for worker_id in 0 .. 4 {
threads << spawn length_filter_read_worker(&values, keys, worker_id)
}
for worker_id, handle in threads {
assert handle.wait() == worker_id
}
assert values.data.string_length_bits == bits
assert values.data.string_first_bits == first_bits
// All readers have joined before changing or releasing the map.
values.clear()
unsafe { values.free() }
}
}
+2 -13
View File
@@ -1551,18 +1551,7 @@ fn (s string) index_multibyte(p string) int {
@[direct_array_access]
fn (s string) index_pair(p string) int {
first := unsafe { p.str[0] }
second := unsafe { p.str[1] }
last_start := s.len - 2
for i := 0; i <= last_start; i++ {
if unsafe { s.str[i] } != first {
next := string_index_byte(unsafe { s.str + i + 1 }, last_start - i, first)
if next < 0 { return -1 }
i += next + 1
}
if unsafe { s.str[i + 1] } == second { return i }
}
return -1
return string_index_pair(s.str, s.len, unsafe { p.str[0] }, unsafe { p.str[1] })
}
// Needles up to this length are searched directly; longer ones use KMP, which
@@ -1759,7 +1748,7 @@ pub fn (s string) count(substr string) int {
// See also: [`string.index_u8`](#string.index_u8) , to get the index of the byte as well.
@[inline]
pub fn (s string) contains_u8(x u8) bool {
return string_index_byte(s.str, s.len, x) >= 0
return string_contains_byte(s.str, s.len, x)
}
// contains returns `true` if the string contains `substr`.
+67
View File
@@ -120,3 +120,70 @@ fn test_byte_search_handles_adjacent_borrows_and_repeated_bytes() {
}
}
}
fn test_byte_contains_checks_every_short_match_position() {
for size in 1 .. 34 {
for start in [0, 1, 3, 7, 15, 31] {
for byte in [u8(0), 1, 0x7f, 0x80, 0xfe, 0xff] {
mut bytes := []u8{len: start + size + 32, init: byte ^ 1}
view := unsafe { tos(&bytes[start], size) }
assert !view.contains_u8(byte)
for position in 0 .. size {
bytes[start + position] = byte
assert view.contains_u8(byte)
bytes[start + position] = byte ^ 1
}
bytes[start + size] = byte
assert !view.contains_u8(byte)
if start > 0 {
bytes[start - 1] = byte
assert !view.contains_u8(byte)
}
}
}
}
}
fn test_pair_search_respects_every_short_window_and_borrowed_boundary() {
for first in [u8(0), 1, 0x7f, 0x80, 0xfe, 0xff] {
for second in [u8(0), 1, 0x7f, 0x80, 0xfe, 0xff] {
needle_bytes := [first, second]
needle := unsafe { tos(&needle_bytes[0], 2) }
for start in 0 .. 8 {
for size in 0 .. 35 {
mut bytes := []u8{len: start + size + 2, init: first ^ 1}
view := unsafe { tos(&bytes[start], size) }
assert view.index_(needle) == byte_search_reference_index(view, needle)
for at in 0 .. size - 1 {
bytes[start + at] = first
bytes[start + at + 1] = second
expected := byte_search_reference_index(view, needle)
assert view.index_(needle) == expected
assert view.contains(needle) == (expected >= 0)
bytes[start + at] = first ^ 1
bytes[start + at + 1] = first ^ 1
}
// A pair straddling the end must not become an in-span match.
if size > 0 { bytes[start + size - 1] = first }
bytes[start + size] = second
assert view.index_(needle) == byte_search_reference_index(view, needle)
}
}
}
}
}
fn test_pair_search_rejects_adjacent_mask_borrows() {
for first in 0 .. 256 {
for second in 0 .. 256 {
needle_bytes := [u8(first), u8(second)]
needle := unsafe { tos(&needle_bytes[0], 2) }
mut bytes := []u8{len: 32, init: if index & 1 == 0 { u8(first) } else { u8(second) ^ 1 }}
view := unsafe { tos(&bytes[0], bytes.len) }
assert view.index_(needle) == byte_search_reference_index(view, needle)
bytes[30] = u8(first)
bytes[31] = u8(second)
assert view.index_(needle) == byte_search_reference_index(view, needle)
}
}
}
+11 -12
View File
@@ -27,10 +27,16 @@ pub fn (mut b Builder) reuse_as_plain_u8_array() []u8 {
return *b
}
// write_ptr writes `len` bytes provided byteptr to the accumulated buffer
@[unsafe]
// write_ptr appends `len` bytes from `ptr` to the accumulated buffer.
@[inline; unsafe]
pub fn (mut b Builder) write_ptr(ptr &u8, len int) {
if len == 0 {
if len <= 0 || ptr == unsafe { nil } {
return
}
if len <= b.cap - b.len && !b.flags.has(.is_slice) {
// The source may borrow the builder's buffer, including unused capacity.
unsafe { vmemmove(&u8(b.data) + b.len, ptr, len) }
b.len += len
return
}
unsafe { b.push_many(ptr, len) }
@@ -138,17 +144,10 @@ pub fn (b &Builder) byte_at(n int) u8 {
return unsafe { (&[]u8(b))[n] }
}
// write appends the string `s` to the buffer
// write_string appends the string `s` to the buffer.
@[expand_simple_interpolation; inline]
pub fn (mut b Builder) write_string(s string) {
if s.len == 0 {
return
}
unsafe { b.push_many(s.str, s.len) }
// for c in s {
// b.buf << c
// }
// b.buf << []u8(s) // TODO
unsafe { b.write_ptr(s.str, s.len) }
}
// write_string2 appends the strings `s1` and `s2` to the buffer.
+81
View File
@@ -0,0 +1,81 @@
import strings
fn test_builder_append_copy_boundaries() {
for initial_cap in [0, 4096] {
mut b := strings.new_builder(initial_cap)
mut expected := []u8{}
for size in [0, 1, 2, 3, 4, 7, 8, 15, 16, 31, 32, 63, 64, 65] {
mut payload := []u8{cap: size}
for i in 0 .. size {
payload << u8(i)
}
b.write_string(payload.bytestr())
expected << payload
unsafe { b.write_ptr(payload.data, payload.len) }
expected << payload
}
assert b.str().bytes() == expected
}
}
fn test_builder_append_exact_capacity_then_grows() {
mut b := strings.new_builder(6)
b.write_string('abc')
unsafe { b.write_ptr(c'def', 3) }
assert b.len == 6
assert b.cap == 6
b.write_string('g')
assert b.cap >= 7
assert b.str() == 'abcdefg'
}
fn test_builder_append_borrows_own_buffer() {
for initial_cap in [6, 32] {
mut b := strings.new_builder(initial_cap)
b.write_string('abcdef')
// Borrow the live bytes so both the in-capacity and growth paths append themselves.
borrowed := unsafe { tos(&u8(b.data), b.len) }
b.write_string(borrowed)
assert b.len == 12
b.ensure_cap(32)
unsafe { b.write_ptr(&u8(b.data) + 2, 4) }
assert b.str() == 'abcdefabcdefcdef'
}
}
fn test_builder_append_overlapping_unused_capacity() {
mut b := strings.new_builder(32)
b.write_string('abcdefgh')
// The source still points into initialized storage after reducing the live length.
unsafe { b.len = 3 }
unsafe { b.write_ptr(&u8(b.data) + 1, 5) }
assert b.str() == 'abcbcdef'
}
fn test_builder_append_detaches_a_slice() {
for use_ptr in [false, true] {
mut backing := 'abcdef'.bytes()
mut b := strings.Builder(backing[1..5])
// A slice with spare capacity must detach before an append writes to shared storage.
unsafe { b.len = 2 }
if use_ptr {
unsafe { b.write_ptr(c'XY', 2) }
} else {
b.write_string('XY')
}
assert backing.bytestr() == 'abcdef'
assert b.str() == 'bcXY'
}
}
fn test_builder_append_ignores_empty_or_nil_input() {
mut b := strings.new_builder(8)
b.write_string('a')
b.write_string('')
unsafe {
b.write_ptr(nil, 0)
b.write_ptr(nil, 3)
b.write_ptr(c'skip', -1)
}
assert b.str() == 'a'
}
@@ -0,0 +1,85 @@
module flat
fn test_source_type_read_cache_rechecks_ids_on_collision() {
left_text := '&read_cache.Left'
left := source_type_id(left_text)
right_text := '[]read_cache.Right'
right := source_type_id(right_text)
left_entry := source_type_entry(source_type_table_load(), left)
right_entry := source_type_entry(source_type_table_load(), right)
slot := int(left & u32(source_type_read_cache_size - 1))
// A fully published entry for another ID must never answer a colliding query.
$if x32 {
C.atomic_store_u32(voidptr(&g_source_type_read_entries[slot]), u32(voidptr(right_entry)))
} $else {
C.atomic_store_u64(voidptr(&g_source_type_read_entries[slot]), u64(voidptr(right_entry)))
}
assert source_type_name(left) == left_text
assert source_type_shape(left) == left_entry.shape
assert source_type_read_entry(left) == left_entry
assert source_type_name(right) == right_text
assert source_type_shape(right) == right_entry.shape
}
fn test_source_type_read_cache_preserves_empty_sentinels() {
assert source_type_name(0) == ''
assert source_type_shape(0) == unsafe { SourceTypeShape(0) }
}
fn test_source_type_read_cache_keeps_canonical_borrowed_spelling() {
mut bytes := 'read_cache.Borrowed'.bytes()
view := unsafe { tos(bytes.data, bytes.len) }
id := source_type_id(view)
assert source_type_name(id) == 'read_cache.Borrowed'
shape := source_type_shape(id)
bytes[0] = `R`
changed := source_type_id(view)
assert changed != id
assert source_type_name(id) == 'read_cache.Borrowed'
assert source_type_shape(id) == shape
assert source_type_name(changed) == 'Read_cache.Borrowed'
}
fn test_source_type_read_cache_retains_entry_across_growth() {
text := '[]read_cache.BeforeGrowth'
id := source_type_id(text)
assert source_type_name(id) == text
entry := source_type_read_entry(id)
mut table := source_type_table_load()
source_type_lock()
table.grow()
C.atomic_store_u32(&g_source_types_lock, 0)
for i in 0 .. source_type_read_cache_size + 1 {
source_type_id('read_cache.Growth_${i}')
}
assert source_type_name(id) == text
assert source_type_shape(id) == entry.shape
assert source_type_read_entry(id) == entry
}
fn cache_source_type_in_disposable_scope() u32 {
$if prealloc {
scope := unsafe { prealloc_scope_begin() }
id := source_type_id('[]read_cache.Disposable'.clone())
assert source_type_name(id) == '[]read_cache.Disposable'
assert source_type_shape(id).has(.array_type)
unsafe {
prealloc_scope_leave(scope)
prealloc_scope_free_after(scope)
prealloc_thread_cleanup()
}
return id
} $else {
id := source_type_id('[]read_cache.Disposable'.clone())
assert source_type_name(id) == '[]read_cache.Disposable'
assert source_type_shape(id).has(.array_type)
return id
}
}
fn test_source_type_read_cache_outlives_publishing_worker_arena() {
handle := spawn cache_source_type_in_disposable_scope()
id := handle.wait()
assert source_type_name(id) == '[]read_cache.Disposable'
assert source_type_shape(id).has(.array_type)
}
+31 -10
View File
@@ -9,6 +9,7 @@ import hash
const source_type_chunk_bits = 10
const source_type_chunk_size = 1 << source_type_chunk_bits
const source_type_chunk_count = 16384
const source_type_read_cache_size = 4096
// Builtin spellings are interned once; hot paths carry their numeric identities.
pub const source_type_void = source_type_id('void')
@@ -248,6 +249,7 @@ struct SourceTypeIndex {
__global g_source_types &SourceTypeTable
__global g_source_types_lock u32
__global g_source_type_recent_entries [16384]voidptr
__global g_source_type_read_entries [source_type_read_cache_size]voidptr
fn source_type_lock() {
for {
@@ -491,28 +493,47 @@ fn source_type_id_uncached(text string) u32 {
return id
}
// source_type_read_entry caches immutable process-owned entries by their published ID.
@[direct_array_access; inline]
fn source_type_read_entry(id u32) &SourceTypeEntry {
slot := int(id & u32(source_type_read_cache_size - 1))
cached := $if x32 {
unsafe { &SourceTypeEntry(voidptr(C.atomic_load_u32(voidptr(&g_source_type_read_entries[slot])))) }
} $else {
unsafe { &SourceTypeEntry(voidptr(C.atomic_load_u64(voidptr(&g_source_type_read_entries[slot])))) }
}
if !isnil(cached) && cached.id == id {
return cached
}
table := source_type_table_load()
if isnil(table) || id > C.atomic_load_u32(&table.count) {
panic('invalid source type ID')
}
entry := source_type_entry(table, id)
// Only a checked published ID reaches this store. Entries never move or expire,
// so a matching ID remains valid across index growth and disposable worker arenas.
$if x32 {
C.atomic_store_u32(voidptr(&g_source_type_read_entries[slot]), u32(voidptr(entry)))
} $else {
C.atomic_store_u64(voidptr(&g_source_type_read_entries[slot]), u64(voidptr(entry)))
}
return entry
}
// source_type_name resolves a source spelling ID at a text boundary.
@[inline]
pub fn source_type_name(id u32) string {
if id == 0 {
return ''
}
table := source_type_table_load()
if isnil(table) || id > C.atomic_load_u32(&table.count) {
panic('invalid source type ID')
}
return source_type_entry(table, id).text
return source_type_read_entry(id).text
}
// source_type_shape returns syntax properties without resolving spelling bytes.
@[inline]
pub fn source_type_shape(id u32) SourceTypeShape {
if id == 0 { return unsafe { SourceTypeShape(0) } }
table := source_type_table_load()
if isnil(table) || id > C.atomic_load_u32(&table.count) {
panic('invalid source type ID')
}
return source_type_entry(table, id).shape
return source_type_read_entry(id).shape
}
// source_type_query returns context-free query results without decoding a spelling.
+78
View File
@@ -0,0 +1,78 @@
module c
import v.flat
import v.types
fn argument_rvalue_node(mut a flat.FlatAst, kind flat.NodeKind, value string, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
kind: kind
value: value
children_start: start
children_count: children.len
})
}
fn test_argument_address_lowering_preserves_values_locals_consts_calls_and_map_elements() {
for argument_kind in ['local', 'const', 'call', 'map_index'] {
for pointer_parameter in [false, true] {
for full_call in [false, true] {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'consume')
value := a.add_val(.int_literal, '7')
map_var := a.add_val(.ident, 'table')
map_key := a.add_val(.int_literal, '0')
inner_callee := a.add_val(.ident, 'make_value')
arg := match argument_kind {
'local' { a.add_val(.ident, 'local') }
'const' { a.add_val(.ident, 'limit') }
'call' { argument_rvalue_node(mut a, .call, '', [inner_callee]) }
else { argument_rvalue_node(mut a, .index, '', [map_var, map_key]) }
}
call := argument_rvalue_node(mut a, .call, '', [callee, arg])
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
int_type := types.Type(types.int_)
param_type := if pointer_parameter {
types.Type(types.Pointer{ base_type: &int_type })
} else {
int_type
}
tc.fn_param_types['consume'] = [param_type]
tc.fn_ret_types['consume'] = types.Type(types.void_)
tc.fn_param_types['make_value'] = []types.Type{}
tc.fn_ret_types['make_value'] = int_type
tc.cur_scope.insert('local', int_type)
tc.const_types['limit'] = int_type
tc.register_synth_type(arg, int_type)
tc.register_synth_type(value, int_type)
tc.register_synth_type(map_key, int_type)
tc.register_synth_type(map_var, types.Type(types.Map{ key_type: &int_type, value_type: &int_type }))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.const_vals['limit'] = value
// Keep expression emission independent of the argument's storage shape.
g.assert_expr_overrides[int(arg)] = 'ARG'
if full_call {
g.gen_call(call, a.node(call))
} else {
g.gen_call_args('consume', a.node(call), 1)
}
ct := tc.c_type(int_type)
expected_arg := if !pointer_parameter {
'ARG'
} else if argument_kind == 'const' {
'(${ct}[]){ARG}'
} else if argument_kind == 'call' || (argument_kind == 'map_index' && !full_call) {
'&((${ct}[]){ARG})[0]'
} else {
'&ARG'
}
expected := if full_call { 'consume(${expected_arg})' } else { expected_arg }
assert g.sb.str() == expected, '${argument_kind}, pointer=${pointer_parameter}, full=${full_call}'
}
}
}
}
+2 -2
View File
@@ -1027,12 +1027,12 @@ fn (mut g FlatGen) to_fixed_size_call_fixed_type(id flat.NodeId) ?types.ArrayFix
if node.kind != .call || node.children_count == 0 {
return none
}
fn_node := g.a.child_node(&node, 0)
fn_node := g.a.node(g.a.child(&node, 0))
if fn_node.kind != .selector || fn_node.value != 'to_fixed_size' || fn_node.children_count == 0 {
return none
}
if node.typ != 0 {
if fixed := array_fixed_type(g.parse_node_type(&node)) {
if fixed := array_fixed_type(*g.parse_annotation_type_reference(node.typ)) {
return fixed
}
}
@@ -0,0 +1,58 @@
module c
import v.flat
import v.types
fn test_borrowed_call_helpers_keep_stack_snapshot_across_arena_growth_and_nested_calls() {
mut a := flat.FlatAst.new()
inner_callee := a.add_val(.ident, 'inner')
a.children << inner_callee
inner := a.add_node(flat.Node{ kind: .call, children_start: 0, children_count: 1 })
outer_callee := a.add_val(.ident, 'outer')
a.children << outer_callee
a.children << inner
outer := a.add_node(flat.Node{ kind: .call, children_start: 1, children_count: 2 })
call_snapshot := a.nodes[int(outer)]
callee_snapshot := a.nodes[int(outer_callee)]
for i in 0 .. 2048 {
a.add_val(.ident, 'padding_${i}')
}
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.fn_param_types['inner'] = []types.Type{}
tc.fn_param_types['outer'] = [types.Type(types.int_)]
tc.fn_ret_types['inner'] = types.Type(types.int_)
tc.fn_ret_types['outer'] = types.Type(types.int_)
tc.register_synth_type(inner, types.Type(types.int_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
assert g.direct_call_name_for_call_node(outer, &call_snapshot, 'outer') == 'outer'
assert g.call_target_key(&callee_snapshot) == 'outer'
assert !g.call_callee_is_module_selector(&call_snapshot)
g.gen_call(outer, &call_snapshot)
assert g.sb.str() == 'outer(inner())'
assert call_snapshot == a.nodes[int(outer)]
assert callee_snapshot == a.nodes[int(outer_callee)]
}
fn test_borrowed_runtime_map_call_uses_stack_child_snapshots() {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'new_map')
key_size := a.add_val(.sizeof_expr, 'int')
value_size := a.add_val(.sizeof_expr, 'string')
a.children << callee
a.children << key_size
a.children << value_size
call := a.add_node(flat.Node{ kind: .call, children_start: 0, children_count: 3 })
call_snapshot := a.nodes[int(call)]
callee_snapshot := a.nodes[int(callee)]
for i in 0 .. 2048 {
a.add_val(.ident, 'padding_${i}')
}
mut g := FlatGen.new()
g.a = &a
assert g.is_runtime_new_map_call(&call_snapshot)
assert g.main_runtime_shadow_call_c_name(&call_snapshot, &callee_snapshot) == none
assert call_snapshot == a.nodes[int(call)]
}
+122
View File
@@ -0,0 +1,122 @@
module c
import v.flat
import v.types
fn test_target_module_static_signature_keeps_empty_missing_and_alias_fallbacks() {
for mode in ['empty_params', 'return_only', 'generic_only', 'return_with_arg', 'generic_with_arg',
'wrong_static_arity', 'no_static'] {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'short.work')
arg := a.add_val(.int_literal, '7')
a.children << callee
with_arg := mode in ['return_with_arg', 'generic_with_arg']
if with_arg { a.children << arg }
call := a.add_node(flat.Node{
kind: .call
children_start: 0
children_count: if with_arg {
2
} else {
1
}
})
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.imports['short'] = 'library'
tc.fn_param_types['library.work'] = []types.Type{}
match mode {
'empty_params' { tc.fn_param_types['short.work'] = []types.Type{} }
'return_only', 'return_with_arg' {
tc.fn_ret_types['short.work'] = types.Type(types.int_)
}
'generic_only', 'generic_with_arg' { tc.fn_generic_params['short.work'] = ['T'] }
'wrong_static_arity' { tc.fn_param_types['short.work'] = [types.Type(types.int_)] }
else {}
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
resolved := g.target_module_call_name('short.work', a.node(call)) or { '' }
expected := match mode {
'empty_params', 'return_only' { 'short.work' }
'wrong_static_arity', 'no_static' { 'library.work' }
else { '' }
}
assert resolved == expected, mode
}
}
fn test_target_module_signature_preserves_variadic_and_synthetic_marker_rules() {
for synthetic in [false, true] {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'short.work')
arg := a.add_val(.ident, if synthetic { 'short' } else { 'value' })
a.children << callee
a.children << arg
call := a.add_node(flat.Node{ kind: .call, children_start: 0, children_count: 2 })
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.imports['short'] = 'library'
tc.cur_scope.insert('short', types.Type(types.int_))
tc.fn_param_types['short.work'] = [types.Type(types.Array{ elem_type: types.Type(types.int_) })]
tc.fn_variadic['short.work'] = true
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
assert g.target_module_call_arg_start('short.work', a.node(call)) == if synthetic {
2
} else {
1
}
resolved := g.target_module_call_name('short.work', a.node(call)) or { '' }
assert resolved == if synthetic { 'short.work' } else { '' }
if synthetic {
assert !g.gen_transformed_method_ident_call(call, a.node(call), a.node(callee))
assert g.sb.len == 0
}
}
}
fn test_transformed_method_checks_receiver_before_discarded_c_name_resolution() {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'library.read')
arg := a.add_val(.int_literal, '7')
a.children << callee
a.children << arg
call := a.add_node(flat.Node{ kind: .call, children_start: 0, children_count: 2 })
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.fn_param_types['library.read'] = [types.Type(types.int_)]
tc.fn_ret_types['library.read'] = types.Type(types.int_)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
assert !g.gen_transformed_method_ident_call(call, a.node(call), a.node(callee))
assert g.sb.len == 0
assert 'library.read' !in g.c_name_cache.entries
g.gen_call(call, a.node(call))
assert g.sb.str() == 'library__read(7)'
}
fn test_transformed_method_keeps_source_and_emitted_parameter_lookup() {
for emitted_only in [false, true] {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'int.read')
arg := a.add_val(.int_literal, '7')
a.children << callee
a.children << arg
call := a.add_node(flat.Node{ kind: .call, children_start: 0, children_count: 2 })
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
key := if emitted_only { 'int__read' } else { 'int.read' }
tc.fn_param_types[key] = [types.Type(types.int_)]
tc.fn_ret_types[key] = types.Type(types.int_)
tc.register_synth_type(arg, types.Type(types.int_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
assert g.gen_transformed_method_ident_call(call, a.node(call), a.node(callee))
assert g.sb.str() == 'int__read(7)'
}
}
@@ -0,0 +1,96 @@
module c
import v.flat
import v.token
import v.types
fn test_call_resolution_snapshot_keeps_absent_and_present_empty_names() {
for present in [false, true] {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'ordinary')
a.children << callee
call := a.add_node(flat.Node{ kind: .call, children_start: 0, children_count: 1 })
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.fn_ret_types['ordinary'] = types.Type(types.void_)
tc.resolved_call_names = []&types.CachedName{len: a.nodes.len}
tc.resolved_call_set = []bool{len: a.nodes.len}
if present {
tc.resolved_call_names[int(call)] = types.cached_name('')
tc.resolved_call_set[int(call)] = true
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.gen_call(call, a.node(call))
assert g.sb.str() == 'ordinary()'
assert (tc.resolved_call_name(call) != none) == present
}
}
fn test_nested_call_resolution_snapshots_keep_each_selector_target() {
for sparse in [false, true] {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'receiver')
a.children << base
inner_selector := a.add_node(flat.Node{ kind: .selector, value: 'inner', children_start: 0, children_count: 1 })
a.children << inner_selector
inner := a.add_node(flat.Node{ kind: .call, children_start: 1, children_count: 1 })
a.children << base
outer_selector := a.add_node(flat.Node{ kind: .selector, value: 'outer', children_start: 2, children_count: 1 })
a.children << outer_selector
a.children << inner
outer := a.add_node(flat.Node{ kind: .call, children_start: 3, children_count: 2 })
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.cur_scope.insert('receiver', types.Type(types.int_))
tc.fn_param_types['impl.inner'] = []types.Type{}
tc.fn_param_types['impl.outer'] = [types.Type(types.int_)]
tc.fn_ret_types['impl.inner'] = types.Type(types.int_)
tc.fn_ret_types['impl.outer'] = types.Type(types.int_)
tc.register_synth_type(inner, types.Type(types.int_))
if sparse {
tc.parallel_check_sparse = true
tc.sparse_resolved_call_names[int(inner)] = 'impl.inner'
tc.sparse_resolved_call_names[int(outer)] = 'impl.outer'
} else {
tc.resolved_call_names = []&types.CachedName{len: a.nodes.len}
tc.resolved_call_set = []bool{len: a.nodes.len}
tc.resolved_call_names[int(inner)] = types.cached_name('impl.inner')
tc.resolved_call_names[int(outer)] = types.cached_name('impl.outer')
tc.resolved_call_set[int(inner)] = true
tc.resolved_call_set[int(outer)] = true
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.gen_call(outer, a.node(outer))
assert g.sb.str() == 'impl__outer(impl__inner())'
assert tc.resolved_call_name(outer) or { '' } == 'impl.outer'
assert tc.resolved_call_name(inner) or { '' } == 'impl.inner'
}
}
fn test_script_local_call_keeps_checker_resolution_unchanged() {
mut a := flat.FlatAst.new()
mut files := token.FileSet.new()
a.source_files[1] = files.add_file('commands.vsh', 1)
callee := a.add_val(.ident, 'ordinary')
a.children << callee
call := a.add_node(flat.Node{ kind: .call, pos: token.new_pos(1, 0), children_start: 0, children_count: 1 })
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.cur_file = 'commands.vsh'
tc.fn_type_files['ordinary'] = 'commands.vsh'
tc.fn_ret_types['ordinary'] = types.Type(types.void_)
tc.resolved_call_names = []&types.CachedName{len: a.nodes.len}
tc.resolved_call_set = []bool{len: a.nodes.len}
tc.resolved_call_names[int(call)] = types.cached_name('other.ordinary')
tc.resolved_call_set[int(call)] = true
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.gen_call(call, a.node(call))
assert g.sb.str() == 'ordinary()'
assert tc.resolved_call_name(call) or { '' } == 'other.ordinary'
}
+138 -27
View File
@@ -1235,10 +1235,15 @@ fn (g &FlatGen) parse_node_type(node &flat.Node) types.Type {
@[inline]
fn (g &FlatGen) parse_node_type_reference(node &flat.Node) &types.Type {
if flat.source_type_shape(node.typ).has(.dotted) {
return g.tc.parse_canonical_type_cached(node.typ)
return g.parse_annotation_type_reference(node.typ)
}
@[inline]
fn (g &FlatGen) parse_annotation_type_reference(type_id u32) &types.Type {
if flat.source_type_shape(type_id).has(.dotted) {
return g.tc.parse_canonical_type_cached(type_id)
}
return g.tc.parse_type_ref(node.type_text_id())
return g.tc.parse_type_ref(type_id)
}
pub fn (g &FlatGen) c_flags() []string {
@@ -5157,6 +5162,11 @@ fn (mut g FlatGen) collect_gen_info(no_parallel bool) {
g.unused_param_seen = &UnusedParamSeen{}
defer_fn_signature_registrations := !no_parallel && g.scope_parallel_workers && g.skip_generics && g.incremental_fn_names.len == 0 && par_cgen_prep_enabled()
type_index_args, type_index_threads := g.start_type_indexes(no_parallel)
if no_parallel && g.incremental_fn_names.len == 0 {
// Serial body generation needs the same immutable name rejection tables
// as the worker path. Field-position indexes still stay worker-only.
g.type_name_short_index = new_type_name_short_index(g.tc)
}
g.reserve_collect_gen_info_maps(no_parallel, defer_fn_signature_registrations)
g.precompute_export_lookups()
if profile {
@@ -13502,6 +13512,17 @@ fn (mut g FlatGen) gen_expr_with_expected_type(id flat.NodeId, expected_type &ty
@[direct_array_access]
fn (mut g FlatGen) gen_expr_with_expected_type_inner(id flat.NodeId, expected_type &types.Type) {
node := unsafe { &g.a.nodes[int(id)] }
// Scalar literals need no storage conversion. Keep the f32 rounding and
// 128-bit widening paths below, including their portable representations.
if expected_type is types.Primitive && expected_type.size <= 64
&& (node.kind in [.int_literal, .bool_literal]
|| (node.kind == .float_literal && !is_f32_primitive(expected_type))) {
if g.known_expr_type_id == int(id) {
g.known_expr_type_id = -1
}
g.gen_expr(id)
return
}
expected := g.canonical_import_alias_type_for_node(expected_type, node)
has_known_actual := g.known_expr_type_id == int(id)
known_actual := g.known_expr_type
@@ -13519,6 +13540,13 @@ fn (mut g FlatGen) gen_expr_with_expected_type_inner(id flat.NodeId, expected_ty
if expected is types.Enum {
g.expected_enum = expected.name_id
}
if expected is types.String && node.kind in [.string_literal, .string_interp]
&& (!has_known_actual || known_actual is types.String) {
// Raw string emission and interpolation children read the expected context.
// Keep that context while skipping conversions for an already string value.
g.gen_expr(id)
return
}
expected_is_ierror := if node.kind == .none_expr || node.kind == .call {
g.is_ierror_type_name(semantic_expected.source_type_id())
} else {
@@ -13552,22 +13580,7 @@ fn (mut g FlatGen) gen_expr_with_expected_type_inner(id flat.NodeId, expected_ty
return
}
}
mut actual := if has_known_actual { known_actual } else { *g.usable_expr_type(id) }
if deref_type := g.source_mut_pointer_param_deref_type(id) {
actual = deref_type
}
if node.kind == .ident {
if local_type := g.local_ident_type(node.value) {
actual = local_type
}
}
if node.kind == .selector {
if node.typ != 0 && (flat.source_type_shape(node.typ).has(.option_type) || flat.source_type_shape(node.typ).has(.result_type)) {
actual = g.tc.parse_resolution_type(node.typ)
} else if declared_type := g.selector_declared_type(id) {
actual = declared_type
}
}
actual := g.expected_expr_actual_type(id, node, has_known_actual, known_actual)
if expected is types.String && actual is types.Pointer && g.pointer_stringifies_as_address(actual.base_type) && !g.type_names_match(actual.base_type, expected) {
g.write('ptr_str(')
g.gen_expr(id)
@@ -13611,6 +13624,14 @@ fn (mut g FlatGen) gen_expr_with_expected_type_inner(id flat.NodeId, expected_ty
return
}
}
if expected is types.Primitive && expected.size <= 64
&& !is_f32_primitive(expected) && semantic_actual is types.Primitive
&& node.kind != .call {
// Mutable-parameter reads and alias casts have been handled above.
// Concrete scalar values need none of the aggregate conversions below.
g.gen_expr(id)
return
}
mut expected_is_shared_alias := false
if _ := g.shared_alias_pointer_type(expected) {
expected_is_shared_alias = true
@@ -13713,6 +13734,28 @@ fn (mut g FlatGen) gen_expr_with_expected_type_inner(id flat.NodeId, expected_ty
g.gen_expr(id)
}
// Storage types override the checked expression type at conversion sites. Resolve
// those first instead of computing a type that would immediately be discarded.
@[inline]
fn (g &FlatGen) expected_expr_actual_type(id flat.NodeId, node &flat.Node, has_known_actual bool, known_actual types.Type) types.Type {
if node.kind == .ident {
if local_type := g.local_ident_type(node.value) {
return local_type
}
} else if node.kind == .selector {
if node.typ != 0 && (flat.source_type_shape(node.typ).has(.option_type)
|| flat.source_type_shape(node.typ).has(.result_type)) {
return g.tc.parse_resolution_type(node.typ)
}
if declared_type := g.selector_declared_type(id) {
return declared_type
}
} else if deref_type := g.source_mut_pointer_param_deref_type(id) {
return deref_type
}
return if has_known_actual { known_actual } else { *g.usable_expr_type(id) }
}
fn is_f32_primitive(typ &types.Type) bool {
return typ is types.Primitive && typ.props.has(.float) && typ.size == 32
}
@@ -14918,6 +14961,9 @@ fn (g &FlatGen) const_storage_name(module_name string, name string) string {
// same name, the main const keeps the bare key and the builtin const moves to
// `builtin.name`, as in the checker (TypeChecker.const_decl_key).
fn (mut g FlatGen) register_const_value(module_name string, file string, name string, value_id flat.NodeId) {
if !isnil(g.const_short_index) {
g.const_short_index.call_names_ready = false
}
mut key := g.const_storage_name(module_name, name)
if key == name && !name.contains('.') {
if owner := g.const_modules[name] {
@@ -15091,18 +15137,31 @@ fn (mut g FlatGen) precompute_const_short_index() {
mut idx := g.const_short_index
idx.entries = g.build_unique_const_ref_names()
idx.built = true
if isnil(g.tc) {
return
}
idx.call_names = [64]u64{}
for name, _ in g.const_vals { idx.mark_call_name(name) }
for name, _ in g.tc.const_exprs { idx.mark_call_name(name) }
for name, _ in g.tc.const_types { idx.mark_call_name(name) }
for name, _ in g.tc.const_suffixes { idx.mark_call_name(name) }
idx.call_const_vals_count = g.const_vals.len
idx.call_const_exprs_count = g.tc.const_exprs.len
idx.call_const_types_count = g.tc.const_types.len
idx.call_const_suffixes_count = g.tc.const_suffixes.len
idx.call_names_ready = true
}
// const_ref_name_from_node converts const ref name from node data for c.
fn (g &FlatGen) const_ref_name_from_node(node flat.Node) string {
fn (g &FlatGen) const_ref_name_from_node(node &flat.Node) string {
if node.kind == .paren && node.children_count > 0 {
return g.const_ref_name_from_node(g.a.child_node(&node, 0))
return g.const_ref_name_from_node(g.a.node(g.a.child(node, 0)))
}
if node.kind == .ident {
return g.const_ref_name(node.value)
}
if node.kind == .selector && node.children_count > 0 {
base := g.a.child_node(&node, 0)
base := g.a.node(g.a.child(node, 0))
if base.kind == .ident {
if g.selector_base_is_local_value(base.value)
|| g.current_module_global_type_for_ident(base.value) != none {
@@ -15111,7 +15170,7 @@ fn (g &FlatGen) const_ref_name_from_node(node flat.Node) string {
// Prefer the import aliases of the file that owns the selector: global
// initializers can be emitted while another file is current.
mut resolved_base := base.value
if module_name := g.import_alias_module_for_file(base.value, g.node_source_file(&node)) {
if module_name := g.import_alias_module_for_file(base.value, g.node_source_file(node)) {
resolved_base = module_name
} else if module_name := g.selector_base_module_for_member(base.value, node.value) {
resolved_base = module_name
@@ -15183,7 +15242,7 @@ fn (g &FlatGen) const_ref_name_from_node_cached_for_collect(node flat.Node, uniq
if node.children_count == 0 {
return ''
}
return g.const_ref_name_from_node_cached_for_collect(g.a.child_node(&node, 0), unique_index, mut cache)
return g.const_ref_name_from_node_cached_for_collect(g.a.node(g.a.child(&node, 0)), unique_index, mut cache)
}
if node.kind == .ident {
if !cgen_name_may_be_const_ref(node.value, unique_index, g.const_vals) {
@@ -15201,7 +15260,7 @@ fn (g &FlatGen) const_ref_name_from_node_cached_for_collect(node flat.Node, uniq
if node.children_count == 0 {
return ''
}
base := g.a.child_node(&node, 0)
base := g.a.node(g.a.child(&node, 0))
if base.kind != .ident {
return ''
}
@@ -16772,6 +16831,39 @@ fn (g &FlatGen) assoc_infix_chain_len(node flat.Node) int {
return depth
}
// checked_boolean_operand recognizes kinds whose usable type is the checked
// type directly, without local storage or generated-selector reinterpretation.
fn (g &FlatGen) checked_boolean_operand(id flat.NodeId) bool {
node := g.a.nodes[int(id)]
if node.kind !in [.bool_literal, .infix] {
return false
}
typ := g.tc.expr_type_reference(id) or { return false }
return typ is types.Primitive && typ.props == .boolean && typ.size == 0
}
// gen_boolean_infix keeps the ordinary operand parentheses and generation
// context once both operands are known to have the primitive boolean type.
fn (mut g FlatGen) gen_boolean_infix(node &flat.Node, lhs_id flat.NodeId, rhs_id flat.NodeId) {
lhs := g.a.nodes[int(lhs_id)]
rhs := g.a.nodes[int(rhs_id)]
if lhs.kind == .infix && !infix_can_skip_child_parens(node.op, lhs.op) {
g.write('(')
g.gen_expr_with_possible_enum_type(lhs_id, types.Type(types.bool_))
g.write(')')
} else {
g.gen_expr_with_possible_enum_type(lhs_id, types.Type(types.bool_))
}
g.write(' ${g.op_str(node.op)} ')
if rhs.kind == .infix && !infix_can_skip_child_parens(node.op, rhs.op) {
g.write('(')
g.gen_expr_with_possible_enum_type(rhs_id, types.Type(types.bool_))
g.write(')')
} else {
g.gen_expr_with_possible_enum_type(rhs_id, types.Type(types.bool_))
}
}
// gen_assoc_infix_chain emits a left-nested `||`/`&&` chain iteratively, producing the
// same flat `a || b || c …` C as the recursive path but without growing the stack per
// link (a big match's condition chain can be hundreds deep).
@@ -17140,9 +17232,21 @@ fn (mut g FlatGen) gen_nonliteral_expr(id flat.NodeId, node &flat.Node) {
}
lhs_id := g.a.child(node, 0)
rhs_id := g.a.child(node, 1)
is_logical := node.op in [.logical_and, .logical_or]
if is_logical && g.checked_boolean_operand(lhs_id) && g.checked_boolean_operand(rhs_id)
&& !g.expr_is_in_translated_file(id) {
g.gen_boolean_infix(node, lhs_id, rhs_id)
return
}
old_expected_enum := g.expected_enum
lhs_type := g.usable_expr_type(lhs_id)
rhs_type := g.usable_expr_type(rhs_id)
if is_logical && lhs_type is types.Primitive && lhs_type.props == .boolean && lhs_type.size == 0
&& rhs_type is types.Primitive && rhs_type.props == .boolean && rhs_type.size == 0
&& !g.expr_is_in_translated_file(id) {
g.gen_boolean_infix(node, lhs_id, rhs_id)
return
}
if g.gen_int128_infix(id, node, lhs_id, rhs_id, lhs_type, rhs_type) {
g.expected_enum = old_expected_enum
return
@@ -17225,7 +17329,7 @@ fn (mut g FlatGen) gen_nonliteral_expr(id flat.NodeId, node &flat.Node) {
return
}
}
if g.gen_safe_integer_division(node, lhs_id, rhs_id, g.usable_expr_type(id)) {
if g.gen_safe_integer_division(id, node, lhs_id, rhs_id) {
g.expected_enum = old_expected_enum
return
}
@@ -27858,10 +27962,11 @@ fn integer_cast_const_fits(value i64, src CheckedIntKind, dst CheckedIntKind) bo
return limit >= 63 || u64(value) < (u64(1) << limit)
}
fn (mut g FlatGen) gen_safe_integer_division(node flat.Node, lhs_id flat.NodeId, rhs_id flat.NodeId, result_type types.Type) bool {
fn (mut g FlatGen) gen_safe_integer_division(id flat.NodeId, node flat.Node, lhs_id flat.NodeId, rhs_id flat.NodeId) bool {
if !g.has_builtins || g.static_c_initializer || node.op !in [.div, .mod] {
return false
}
result_type := g.usable_expr_type(id)
checked_integer_bounds(result_type) or { return false }
c_type := g.translated_numeric_c_type(lhs_id, result_type)
if c_type.len == 0 {
@@ -28159,6 +28264,12 @@ fn (mut g FlatGen) write(s string) {
}
return
}
if s.len == 1 {
byte := s[0]
g.sb.write_u8(byte)
g.line_start = byte == `\n`
return
}
// Append bytes directly instead of copying another string header into the builder.
unsafe { g.sb.write_ptr(s.str, s.len) }
g.line_start = s[s.len - 1] == `\n`
+183
View File
@@ -0,0 +1,183 @@
module c
import v.flat
import v.types
fn test_const_call_name_filter_includes_every_lookup_table() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.const_vals['alpha.value_only'] = flat.NodeId(0)
tc.const_exprs['beta.expr_only'] = flat.NodeId(0)
tc.const_types['gamma.type_only'] = types.Type(types.int_)
tc.const_suffixes['suffix_only'] = 'delta.primary'
assert !g.const_call_name_is_absent(flat.Node{ kind: .ident, value: 'not_built' })
g.precompute_const_short_index()
for name in ['value_only', 'expr_only', 'type_only', 'suffix_only'] {
for kind in [flat.NodeKind.ident, .selector] {
assert !g.const_call_name_is_absent(flat.Node{ kind: kind, value: name })
}
}
assert g.const_call_name_is_absent(flat.Node{ kind: .ident, value: 'definitely_missing' })
for name in ['alpha.missing', 'alpha__missing'] {
assert g.const_call_name_is_absent(flat.Node{ kind: .ident, value: name })
}
assert !g.const_call_name_is_absent(flat.Node{ kind: .index, value: 'missing' })
}
fn test_const_call_name_filter_keeps_growth_and_registration_fallbacks() {
for table in 0 .. 4 {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.precompute_const_short_index()
node := flat.Node{ kind: .ident, value: 'new_const' }
assert g.const_call_name_is_absent(node)
match table {
0 { g.const_vals['new_const'] = flat.NodeId(0) }
1 { tc.const_exprs['new_const'] = flat.NodeId(0) }
2 { tc.const_types['new_const'] = types.Type(types.int_) }
else { tc.const_suffixes['new_const'] = 'primary' }
}
assert !g.const_call_name_is_absent(node)
g.precompute_const_short_index()
assert !g.const_call_name_is_absent(node)
}
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.register_const_value('main', '', 'existing', flat.NodeId(0))
g.precompute_const_short_index()
assert g.const_short_index.call_names_ready
g.register_const_value('main', '', 'existing', flat.NodeId(1))
assert !g.const_short_index.call_names_ready
}
fn test_const_call_name_filter_preserves_function_constants_and_local_shadowing() {
mut a := flat.FlatAst.new()
target := a.add_val(.ident, 'target')
alias := a.add_val(.ident, 'callback')
base := a.add_val(.ident, 'short')
a.children << base
selector := a.add_node(flat.Node{ kind: .selector, value: 'callback', children_start: 0, children_count: 1 })
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.imports['short'] = 'library'
tc.fn_ret_types['target'] = types.Type(types.int_)
tc.const_types['library.callback'] = types.Type(types.FnType{})
tc.const_exprs['library.callback'] = target
tc.const_suffixes['callback'] = 'library.callback'
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.const_vals['library.callback'] = target
g.const_modules['library.callback'] = 'library'
g.precompute_const_short_index()
assert g.call_target_name(alias) == 'target'
assert g.call_target_name(selector) == 'target'
// A same-spelled function declaration wins over a module constant.
tc.fn_ret_types['callback'] = types.Type(types.int_)
assert g.call_target_name(alias) == 'callback'
tc.fn_ret_types.delete('callback')
tc.cur_scope = types.new_scope(tc.file_scope)
tc.cur_scope.insert('callback', types.Type(types.FnType{}))
assert g.call_target_name(alias) == 'callback'
assert g.const_fn_call_target_name(a.node(alias)) == none
}
fn test_const_call_name_suffix_matches_nonoverlapping_encoded_qualification() {
for name in ['', 'plain', '_bar', 'foo___bar', 'foo_____bar', 'a.b__c', 'a__b.c', 'callback__part',
'末尾.foo___字段', 'a.', '__', '___', 'a.___'] {
assert const_call_name_suffix(name) == name.replace('__', '.').all_after_last('.')
}
mut names := ['']
for _ in 0 .. 6 {
mut next := []string{cap: names.len * 3}
for prefix in names {
for suffix in ['_', '.', 'a'] {
name := prefix + suffix
assert const_call_name_suffix(name) == name.replace('__', '.').all_after_last('.')
next << name
}
}
names = next.clone()
}
}
fn test_const_call_name_filter_keeps_qualified_and_encoded_suffixes_in_every_table() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.const_vals['module.foo___value'] = flat.NodeId(0)
tc.const_exprs['module.foo_____expr'] = flat.NodeId(0)
tc.const_types['module.callback__part'] = types.Type(types.int_)
tc.const_suffixes['other.alias___suffix'] = 'unrelated.target'
g.precompute_const_short_index()
for names in [['foo___value', 'module.foo._value', '_value'],
['foo_____expr', 'module.foo.._expr', '_expr'],
['callback__part', 'module.callback.part', 'part'],
['alias___suffix', 'other.alias._suffix', '_suffix']] {
for name in names {
for kind in [flat.NodeKind.ident, .selector] {
assert !g.const_call_name_is_absent(flat.Node{ kind: kind, value: name })
}
}
}
}
fn test_const_call_name_filter_keeps_encoded_function_constant_resolution() {
for spelling in ['library.foo._callback', 'library__foo___callback'] {
mut a := flat.FlatAst.new()
target := a.add_val(.ident, 'target')
alias := a.add_val(.ident, spelling)
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.fn_ret_types['target'] = types.Type(types.int_)
tc.const_exprs['library.foo._callback'] = target
tc.const_types['library.foo._callback'] = types.Type(types.FnType{})
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.const_vals['library.foo._callback'] = target
g.precompute_const_short_index()
assert g.call_target_name(alias) == 'target'
}
}
fn test_const_call_name_filter_keeps_overlapping_underscore_fallbacks() {
for pair in [
['module___callback', ''],
['module_____callback', ''],
['module_______callback', ''],
['module__callback', 'module'],
['module___callback', 'module_'],
['module_____callback', 'module___'],
['module_______callback', 'module_____'],
] {
mut a := flat.FlatAst.new()
target := a.add_val(.ident, 'target')
alias := a.add_val(.ident, pair[0])
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.fn_ret_types['target'] = types.Type(types.int_)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.const_vals['callback'] = target
if pair[1].len > 0 {
g.const_modules['callback'] = pair[1]
}
unfiltered := g.call_target_name(alias)
assert unfiltered == 'target'
g.precompute_const_short_index()
assert g.call_target_name(alias) == unfiltered
}
}
+134
View File
@@ -0,0 +1,134 @@
module c
import v.flat
import v.types
fn expected_actual_node(mut a flat.FlatAst, kind flat.NodeKind, op flat.Op, value string, typ string, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
kind: kind
op: op
value: value
typ: flat.source_type_id(typ)
children_start: start
children_count: children.len
})
}
fn test_expected_actual_local_storage_overrides_checked_and_known_types() {
mut a := flat.FlatAst.new()
id := a.add_node(flat.Node{ kind: .ident, value: 'value', typ: flat.source_type_id('string') })
mut tc := types.TypeChecker.new(&a)
tc.file_scope = &types.Scope{}
tc.cur_scope = &types.Scope{ parent: tc.file_scope }
int_type := types.Type(types.int_)
pointer := types.Type(types.Pointer{ base_type: int_type })
tc.cur_scope.insert('value', pointer)
tc.register_synth_type(id, types.Type(types.string_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
for known in [false, true] {
assert g.expected_expr_actual_type(id, a.node(id), known, types.Type(types.bool_)) == pointer
g.known_expr_type_id = if known { int(id) } else { -1 }
g.known_expr_type = types.Type(types.bool_)
g.assert_expr_overrides[int(id)] = 'value'
assert g.expr_to_string_with_expected_type(id, int_type) == '*value'
assert g.known_expr_type_id == -1
}
g.cur_param_types['value'] = types.new_type_reference(int_type)
assert g.expected_expr_actual_type(id, a.node(id), true, pointer) == int_type
}
fn test_expected_actual_declared_selector_storage_overrides_checked_and_known_types() {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'holder')
field := expected_actual_node(mut a, .selector, .none, 'value', '', [base])
mut tc := types.TypeChecker.new(&a)
int_type := types.Type(types.int_)
pointer := types.Type(types.Pointer{ base_type: int_type })
tc.structs['Holder'] = [types.StructField{ name: 'value', typ: pointer }]
tc.register_synth_type(field, types.Type(types.string_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cur_param_types['holder'] = tc.parse_type(flat.source_type_id('Holder'))
for known in [false, true] {
assert g.expected_expr_actual_type(field, a.node(field), known, int_type) == pointer
g.known_expr_type_id = if known { int(field) } else { -1 }
g.known_expr_type = int_type
g.assert_expr_overrides[int(field)] = 'holder.value'
assert g.expr_to_string_with_expected_type(field, int_type) == '*holder.value'
assert g.known_expr_type_id == -1
}
}
fn test_expected_actual_optional_selector_annotation_wins_over_storage() {
for annotation in ['?int', '!int'] {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'holder')
field := expected_actual_node(mut a, .selector, .none, 'value', annotation, [base])
mut tc := types.TypeChecker.new(&a)
tc.structs['Holder'] = [types.StructField{ name: 'value', typ: types.Type(types.string_) }]
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cur_param_types['holder'] = tc.parse_type(flat.source_type_id('Holder'))
expected := tc.parse_resolution_type(flat.source_type_id(annotation))
for known in [false, true] {
assert g.expected_expr_actual_type(field, a.node(field), known, types.Type(types.bool_)) == expected
}
}
}
fn test_expected_actual_source_mut_pointer_deref_overrides_known_types_through_wrappers() {
mut a := flat.FlatAst.new()
param := a.add_val(.ident, 'slot')
deref := expected_actual_node(mut a, .prefix, .mul, source_mut_pointer_deref_marker, '', [param])
paren := expected_actual_node(mut a, .paren, .none, '', '', [deref])
block := expected_actual_node(mut a, .block, .none, '', '', [paren])
mut tc := types.TypeChecker.new(&a)
int_type := types.Type(types.int_)
pointer := types.Type(types.Pointer{ base_type: types.Type(types.Pointer{ base_type: int_type }) })
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cur_param_types['slot'] = types.new_type_reference(pointer)
g.cur_mut_params['slot'] = true
g.cur_mut_pointer_params['slot'] = true
g.cur_mut_param_owners['slot'] = tc.cur_scope.insert_with_owner('slot', pointer)
for id in [deref, paren, block] {
for known in [false, true] {
assert g.expected_expr_actual_type(id, a.node(id), known, types.Type(types.string_)) == int_type
}
}
}
fn test_expected_actual_unknown_storage_keeps_known_and_checker_fallbacks() {
mut a := flat.FlatAst.new()
id := a.add_val(.ident, 'unbound')
base := a.add_val(.ident, 'unknown_base')
field := expected_actual_node(mut a, .selector, .none, 'field', '', [base])
mut tc := types.TypeChecker.new(&a)
int_type := types.Type(types.int_)
for node_id in [id, field] { tc.register_synth_type(node_id, int_type) }
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
for node_id in [id, field] {
assert g.expected_expr_actual_type(node_id, a.node(node_id), true, types.Type(types.string_)) == types.Type(types.string_)
assert g.expected_expr_actual_type(node_id, a.node(node_id), false, types.Type(types.string_)) == int_type
}
g.known_expr_type_id = int(field)
g.known_expr_type = types.Type(types.string_)
g.assert_expr_overrides[int(id)] = 'unbound'
old_expected := types.Type(types.bool_)
old_enum := flat.source_type_id('PreviousEnum')
g.expected_expr_type = old_expected
g.expected_enum = old_enum
assert g.expr_to_string_with_expected_type(id, int_type) == 'unbound'
assert g.known_expr_type_id == int(field)
assert g.expected_expr_type == old_expected
assert g.expected_enum == old_enum
}
+176
View File
@@ -0,0 +1,176 @@
module c
import v.flat
import v.token
import v.types
fn expected_scalar_alias(typ types.Type) types.Type {
return types.Alias{
name_id: flat.source_type_id('ScalarAlias')
base_type: typ
}
}
fn expected_scalar_output(mut g FlatGen, id flat.NodeId, expected types.Type, actual types.Type, known bool) string {
old_expected := types.Type(types.string_)
old_enum := flat.source_type_id('PreviousEnum')
g.expected_expr_type = old_expected
g.expected_enum = old_enum
g.known_expr_type = actual
g.known_expr_type_id = if known { int(id) } else { -1 }
output := g.expr_to_string_with_expected_type(id, expected)
assert g.expected_expr_type == old_expected
assert g.expected_enum == old_enum
assert g.known_expr_type_id == -1
return output
}
fn expected_scalar_node(mut a flat.FlatAst, kind flat.NodeKind, op flat.Op, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
kind: kind
op: op
children_start: start
children_count: children.len
})
}
fn test_expected_scalar_literals_preserve_fallback_output_and_context() {
for kind, values in {
flat.NodeKind.int_literal: ['0', '1_000', '0o7_7', '18446744073709551615']
flat.NodeKind.float_literal: ['0.0', '1_000.5', '1.0e+1_0']
flat.NodeKind.bool_literal: ['true', 'false']
} {
for value in values {
actual := if kind == .float_literal {
types.Type(types.f64_)
} else if kind == .bool_literal {
types.Type(types.bool_)
} else {
types.Type(types.u64_)
}
expected_types := if kind == .bool_literal {
[types.Type(types.bool_)]
} else if kind == .float_literal {
[types.Type(types.f64_)]
} else {
[types.Type(types.int_), types.Type(types.u64_), types.Type(types.f32_),
types.Type(types.f64_)]
}
mut a := flat.FlatAst.new()
id := a.add_val(kind, value)
mut tc := types.TypeChecker.new(&a)
tc.register_synth_type(id, actual)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
for expected in expected_types {
for known in [false, true] {
fallback := expected_scalar_output(mut g, id, expected_scalar_alias(expected), actual, known)
assert expected_scalar_output(mut g, id, expected, actual, known) == fallback, value
}
}
for replacement in ['assert_literal', 'callback_literal'] {
g.assert_expr_overrides.clear()
g.callback_target_overrides.clear()
if replacement == 'assert_literal' {
g.assert_expr_overrides[int(id)] = replacement
} else {
g.callback_target_overrides[int(id)] = replacement
}
assert expected_scalar_output(mut g, id, actual, actual, true) == replacement
}
}
}
}
fn test_expected_scalar_expressions_preserve_fallback_and_unrelated_known_type() {
mut a := flat.FlatAst.new()
left := a.add_val(.int_literal, '3')
right := a.add_val(.int_literal, '4')
value := a.add_val(.ident, 'value')
sum := expected_scalar_node(mut a, .infix, .plus, [left, right])
paren := expected_scalar_node(mut a, .paren, .none, [sum])
negative := expected_scalar_node(mut a, .prefix, .minus, [paren])
mut tc := types.TypeChecker.new(&a)
int_type := types.Type(types.int_)
tc.cur_scope.insert('value', int_type)
for id in [left, right, value, sum, paren, negative] {
tc.register_synth_type(id, int_type)
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
for id in [value, sum, paren, negative] {
for known in [false, true] {
fallback := expected_scalar_output(mut g, id, expected_scalar_alias(int_type), int_type, known)
assert expected_scalar_output(mut g, id, int_type, int_type, known) == fallback
}
}
g.known_expr_type_id = int(right)
g.known_expr_type = types.Type(types.u64_)
assert g.expr_to_string_with_expected_type(left, int_type) == '3'
assert g.known_expr_type_id == int(right)
assert g.known_expr_type == types.Type(types.u64_)
}
fn test_expected_scalar_shortcuts_keep_f32_rounding_and_int128_widening() {
mut a := flat.FlatAst.new()
fraction := a.add_val(.float_literal, '1.0000000596046448')
negative := expected_scalar_node(mut a, .prefix, .minus, [fraction])
integer := a.add_val(.int_literal, '7')
mut tc := types.TypeChecker.new(&a)
tc.register_synth_type(fraction, types.Type(types.f64_))
tc.register_synth_type(negative, types.Type(types.f64_))
tc.register_synth_type(integer, types.Type(types.int_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
assert expected_scalar_output(mut g, fraction, types.Type(types.f32_), types.Type(types.f64_), true) == '(float)(1.0000000596046448)'
assert expected_scalar_output(mut g, negative, types.Type(types.f32_), types.Type(types.f64_), true) == '-(float)(1.0000000596046448)'
for portable in [false, true] {
g.compile_defines = if portable { ['v3_no_native_int128'] } else { []string{} }
assert expected_scalar_output(mut g, integer, types.Type(types.i128_), types.Type(types.int_), true) == '__v_i128_from_i64((i64)(7))'
assert expected_scalar_output(mut g, integer, types.Type(types.u128_), types.Type(types.int_), true) == '__v_u128_from_i64((i64)(7))'
}
}
fn test_expected_scalar_call_keeps_declared_pointer_return_and_mut_parameter_read() {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'read_pointer')
call := expected_scalar_node(mut a, .call, .none, [callee])
param := a.add_val(.ident, 'value')
mut tc := types.TypeChecker.new(&a)
int_type := types.Type(types.int_)
pointer_type := types.Type(types.Pointer{ base_type: int_type })
tc.fn_ret_types['read_pointer'] = pointer_type
tc.register_synth_type(call, int_type)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.assert_expr_overrides[int(call)] = 'read_pointer()'
assert expected_scalar_output(mut g, call, int_type, int_type, true) == '*read_pointer()'
g.cur_param_types['value'] = types.new_type_reference(pointer_type)
g.cur_mut_params['value'] = true
g.cur_mut_param_owners['value'] = tc.cur_scope.insert_with_owner('value', pointer_type)
assert expected_scalar_output(mut g, param, int_type, int_type, true) == '*value'
}
fn test_expected_scalar_translated_literals_keep_boolean_and_int_normalization() {
mut a := flat.FlatAst.new()
mut files := token.FileSet.new()
a.source_files[1] = files.add_file('translated_scalar.v', 1)
wide := a.add_node(flat.Node{ kind: .int_literal, value: '256', pos: token.new_pos(1, 0) })
fraction := a.add_node(flat.Node{ kind: .float_literal, value: '0.5', pos: token.new_pos(1, 0) })
mut tc := types.TypeChecker.new(&a)
tc.translated_files['translated_scalar.v'] = true
tc.register_synth_type(wide, types.Type(types.int_))
tc.register_synth_type(fraction, types.Type(types.f64_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
assert expected_scalar_output(mut g, wide, types.Type(types.bool_), types.Type(types.int_), true) == '((256) != 0)'
assert expected_scalar_output(mut g, fraction, types.Type(types.bool_), types.Type(types.f64_), true) == '((0.5) != 0)'
assert expected_scalar_output(mut g, wide, types.Type(types.int_), types.Type(types.int_), true) == '((i32)(256))'
}
+118
View File
@@ -0,0 +1,118 @@
module c
import v.flat
import v.types
fn expected_string_alias() types.Type {
return types.Alias{
name_id: flat.source_type_id('StringAlias')
base_type: types.new_type_reference(types.Type(types.string_))
}
}
fn expected_string_node(mut a flat.FlatAst, kind flat.NodeKind, value string, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
kind: kind
value: value
children_start: start
children_count: children.len
})
}
fn expected_string_output(mut g FlatGen, id flat.NodeId, expected types.Type, known types.Type, has_known bool) string {
g.known_expr_type_id = if has_known { int(id) } else { -1 }
g.known_expr_type = known
return g.expr_to_string_with_expected_type(id, expected)
}
fn test_expected_string_literal_matches_conversion_fallback_and_restores_context() {
for raw in [false, true] {
mut a := flat.FlatAst.new()
id := a.add_node(flat.Node{
kind: .string_literal
value: 'hello'
typ: flat.source_type_id(if raw { 'raw:string' } else { 'string' })
})
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
old_expected := types.Type(types.Pointer{ base_type: types.Type(types.u8_) })
old_enum := flat.source_type_id('PreviousEnum')
g.expected_expr_type = old_expected
g.expected_enum = old_enum
for has_known in [false, true] {
actual := types.Type(types.string_)
result := expected_string_output(mut g, id, actual, actual, has_known)
fallback := expected_string_output(mut g, id, expected_string_alias(), actual, has_known)
assert result == '_str_0'
assert result == fallback
assert g.known_expr_type_id == -1
assert g.expected_expr_type == old_expected
assert g.expected_enum == old_enum
}
}
}
fn test_expected_string_interpolation_and_lowered_join_keep_child_context() {
for join in [false, true] {
mut a := flat.FlatAst.new()
left := a.add_node(flat.Node{ kind: .string_literal, value: 'hello', typ: flat.source_type_id('raw:string') })
right := a.add_val(.string_literal, 'world')
id := expected_string_node(mut a, .string_interp, if join { '__v3_string_join' } else { '' }, [
left,
right,
])
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
old_expected := types.Type(types.Pointer{ base_type: types.Type(types.u8_) })
g.expected_expr_type = old_expected
for has_known in [false, true] {
g.tmp_count = 0
result := expected_string_output(mut g, id, types.Type(types.string_), types.Type(types.string_), has_known)
g.tmp_count = 0
fallback := expected_string_output(mut g, id, expected_string_alias(), types.Type(types.string_), has_known)
assert result == fallback
assert result.contains('_str_0') && result.contains('_str_1')
assert !result.contains('"hello"')
assert g.known_expr_type_id == -1
assert g.expected_expr_type == old_expected
}
}
}
fn test_expected_string_conflicting_known_pointer_keeps_conversion() {
mut a := flat.FlatAst.new()
id := a.add_val(.string_literal, 'hello')
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
known := types.Type(types.Pointer{ base_type: types.Type(types.int_) })
result := expected_string_output(mut g, id, types.Type(types.string_), known, true)
assert result == 'ptr_str(_str_0)'
assert g.known_expr_type_id == -1
}
fn test_expected_string_preserves_expression_overrides_and_unrelated_known_id() {
mut a := flat.FlatAst.new()
id := a.add_val(.string_literal, 'hello')
other := a.add_val(.int_literal, '7')
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.assert_expr_overrides[int(id)] = 'replacement'
g.known_expr_type_id = int(other)
g.known_expr_type = types.Type(types.int_)
assert g.expr_to_string_with_expected_type(id, types.Type(types.string_)) == 'replacement'
assert g.known_expr_type_id == int(other)
g.assert_expr_overrides.clear()
g.callback_target_overrides[int(id)] = 'callback_replacement'
assert expected_string_output(mut g, id, types.Type(types.string_), types.Type(types.string_), true) == 'callback_replacement'
assert g.known_expr_type_id == -1
}
+134 -72
View File
@@ -1947,9 +1947,9 @@ fn (mut g FlatGen) direct_call_name_for_call(id flat.NodeId, name string) string
return g.direct_call_name(name)
}
fn (mut g FlatGen) direct_call_name_for_call_node(id flat.NodeId, node flat.Node, name string) string {
fn (mut g FlatGen) direct_call_name_for_call_node(id flat.NodeId, node &flat.Node, name string) string {
if node.children_count > 0 {
fn_node := g.a.child_node(&node, 0)
fn_node := g.a.child_node(node, 0)
if shadow_name := g.main_runtime_shadow_call_c_name(node, fn_node) {
return shadow_name
}
@@ -7143,16 +7143,16 @@ fn (mut g FlatGen) gen_optional_error_from_call(ct string, node flat.Node) {
g.write('}')
}
fn (g &FlatGen) is_runtime_new_map_call(node flat.Node) bool {
fn (g &FlatGen) is_runtime_new_map_call(node &flat.Node) bool {
if node.children_count < 3 {
return false
}
key_size := g.a.child_node(&node, 1)
value_size := g.a.child_node(&node, 2)
key_size := g.a.child_node(node, 1)
value_size := g.a.child_node(node, 2)
return key_size.kind == .sizeof_expr && value_size.kind == .sizeof_expr
}
fn (g &FlatGen) main_runtime_shadow_call_c_name(node flat.Node, fn_node flat.Node) ?string {
fn (g &FlatGen) main_runtime_shadow_call_c_name(node &flat.Node, fn_node &flat.Node) ?string {
if fn_node.kind != .ident || g.is_runtime_new_map_call(node) {
return none
}
@@ -7290,7 +7290,7 @@ fn (mut g FlatGen) gen_owned_capture_closure_create(id flat.NodeId, node flat.No
return true
}
fn (g &FlatGen) trace_call_name(fn_node flat.Node, fn_name string, target_name string, resolved_target_name string) ?string {
fn (g &FlatGen) trace_call_name(fn_node &flat.Node, fn_name string, target_name string, resolved_target_name string) ?string {
if !g.trace_calls || g.inside_trace_call {
return none
}
@@ -7789,6 +7789,26 @@ fn (mut g FlatGen) gen_builtin_panic_call(node flat.Node) {
g.write(')')
}
// call_ident_cast_type_name preserves bare-type priority without repeating its
// declaration probes or qualifying local function values.
@[inline]
fn (g &FlatGen) call_ident_cast_type_name(name string, is_local_fn_value bool) ?string {
if is_local_fn_value || !g.ident_may_name_type(name) {
return none
}
if g.tc.type_aliases.contains(name) || name in g.tc.structs || name in g.tc.enum_names
|| g.tc.sum_types.contains(name) || name in g.tc.interface_names {
return name
}
qname := g.tc.qualify_name(name)
if qname != name && (g.tc.type_aliases.contains(qname) || qname in g.tc.structs
|| qname in g.tc.enum_names || g.tc.sum_types.contains(qname)
|| qname in g.tc.interface_names) {
return qname
}
return none
}
// gen_call emits call output for c.
@[direct_array_access]
fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
@@ -7803,7 +7823,8 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
mut fn_node := g.a.child_node(node, 0)
callee_name := g.call_target_name(g.a.child(node, 0))
mut target_name := callee_name
mut resolved_target_name := g.tc.resolved_call_name(id) or { '' }
checker_resolved_name := g.tc.resolved_call_name(id)
mut resolved_target_name := checker_resolved_name or { '' }
if fn_node.kind == .ident && node.pos.is_valid() && g.tc.cur_module == 'main'
&& g.tc.cur_file.ends_with('.vsh') {
if g.file_declares_fn(id, fn_node.value) {
@@ -8242,7 +8263,7 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
g.write(')')
return
}
if resolved := g.tc.resolved_call_name(id) {
if resolved := checker_resolved_name {
if arg_start := g.resolved_selector_module_arg_start(resolved, node) {
emitted_resolved := g.direct_call_name_for_call_node(id, node, resolved)
g.write(emitted_resolved)
@@ -8822,24 +8843,9 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
fn_id := g.a.child(node, 0)
fn_ident := g.a.nodes[int(fn_id)]
if fn_ident.kind == .ident {
// Most callees are functions; only a type name can be a cast here.
may_name_type := g.ident_may_name_type(fn_ident.value)
qname := if may_name_type { g.tc.qualify_name(fn_ident.value) } else { '' }
is_local_fn_value := g.current_param_type(fn_ident.value) != none
|| g.cur_scope_has_local_name(fn_ident.value)
if may_name_type && !is_local_fn_value && (g.tc.type_aliases.contains(fn_ident.value) || g.tc.type_aliases.contains(qname)
|| fn_ident.value in g.tc.structs || qname in g.tc.structs
|| fn_ident.value in g.tc.enum_names || qname in g.tc.enum_names
|| g.tc.sum_types.contains(fn_ident.value) || g.tc.sum_types.contains(qname)
|| fn_ident.value in g.tc.interface_names || qname in g.tc.interface_names) {
type_name := if g.tc.type_aliases.contains(fn_ident.value)
|| fn_ident.value in g.tc.structs || fn_ident.value in g.tc.enum_names
|| g.tc.sum_types.contains(fn_ident.value)
|| fn_ident.value in g.tc.interface_names {
fn_ident.value
} else {
qname
}
if type_name := g.call_ident_cast_type_name(fn_ident.value, is_local_fn_value) {
target_type := g.tc.parse_type(flat.source_type_id(type_name))
if target_type is types.Interface && node.children_count > 1 {
g.gen_expr_with_expected_type(g.a.child(node, 1), target_type)
@@ -8951,12 +8957,13 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
}
callee_is_local_fn_value := uses_fn_value_param_types
&& g.local_fn_value_callee(g.a.child(node, 0))
callee_has_named_decl := fn_node.kind == .ident
may_need_fn_value_params := !uses_fn_value_param_types && param_types.len == 0
&& !is_method
callee_has_named_decl := may_need_fn_value_params && fn_node.kind == .ident
&& (fn_decl_module_key(g.tc.cur_module, fn_node.value) in g.fn_decl_param_types
|| actual_fn in g.fn_decl_param_types
|| emitted_callee_name in g.fn_decl_param_types)
may_recover_fn_value_params := !uses_fn_value_param_types && param_types.len == 0
&& !is_method && !callee_has_named_decl
may_recover_fn_value_params := may_need_fn_value_params && !callee_has_named_decl
has_optional_params := params_have_optional_result(param_types)
// Only optional arguments consume this ABI choice. Preserve the lookup
// when an unresolved function value can still supply its parameters below.
@@ -9078,7 +9085,7 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
arg_start = 1
}
} else if node.children_count == param_types.len + 2 {
resolved_call := g.tc.resolved_call_name(id) or { '' }
resolved_call := checker_resolved_name or { '' }
if resolved_call.contains('.') {
resolved_base := resolved_call.all_before_last('.')
first_arg := g.a.child_node(node, 1)
@@ -9394,16 +9401,14 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
}
}
}
is_rvalue := arg_node.kind in [.call, .struct_init, .cast_expr, .assoc]
|| (arg_node.kind == .index && arg_node.value == 'range')
|| g.arg_is_const_ident(arg_node)
if needs_addr && g.arg_is_const_ident(arg_node) {
value_type := g.addressed_const_arg_value_type(arg_id, param_types[arg_idx])
ct := g.tc.c_type(value_type)
g.write('(${ct}[]){')
g.gen_expr_with_expected_type(arg_id, value_type)
g.write('}')
} else if needs_addr && is_rvalue {
} else if needs_addr && (arg_node.kind in [.call, .struct_init, .cast_expr, .assoc]
|| (arg_node.kind == .index && arg_node.value == 'range')) {
pt := param_types[arg_idx]
ct := g.tc.c_type(types.unwrap_pointer(pt))
if g.c_typedef_nil_call(arg_id) {
@@ -9505,7 +9510,7 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node &flat.Node) {
}
}
fn (mut g FlatGen) gen_c_va_macro_call(node flat.Node, target_name string, resolved_name string) bool {
fn (mut g FlatGen) gen_c_va_macro_call(node &flat.Node, target_name string, resolved_name string) bool {
name := if resolved_name in ['C.va_arg', 'C.va_start', 'C.va_end', 'C.va_copy'] {
resolved_name
} else {
@@ -9520,7 +9525,7 @@ fn (mut g FlatGen) gen_c_va_macro_call(node flat.Node, target_name string, resol
g.write('0')
return true
}
type_arg_name_source_id := g.generic_call_type_arg_name(g.a.child(&node, 1))
type_arg_name_source_id := g.generic_call_type_arg_name(g.a.child(node, 1))
target_type := g.tc.parse_type(type_arg_name_source_id)
mut target_ct := g.tc.c_type(target_type)
@@ -9528,7 +9533,7 @@ fn (mut g FlatGen) gen_c_va_macro_call(node flat.Node, target_name string, resol
target_ct = g.resolve_fn_ptr_type(target_ct)
}
g.write('${macro_name}(')
arg_id := g.a.child(&node, 2)
arg_id := g.a.child(node, 2)
if !g.gen_c_va_list_macro_arg_direct(1, arg_id, name) {
g.gen_expr(arg_id)
}
@@ -9540,7 +9545,7 @@ fn (mut g FlatGen) gen_c_va_macro_call(node flat.Node, target_name string, resol
if i > 1 {
g.write(', ')
}
arg_id := g.a.child(&node, i)
arg_id := g.a.child(node, i)
if !g.gen_c_va_list_macro_arg_direct(i - 1, arg_id, name) {
g.gen_expr(arg_id)
}
@@ -10402,6 +10407,9 @@ fn (g &FlatGen) call_target_name(id flat.NodeId) string {
node := g.a.nodes[int(id)]
match node.kind {
.ident {
if g.const_call_name_is_absent(node) {
return node.value
}
// A bare call target resolves to a function in the current module before a
// same-spelled imported or module constant. For example, builtin.f32_max()
// must not be rewritten to the math.internal.f32_max constant.
@@ -10442,8 +10450,60 @@ fn (g &FlatGen) call_target_name(id flat.NodeId) string {
}
}
// Match replace('__', '.').all_after_last('.') without allocating transformed names.
// Consume underscore pairs from the left so an odd trailing underscore is preserved.
@[inline]
fn const_call_name_suffix(name string) string {
mut start := 0
mut i := 0
for i < name.len {
if name[i] == `.` {
start = i + 1
} else if name[i] == `_` && i + 1 < name.len && name[i + 1] == `_` {
i++
start = i + 1
}
i++
}
return name[start..]
}
fn (mut idx ConstShortIndex) mark_call_name(name string) {
slot := global_name_filter_slot(const_call_name_suffix(name))
idx.call_names[slot >> 6] |= u64(1) << u32(slot & 63)
}
// Normal generation freezes the const tables before sharing this index with workers.
// Focused helpers and newly registered constants keep the complete lookup path.
@[inline]
fn (g &FlatGen) const_call_name_is_absent(node &flat.Node) bool {
if node.kind !in [.ident, .selector] || isnil(g.const_short_index) {
return false
}
idx := g.const_short_index
if !idx.call_names_ready || idx.call_const_vals_count != g.const_vals.len
|| idx.call_const_exprs_count != g.tc.const_exprs.len
|| idx.call_const_types_count != g.tc.const_types.len
|| idx.call_const_suffixes_count != g.tc.const_suffixes.len {
return false
}
suffix := const_call_name_suffix(node.value)
// Rightmost overlapping underscore pairs can strip this leading underscore.
if suffix.len > 0 && suffix[0] == `_` {
return false
}
slot := global_name_filter_slot(suffix)
if idx.call_names[slot >> 6] & (u64(1) << u32(slot & 63)) != 0 {
return false
}
return true
}
@[direct_array_access]
fn (g &FlatGen) const_fn_call_target_name(node flat.Node) ?string {
fn (g &FlatGen) const_fn_call_target_name(node &flat.Node) ?string {
if g.const_call_name_is_absent(node) {
return none
}
// Parameters and local callbacks keep their lexical binding, even if another
// module publishes a function constant with the same name.
if node.kind == .ident && g.selector_base_is_local_value(node.value) {
@@ -10490,7 +10550,7 @@ fn (g &FlatGen) const_fn_call_type(node flat.Node, key string, expr_id flat.Node
return g.tc.const_types[key] or { g.tc.resolve_type(expr_id) }
}
fn (g &FlatGen) const_key_for_call_target(node flat.Node) ?string {
fn (g &FlatGen) const_key_for_call_target(node &flat.Node) ?string {
name := g.call_target_key(node)
if name.len == 0 {
return none
@@ -10506,7 +10566,7 @@ fn (g &FlatGen) const_key_for_call_target(node flat.Node) ?string {
return none
}
fn (g &FlatGen) call_target_key(node flat.Node) string {
fn (g &FlatGen) call_target_key(node &flat.Node) string {
match node.kind {
.ident {
if node.value.len == 0 {
@@ -10524,7 +10584,7 @@ fn (g &FlatGen) call_target_key(node flat.Node) string {
if node.children_count == 0 {
return node.value
}
base := g.a.child_node(&node, 0)
base := g.a.child_node(node, 0)
if base.kind != .ident {
return ''
}
@@ -10537,7 +10597,7 @@ fn (g &FlatGen) call_target_key(node flat.Node) string {
}
.index {
if node.children_count > 0 {
return g.call_target_key(g.a.child_node(&node, 0))
return g.call_target_key(g.a.child_node(node, 0))
}
return node.value
}
@@ -14531,16 +14591,16 @@ fn (mut g FlatGen) gen_guarded_anon_self_call_stmt(node flat.Node) bool {
return true
}
fn (g &FlatGen) resolved_selector_module_arg_start(resolved string, node flat.Node) ?int {
fn (g &FlatGen) resolved_selector_module_arg_start(resolved string, node &flat.Node) ?int {
if !resolved.contains('.') || node.children_count < 2 {
return none
}
callee := g.a.child_node(&node, 0)
callee := g.a.child_node(node, 0)
if callee.kind != .selector || callee.children_count == 0 {
return none
}
base := g.a.child_node(callee, 0)
first_arg := g.a.child_node(&node, 1)
first_arg := g.a.child_node(node, 1)
if base.kind != .ident || !g.selector_module_base_arg_matches(first_arg, base.value) {
return none
}
@@ -14566,11 +14626,11 @@ fn (g &FlatGen) selector_module_base_arg_matches(arg flat.Node, base string) boo
return false
}
fn (g &FlatGen) selector_call_can_emit_direct(resolved string, node flat.Node) bool {
fn (g &FlatGen) selector_call_can_emit_direct(resolved string, node &flat.Node) bool {
if resolved.len == 0 || node.children_count == 0 {
return false
}
fn_node := g.a.child_node(&node, 0)
fn_node := g.a.child_node(node, 0)
if fn_node.kind != .selector {
return false
}
@@ -14756,17 +14816,17 @@ fn (g &FlatGen) target_module_call_name(target string, node flat.Node) ?string {
}
}
static_name := target
if static_name in g.tc.fn_param_types || static_name in g.tc.fn_ret_types
|| static_name in g.tc.fn_generic_params {
params := g.tc.fn_param_types[static_name] or {
if static_name in g.tc.fn_ret_types && node.children_count == arg_start {
return static_name
}
return none
}
if params := g.tc.fn_param_types[static_name] {
if g.module_call_arg_count_matches(static_name, params, node.children_count - arg_start) {
return static_name
}
} else if static_name in g.tc.fn_ret_types {
if node.children_count == arg_start {
return static_name
}
return none
} else if static_name in g.tc.fn_generic_params {
return none
}
mod_name := g.selector_base_module(base) or {
if base == g.tc.cur_module {
@@ -14932,16 +14992,16 @@ fn (mut g FlatGen) gen_mut_param_value_call_arg(arg_id flat.NodeId, arg_node fla
}
// gen_call_args emits call args output for c.
fn (mut g FlatGen) gen_call_args(fn_name string, node flat.Node, start int) {
fn (mut g FlatGen) gen_call_args(fn_name string, node &flat.Node, start int) {
callee_name := if node.children_count > 0 {
g.call_target_name(g.a.child(&node, 0))
g.call_target_name(g.a.child(node, 0))
} else {
''
}
callee_fn_value_params := if start == 1 && node.children_count > 0
&& !fn_name.starts_with('C.') && !callee_name.starts_with('C.')
&& !g.call_callee_is_module_selector(node) {
g.fn_value_call_param_types(g.a.child(&node, 0))
g.fn_value_call_param_types(g.a.child(node, 0))
} else {
none
}
@@ -14950,7 +15010,7 @@ fn (mut g FlatGen) gen_call_args(fn_name string, node flat.Node, start int) {
// The caller retains the callee lookup across call lowering; the AST and lexical scope
// are unchanged until argument emission starts.
fn (mut g FlatGen) gen_call_args_for_callee(fn_name string, node flat.Node, start int, callee_name string, callee_fn_value_params ?[]types.Type) {
fn (mut g FlatGen) gen_call_args_for_callee(fn_name string, node &flat.Node, start int, callee_name string, callee_fn_value_params ?[]types.Type) {
is_c_call := fn_name.starts_with('C.') || callee_name.starts_with('C.')
mut param_types := g.param_types_for(fn_name, fn_name.all_after_last('.'))
callee_is_module_selector := g.call_callee_is_module_selector(node)
@@ -14977,7 +15037,7 @@ fn (mut g FlatGen) gen_call_args_for_callee(fn_name string, node flat.Node, star
|| g.call_uses_concrete_optional_params(callee_name)
|| g.call_uses_concrete_optional_params(g.direct_call_name(callee_name))
|| (node.children_count > 0
&& g.call_callee_uses_specialized_generic_abi(g.a.child(&node, 0))))
&& g.call_callee_uses_specialized_generic_abi(g.a.child(node, 0))))
c_variadic_prefix := if is_c_call {
g.c_fn_abi_variadic_prefix(fn_name, callee_name, param_types)
} else {
@@ -15007,7 +15067,7 @@ fn (mut g FlatGen) gen_call_args_for_callee(fn_name string, node flat.Node, star
is_variadic := variadic_idx >= 0 && num_args > param_types.len
for i in start .. node.children_count {
arg_idx := i - start
arg_id := g.a.child(&node, i)
arg_id := g.a.child(node, i)
arg_node := g.a.nodes[int(arg_id)]
if (param_types.len > 0 || uses_fn_value_param_types) && !is_native_variadic_fn
&& variadic_idx < 0 && arg_idx >= typed_param_count {
@@ -15118,7 +15178,7 @@ fn (mut g FlatGen) gen_call_args_for_callee(fn_name string, node flat.Node, star
}
}
if arg_idx == 1 && fn_name in ['array_push', 'array__push']
&& g.gen_shared_array_push_arg(node.value, g.a.child(&node, start), arg_id) {
&& g.gen_shared_array_push_arg(node.value, g.a.child(node, start), arg_id) {
continue
}
if arg_idx == 0 && call_name_is_isnil(fn_name, callee_name)
@@ -15365,17 +15425,15 @@ fn (mut g FlatGen) gen_call_args_for_callee(fn_name string, node flat.Node, star
if arg_idx < typed_param_count && g.gen_interface_pointer_arg(arg_id, param_types[arg_idx]) {
continue
}
is_rvalue := arg_node.kind in [.call, .struct_init, .cast_expr, .assoc]
|| (arg_node.kind == .index && arg_node.value == 'range')
|| (!arg_node.is_mut && g.map_index_value_is_rvalue(arg_id))
|| g.arg_is_const_ident(arg_node)
if needs_addr && g.arg_is_const_ident(arg_node) {
value_type := g.addressed_const_arg_value_type(arg_id, param_types[arg_idx])
ct := g.tc.c_type(value_type)
g.write('(${ct}[]){')
g.gen_expr_with_expected_type(arg_id, value_type)
g.write('}')
} else if needs_addr && is_rvalue {
} else if needs_addr && (arg_node.kind in [.call, .struct_init, .cast_expr, .assoc]
|| (arg_node.kind == .index && arg_node.value == 'range')
|| (!arg_node.is_mut && g.map_index_value_is_rvalue(arg_id))) {
pt := param_types[arg_idx]
ct := g.tc.c_type(types.unwrap_pointer(pt))
g.write('&((${ct}[]){')
@@ -15562,11 +15620,11 @@ fn (mut g FlatGen) gen_variadic_array_args(node flat.Node, start int, elem_type
g.write('})')
}
fn (g &FlatGen) call_callee_is_module_selector(node flat.Node) bool {
fn (g &FlatGen) call_callee_is_module_selector(node &flat.Node) bool {
if node.children_count == 0 {
return false
}
callee := g.a.child_node(&node, 0)
callee := g.a.child_node(node, 0)
if callee.kind != .selector || callee.children_count == 0 {
return false
}
@@ -15847,19 +15905,20 @@ fn (mut g FlatGen) gen_transformed_method_ident_call(id flat.NodeId, node flat.N
// A transformed module selector can retain its module ident as the first child.
// Let module-call generation discard that pseudo-argument before considering a
// same-named mutable receiver method.
if _ := g.target_module_call_name(fn_node.value, node) {
if g.target_module_call_arg_start(fn_node.value, node) == 2 {
if g.target_module_call_arg_start(fn_node.value, node) == 2 {
if _ := g.target_module_call_name(fn_node.value, node) {
return false
}
}
mut receiver_id := g.a.child(&node, 1)
emitted_name := g.direct_call_name_for_call_node(id, node, fn_node.value)
mut emitted_name := ''
method_short := c_short_name_view(fn_node.value)
// Look up the resolved V method name first. Operator overloads use their
// operator token in V, but C-name sanitization can collide with an ordinary
// method (for example `%` and `mod`).
mut params := g.param_types_for(fn_node.value, method_short)
if params.len == 0 {
emitted_name = g.direct_call_name_for_call_node(id, node, fn_node.value)
params = g.param_types_for(emitted_name, emitted_name)
}
if params.len == 0 {
@@ -15871,6 +15930,9 @@ fn (mut g FlatGen) gen_transformed_method_ident_call(id flat.NodeId, node flat.N
if receiver_owner.len == 0 || expected_receiver_name != receiver_owner {
return false
}
if emitted_name.len == 0 {
emitted_name = g.direct_call_name_for_call_node(id, node, fn_node.value)
}
// Reflected method selection can retain an embedded-field selector from the
// checker even after the concrete call target resolves to a method on the
// complete program context. Recover that outer receiver from the selector;
+3
View File
@@ -96,6 +96,9 @@ const int128_shift_ops = [flat.Op.left_shift, .right_shift, .right_shift_unsigne
// its existing path.
fn (mut g FlatGen) gen_int128_infix(id flat.NodeId, node flat.Node, lhs_id flat.NodeId, rhs_id flat.NodeId, lhs_type types.Type, rhs_type types.Type) bool {
op := node.op
if op !in int128_arith_ops && op !in int128_comparison_ops && op !in int128_shift_ops {
return false
}
lhs_signed := int128_signedness(lhs_type)
rhs_signed := int128_signedness(rhs_type)
if op in int128_shift_ops {
+238
View File
@@ -0,0 +1,238 @@
module c
import v.flat
import v.token
import v.types
fn lazy_cgen_node(mut a flat.FlatAst, kind flat.NodeKind, op flat.Op, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
kind: kind
op: op
children_start: start
children_count: children.len
})
}
fn test_checked_boolean_metadata_keeps_storage_and_unknown_types_on_fallback() {
mut a := flat.FlatAst.new()
left := a.add_val(.bool_literal, 'true')
right := a.add_val(.bool_literal, 'false')
comparison := lazy_cgen_node(mut a, .infix, .eq, [left, right])
negation := lazy_cgen_node(mut a, .prefix, .not, [left])
ident := a.add_val(.ident, 'flag')
selector := lazy_cgen_node(mut a, .selector, .none, [ident])
call := lazy_cgen_node(mut a, .call, .none, [ident])
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
for id in [left, comparison, negation, ident, selector, call] {
a.nodes[int(id)].typ = flat.source_type_bool
assert !g.checked_boolean_operand(id)
tc.register_synth_type(id, types.Type(types.bool_))
assert g.checked_boolean_operand(id) == (id == comparison)
}
for typ in [types.Type(types.int_), types.Type(types.f32_), types.Type(types.i128_),
types.Type(types.Unknown{}), types.Type(types.Struct{ name_id: flat.source_type_id('Flag') })] {
tc.register_synth_type(comparison, typ)
assert !g.checked_boolean_operand(comparison)
}
}
fn test_boolean_infix_keeps_parentheses_overrides_and_expected_context() {
mut a := flat.FlatAst.new()
left := a.add_val(.bool_literal, 'true')
right := a.add_val(.bool_literal, 'false')
comparison := lazy_cgen_node(mut a, .infix, .ne, [left, right])
choice := lazy_cgen_node(mut a, .infix, .logical_or, [left, right])
conjunction := lazy_cgen_node(mut a, .infix, .logical_and, [comparison, choice])
mut tc := types.TypeChecker.new(&a)
for id in [left, right, comparison, choice, conjunction] {
tc.register_synth_type(id, types.Type(types.bool_))
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.expected_enum = flat.source_type_id('OuterEnum')
g.expected_expr_type = types.Type(types.f32_)
assert g.expr_to_string(conjunction) == '(true != false) && (true || false)'
g.assert_expr_overrides[int(comparison)] = 'left_override'
g.callback_target_overrides[int(choice)] = 'right_override'
assert g.expr_to_string(conjunction) == '(left_override) && (right_override)'
assert g.expected_enum == flat.source_type_id('OuterEnum')
assert g.expected_expr_type == types.Type(types.f32_)
// Checked boolean infixes need no usable-type memo probes.
g.memo_usable_expr_types = true
g.begin_usable_expr_type_memo()
assert g.expr_to_string(conjunction) == '(left_override) && (right_override)'
for id in [comparison, choice] {
assert g.usable_expr_type_memo.gens[int(id) & usable_expr_type_memo_mask] != g.usable_expr_type_memo.generation
}
}
fn test_boolean_infix_resolves_unannotated_identifiers_and_stale_annotations() {
mut a := flat.FlatAst.new()
left := a.add_val(.ident, 'left')
right := a.add_val(.ident, 'right')
conjunction := lazy_cgen_node(mut a, .infix, .logical_and, [left, right])
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cur_param_types['left'] = types.Type(types.bool_)
g.cur_param_types['right'] = types.Type(types.bool_)
assert g.expr_to_string(conjunction) == 'left && right'
// A flat bool annotation must not override the authoritative checked type.
first := lazy_cgen_node(mut a, .infix, .eq, [left, right])
second := lazy_cgen_node(mut a, .infix, .eq, [left, right])
outer := lazy_cgen_node(mut a, .infix, .logical_and, [first, second])
a.nodes[int(first)].typ = flat.source_type_bool
tc.register_synth_type(first, types.Type(types.Struct{ name_id: flat.source_type_id('CustomFlag') }))
tc.register_synth_type(second, types.Type(types.bool_))
g.assert_expr_overrides[int(first)] = 'custom_flag'
g.assert_expr_overrides[int(second)] = 'boolean_flag'
assert g.expr_to_string(outer) == 'custom_flag && boolean_flag'
}
fn test_boolean_infix_translated_and_unknown_metadata_still_use_normal_resolution() {
mut a := flat.FlatAst.new()
left := a.add_val(.bool_literal, 'true')
right := a.add_val(.bool_literal, 'false')
conjunction := lazy_cgen_node(mut a, .infix, .logical_and, [left, right])
mut files := token.FileSet.new()
a.source_files[1] = files.add_file('translated_boolean.v', 1)
a.nodes[int(conjunction)].pos = token.new_pos(1, 0)
mut tc := types.TypeChecker.new(&a)
tc.translated_files['translated_boolean.v'] = true
for id in [left, right, conjunction] {
tc.register_synth_type(id, types.Type(types.bool_))
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.memo_usable_expr_types = true
g.begin_usable_expr_type_memo()
assert g.expr_to_string(conjunction) == 'true && false'
for id in [left, right] {
assert g.usable_expr_type_memo.gens[int(id) & usable_expr_type_memo_mask] == g.usable_expr_type_memo.generation
}
g.end_usable_expr_type_memo()
tc.translated_files.clear()
tc.register_synth_type(left, types.Type(types.Unknown{}))
tc.register_synth_type(right, types.Type(types.Unknown{}))
g.begin_usable_expr_type_memo()
assert g.expr_to_string(conjunction) == 'true && false'
for id in [left, right] {
assert g.usable_expr_type_memo.gens[int(id) & usable_expr_type_memo_mask] == g.usable_expr_type_memo.generation
}
}
fn test_discarded_infix_handlers_do_not_resolve_the_result_type() {
mut a := flat.FlatAst.new()
left := a.add_val(.int_literal, '7')
right := a.add_val(.int_literal, '2')
result := lazy_cgen_node(mut a, .infix, .plus, [left, right])
mut tc := types.TypeChecker.new(&a)
tc.register_synth_type(result, types.Type(types.int_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.memo_usable_expr_types = true
g.begin_usable_expr_type_memo()
for builtins in [false, true] {
g.has_builtins = builtins
assert !g.gen_safe_integer_division(result, a.nodes[int(result)], left, right)
}
a.nodes[int(result)].op = .div
g.static_c_initializer = true
assert !g.gen_safe_integer_division(result, a.nodes[int(result)], left, right)
for op in [flat.Op.logical_and, .logical_or, .power, .arrow] {
a.nodes[int(result)].op = op
assert !g.gen_int128_infix(result, a.nodes[int(result)], left, right, types.Type(types.int_), types.Type(types.int_))
}
assert g.usable_expr_type_memo.gens[int(result) & usable_expr_type_memo_mask] != g.usable_expr_type_memo.generation
}
fn test_lazy_division_and_int128_handlers_keep_supported_operations() {
mut a := flat.FlatAst.new()
left := a.add_val(.int_literal, '7')
right := a.add_val(.int_literal, '2')
result := lazy_cgen_node(mut a, .infix, .div, [left, right])
mut tc := types.TypeChecker.new(&a)
for id in [left, right, result] {
tc.register_synth_type(id, types.Type(types.int_))
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.has_builtins = true
division := g.expr_to_string(result)
assert division.contains('division by zero')
assert division.contains('v_panic(')
a.nodes[int(result)].op = .mod
assert g.expr_to_string(result).contains('modulo by zero')
for portable in [false, true] {
g.compile_defines = if portable { ['v3_no_native_int128'] } else { []string{} }
g.has_builtins = false
tc.register_synth_type(result, types.Type(types.i128_))
a.nodes[int(result)].op = .plus
assert g.expr_to_string(result) == '__v_i128_add(__v_i128_from_i64((i64)(7)), __v_i128_from_i64((i64)(2)))'
a.nodes[int(result)].op = .div
assert g.expr_to_string(result) == '__v_i128_div(__v_i128_from_i64((i64)(7)), __v_i128_from_i64((i64)(2)))'
}
}
fn test_prefix_type_query_keeps_pointer_annotations_and_avoids_unused_child_queries() {
mut a := flat.FlatAst.new()
child := a.add_val(.ident, 'value')
negation := lazy_cgen_node(mut a, .prefix, .not, [child])
address := lazy_cgen_node(mut a, .prefix, .amp, [child])
deref := lazy_cgen_node(mut a, .prefix, .mul, [address])
mut tc := types.TypeChecker.new(&a)
tc.register_synth_type(negation, types.Type(types.bool_))
tc.register_synth_type(child, types.Type(types.int_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.memo_usable_expr_types = true
g.begin_usable_expr_type_memo()
assert *g.usable_expr_type(negation) == types.Type(types.bool_)
assert g.usable_expr_type_memo.gens[int(child) & usable_expr_type_memo_mask] != g.usable_expr_type_memo.generation
assert g.usable_expr_type(address).name() == '&int'
assert *g.usable_expr_type(deref) == types.Type(types.int_)
g.end_usable_expr_type_memo()
a.nodes[int(deref)].typ = flat.source_type_id('&bool')
g.begin_usable_expr_type_memo()
assert g.usable_expr_type(deref).name() == '&bool'
assert g.usable_expr_type_memo.gens[int(address) & usable_expr_type_memo_mask] != g.usable_expr_type_memo.generation
}
fn test_ierror_alias_tracking_keeps_array_get_alias_propagation() {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'borrowed')
callee := a.add_val(.ident, 'array__get')
call := lazy_cgen_node(mut a, .call, .none, [callee, base])
paren := lazy_cgen_node(mut a, .paren, .none, [call])
deref := lazy_cgen_node(mut a, .prefix, .mul, [paren])
lhs := a.add_val(.ident, 'alias')
mut tc := types.TypeChecker.new(&a)
tc.cur_scope = types.new_scope(tc.cur_scope)
tc.cur_scope.insert('borrowed', types.Type(types.int_))
tc.cur_scope.insert('alias', types.Type(types.int_))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.ierror_stack_pointer_aliases << map[string]bool{}
g.declare_ierror_pointer_alias('borrowed', true)
for id in [call, paren, deref] {
assert g.ierror_array_get_pointer_alias_needs_copy(a.nodes[int(id)])
assert g.ierror_pointer_payload_expr_needs_heap_copy(a.nodes[int(id)])
g.track_ierror_stack_pointer_alias(a.nodes[int(lhs)], a.nodes[int(id)])
assert g.ierror_pointer_alias_needs_copy('alias')
g.assign_ierror_pointer_alias('alias', false)
g.track_ierror_stack_pointer_alias_assign(a.nodes[int(lhs)], a.nodes[int(id)])
assert g.ierror_pointer_alias_needs_copy('alias')
}
}
+27 -3
View File
@@ -113,8 +113,14 @@ fn (mut c CNameCache) remember(name string, value string) {
@[heap]
struct ConstShortIndex {
mut:
built bool
entries map[string]string
built bool
entries map[string]string
call_names_ready bool
call_names [64]u64
call_const_vals_count int
call_const_exprs_count int
call_const_types_count int
call_const_suffixes_count int
}
// FnNameFactCache memoizes per-fn-name conclusions (1 = yes, -1 = no) that
@@ -285,14 +291,21 @@ fn (mut c ContextStringLookupCache) select_context(file string, module_name stri
c.last_valid = false
}
struct SourceTypeLookupRecentEntry {
name u32
value u32
valid bool
}
// ContextSourceTypeLookupCache memoizes source-spelling lookups within one file and module.
// Zero values record misses; last_valid distinguishes an empty cache from a cached miss.
// Zero values record misses; validity distinguishes an empty cache from a cached miss.
@[heap]
struct ContextSourceTypeLookupCache {
mut:
file string = '\x00'
module string
entries map[u32]u32
recent [64]SourceTypeLookupRecentEntry
last_name u32
last_value u32
last_valid bool
@@ -301,7 +314,16 @@ mut:
@[inline]
fn (mut c ContextSourceTypeLookupCache) get(name u32) ?u32 {
if c.last_valid && c.last_name == name { return c.last_value }
slot := int(name & 63)
entry := c.recent[slot]
if entry.valid && entry.name == name {
c.last_name = name
c.last_value = entry.value
c.last_valid = true
return entry.value
}
if cached := c.entries[name] {
c.recent[slot] = SourceTypeLookupRecentEntry{ name: name, value: cached, valid: true }
c.last_name = name
c.last_value = cached
c.last_valid = true
@@ -313,6 +335,7 @@ fn (mut c ContextSourceTypeLookupCache) get(name u32) ?u32 {
@[inline]
fn (mut c ContextSourceTypeLookupCache) put(name u32, value u32) {
c.entries[name] = value
c.recent[int(name & 63)] = SourceTypeLookupRecentEntry{ name: name, value: value, valid: true }
c.last_name = name
c.last_value = value
c.last_valid = true
@@ -328,6 +351,7 @@ fn (mut c ContextSourceTypeLookupCache) select_context(file string, module_name
c.file = file
c.module = module_name
c.entries = map[u32]u32{}
c.recent = [64]SourceTypeLookupRecentEntry{}
c.last_name = 0
c.last_value = 0
c.last_valid = false
+42 -1
View File
@@ -1553,6 +1553,7 @@ fn ordinary_local_ident_output(name string, mode string, fast bool) string {
tc.cur_scope = scope
typ := if mode == 'void' { types.Type(types.Void{}) } else { types.Type(types.int_) }
owner := scope.insert_with_owner(name, typ)
other_owner := scope.insert_with_owner('other_local', types.Type(types.int_))
if mode == 'void' {
tc.fn_param_types[name] = []types.Type{}
tc.fn_ret_types[name] = types.Type(types.int_)
@@ -1565,6 +1566,10 @@ fn ordinary_local_ident_output(name string, mode string, fast bool) string {
'deref' { g.local_implicit_deref_by_owner[owner.storage_key()] = true }
'shadow' { g.shadowed_global_locals[owner.storage_key()] = true }
'cfn' { g.cur_c_fn_calls[g.cname(name)] = true }
'other_shared' { g.local_shared_storage_by_owner[other_owner.storage_key()] = true }
'other_deref' { g.local_implicit_deref_by_owner[other_owner.storage_key()] = true }
'other_shadow' { g.shadowed_global_locals[other_owner.storage_key()] = true }
'other_cfn' { g.cur_c_fn_calls['other_c_call'] = true }
'suffix' { tc.c_globals['C.${name}'] = types.Type(types.int_) }
'typedef' { g.inlined_c_typedef_names[g.cname(name)] = true }
'global' { g.global_modules[name] = 'other' }
@@ -1579,13 +1584,49 @@ fn ordinary_local_ident_output(name string, mode string, fast bool) string {
fn test_ordinary_local_ident_keeps_storage_rename_and_placeholder_output() {
for name in ['value', 'abs', 'array', 'argv', 'int_str', 'pkg.value', '__v3_internal_symbol_local'] {
for mode in ['', 'shared', 'deref', 'shadow', 'cfn', 'suffix', 'typedef', 'global', 'void'] {
for mode in ['', 'shared', 'deref', 'shadow', 'cfn', 'other_shared', 'other_deref',
'other_shadow', 'other_cfn', 'suffix', 'typedef', 'global', 'void'] {
legacy := ordinary_local_ident_output(name, mode, false)
assert ordinary_local_ident_output(name, mode, true) == legacy, '${name} / ${mode}: ${legacy}'
}
}
}
fn test_ordinary_local_ident_ignores_outer_binding_storage_and_unrelated_c_calls() {
mut a := flat.FlatAst.new()
id := a.add_val(.ident, 'value')
mut tc := types.TypeChecker.new(&a)
mut outer_scope := types.new_scope(unsafe { nil })
outer := outer_scope.insert_with_owner('value', types.Type(types.int_))
mut inner_scope := types.new_scope(outer_scope)
inner := inner_scope.insert_with_owner('value', types.Type(types.int_))
tc.cur_scope = inner_scope
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.local_shared_storage_by_owner[outer.storage_key()] = true
g.local_implicit_deref_by_owner[outer.storage_key()] = true
g.shadowed_global_locals[outer.storage_key()] = true
g.cur_c_fn_calls['other_c_call'] = true
g.precompute_local_global_suffix_names()
g.precompute_c_typedef_shadow_names()
g.precompute_local_name_exceptions()
assert g.ordinary_local_cname('value', inner)? == 'value'
assert g.ordinary_local_cname('value', outer) == none
g.gen_nonliteral_expr(id, a.node(id))
assert g.sb.str() == 'value'
// A recorded C call reserves its emitted name even when the map value is false.
g.cur_c_fn_calls['value'] = false
assert g.ordinary_local_cname('value', inner) == none
g.cur_c_fn_calls.delete('value')
g.shadowed_global_locals[inner.storage_key()] = false
assert g.ordinary_local_cname('value', inner) == none
g.shadowed_global_locals.delete(inner.storage_key())
g.local_shared_storage_by_owner[inner.storage_key()] = false
g.local_implicit_deref_by_owner[inner.storage_key()] = false
assert g.ordinary_local_cname('value', inner)? == 'value'
}
fn test_local_name_exceptions_preserve_raw_and_emitted_names_and_rebuilds() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
+104
View File
@@ -0,0 +1,104 @@
module c
import v.flat
import v.token
import v.types
fn add_snapshot_query_node(mut a flat.FlatAst, kind flat.NodeKind, value string, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
kind: kind
value: value
children_start: i32(start)
children_count: flat.child_count(children.len)
})
}
fn grow_snapshot_query_arena(mut a flat.FlatAst) {
for _ in 0 .. 2048 {
a.add_val(.int_literal, '0')
}
}
fn test_snapshot_closure_search_keeps_first_nested_match_after_arena_growth() {
mut a := flat.FlatAst.new()
first := a.add_val(.ident, 'main.__anon_fn_first')
second := a.add_val(.ident, 'main.__anon_fn_second')
nested := add_snapshot_query_node(mut a, .paren, '', [first])
root := add_snapshot_query_node(mut a, .call, '', [nested, second])
snapshot := a.nodes[int(root)]
grow_snapshot_query_arena(mut a)
a.nodes[int(root)].children_count = 0
mut g := FlatGen.new()
g.a = &a
assert g.runtime_closure_lifted_c_name(snapshot) or { '' } == g.cname('main.__anon_fn_first')
assert g.runtime_closure_lifted_c_name(a.node(root)) == none
}
fn test_snapshot_const_selector_keeps_owning_file_import_and_local_shadow() {
mut a := flat.FlatAst.new()
mut files := token.FileSet.new()
a.source_files[1] = files.add_file('owner.v', 1)
base := a.add_val(.ident, 'dep')
selector := add_snapshot_query_node(mut a, .selector, 'limit', [base])
a.nodes[int(selector)].pos = token.new_pos(1, 0)
snapshot := a.nodes[int(selector)]
grow_snapshot_query_arena(mut a)
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.cur_file = 'other.v'
tc.file_imports['owner.v\ndep'] = 'library'
tc.file_imports['other.v\ndep'] = 'other'
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.const_vals['library.limit'] = flat.NodeId(0)
g.const_vals['other.limit'] = flat.NodeId(0)
assert g.const_ref_name_from_node(snapshot) == 'library.limit'
g.cur_param_types['dep'] = &types.Type(types.int_)
assert g.const_ref_name_from_node(snapshot) == ''
mut cache := map[string]string{}
assert g.const_ref_name_from_node_cached_for_collect(snapshot, map[string]string{}, mut cache) == ''
}
fn test_snapshot_source_file_walk_keeps_descendant_order_and_fallback() {
mut a := flat.FlatAst.new()
mut files := token.FileSet.new()
a.source_files[1] = files.add_file('first.v', 1)
a.source_files[2] = files.add_file('second.v', 1)
first := a.add_node(flat.Node{ kind: .ident, pos: token.new_pos(1, 0) })
second := a.add_node(flat.Node{ kind: .ident, pos: token.new_pos(2, 0) })
nested := add_snapshot_query_node(mut a, .paren, '', [second])
root := add_snapshot_query_node(mut a, .call, '', [first, nested])
snapshot := a.nodes[int(root)]
grow_snapshot_query_arena(mut a)
a.nodes[int(root)].children_count = 0
mut tc := types.TypeChecker.new(&a)
tc.cur_file = 'checker.v'
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cur_fn_source_file = 'function.v'
assert g.node_source_file(&snapshot) == 'second.v'
assert g.node_source_file(a.node(root)) == 'function.v'
}
fn test_fixed_size_query_keeps_annotation_and_rejects_ordinary_calls() {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'values')
selector := add_snapshot_query_node(mut a, .selector, 'to_fixed_size', [base])
call := add_snapshot_query_node(mut a, .call, '', [selector])
a.nodes[int(call)].typ = flat.source_type_id('[2]int')
ordinary_callee := a.add_val(.ident, 'ordinary')
ordinary := add_snapshot_query_node(mut a, .call, '', [ordinary_callee])
grow_snapshot_query_arena(mut a)
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
fixed := g.to_fixed_size_call_fixed_type(call) or { panic('missing fixed array') }
assert fixed.len == 2
assert fixed.elem_type.name() == 'int'
assert g.to_fixed_size_call_fixed_type(ordinary) == none
}
+24 -16
View File
@@ -18,14 +18,16 @@ enum CgenModePool {
}
struct CgenModeOutput {
code string
literals []string
fn_names []string
scoped bool
parallel bool
pool_present bool
pool_size int
pool_tasks u64
code string
literals []string
fn_names []string
scoped bool
parallel bool
pool_present bool
pool_size int
pool_tasks u64
type_names_present bool
field_index_present bool
mut:
launches u64
launch_errors u64
@@ -114,14 +116,16 @@ fn cgen_mode_generate(path string, pool_mode CgenModePool, no_parallel bool) Cge
g.set_scope_parallel_workers(true)
code := g.gen_with_used_options(a, used, &tc, no_parallel)
mut result := CgenModeOutput{
code: code
literals: g.str_lits.clone()
fn_names: g.used_fn_names.clone()
scoped: g.scope_parallel_workers
parallel: g.parallel_used
pool_present: !isnil(a.worker_pool)
pool_size: a.worker_count()
pool_tasks: a.worker_tasks_run()
code: code
literals: g.str_lits.clone()
fn_names: g.used_fn_names.clone()
scoped: g.scope_parallel_workers
parallel: g.parallel_used
pool_present: !isnil(a.worker_pool)
pool_size: a.worker_count()
pool_tasks: a.worker_tasks_run()
type_names_present: !isnil(g.type_name_short_index)
field_index_present: !isnil(g.struct_field_index_base)
}
if !isnil(a.worker_pool) {
stats := a.worker_pool.stats()
@@ -202,6 +206,10 @@ fn test_one_job_codegen_keeps_a_lazy_pool_and_matches_explicit_serial() {
defer { os.rm(path) or {} }
serial := cgen_mode_generate(path, .lazy, true)
default_mode := cgen_mode_generate(path, .lazy, false)
assert serial.type_names_present
assert default_mode.type_names_present
assert !serial.field_index_present
assert !default_mode.field_index_present
assert default_mode.code == serial.code
assert default_mode.literals == serial.literals
assert default_mode.fn_names == serial.fn_names
+104
View File
@@ -0,0 +1,104 @@
module c
import v.flat
import v.types
fn test_serial_collection_builds_complete_name_index_without_field_index() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.structs['models.Record'] = []types.StructField{}
tc.type_aliases.set_id('models.Alias', flat.source_type_int)
tc.enum_names['models.State'] = true
tc.flag_enums['models.Flags'] = true
tc.sum_types.set_ids('models.Choice', [flat.source_type_int, flat.source_type_string])
tc.interface_names['models.Reader'] = true
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
assert isnil(g.type_name_short_index)
assert g.ident_may_name_type('missing')
g.collect_gen_info(true)
assert !isnil(g.type_name_short_index)
assert isnil(g.struct_field_index_base)
for name in ['Record', 'Alias', 'State', 'Choice', 'Reader'] {
assert g.ident_may_name_type(name)
assert g.ident_may_name_type('other.${name}')
}
assert g.name_may_be_type_alias('Alias')
assert !g.name_may_be_type_alias('Record')
for name in ['State', 'Flags', 'Alias'] {
assert g.name_may_reach_enum(name)
}
assert !g.ident_may_name_type('missing')
assert !g.name_may_reach_enum('Record')
assert g.ident_may_name_type('Unknown[int]')
assert g.name_may_be_type_alias('Unknown[int]')
assert g.name_may_reach_enum('Unknown[int]')
// A later full collection rebuilds membership.
tc.structs['later.Added'] = []types.StructField{}
g.collect_gen_info(true)
assert g.ident_may_name_type('Added')
// Incremental collection keeps the existing conservative fallback.
mut incremental := FlatGen.new()
incremental.a = &a
incremental.tc = &tc
incremental.incremental_fn_names['only_new_body'] = true
incremental.collect_gen_info(true)
assert isnil(incremental.type_name_short_index)
assert incremental.ident_may_name_type('missing')
}
fn test_value_c_type_alias_filter_preserves_c_spelling_main_prefix_and_long_keys() {
for prefix in ['', 'main.'] {
for suffix in ['Value', 'Value' + 'x'.repeat(600)] {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'main'
tc.type_aliases.set_id('${prefix}model__${suffix}', flat.source_type_f64)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
value := types.Type(types.Struct{ name_id: flat.source_type_id('model.${suffix}') })
before := g.value_c_type(&value)
expected := types.Type(types.f64_)
assert before == g.tc.c_type(&expected)
g.type_name_short_index = new_type_name_short_index(&tc)
assert g.name_may_be_type_alias('model__${suffix}')
assert g.value_c_type(&value) == before
plain := types.Type(types.Struct{ name_id: flat.source_type_id('model.Other') })
assert !g.name_may_be_type_alias('model__Other')
filtered := g.value_c_type(&plain)
g.type_name_short_index = unsafe { nil }
assert g.value_c_type(&plain) == filtered
}
}
}
fn test_cast_name_resolution_keeps_bare_priority_qualification_and_local_shadowing() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'sample'
tc.structs['Record'] = []types.StructField{}
tc.type_aliases.set_id('sample.Record', flat.source_type_int)
tc.type_aliases.set_id('sample.Alias', flat.source_type_int)
tc.enum_names['sample.State'] = true
tc.sum_types.set_ids('sample.Choice', [flat.source_type_int, flat.source_type_string])
tc.interface_names['sample.Reader'] = true
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
for with_index in [false, true] {
if with_index {
g.type_name_short_index = new_type_name_short_index(&tc)
}
assert g.call_ident_cast_type_name('Record', false) or { '' } == 'Record'
for name in ['Alias', 'State', 'Choice', 'Reader'] {
assert g.call_ident_cast_type_name(name, false) or { '' } == 'sample.${name}'
assert g.call_ident_cast_type_name('sample.${name}', false) or { '' } == 'sample.${name}'
}
assert g.call_ident_cast_type_name('absent', false) == none
for name in ['Record', 'Alias', 'State', 'Choice', 'Reader'] {
assert g.call_ident_cast_type_name(name, true) == none
}
}
}
@@ -0,0 +1,94 @@
module c
import v.flat
import v.types
fn test_source_type_recent_cache_preserves_zero_keys_and_cached_misses() {
mut cache := ContextSourceTypeLookupCache{}
assert cache.get(0) == none
assert cache.get(1) == none
cache.put(0, 0)
cache.put(1, 42)
assert cache.get(0)? == 0
assert cache.get(1)? == 42
cache.put(1, 0)
cache.put(2, 3)
assert cache.get(1)? == 0
assert cache.get(3) == none
cache.put(0, 19)
cache.put(2, 3)
assert cache.get(0)? == 19
}
fn test_source_type_recent_cache_collisions_repopulate_from_the_full_map() {
mut cache := ContextSourceTypeLookupCache{}
for i in 0 .. 512 {
cache.put(u32(i), u32(i * 3))
}
for i in 0 .. 512 {
assert cache.get(u32(i))? == u32(i * 3)
slot := int(u32(i) & 63)
assert cache.recent[slot].valid
assert cache.recent[slot].name == u32(i)
}
for _ in 0 .. 4 {
for name in [u32(0), 64, 128, 192, 256, 320, 384, 448] {
assert cache.get(name)? == name * 3
}
}
cache.put(64, 0)
assert cache.get(0)? == 0
assert cache.get(64)? == 0
assert cache.get(512) == none
}
fn test_source_type_recent_cache_context_changes_clear_all_cache_layers() {
mut cache := ContextSourceTypeLookupCache{}
cache.select_context('first.v', 'first')
cache.put(0, 0)
cache.put(1, 42)
cache.select_context('first.v'.clone(), 'first'.clone())
assert cache.get(0)? == 0
assert cache.get(1)? == 42
for context in [['second.v', 'first'], ['second.v', 'second'], ['first.v', 'first']] {
cache.select_context(context[0], context[1])
assert !cache.last_valid
assert cache.entries.len == 0
for entry in cache.recent {
assert !entry.valid
}
assert cache.get(0) == none
assert cache.get(1) == none
cache.put(0, 0)
cache.put(1, 43)
}
}
fn test_source_type_recent_cache_workers_and_scratch_batches_are_private() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.enum_selector_cache.select_context('same.v', 'main')
g.enum_selector_cache.put(1, 2)
mut worker := g.new_parallel_worker(1)
assert worker.enum_selector_cache != g.enum_selector_cache
assert worker.enum_selector_cache.get(1) == none
worker.enum_selector_cache.select_context('same.v', 'main')
worker.enum_selector_cache.put(1, 3)
assert g.enum_selector_cache.get(1)? == 2
assert worker.enum_selector_cache.get(1)? == 3
own := g.enum_selector_cache
saved := g.begin_scratch_lookup_caches()
assert g.enum_selector_cache != own
assert g.enum_selector_cache.get(1) == none
g.enum_selector_cache.select_context('same.v', 'main')
g.enum_selector_cache.put(1, 4)
g.restore_scratch_lookup_caches(saved)
assert g.enum_selector_cache == own
assert g.enum_selector_cache.get(1)? == 2
g.reset_context_lookup_caches()
assert g.enum_selector_cache != own
assert g.enum_selector_cache.get(1) == none
}
+37 -27
View File
@@ -6471,7 +6471,7 @@ fn (g &FlatGen) body_resolve_type_reference(id flat.NodeId) &types.Type {
if memo.resolve_gens[slot] == memo.generation && memo.resolve_ids[slot] == idx {
return memo.resolve_values[slot]
}
result := g.tc.intern_type_reference(g.tc.resolve_type(id))
result := g.tc.resolve_type_reference(id)
if !cgen_type_contains_unknown(result) {
memo.resolve_ids[slot] = idx
memo.resolve_gens[slot] = memo.generation
@@ -6480,7 +6480,7 @@ fn (g &FlatGen) body_resolve_type_reference(id flat.NodeId) &types.Type {
return result
}
}
return g.tc.intern_type_reference(g.tc.resolve_type(id))
return g.tc.resolve_type_reference(id)
}
// cgen_type_contains_unknown reports an unknown type anywhere in `typ`.
@@ -7158,16 +7158,16 @@ fn (g &FlatGen) usable_expr_type_reference_uncached(id flat.NodeId) &types.Type
return annotated
}
}
child_type := g.usable_expr_type(g.a.child(node, 0))
if node.op == .amp {
return g.tc.intern_type_reference(types.Type(types.Pointer{
base_type: child_type
}))
}
if node.op == .mul {
if node.op in [.amp, .mul] {
child_type := g.usable_expr_type(g.a.child(node, 0))
if node.op == .amp {
return g.tc.intern_type_reference(types.Type(types.Pointer{
base_type: child_type
}))
}
pointer := unalias_type(child_type)
if pointer is types.Pointer {
return g.tc.intern_type_reference(pointer.base_type)
return pointer.base_type
}
}
}
@@ -7208,7 +7208,7 @@ fn (g &FlatGen) usable_expr_type_reference_uncached(id flat.NodeId) &types.Type
// `shared` local.
if typ is types.Pointer && g.local_shared_storage_by_owner.len > 0
&& g.local_storage_is_shared(node.value) {
return g.tc.intern_type_reference(typ.base_type)
return typ.base_type
}
return typ
}
@@ -7326,13 +7326,13 @@ fn (g &FlatGen) usable_expr_type_reference_uncached(id flat.NodeId) &types.Type
}
}
if base_type is types.Array {
return g.tc.intern_type_reference(base_type.elem_type)
return base_type.elem_type
}
if base_type is types.ArrayFixed {
return g.tc.intern_type_reference(base_type.elem_type)
return base_type.elem_type
}
if base_type is types.Map {
return g.tc.intern_type_reference(base_type.value_type)
return base_type.value_type
}
if base_type is types.String {
return g.tc.intern_type_reference(types.builtin_type_value('u8'))
@@ -7982,7 +7982,7 @@ fn (mut g FlatGen) gen_decl_assign(node flat.Node) {
g.current_decl_is_mut = old_decl_is_mut
}
if node.value == '__v3_zeroed_stack_value_decl'
&& g.a.child_node(&node, 0).value !in g.defer_capture_types {
&& g.a.node(g.a.child(&node, 0)).value !in g.defer_capture_types {
g.gen_staging_value_decl(node)
return
}
@@ -8231,7 +8231,7 @@ fn (mut g FlatGen) gen_decl_assign(node flat.Node) {
mut init_cap := '0'
mut init_val := ''
for j in 0 .. rhs.children_count {
child := g.a.child_node(&rhs, j)
child := g.a.node(g.a.child(&rhs, j))
if child.kind == .field_init {
if child.value == 'len' {
init_len = g.expr_to_string(g.a.child(child, 0))
@@ -8362,7 +8362,7 @@ fn (mut g FlatGen) gen_decl_assign(node flat.Node) {
if rhs.kind == .call && lhs.typ_text().starts_with('(') && lhs.typ_text().contains(',') {
mut declared_ret := g.declared_call_return_type(rhs_id)
if rhs.children_count > 0 {
callee := g.a.child_node(&rhs, 0)
callee := g.a.node(g.a.child(&rhs, 0))
if specialized_ret := g.specialized_selector_method_call_return_type(rhs, callee) {
declared_ret = specialized_ret
}
@@ -8372,7 +8372,7 @@ fn (mut g FlatGen) gen_decl_assign(node flat.Node) {
}
}
if rhs.kind == .call && rhs.children_count > 0 {
callee := g.a.child_node(&rhs, 0)
callee := g.a.node(g.a.child(&rhs, 0))
if callee.kind == .selector {
if ret := g.selector_fn_field_return_type(callee) {
v_type = ret
@@ -8570,8 +8570,13 @@ fn (mut g FlatGen) gen_decl_assign(node flat.Node) {
semantic_v_type := unalias_type(v_type)
// Ownership replacement temporaries carry the destination type. Box before
// the concrete payload type is lost, preserving the later drop and store.
boxed_optional := g.optional_interface_assignment_boxes_payload(semantic_v_type,
optional_result_unalias_type(g.usable_expr_type(rhs_id)), rhs_id) or { '' }
boxed_optional := if semantic_v_type is types.OptionType
&& cgen_unalias_type(optional_result_wrapped_base(semantic_v_type)) is types.Interface {
g.optional_interface_assignment_boxes_payload(semantic_v_type,
optional_result_unalias_type(g.usable_expr_type(rhs_id)), rhs_id) or { '' }
} else {
''
}
ct0 := if rhs.kind == .struct_init
&& flat.source_type_shape(g.struct_init_effective_type_name(rhs_id, rhs)).has(.channel_type) {
'chan'
@@ -8814,7 +8819,6 @@ fn (mut g FlatGen) track_ierror_stack_pointer_alias(lhs flat.Node, rhs flat.Node
return
}
needs_copy := g.ierror_pointer_payload_expr_needs_heap_copy(rhs)
|| g.ierror_array_get_pointer_alias_needs_copy(rhs)
g.declare_ierror_pointer_alias(lhs.value, needs_copy)
}
@@ -8849,7 +8853,7 @@ fn (g &FlatGen) runtime_closure_lifted_c_name(node flat.Node) ?string {
return g.cname(node.value)
}
for i in 0 .. node.children_count {
if c_name := g.runtime_closure_lifted_c_name(g.a.child_node(&node, i)) {
if c_name := g.runtime_closure_lifted_c_name(g.a.node(g.a.child(&node, i))) {
return c_name
}
}
@@ -8897,8 +8901,7 @@ fn (mut g FlatGen) track_ierror_stack_pointer_alias_assign(lhs flat.Node, rhs fl
if lhs.kind != .ident || lhs.value.len == 0 {
return
}
g.assign_ierror_pointer_alias(lhs.value, g.ierror_pointer_payload_expr_needs_heap_copy(rhs)
|| g.ierror_array_get_pointer_alias_needs_copy(rhs))
g.assign_ierror_pointer_alias(lhs.value, g.ierror_pointer_payload_expr_needs_heap_copy(rhs))
}
fn (mut g FlatGen) track_local_pointer_alias_assign(lhs flat.Node, rhs_id flat.NodeId) {
@@ -10417,14 +10420,21 @@ fn mark_global_name_filter(mut bits [64]u64, name string) {
// spelling override. Void bindings remain on the function-placeholder path.
@[inline]
fn (g &FlatGen) ordinary_local_cname(name string, owner types.ScopeBindingOwner) ?string {
if isnil(g.local_name_exceptions) || g.local_shared_storage_by_owner.len > 0
|| g.local_implicit_deref_by_owner.len > 0 || g.shadowed_global_locals.len > 0
|| g.cur_c_fn_calls.len > 0 || local_name_shadows_c_runtime(name) {
if isnil(g.local_name_exceptions) || local_name_shadows_c_runtime(name) {
return none
}
typ := owner.bound_type() or { return none }
if typ is types.Void { return none }
key := owner.storage_key()
if (g.local_shared_storage_by_owner.len > 0 && g.local_shared_storage_by_owner[key])
|| (g.local_implicit_deref_by_owner.len > 0 && g.local_implicit_deref_by_owner[key])
|| (g.shadowed_global_locals.len > 0 && key in g.shadowed_global_locals) {
return none
}
cname := g.cname(name)
if g.cur_c_fn_calls.len > 0 && cname in g.cur_c_fn_calls {
return none
}
// Some source names acquire a reserved-word prefix. Check the emitted
// spelling too, so a typedef named v_abs still protects a local named abs.
if name in g.local_name_exceptions.names
+9 -6
View File
@@ -224,11 +224,14 @@ fn (mut g FlatGen) value_c_type(t &types.Type) string {
return qualified + pointer_suffix
}
}
if target_source_id := g.tc.type_aliases.type_id(ct) {
return g.tc.c_type(unalias_type(g.tc.parse_type(target_source_id)))
}
if target_source_id := g.tc.type_aliases.type_id('main.${ct}') {
return g.tc.c_type(unalias_type(g.tc.parse_type(target_source_id)))
// Both alias keys have the same final component, including C spellings.
if g.name_may_be_type_alias(ct) {
if target_source_id := g.tc.type_aliases.type_id(ct) {
return g.tc.c_type(unalias_type(g.tc.parse_type(target_source_id)))
}
if target_source_id := main_type_alias_id(g.tc.type_aliases, ct) {
return g.tc.c_type(unalias_type(g.tc.parse_type(target_source_id)))
}
}
return ct
}
@@ -928,7 +931,7 @@ fn (g &FlatGen) node_source_file(node &flat.Node) string {
}
}
for i in 0 .. child.children_count {
pending << g.a.child_node(&child, i)
pending << g.a.node(g.a.child(&child, i))
}
}
if g.cur_fn_source_file.len > 0 {
+85
View File
@@ -299,3 +299,88 @@ fn test_body_resolve_reference_checks_the_full_id_on_collisions() {
assert g.body_resolve_type_reference(flat.NodeId(usable_expr_type_memo_slots)) is types.String
}
}
fn test_usable_expr_type_keeps_owned_container_child_references() {
for canonical in [false, true] {
for container_kind in 0 .. 4 {
mut a := flat.FlatAst.new()
value := a.add_val(.ident, 'value')
a.children << value
access := a.add_node(flat.Node{
kind: if container_kind == 0 { .prefix } else { .index }
op: .mul
children_start: 0
children_count: 1
})
mut tc := types.TypeChecker.new(&a)
// A raw heap-owned child need not already have interner identity.
child := types.new_type_reference(types.Type(types.String{}))
container := match container_kind {
0 { types.Type(types.Pointer{ base_type: child }) }
1 { types.Type(types.Array{ elem_type: child }) }
2 { types.Type(types.ArrayFixed{ elem_type: child, len: 3 }) }
else {
types.Type(types.Map{ key_type: types.new_type_reference(types.Type(types.int_)), value_type: child })
}
}
owned := if canonical {
tc.intern_type_reference(container)
} else {
types.new_type_reference(container)
}
expected := match owned {
types.Pointer { owned.base_type }
types.Array { owned.elem_type }
types.ArrayFixed { owned.elem_type }
types.Map { owned.value_type }
else { panic('expected container') }
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cur_param_types['value'] = owned
g.memo_usable_expr_types = true
g.begin_usable_expr_type_memo()
before := g.usable_expr_type(access)
assert voidptr(before) == voidptr(expected)
assert *before == types.Type(types.String{})
g.cur_param_types['value'] = tc.intern_type_reference(types.Type(types.Array{ elem_type: types.new_type_reference(types.Type(types.bool_)) }))
for i in 0 .. 1024 {
tc.intern_type_reference(types.Type(types.ArrayFixed{ elem_type: child, len: i + 1 }))
}
assert voidptr(g.usable_expr_type(access)) == voidptr(expected)
assert *before == types.Type(types.String{})
g.end_usable_expr_type_memo()
g.begin_usable_expr_type_memo()
if container_kind != 0 {
assert *g.usable_expr_type(access) == types.Type(types.bool_)
}
assert *before == types.Type(types.String{})
}
}
}
fn test_usable_expr_type_shared_parameter_keeps_owned_value_reference() {
mut a := flat.FlatAst.new()
id := a.add_val(.ident, 'value')
mut tc := types.TypeChecker.new(&a)
child := types.new_type_reference(types.Type(types.String{}))
parameter := types.new_type_reference(types.Type(types.Pointer{ base_type: child }))
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cur_param_types['value'] = parameter
g.push_scope()
owner := tc.cur_scope.insert_with_owner('value', *parameter)
g.declare_local_shared_storage(owner, true)
g.memo_usable_expr_types = true
g.begin_usable_expr_type_memo()
before := g.usable_expr_type(id)
assert voidptr(before) == voidptr(child)
g.end_usable_expr_type_memo()
g.declare_local_shared_storage(owner, false)
g.begin_usable_expr_type_memo()
assert voidptr(g.usable_expr_type(id)) == voidptr(parameter)
assert *before == types.Type(types.String{})
g.pop_scope()
}
+75
View File
@@ -0,0 +1,75 @@
module c
import strings
fn write_byte_reference(mut g FlatGen, s string) {
if g.line_start {
g.write_indent()
}
if s.len == 0 {
if g.indent > 0 {
g.line_start = false
}
return
}
unsafe { g.sb.write_ptr(s.str, s.len) }
g.line_start = s[s.len - 1] == `\n`
}
fn test_single_byte_emission_matches_reference_for_all_bytes_and_indentation() {
for capacity in [0, 1, 32] {
for indent in [0, 1, 3] {
for line_start in [false, true] {
for value in 0 .. 256 {
text := [u8(value)].bytestr()
mut actual := FlatGen{ sb: strings.new_builder(capacity), indent: indent, line_start: line_start }
mut expected := FlatGen{ sb: strings.new_builder(capacity), indent: indent, line_start: line_start }
actual.write(text)
write_byte_reference(mut expected, text)
assert actual.sb == expected.sb
assert actual.line_start == expected.line_start
}
}
}
}
}
fn test_single_byte_emission_preserves_mixed_writes_and_repeated_growth() {
mut actual := FlatGen{ sb: strings.new_builder(0), indent: 2, line_start: true }
mut expected := FlatGen{ sb: strings.new_builder(0), indent: 2, line_start: true }
for _ in 0 .. 128 {
for text in ['', '(', ')', 'longer generated identifier', '\x00', '\n', '字段', 'a\x00b'] {
actual.write(text)
write_byte_reference(mut expected, text)
assert actual.sb == expected.sb
assert actual.line_start == expected.line_start
}
}
}
fn test_single_byte_emission_keeps_borrowed_bytes_in_existing_capacity() {
mut g := FlatGen{ sb: strings.new_builder(32) }
g.sb.write_string('x\n')
first := unsafe { (&u8(g.sb.data)).vstring_with_len(1) }
g.write(first)
assert g.sb == 'x\nx'.bytes()
assert !g.line_start
last := unsafe { (&u8(g.sb.data) + 1).vstring_with_len(1) }
g.write(last)
assert g.sb == 'x\nx\n'.bytes()
assert g.line_start
g.line_start = false
unsafe { *(&u8(g.sb.data) + g.sb.len) = `z` }
unused := unsafe { (&u8(g.sb.data) + g.sb.len).vstring_with_len(1) }
g.write(unused)
assert g.sb == 'x\nx\nz'.bytes()
assert !g.line_start
}
fn test_single_byte_emission_unshares_slice_builders() {
mut backing := []u8{len: 32, init: `q`}
mut g := FlatGen{ sb: strings.Builder(backing[0..1]) }
g.write('x')
assert g.sb == 'qx'.bytes()
assert backing[1] == `q`
}
@@ -0,0 +1,37 @@
module markused
import v.flat
import v.types
fn test_contextual_alias_lookup_preserves_order_wrappers_and_current_alias_registry() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
collector := CallCollector{ a: &a, tc: &tc }
imports := {
'a': 'dep'
}
for name in ['dep.Value', 'a.Value', 'owner.dep.Value', 'owner.a.Value'] {
tc.type_aliases.set(name, 'int')
}
for expected in ['dep.Value', 'a.Value', 'owner.dep.Value', 'owner.a.Value'] {
actual := collector.contextual_alias_type_name(flat.source_type_id(' []&?a.Value '), 'owner', imports) or {
panic('an alias candidate must remain')
}
assert flat.source_type_name(actual) == '[]&?' + expected
tc.type_aliases.delete(expected)
}
assert collector.contextual_alias_type_name(flat.source_type_id('[]&?a.Value'), 'owner', imports) == none
}
fn test_contextual_alias_lookup_keeps_builtin_and_main_names_unqualified() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
collector := CallCollector{ a: &a, tc: &tc }
tc.type_aliases.set('owner.Value', 'int')
tc.type_aliases.set('builtin.Value', 'int')
tc.type_aliases.set('main.Value', 'int')
for cur_module in ['main', 'builtin', ''] {
assert collector.contextual_alias_type_name(flat.source_type_id('Value'), cur_module, map[string]string{}) == none
}
assert flat.source_type_name(collector.contextual_alias_type_name(flat.source_type_id('Value'), 'owner', map[string]string{})?) == 'owner.Value'
}
+22 -30
View File
@@ -855,6 +855,7 @@ fn mark_used_with_test_files(a &flat.FlatAst, tc &types.TypeChecker, test_files
mut body_index := map[int]int{}
mut body_results := []BodyCalls{}
if body_ids.len >= min_eager_markused_bodies
&& (!use_prepared || markused_can_parallel_precollect(&collector))
&& (!tc.skips_library_bodies || a.worker_count() > 0) {
mut checked_ids := []int{}
mut checked_modules := []string{}
@@ -1951,7 +1952,7 @@ fn markused_reachable_modules(a &flat.FlatAst, tc &types.TypeChecker) map[string
mut has_user_root := false
file_modules := markused_top_level_file_modules(a, tc)
for file_idx in tc.top_level_idx {
file_node := a.nodes[file_idx]
file_node := &a.nodes[file_idx]
if file_node.kind != .file {
continue
}
@@ -1969,7 +1970,7 @@ fn markused_reachable_modules(a &flat.FlatAst, tc &types.TypeChecker) map[string
mut imports := module_imports[module_name] or { []string{} }
if file_idx >= a.user_code_start || !markused_file_is_test(file_node.value) {
for i in 0 .. file_node.children_count {
child := a.child_node(&file_node, i)
child := a.child_node(file_node, i)
if child.kind == .import_decl && child.value.len > 0 {
imports << child.value
}
@@ -5633,7 +5634,7 @@ fn (c &CallCollector) collect_body_with_generic_detection(node &flat.Node, cur_m
inferred_visible_idents = markused_visible_local_idents(c.a, node, visibility_names)
}
} else {
_, local_types, local_ident_types, inferred_visible_idents = c.local_value_info_for_names(node, cur_module, imports, local_values, has_inference_binders)
_, local_types, local_ident_types, inferred_visible_idents = c.local_value_info_for_names(node, cur_module, imports, local_values, visibility_names, has_inference_binders)
}
if receiver_name.len > 0 && receiver_struct != 0 {
local_types.set_id(receiver_name, receiver_struct)
@@ -6583,16 +6584,16 @@ fn markused_direct_sum_type(typ types.Type) bool {
@[direct_array_access]
fn (c &CallCollector) local_value_info_with_visibility(node &flat.Node, cur_module string, imports map[string]string) (map[string]bool, flat.TypeTextBindings, flat.NodeTypeTextBindings, map[int]bool) {
names, has_inference_binders := c.local_value_names(node)
return c.local_value_info_for_names(node, cur_module, imports, names, has_inference_binders)
visibility_names := c.local_value_visibility_names(names, cur_module, imports)
return c.local_value_info_for_names(node, cur_module, imports, names, visibility_names, has_inference_binders)
}
@[direct_array_access]
fn (c &CallCollector) local_value_info_for_names(node &flat.Node, cur_module string, imports map[string]string, names map[string]bool, has_inference_binders bool) (map[string]bool, flat.TypeTextBindings, flat.NodeTypeTextBindings, map[int]bool) {
fn (c &CallCollector) local_value_info_for_names(node &flat.Node, cur_module string, imports map[string]string, names map[string]bool, visibility_names map[string]bool, has_inference_binders bool) (map[string]bool, flat.TypeTextBindings, flat.NodeTypeTextBindings, map[int]bool) {
mut type_names := c.new_local_types()
mut ident_types := c.new_ident_types()
// Scope visibility only distinguishes local names that can also name a
// function or constant. Other bodies do not need a second binding walk.
visibility_names := c.local_value_visibility_names(names, cur_module, imports)
// function or constant. The caller already identified those names.
visible_local_idents := if visibility_names.len > 0 {
markused_visible_local_idents(c.a, node, visibility_names)
} else {
@@ -7029,13 +7030,7 @@ fn (c &CallCollector) contextual_alias_type_name(type_text_id u32, cur_module st
return flat.source_type_id(prefix + inner)
}
}
resolved := markused_resolve_imported_source_type(clean_id, imports)
for candidate in markused_alias_type_candidates(resolved, clean_id, cur_module) {
if c.tc.type_aliases.contains(flat.source_type_name(candidate)) {
return candidate
}
}
return none
return c.alias_type_name_from_bytes(clean, clean_id, cur_module, imports)
}
fn markused_fn_signature_name_candidates(name string, cur_module string) []string {
@@ -7963,45 +7958,42 @@ fn (c &CallCollector) alias_type_from_callee(callee &flat.Node, cur_module strin
}
fn (c &CallCollector) alias_type_from_name(name_id u32, cur_module string, imports map[string]string) ?types.Type {
return c.alias_type_from_name_bytes(flat.source_type_name(name_id), name_id, cur_module, imports)
alias_id := c.alias_type_name_from_bytes(flat.source_type_name(name_id), name_id, cur_module, imports) or { return none }
return c.tc.parse_canonical_type(alias_id)
}
// alias_type_from_name_text checks the spellings of markused_alias_type_candidates,
// in the same order, for a type alias. Only a spelling that names one gets a
// source type ID: most names here are callees, and interning all of their
// candidate spellings filled the process-wide type text table.
// alias_type_from_name_text parses only an alias found by its source spelling.
fn (c &CallCollector) alias_type_from_name_text(name string, cur_module string, imports map[string]string) ?types.Type {
return c.alias_type_from_name_bytes(name, 0, cur_module, imports)
alias_id := c.alias_type_name_from_bytes(name, 0, cur_module, imports) or { return none }
return c.tc.parse_canonical_type(alias_id)
}
fn (c &CallCollector) alias_type_from_name_bytes(name string, name_id u32, cur_module string, imports map[string]string) ?types.Type {
// alias_type_name_from_bytes checks candidate spellings in their original order
// and interns only a spelling registered as a type alias.
fn (c &CallCollector) alias_type_name_from_bytes(name string, name_id u32, cur_module string, imports map[string]string) ?u32 {
if c.tc.type_aliases.ids.len == 0 {
return none
}
resolved := markused_resolve_imported_name(name, imports)
if resolved.len > 0 && c.tc.type_aliases.contains(resolved) {
return c.tc.parse_canonical_type(if name_id != 0 && resolved == name {
return if name_id != 0 && resolved == name {
name_id
} else {
flat.source_type_id(resolved)
})
}
}
if name != resolved && name.len > 0 && c.tc.type_aliases.contains(name) {
return c.tc.parse_canonical_type(if name_id != 0 {
name_id
} else {
flat.source_type_id(name)
})
return if name_id != 0 { name_id } else { flat.source_type_id(name) }
}
if cur_module.len > 0 && cur_module != 'main' && cur_module != 'builtin' {
qualified := '${cur_module}.${resolved}'
if c.tc.type_aliases.contains(qualified) {
return c.tc.parse_canonical_type(flat.source_type_id(qualified))
return flat.source_type_id(qualified)
}
if name != resolved {
qualified_name := '${cur_module}.${name}'
if c.tc.type_aliases.contains(qualified_name) {
return c.tc.parse_canonical_type(flat.source_type_id(qualified_name))
return flat.source_type_id(qualified_name)
}
}
}
@@ -1,5 +1,9 @@
module markused
fn markused_can_parallel_precollect(_ &CallCollector) bool {
return false
}
// precollect_body_calls falls back to the serial analysis when v3 is built
// with the internal `v3_no_parallel` define.
fn precollect_body_calls(collector CallCollector, body_ids []int, body_modules []string, body_import_contexts []int) []BodyCalls {
@@ -156,6 +156,12 @@ fn clone_body_calls(value BodyCalls) BodyCalls {
}
}
// Only eager-collect prepared bodies when helpers can offset scanning unreachable bodies.
fn markused_can_parallel_precollect(collector &CallCollector) bool {
return collector.a.worker_count() > 0
|| (collector.tc.scoped_parallel_workers_enabled() && runtime.nr_jobs() > 1)
}
// precollect_body_calls analyzes every fn_decl body, across threads when there
// is enough work.
fn precollect_body_calls(collector CallCollector, body_ids []int, body_modules []string, body_import_contexts []int) []BodyCalls {
+68
View File
@@ -0,0 +1,68 @@
module markused
import os
import v.parser
import v.pref
import v.types
fn test_serial_prepared_reachability_matches_eager_body_collection() {
root := os.join_path(os.vtmp_dir(), 'markused_serial_lazy_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
main_path := os.join_path(root, 'main.v')
dep_path := os.join_path(root, 'dependency.v')
os.write_file(main_path, 'module main
import dependency
fn main() { _ := dependency.entry() }
')!
mut source := 'module dependency
const answer = const_helper()
fn const_helper() int { return 7 }
fn default_helper() int { return answer }
struct Item { value int = default_helper() }
fn (item Item) read() int { return item.value }
fn middle() int {
item := Item{}
return item.read()
}
fn dispatch(callback fn () int) int { return callback() }
pub fn entry() int { return dispatch(middle) }
fn unreachable_helper() int { return 101 }
'
// Cross the eager threshold with bodies whose implicit calls and const refs
// must stay unreachable, despite having been scanned by the eager control.
for i in 0 .. min_eager_markused_bodies {
source += 'fn unused_${i}() int { return unreachable_helper() + answer }\n'
}
os.write_file(dep_path, source)!
mut p := parser.Parser.new(pref.new_preferences())
mut a := p.parse_files([main_path, dep_path])
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.check_semantics()
assert tc.errors.len == 0, tc.errors.str()
assert a.worker_count() == 0
collector := CallCollector{ a: a, tc: &tc }
assert !markused_can_parallel_precollect(&collector)
mut prepared := prepare_markused_declarations(a, &tc, true)
defer { prepared.release() }
assert prepared.ready && prepared.body_ids.len > min_eager_markused_bodies
lazy := mark_used_without_generic_detection_prepared(a, &tc, mut prepared)
if output := os.getenv_opt('MARKUSED_SERIAL_LAZY_RECEIPT') {
os.write_file(output, lazy.keys().sorted().join('\n'))!
}
for name in ['entry', 'dispatch', 'middle', 'Item.read', 'default_helper', 'const_helper'] {
assert lazy['dependency.${name}'], name
}
for name in ['unreachable_helper', 'unused_0', 'unused_${min_eager_markused_bodies - 1}'] {
assert !lazy['dependency.${name}'], name
}
$if !v3_no_parallel ? {
a.ensure_workers(2)
defer { a.close_workers() }
assert markused_can_parallel_precollect(&collector)
parallel := mark_used_without_generic_detection_prepared(a, &tc, mut prepared)
assert parallel == lazy
}
}
+14
View File
@@ -0,0 +1,14 @@
module scanner
// A byte lookup avoids repeating the ASCII identifier ranges for every tail byte.
const identifier_tail_bytes = new_identifier_tail_bytes()
fn new_identifier_tail_bytes() [256]bool {
mut bytes := [256]bool{}
for i in 0 .. 128 {
c := u8(i)
bytes[i] = (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`)
|| (c >= `0` && c <= `9`) || c == `_`
}
return bytes
}
+35
View File
@@ -0,0 +1,35 @@
module scanner
import v.pref
import v.token
fn test_identifier_tail_preserves_every_byte_boundary() {
for i in 0 .. 256 {
c := u8(i)
source := [u8(`a`), c, `Z`].bytestr()
mut files := token.FileSet.new()
file := files.add_file('identifier_tail.v', source.len)
mut s := new_scanner(&pref.Preferences{}, .normal)
s.init(file, source)
assert s.scan() == .name
tail := (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`)
|| (c >= `0` && c <= `9`) || c == `_`
assert s.lit == if tail { source } else { 'a' }, 'byte ${i}'
assert s.pos == 0
assert s.offset == if tail { 3 } else { 1 }, 'byte ${i}'
assert s.diagnostics.len == 0, 'byte ${i}'
}
}
fn test_identifier_tail_keeps_prefixes_keywords_and_semicolons() {
source := "_a0Z abc123 return_value @type r'raw' c'x' js'js'\nreturn"
mut files := token.FileSet.new()
file := files.add_file('identifier_tokens.v', source.len)
mut s := new_scanner(&pref.Preferences{ backend: 'fastc' }, .normal)
s.init(file, source)
for expected in [token.Token.name, .name, .name, .name, .string, .char, .string, .semicolon,
.key_return, .semicolon, .eof] {
assert s.scan() == expected
}
assert s.diagnostics.len == 0
}
+1 -3
View File
@@ -440,9 +440,7 @@ pub fn (mut s Scanner) scan() token.Token {
src := s.src
mut off := s.offset
for off < src.len {
c3 := src[off]
if (c3 >= `a` && c3 <= `z`) || (c3 >= `A` && c3 <= `Z`)
|| (c3 >= `0` && c3 <= `9`) || c3 == `_` {
if identifier_tail_bytes[src[off]] {
off++
continue
}
+173
View File
@@ -0,0 +1,173 @@
module main
struct Entry {
value int
}
struct Tree {
entries []Entry
}
struct Payload {
pointer &int
}
struct Holder {
inner Payload
}
fn extract(copy Payload) &int { return copy.pointer }
fn (holder &Holder) copy_payload() &int { return extract(holder.inner) }
fn payload_snapshot(value int) &int {
local := value
holder := Holder{ inner: Payload{ pointer: &local } }
return holder.copy_payload()
}
fn (tree &Tree) index(node &Entry) int { return node.value }
fn (tree &Tree) child_node(node &Entry) &Entry {
id := tree.index(node)
return unsafe { &tree.entries[id] }
}
fn (tree &Tree) type_reference(node &Entry) &Entry {
if node.value == 0 { return unsafe { &tree.entries[0] } }
return unsafe { &tree.entries[1] }
}
fn (tree &Tree) copied_payload(node &Entry) &Entry {
_ = node.value
return tree.child_node(node)
}
fn identity(node &Entry) &Entry { return node }
fn inline_address(node &Entry) &int { return &node.value }
fn snapshot_lookup(tree &Tree, node Entry) &Entry { return tree.child_node(&node) }
fn snapshot_type(tree &Tree, node Entry) &Entry { return tree.type_reference(&node) }
fn snapshot_identity(node Entry) &Entry { return identity(&node) }
struct AuditPayload {
mut:
pointer &int
}
struct AuditEnvelope {
mut:
inner AuditPayload
}
struct AuditSink {
mut:
pointer &int = unsafe { nil }
}
fn audit_extract(copy AuditPayload) &int { return copy.pointer }
fn audit_extract_optional(copy AuditPayload) ?&int { return copy.pointer }
fn audit_extract_result(copy AuditPayload) !&int { return copy.pointer }
fn audit_storage(input &AuditEnvelope) &int { return audit_extract(input.inner) }
fn audit_storage_optional(input &AuditEnvelope) ?&int { return audit_extract_optional(input.inner) }
fn audit_storage_result(input &AuditEnvelope) !&int { return audit_extract_result(input.inner) }
fn audit_storage_field(input &AuditEnvelope) &&int { return &input.inner.pointer }
fn audit_store_payload(input &AuditEnvelope, mut sink AuditSink) &int {
sink.pointer = input.inner.pointer
return sink.pointer
}
fn audit_direct(value int) &int {
local := value
payload := AuditEnvelope{ inner: AuditPayload{ pointer: &local } }
return audit_storage(&payload)
}
fn audit_payload_assignment(value int) &int {
local := value
mut payload := AuditEnvelope{ inner: AuditPayload{ pointer: unsafe { nil } } }
payload.inner.pointer = &local
return audit_storage(&payload)
}
fn audit_optional(value int) ?&int {
local := value
payload := AuditEnvelope{ inner: AuditPayload{ pointer: &local } }
return audit_storage_optional(&payload)
}
fn audit_result(value int) !&int {
local := value
payload := AuditEnvelope{ inner: AuditPayload{ pointer: &local } }
return audit_storage_result(&payload)
}
fn audit_field(value int) &&int {
local := value
payload := AuditEnvelope{ inner: AuditPayload{ pointer: &local } }
return audit_storage_field(&payload)
}
fn audit_mutable_store(value int, mut sink AuditSink) &int {
local := value
payload := AuditEnvelope{ inner: AuditPayload{ pointer: &local } }
return audit_store_payload(&payload, mut sink)
}
struct AuditCallback {
audit_storage fn (&AuditEnvelope) &AuditEnvelope
}
fn audit_callback_identity(input &AuditEnvelope) &AuditEnvelope { return input }
fn audit_callback_escape(value int) &AuditEnvelope {
local := value
payload := AuditEnvelope{ inner: AuditPayload{ pointer: &local } }
callback := AuditCallback{ audit_storage: audit_callback_identity }
return callback.audit_storage(&payload)
}
fn stack_churn(value int) int {
mut values := [256]int{}
for i in 0 .. values.len { values[i] = value + i }
return values[127]
}
fn test_storage_borrow_keeps_escaping_payloads_alive() {
tree := Tree{ entries: [Entry{ value: 0 }, Entry{ value: 1 }] }
for i in 0 .. 200 {
direct := audit_direct(i)
optional := audit_optional(i) or { panic('missing optional') }
result := audit_result(i) or { panic(err) }
field := audit_field(i)
mut sink := AuditSink{}
stored := audit_mutable_store(i, mut sink)
written := audit_payload_assignment(i)
callback := audit_callback_escape(i)
copied := payload_snapshot(i)
identity_value := snapshot_identity(Entry{ value: i })
assert stack_churn(i) == i + 127
assert *direct == i
assert *optional == i
assert *result == i
assert **field == i
assert *stored == i
assert *sink.pointer == i
assert *written == i
assert *callback.inner.pointer == i
assert *copied == i
assert identity_value.value == i
assert snapshot_lookup(&tree, Entry{ value: 1 }).value == 1
assert snapshot_type(&tree, Entry{ value: 0 }).value == 0
}
}
+147
View File
@@ -0,0 +1,147 @@
module transform
import v.flat
import v.types
fn builtin_shape_call_name(a &flat.FlatAst, id flat.NodeId) string {
node := a.nodes[int(id)]
assert node.kind == .call
return a.nodes[int(a.child(&node, 0))].value
}
fn test_builtin_call_shape_keeps_ordinary_bare_and_indexed_function_calls() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.validating_generic_spec = true
for name in ['ordinary', 'has', 'free', 'clone', 'type_name', 'from', 'from_string'] {
for arity in 0 .. 3 {
mut args := []flat.NodeId{}
for i in 0 .. arity {
args << t.make_int_literal(i)
}
call := t.make_call_expr_typed(t.make_ident(name), args, flat.source_type_int)
node := a.nodes[int(call)]
nodes_before := a.nodes.len
pending_before := t.pending_stmts.len
assert t.try_lower_builtin_call(call, node) == none
assert a.nodes.len == nodes_before
assert t.pending_stmts.len == pending_before
}
}
indexed := t.make_index(t.make_ident('ordinary'), t.make_ident('int'), u32(0))
call := t.make_call_expr_typed(indexed, [t.make_int_literal(1)], flat.source_type_int)
assert t.try_lower_builtin_call(call, a.nodes[int(call)]) == none
}
fn test_builtin_call_shape_preserves_primitive_named_and_generic_casts() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.begin_sparse_transform_node_caches(0)
tc.structs['Target'] = []types.StructField{}
tc.structs['Box'] = []types.StructField{}
tc.structs['Box[int]'] = []types.StructField{}
mut t := new_transformer(mut a, &tc, map[string]bool{})
for target in ['int', 'Target', 'Box[int]'] {
call := t.make_call_expr_typed(t.make_ident(target), [t.make_int_literal(1)],
flat.source_type_id(target))
if target == 'Target' {
tc.sparse_resolved_call_names[int(call)] = target
}
lowered := t.try_lower_builtin_call(call, a.nodes[int(call)]) or {
panic('cast ${target} was skipped')
}
assert a.nodes[int(lowered)].kind == .cast_expr
assert a.nodes[int(lowered)].source_value_id() == flat.source_type_id(target)
}
// A local with the type's spelling must keep the resolved-name cast guard.
t.set_var_type('Target', flat.source_type_id('fn (int) int'))
shadowed := t.make_call_expr_typed(t.make_ident('Target'), [t.make_int_literal(2)],
flat.source_type_int)
tc.sparse_resolved_call_names[int(shadowed)] = 'Target'
assert t.try_lower_builtin_call(shadowed, a.nodes[int(shadowed)]) == none
}
fn test_builtin_call_shape_preserves_bare_and_qualified_flag_default_value() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
for qualified in [false, true] {
callee := if qualified {
t.make_selector(t.make_ident('builtin'), 'flag_default_value', u32(0))
} else {
t.make_ident('flag_default_value')
}
arg := t.make_string_literal('value')
call := t.make_call_expr_typed(callee, [arg], flat.source_type_string)
lowered := t.try_lower_builtin_call(call, a.nodes[int(call)]) or {
panic('flag_default_value was skipped')
}
assert builtin_shape_call_name(&a, lowered) == 'string__plus'
outer := a.nodes[int(lowered)]
inner := a.child(&outer, 1)
assert builtin_shape_call_name(&a, inner) == 'string__plus'
inner_node := a.nodes[int(inner)]
escaped := a.child(&inner_node, 2)
assert builtin_shape_call_name(&a, escaped) == 'escape_default_string'
}
}
fn test_builtin_call_shape_preserves_static_assoc_with_generic_index_wrapper() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.structs['Box'] = StructInfo{ name: flat.source_type_id('Box'), module: 'main' }
static_name := flat.encode_static_type_method_name('Box', 'make')
t.fn_ret_types.set_id(static_name, flat.source_type_int)
for generic in [false, true] {
selector := t.make_selector(t.make_ident('Box'), 'make', u32(0))
callee := if generic {
t.make_index(selector, t.make_ident('int'), u32(0))
} else {
selector
}
call := t.make_call_expr_typed(callee, []flat.NodeId{}, flat.source_type_int)
lowered := t.try_lower_builtin_call(call, a.nodes[int(call)]) or {
panic('static associated call was skipped')
}
assert builtin_shape_call_name(&a, lowered) == static_name
assert a.nodes[int(lowered)].children_count == 1
assert a.nodes[int(lowered)].call_type_args() == if generic {
[flat.source_type_int]
} else {
[]u32{}
}
}
}
fn test_builtin_call_shape_preserves_selector_folding_and_c_interop() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
selector := t.make_selector(t.make_string_literal('aBc'), 'to_upper', flat.source_type_string)
call := t.make_call_expr_typed(selector, []flat.NodeId{}, flat.source_type_string)
lowered := t.try_lower_builtin_call(call, a.nodes[int(call)]) or {
panic('literal selector call was skipped')
}
assert a.nodes[int(lowered)].kind == .string_literal
assert a.nodes[int(lowered)].value == 'ABC'
c_callee := t.make_selector(t.make_ident('C'), 'printf', u32(0))
c_call := t.make_call_expr_typed(c_callee, []flat.NodeId{}, flat.source_type_int)
assert t.try_lower_builtin_call(c_call, a.nodes[int(c_call)]) == none
}
fn test_builtin_call_shape_preserves_flag_enum_zero() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.enum_names['Flags'] = true
tc.flag_enums['Flags'] = true
mut t := new_transformer(mut a, &tc, map[string]bool{})
selector := t.make_selector(t.make_ident('Flags'), 'zero', u32(0))
call := t.make_call_expr_typed(selector, []flat.NodeId{}, flat.source_type_id('Flags'))
lowered := t.try_lower_builtin_call(call, a.nodes[int(call)]) or {
panic('flag enum zero was skipped')
}
assert a.nodes[int(lowered)].kind == .cast_expr
assert a.nodes[int(lowered)].source_value_id() == flat.source_type_id('Flags')
}
+38
View File
@@ -0,0 +1,38 @@
module transform
// Frozen key sets permit negative constant lookups without constructing
// qualified names. Bits never retain source or disposable worker strings.
struct ConstSelectorNameFilter {
mut:
const_count int
suffix_count int
bits [64]u64
}
fn (mut t Transformer) prepare_const_selector_filter() {
if isnil(t.tc) { return }
mut filter := &ConstSelectorNameFilter{
const_count: t.tc.const_types.len
suffix_count: t.const_suffixes.len
}
for name, _ in t.tc.const_types { filter.add(name) }
for name, _ in t.const_suffixes { filter.add(name) }
t.const_selector_filter = filter
}
@[direct_array_access; inline]
fn (mut filter ConstSelectorNameFilter) add(name string) {
bit := file_import_alias_bit(short_name_view(name))
filter.bits[bit >> 6] |= u64(1) << (bit & 63)
}
@[direct_array_access; inline]
fn (t &Transformer) const_selector_may_match(name string) bool {
if isnil(t.tc) || isnil(t.const_selector_filter)
|| t.const_selector_filter.const_count != t.tc.const_types.len
|| t.const_selector_filter.suffix_count != t.const_suffixes.len {
return true
}
bit := file_import_alias_bit(short_name_view(name))
return t.const_selector_filter.bits[bit >> 6] & (u64(1) << (bit & 63)) != 0
}
@@ -0,0 +1,148 @@
module transform
import v.flat
import v.types
fn test_const_selector_filter_preserves_qualified_suffix_import_and_builtin_visibility() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
for name in ['main_only', 'shared', 'builtin.shared', 'alpha.value', 'nested.mod.limit',
'beta.value', 'module', 'éclair', '@escaped', '_private', 'odd..ending', 'trailing.'] {
tc.const_types[name] = types.Type(types.int_)
}
tc.const_modules['main_only'] = 'main'
tc.const_modules['shared'] = 'main'
tc.file_imports[file_import_key('one.v', 'dep')] = 'nested.mod'
tc.file_imports[file_import_key('two.v', 'dep')] = 'alpha'
mut slow := new_transformer(mut a, &tc, map[string]bool{})
mut fast := new_transformer(mut a, &tc, map[string]bool{})
slow.collect_const_suffixes()
fast.collect_const_suffixes()
// Suffix keys are a separate authority; custom spellings need not have the
// same final segment as the constant key they resolve to.
slow.const_suffixes['alternate'] = 'nested.mod.limit'
fast.const_suffixes['alternate'] = 'nested.mod.limit'
fast.prepare_const_selector_filter()
names := ['main_only', 'shared', 'builtin.shared', 'alpha.value', 'nested.mod.limit', 'beta.value',
'value', 'mod.limit', 'limit', 'dep.limit', 'dep.value', 'alternate', 'module', 'mXdule',
'éclair', '@escaped', '_private', 'odd..ending', 'trailing.', 'missing', 'missing.field',
'']
for name in names {
assert fast.const_type_key(name) == slow.const_type_key(name)
for module_name in ['', 'main', 'builtin', 'alpha', 'other'] {
for file in ['one.v', 'two.v', 'missing.v'] {
assert fast.const_type_key_in_context(name, module_name, file) == slow.const_type_key_in_context(name, module_name, file)
}
}
}
assert fast.const_type_key_in_context('dep.limit', 'main', 'one.v') or { '' } == 'nested.mod.limit'
assert fast.const_type_key_in_context('shared', 'alpha', 'one.v') or { '' } == 'builtin.shared'
assert fast.const_type_key_in_context('main_only', 'alpha', 'one.v') == none
assert fast.const_type_key('alternate') or { '' } == 'nested.mod.limit'
assert fast.const_selector_may_match('mXdule')
assert !fast.const_selector_may_match('absent_selector_field')
}
fn test_const_selector_filter_preserves_raw_expression_and_local_shadowing() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.const_types['answer'] = types.Type(types.int_)
tc.const_types['config.value'] = types.Type(types.string_)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.collect_const_suffixes()
answer := a.add_val(.ident, 'answer')
base := a.add_val(.ident, 'config')
start := a.children.len
a.children << base
selector := a.add_node(flat.Node{ kind: .selector, value: 'value', children_start: start, children_count: 1 })
assert t.raw_const_type_name_for_expr(answer) or { u32(0) } == flat.source_type_int
assert t.raw_const_type_name_for_expr(selector) or { u32(0) } == flat.source_type_string
t.prepare_const_selector_filter()
assert t.raw_const_type_name_for_expr(answer) or { u32(0) } == flat.source_type_int
assert t.raw_const_type_name_for_expr(selector) or { u32(0) } == flat.source_type_string
t.set_var_type('answer', flat.source_type_bool)
assert t.raw_const_type_name_for_expr(answer) == none
t.set_var_type('config', flat.source_type_id('Config'))
assert t.raw_const_type_name_for_expr(selector) == none
missing := a.add_val(.ident, 'absent_selector_field')
assert t.raw_const_type_name_for_expr(missing) == none
}
fn test_const_selector_filter_falls_back_when_either_authority_grows_and_rebuilds() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
assert t.const_selector_may_match('later')
t.prepare_const_selector_filter()
assert !t.const_selector_may_match('later')
tc.const_types['later'] = types.Type(types.int_)
assert t.const_type_key('later') or { '' } == 'later'
t.prepare_const_selector_filter()
assert !t.const_selector_may_match('alternate')
t.const_suffixes['alternate'] = 'later'
assert t.const_type_key('alternate') or { '' } == 'later'
t.prepare_const_selector_filter()
assert t.const_selector_may_match('alternate')
tc.const_types.delete('later')
t.const_suffixes = map[string]string{}
t.prepare_const_selector_filter()
assert !t.const_selector_may_match('later')
assert !t.const_selector_may_match('alternate')
}
fn test_const_selector_filter_forks_share_bits_and_mutable_query_buffers_are_read_again() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.const_types['open'] = types.Type(types.int_)
tc.const_types['shut'] = types.Type(types.int_)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.prepare_const_selector_filter()
worker := t.fork_program_view(&a, &tc, map[string]bool{}, false)
scan := t.fork_scan_worker(&tc)
assert voidptr(worker.const_selector_filter) == voidptr(t.const_selector_filter)
assert voidptr(scan.const_selector_filter) == voidptr(t.const_selector_filter)
mut bytes := 'open'.bytes()
name := unsafe { tos(bytes.data, bytes.len) }
for instance in [&t, &worker, scan] {
assert instance.const_type_key(name) or { '' } == 'open'
}
for i, ch in 'shut'.bytes() { bytes[i] = ch }
for instance in [&t, &worker, scan] {
assert instance.const_type_key(name) or { '' } == 'shut'
}
for i, ch in 'zzzz'.bytes() { bytes[i] = ch }
for instance in [&t, &worker, scan] {
assert instance.const_type_key(name) == none
}
}
fn test_const_selector_filter_reads_same_key_value_updates_without_rebuilding() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.const_types['answer'] = types.Type(types.int_)
tc.const_types['label'] = types.Type(types.string_)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.collect_const_suffixes()
t.const_suffixes['alternate'] = 'answer'
t.prepare_const_selector_filter()
worker := t.fork_program_view(&a, &tc, map[string]bool{}, false)
scan := t.fork_scan_worker(&tc)
const_count := tc.const_types.len
suffix_count := t.const_suffixes.len
for instance in [&t, &worker, scan] {
assert instance.const_type_name('answer') or { u32(0) } == flat.source_type_int
assert instance.const_type_name('alternate') or { u32(0) } == flat.source_type_int
}
// Membership is frozen; both authorities still read the current values.
tc.const_types['answer'] = types.Type(types.bool_)
t.const_suffixes['alternate'] = 'label'
t.const_suffixes['answer'] = 'label'
assert tc.const_types.len == const_count
assert t.const_suffixes.len == suffix_count
for instance in [&t, &worker, scan] {
assert instance.const_type_name('answer') or { u32(0) } == flat.source_type_bool
assert instance.const_type_name('alternate') or { u32(0) } == flat.source_type_string
assert instance.const_type_key('answer') or { '' } == 'answer'
assert instance.const_type_key_in_context('answer', 'main', '') or { '' } == 'answer'
}
}
+40
View File
@@ -0,0 +1,40 @@
module transform
// The enum table is frozen after preparation. Record only bits, so workers do
// not retain source strings or strings owned by a disposable worker arena.
struct EnumSelectorNameFilter {
mut:
count int
bits [64]u64
}
fn (mut t Transformer) prepare_enum_selector_filter() {
mut filter := &EnumSelectorNameFilter{ count: t.enum_types.len }
for name, fields in t.enum_types {
// Qualified type selectors may retain several dotted segments in value.
// Include every suffix accepted by base + '.' + field_name resolution.
filter.add(name)
for i, ch in name {
if ch == `.` {
filter.add(unsafe { name.substr_unsafe(i + 1, name.len) })
}
}
for field in fields { filter.add(field) }
}
t.enum_selector_filter = filter
}
@[direct_array_access; inline]
fn (mut filter EnumSelectorNameFilter) add(name string) {
bit := file_import_alias_bit(name)
filter.bits[bit >> 6] |= u64(1) << (bit & 63)
}
@[direct_array_access; inline]
fn (t &Transformer) enum_selector_may_match(name string) bool {
if isnil(t.enum_selector_filter) || t.enum_selector_filter.count != t.enum_types.len {
return true
}
bit := file_import_alias_bit(name)
return t.enum_selector_filter.bits[bit >> 6] & (u64(1) << (bit & 63)) != 0
}
@@ -0,0 +1,134 @@
module transform
import v.flat
import v.types
fn enum_filter_selector(mut a flat.FlatAst, base flat.NodeId, name string) flat.Node {
start := a.children.len
a.children << base
return flat.Node{ kind: .selector, value: name, children_start: start, children_count: 1 }
}
fn test_enum_selector_filter_keeps_alias_module_generated_and_regular_value_resolution() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.type_aliases.set_id('Alias', flat.source_type_id('model.Kind'))
tc.type_aliases.set_id('NextAlias', flat.source_type_id('Alias'))
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.cur_module = 'model'
t.enum_types['model.Kind'] = ['ready', 'waiting']
t.enum_types['Kind'] = ['ready', 'waiting']
t.enum_types['model.generated'] = ['module', '@escaped', 'éclair']
t.enum_types['generated'] = ['module', '@escaped', 'éclair']
mut nodes := []flat.Node{}
for base in ['Kind', 'Alias', 'NextAlias', 'model.Kind', 'generated'] {
base_id := a.add_val(.ident, base)
for field in ['ready', 'waiting', 'module', '@escaped', 'éclair', 'mXdule', 'missing'] {
nodes << enum_filter_selector(mut a, base_id, field)
}
}
module_id := a.add_val(.ident, 'model')
nodes << enum_filter_selector(mut a, module_id, 'Kind')
nodes << enum_filter_selector(mut a, module_id, 'generated')
mut before := []u32{}
for node in nodes { before << t.resolve_selector_type_uncached(node) }
assert before[0] == flat.source_type_id('Kind')
assert before[7] == flat.source_type_id('model.Kind')
assert before[before.len - 2] == flat.source_type_id('model.Kind')
assert before.last() == flat.source_type_id('model.generated')
t.prepare_enum_selector_filter()
for i, node in nodes {
assert t.resolve_selector_type_uncached(node) == before[i]
}
assert file_import_alias_bit('module') == file_import_alias_bit('mXdule')
assert t.enum_selector_may_match('mXdule')
assert !t.enum_selector_may_match('absent_selector_field')
}
fn test_enum_selector_filter_retains_non_enum_selector_results() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
tc.const_types['Config.limit'] = types.Type(types.int_)
t.structs['Record'] = StructInfo{ fields: [FieldInfo{ name: 'amount', typ: flat.source_type_string }] }
t.set_var_type('record', flat.source_type_id('Record'))
t.set_var_type('items', flat.source_type_id('[]int'))
base := a.add_val(.ident, 'items')
len_node := enum_filter_selector(mut a, base, 'len')
assert t.resolve_selector_type_uncached(len_node) == flat.source_type_int
record_id := a.add_val(.ident, 'record')
field_node := enum_filter_selector(mut a, record_id, 'amount')
config_id := a.add_val(.ident, 'Config')
const_node := enum_filter_selector(mut a, config_id, 'limit')
assert t.resolve_selector_type_uncached(field_node) == flat.source_type_string
assert t.resolve_selector_type_uncached(const_node) == flat.source_type_int
t.prepare_enum_selector_filter()
assert !t.enum_selector_may_match('len')
assert t.resolve_selector_type_uncached(len_node) == flat.source_type_int
assert t.resolve_selector_type_uncached(field_node) == flat.source_type_string
assert t.resolve_selector_type_uncached(const_node) == flat.source_type_int
}
fn test_enum_selector_filter_rechecks_mutable_query_bytes_and_shares_only_frozen_bits() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.enum_types['Kind'] = ['open', 'shut']
t.prepare_enum_selector_filter()
worker := t.fork_program_view(&a, &tc, map[string]bool{}, false)
scan := t.fork_scan_worker(&tc)
assert voidptr(worker.enum_selector_filter) == voidptr(t.enum_selector_filter)
assert voidptr(scan.enum_selector_filter) == voidptr(t.enum_selector_filter)
mut bytes := 'open'.bytes()
// Queries may refer to a reused scratch buffer. The filter records only bits.
name := unsafe { tos(bytes.data, bytes.len) }
for instance in [&t, &worker, scan] {
assert instance.enum_selector_may_match(name)
}
for i, ch in 'shut'.bytes() { bytes[i] = ch }
for instance in [&t, &worker, scan] {
assert instance.enum_selector_may_match(name)
}
for i, ch in 'zzzz'.bytes() { bytes[i] = ch }
for instance in [&t, &worker, scan] {
assert !instance.enum_selector_may_match(name)
}
}
fn test_enum_selector_filter_falls_back_for_new_enums_and_rebuilds_without_stale_bits() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
assert t.enum_selector_may_match('later')
t.prepare_enum_selector_filter()
assert !t.enum_selector_may_match('later')
t.enum_types['Added'] = ['later']
assert t.enum_selector_may_match('later')
base := a.add_val(.ident, 'Added')
node := enum_filter_selector(mut a, base, 'later')
assert t.resolve_selector_type_uncached(node) == flat.source_type_id('Added')
t.prepare_enum_selector_filter()
assert t.enum_selector_may_match('later')
t.enum_types = map[string][]string{}
t.prepare_enum_selector_filter()
assert !t.enum_selector_may_match('later')
assert !t.enum_selector_may_match('Added')
}
fn test_enum_selector_filter_preserves_dotted_selector_values() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.enum_types['model.nested.Kind'] = ['ready', 'state.value']
module_id := a.add_val(.ident, 'model')
enum_id := a.add_val(.ident, 'model.nested.Kind')
nodes := [enum_filter_selector(mut a, module_id, 'nested.Kind'),
enum_filter_selector(mut a, enum_id, 'state.value')]
for node in nodes {
assert t.resolve_selector_type_uncached(node) == flat.source_type_id('model.nested.Kind')
}
t.prepare_enum_selector_filter()
for node in nodes {
assert t.resolve_selector_type_uncached(node) == flat.source_type_id('model.nested.Kind')
}
}
@@ -0,0 +1,111 @@
module transform
import v.flat
import v.types
struct TransparentEscapeState {
mut:
amp_ptrs map[string]bool
amp_sources map[string][]string
ptr_aliases map[string]string
method_receivers map[string]string
capture_aliases map[string][]string
interface_boxes map[string]bool
returned map[string]bool
local_names map[string]bool
local_added []string
backing_sources map[string][]string
}
fn transparent_escape_node(mut a flat.FlatAst, node flat.Node, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
...node
children_start: start
children_count: children.len
})
}
fn (mut state TransparentEscapeState) scan(mut t Transformer, id flat.NodeId, clear bool) {
t.scan_escape_pass(id, mut state.amp_ptrs, mut state.amp_sources, mut state.ptr_aliases,
mut state.method_receivers, mut state.capture_aliases, mut state.interface_boxes,
mut state.returned, mut state.local_names, mut state.local_added,
mut state.backing_sources, clear)
}
fn test_transparent_escape_nodes_preserve_child_order_and_state() {
for clear in [false, true] {
for kind in [flat.NodeKind.expr_stmt, .paren, .cast_expr, .as_expr, .field_init, .struct_init,
.array_literal, .array_init, .map_init, .selector, .index, .prefix, .postfix, .range,
.assoc, .assert_stmt, .defer_stmt, .lock_expr, .string_interp, .infix, .or_expr] {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
decl := t.make_decl_assign('alias', t.make_ident('first'))
assign := t.make_assign(t.make_ident('alias'), t.make_ident('second'))
ret := transparent_escape_node(mut a, flat.Node{ kind: .return_stmt }, [t.make_ident('alias')])
nested_assign := t.make_assign(t.make_ident('alias'), t.make_ident('nested'))
nested_fn := transparent_escape_node(mut a, flat.Node{ kind: .fn_literal }, [nested_assign])
// Stateful descendants expose traversal order independently of wrapper syntax.
sequence := transparent_escape_node(mut a, flat.Node{ kind: .infix, op: .plus }, [
decl,
assign,
ret,
nested_fn,
])
wrapper := transparent_escape_node(mut a, flat.Node{ kind: kind, op: .plus }, [sequence])
mut state := TransparentEscapeState{}
state.method_receivers['alias'] = 'receiver'
state.capture_aliases['alias'] = ['captured']
state.interface_boxes['alias'] = true
state.scan(mut t, wrapper, clear)
assert state.ptr_aliases['alias'] == 'second', '${kind}'
assert state.local_names['alias'], '${kind}'
assert state.local_added == ['alias'], '${kind}'
assert state.returned['alias'], '${kind}'
assert ('alias' in state.method_receivers) == !clear, '${kind}'
assert ('alias' in state.capture_aliases) == !clear, '${kind}'
assert ('alias' in state.interface_boxes) == !clear, '${kind}'
}
}
}
fn test_transparent_escape_scan_reloads_children_after_reflected_ast_growth() {
mut a := flat.FlatAst.new()
a.add_val(.module_decl, 'main')
receiver := a.add_node(flat.Node{ kind: .param, op: .dot, value: 'self', typ: flat.source_type_id('App') })
transparent_escape_node(mut a, flat.Node{ kind: .fn_decl, value: 'App.first', typ: flat.source_type_int }, [receiver])
mut tc := types.TypeChecker.new(&a)
tc.structs['App'] = []types.StructField{}
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.cur_module = 'main'
body_assign := t.make_assign(t.make_ident('alias'), t.make_ident('reflected'))
body := t.make_block([body_assign])
loop := transparent_escape_node(mut a, flat.Node{
kind: .comptime_for
value: 'method|methods'
typ: flat.source_type_id('App')
}, [body])
later := t.make_decl_assign('after', t.make_ident('alias'))
wrapper := transparent_escape_node(mut a, flat.Node{ kind: .array_literal }, [
loop,
later,
])
// Tight buffers force the reflected analysis clones to grow both arrays.
a.nodes = []flat.Node{len: a.nodes.len, cap: a.nodes.len, init: a.nodes[index]}
a.children = []flat.NodeId{len: a.children.len, cap: a.children.len, init: a.children[index]}
nodes_before := a.nodes.clone()
children_before := a.children.clone()
nodes_cap := a.nodes.cap
children_cap := a.children.cap
mut state := TransparentEscapeState{}
state.scan(mut t, wrapper, true)
assert state.ptr_aliases['alias'] == 'reflected'
assert state.ptr_aliases['after'] == 'alias'
assert state.local_names['after']
assert a.nodes.cap > nodes_cap
assert a.children.cap > children_cap
assert a.nodes == nodes_before
assert a.children == children_before
}
+103 -85
View File
@@ -1081,6 +1081,9 @@ fn (t &Transformer) raw_const_type_name_for_expr(id flat.NodeId) ?u32 {
return none
}
node := t.a.nodes[int(id)]
if node.kind in [.ident, .selector] && !t.const_selector_may_match(node.value) {
return none
}
if t.selector_const_base_is_value(node) {
return none
}
@@ -1221,13 +1224,10 @@ fn (t &Transformer) generic_call_base_is_fn_value(base_id flat.NodeId, base flat
|| t.is_known_type_name(base.source_value_id()) {
return false
}
for candidate in [t.raw_var_type(base.value), t.var_type(base.value), base.typ,
t.node_type(base_id)] {
if t.is_fn_pointer_type_name(candidate) {
return true
}
}
return false
return t.is_fn_pointer_type_name(t.raw_var_type(base.value))
|| t.is_fn_pointer_type_name(t.var_type(base.value))
|| t.is_fn_pointer_type_name(base.typ)
|| t.is_fn_pointer_type_name(t.node_type(base_id))
}
// generic_call_type_arg_name supports generic call type arg name handling for Transformer.
@@ -3473,7 +3473,7 @@ fn (mut t Transformer) call_param_types_for_node(call_name string, node flat.Nod
}
if node.children_count > 0 {
fn_id := t.a.child(&node, 0)
fn_node := t.a.child_node(&node, 0)
fn_node := t.a.node(t.a.child(&node, 0))
if fn_node.kind == .ident && (t.var_type(fn_node.value)) != 0 {
if fn_type := t.call_callee_fn_type(fn_id) {
return fn_type.params.map(*it.typ)
@@ -3535,12 +3535,16 @@ fn (t &Transformer) call_callee_fn_type(fn_id flat.NodeId) ?types.FnType {
}
node := t.a.nodes[int(fn_id)]
if node.kind == .ident {
for local_type in [flat.source_type_name(t.raw_var_type(node.value)),
flat.source_type_name(t.var_type(node.value))] {
if local_type.len == 0 {
for candidate in 0 .. 2 {
local_type := if candidate == 0 {
t.raw_var_type(node.value)
} else {
t.var_type(node.value)
}
if local_type == 0 {
continue
}
if fn_type := transform_fn_type(t.tc.parse_type(flat.source_type_id(local_type))) {
if fn_type := transform_fn_type(t.tc.parse_type(local_type)) {
return fn_type
}
}
@@ -5544,10 +5548,10 @@ fn (t &Transformer) transform_param_type_is_void_pointer(type_name u32) bool {
}
fn (t &Transformer) call_arg_is_fn_pointer_value(arg_id flat.NodeId, arg_node flat.Node) bool {
for type_name in [arg_node.typ, t.node_type(arg_id), t.resolve_expr_type(arg_id)] {
if t.is_fn_pointer_type_name(type_name) {
return true
}
if t.is_fn_pointer_type_name(arg_node.typ)
|| t.is_fn_pointer_type_name(t.node_type(arg_id))
|| t.is_fn_pointer_type_name(t.resolve_expr_type(arg_id)) {
return true
}
if arg_node.kind == .ident {
type_name_source_id := t.var_type(arg_node.value)
@@ -14433,78 +14437,87 @@ fn (mut t Transformer) try_lower_builtin_call(_id flat.NodeId, node flat.Node) ?
if named_cast_call := t.try_lower_generic_named_type_cast_call(node) {
return named_cast_call
}
if flag_call := t.try_lower_flag_enum_call(_id, node) {
return flag_call
selector_callee := t.a.child_node(&node, 0)
has_selector_callee := selector_callee.kind == .selector && selector_callee.children_count > 0
if has_selector_callee {
if flag_call := t.try_lower_flag_enum_call(_id, node) {
return flag_call
}
}
if flag_default := t.try_lower_flag_default_value_call(node) {
return flag_default
}
if pool_call := t.try_lower_pool_generic_method_call(node) {
return pool_call
if has_selector_callee {
if pool_call := t.try_lower_pool_generic_method_call(node) {
return pool_call
}
}
if static_call := t.try_lower_static_assoc_call(_id, node) {
return static_call
}
if move_call := t.try_lower_move_method_call(_id, node) {
return move_call
}
if channel_call := t.try_lower_channel_method_call(_id, node) {
return channel_call
}
if map_call := t.try_lower_map_method_call(_id, node) {
return map_call
}
if pointer_str_call := t.try_lower_pointer_str_method_call(_id, node) {
return pointer_str_call
}
if array_call := t.try_lower_array_method_call(_id, node) {
return array_call
}
if smartcast_receiver_call := t.try_lower_smartcast_target_receiver_method_call(_id, node) {
return smartcast_receiver_call
}
if type_name_call := t.try_lower_sum_type_name_method_call(node) {
return type_name_call
}
if iface_runtime_call := t.try_lower_interface_runtime_method_call(node) {
return iface_runtime_call
}
if !t.validate_specialized_enum_from_call(_id, node) {
return t.make_empty()
}
specialized_enum_type := (if t.validating_generic_spec {
(t.builtin_enum_from_type(_id, node) or { u32(0) })
} else {
u32(0)
})
if enum_call := t.try_lower_enum_from_string_call(_id, node) {
return enum_call
}
if specialized_enum_type != 0 {
return t.lower_specialized_enum_from_call(node, specialized_enum_type)
}
if node.children_count > 0 {
string_callee := t.a.child_node(&node, 0)
if string_callee.kind == .selector && string_callee.children_count > 0 {
if folded := t.try_fold_comptime_string_method(node, string_callee) {
return folded
}
if folded := t.try_fold_literal_string_call(node, string_callee) {
return folded
method_callee := t.a.child_node(&node, 0)
if method_callee.kind == .selector && method_callee.children_count > 0 {
if move_call := t.try_lower_move_method_call(_id, node) {
return move_call
}
if channel_call := t.try_lower_channel_method_call(_id, node) {
return channel_call
}
if map_call := t.try_lower_map_method_call(_id, node) {
return map_call
}
if pointer_str_call := t.try_lower_pointer_str_method_call(_id, node) {
return pointer_str_call
}
if array_call := t.try_lower_array_method_call(_id, node) {
return array_call
}
if smartcast_receiver_call := t.try_lower_smartcast_target_receiver_method_call(_id, node) {
return smartcast_receiver_call
}
if type_name_call := t.try_lower_sum_type_name_method_call(node) {
return type_name_call
}
if iface_runtime_call := t.try_lower_interface_runtime_method_call(node) {
return iface_runtime_call
}
if !t.validate_specialized_enum_from_call(_id, node) {
return t.make_empty()
}
specialized_enum_type := (if t.validating_generic_spec {
(t.builtin_enum_from_type(_id, node) or { u32(0) })
} else {
u32(0)
})
if enum_call := t.try_lower_enum_from_string_call(_id, node) {
return enum_call
}
if specialized_enum_type != 0 {
return t.lower_specialized_enum_from_call(node, specialized_enum_type)
}
if node.children_count > 0 {
string_callee := t.a.child_node(&node, 0)
if string_callee.kind == .selector && string_callee.children_count > 0 {
if folded := t.try_fold_comptime_string_method(node, string_callee) {
return folded
}
if folded := t.try_fold_literal_string_call(node, string_callee) {
return folded
}
}
}
}
if free_call := t.try_lower_owned_string_free_call(_id, node) {
return free_call
}
if receiver_call := t.try_lower_receiver_method_call(_id, node) {
return receiver_call
}
if clone_call := t.try_lower_struct_clone_method_call(_id, node) {
return clone_call
}
if string_call := t.try_lower_string_method_call(node) {
return string_call
if free_call := t.try_lower_owned_string_free_call(_id, node) {
return free_call
}
if receiver_call := t.try_lower_receiver_method_call(_id, node) {
return receiver_call
}
if clone_call := t.try_lower_struct_clone_method_call(_id, node) {
return clone_call
}
if string_call := t.try_lower_string_method_call(node) {
return string_call
}
}
fn_id := t.a.children[node.children_start]
if int(fn_id) < 0 {
@@ -17924,8 +17937,13 @@ fn (mut t Transformer) receiver_base_for_resolved_method(base_id flat.NodeId, me
method_iface_comparison_type_id := method_iface_source_id
key := t.expr_key(base_id)
for source_type in [t.raw_var_type_for_expr(base_id) or { u32(0) }, t.original_expr_type(base_id),
t.node_type(base_id)] {
// Resolve later views only when the preceding receiver type did not match.
for candidate in 0 .. 3 {
source_type := match candidate {
0 { t.raw_var_type_for_expr(base_id) or { u32(0) } }
1 { t.original_expr_type(base_id) }
else { t.node_type(base_id) }
}
clean_source_source_id := t.trim_pointer_type(source_type)
if clean_source_source_id != 0 && method_receiver_source_id != 0
@@ -18335,7 +18353,7 @@ fn (t &Transformer) explicit_generic_call_arg_ids(node flat.Node) []u32 {
if node.children_count == 0 {
return []u32{}
}
fn_node := t.a.child_node(&node, 0)
fn_node := t.a.node(t.a.child(&node, 0))
if fn_node.kind != .index || fn_node.children_count < 2 || fn_node.value == 'range' {
return []u32{}
}
@@ -19675,7 +19693,7 @@ fn (t &Transformer) get_call_return_type(id flat.NodeId, node flat.Node) u32 {
if !isnil(t.tc) && t.a.has_vsh_source && t.cur_module == 'main'
&& t.cur_file.ends_with('.vsh')
&& node.children_count > 0 {
callee := t.a.child_node(&node, 0)
callee := t.a.node(t.a.child(&node, 0))
if callee.kind == .ident {
selected := t.resolve_call_name(node)
if selected != callee.value {
@@ -19689,7 +19707,7 @@ fn (t &Transformer) get_call_return_type(id flat.NodeId, node flat.Node) u32 {
return ret_source_id
}
if node.children_count > 0 {
fn_node := t.a.child_node(&node, 0)
fn_node := t.a.node(t.a.child(&node, 0))
if fn_node.kind == .selector && fn_node.children_count > 0 {
receiver_type_id := t.trim_pointer_type(t.node_type(t.a.child(fn_node, 0)))
if args_source_ids := t.current_specialized_receiver_args(receiver_type_id) {
@@ -19731,7 +19749,7 @@ fn (t &Transformer) get_call_return_type(id flat.NodeId, node flat.Node) u32 {
}
}
if node.children_count > 0 {
fn_node := t.a.child_node(&node, 0)
fn_node := t.a.node(t.a.child(&node, 0))
if fn_node.kind == .ident && (t.var_type(fn_node.value)) == 0 {
if ret_source_id := t.local_fn_decl_return_type(fn_node.value) {
return t.call_return_type_name(ret_source_id, node)
@@ -19742,7 +19760,7 @@ fn (t &Transformer) get_call_return_type(id flat.NodeId, node flat.Node) u32 {
return ret_source_id
}
if node.children_count > 0 {
fn_node := t.a.child_node(&node, 0)
fn_node := t.a.node(t.a.child(&node, 0))
if fn_node.kind == .ident {
local_type_source_id := t.var_type(fn_node.value)
+48
View File
@@ -0,0 +1,48 @@
module transform
import v.flat
import v.types
fn test_fresh_ident_keeps_existing_body_memo_and_binding_type() {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'items')
index := a.add_val(.int_literal, '0')
start := a.begin_children()
a.add_child(base)
a.add_child(index)
lookup := a.add_node(flat.Node{
kind: .index
children_start: i32(start)
children_count: 2
})
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.set_var_type('items', flat.source_type_id('[]string'))
t.begin_node_type_memo(0, a.nodes.len - 1)
assert t.node_type(lookup) == flat.source_type_string
generation := t.node_type_memo.generation
filled := t.node_type_memo.filled[int(lookup)]
for name in ['items', 'unbound'] {
prior_len := a.nodes.len
if name == 'items' {
tc.expr_type_set = []bool{len: prior_len + 1}
tc.expr_type_set[prior_len] = true
}
fresh := t.make_ident(name)
assert int(fresh) == prior_len
node := a.node(fresh)
if name == 'items' {
assert !tc.expr_type_set[int(fresh)]
}
assert node.kind == .ident
assert node.value == name
assert node.typ == if name == 'items' { flat.source_type_id('[]string') } else { u32(0) }
assert t.node_type_memo.generation == generation
assert t.node_type_memo.filled[int(lookup)] == filled
assert t.node_type(lookup) == flat.source_type_string
}
// Real writes to an attached descendant still invalidate its cached ancestor.
t.set_var_type('other', flat.source_type_id('[]bool'))
t.set_node_value(int(base), 'other')
assert t.node_type(lookup) == flat.source_type_bool
}
@@ -0,0 +1,115 @@
module transform
import v.flat
import v.types
fn generic_snapshot_node(mut a flat.FlatAst, template flat.Node, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
...template
children_start: start
children_count: flat.child_count(children.len)
})
}
fn test_generic_call_arguments_read_snapshot_after_ast_growth() {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'convert')
int_arg := a.add_val(.ident, 'int')
string_arg := a.add_val(.ident, 'string')
index := generic_snapshot_node(mut a, flat.Node{ kind: .index }, [callee, int_arg, string_arg])
call := generic_snapshot_node(mut a, flat.Node{ kind: .call }, [index])
snapshot := a.nodes[int(call)]
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
for _ in 0 .. a.nodes.cap + 100 {
a.add_val(.ident, 'growth')
a.children << flat.empty_node
}
assert t.generic_call_type_args(a.nodes[int(a.child(&snapshot, 0))]) == [
flat.source_type_int,
flat.source_type_string,
]
assert t.explicit_generic_call_args(snapshot, 'main') or { panic('missing type arguments') } == [
flat.source_type_int,
flat.source_type_string,
]
mut stored := flat.Node{ kind: .call }
stored.set_call_type_args([flat.source_type_u64])
assert stored.value_text() == 'u64'
assert t.explicit_generic_call_args(stored, 'main') or { panic('missing stored type argument') } == [flat.source_type_u64]
assert (flat.Node{ kind: .call }).value_text() == ''
assert t.explicit_generic_call_args(flat.Node{ kind: .call }, 'main') == none
a.nodes[int(index)].value = 'range'
assert t.generic_call_type_args(a.nodes[int(a.child(&snapshot, 0))]).len == 0
assert t.explicit_generic_call_args(snapshot, 'main') == none
}
fn test_orm_fields_keep_order_duplicates_and_reassignment_behavior() {
mut a := flat.FlatAst.new()
first := a.add_val(.field_init, 'first')
second := a.add_val(.field_init, 'second')
ignored := a.add_val(.ident, 'ignored')
init := generic_snapshot_node(mut a, flat.Node{ kind: .struct_init, value: 'Record' }, [
first,
ignored,
second,
first,
])
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.record_orm_initialized_fields('record', init)
assert t.orm_initialized_fields['record'] == ['first', 'second']
t.record_orm_initialized_fields('', init)
assert t.orm_initialized_fields.len == 1
t.record_orm_initialized_fields('record', flat.empty_node)
assert t.orm_initialized_fields['record'] == ['first', 'second']
t.record_orm_initialized_fields('record', ignored)
assert 'record' !in t.orm_initialized_fields
}
fn test_collected_declarations_preserve_field_children_and_enum_values() {
mut a := flat.FlatAst.new()
a.add_val(.module_decl, 'model')
default_value := a.add_val(.int_literal, '42')
field := generic_snapshot_node(mut a, flat.Node{ kind: .field_decl, value: 'value', typ: flat.source_type_int }, [default_value])
generic_snapshot_node(mut a, flat.Node{ kind: .struct_decl, value: 'Record' }, [field])
variant := a.add_val(.ident, 'int')
generic_snapshot_node(mut a, flat.Node{ kind: .type_decl, value: 'Choice' }, [variant])
first := generic_snapshot_node(mut a, flat.Node{ kind: .enum_field, value: 'first' }, [default_value])
second := a.add_val(.enum_field, 'second')
generic_snapshot_node(mut a, flat.Node{ kind: .enum_decl, value: 'Kind' }, [first, second])
global := a.add_node(flat.Node{ kind: .ident, value: 'counter', typ: flat.source_type_i64 })
generic_snapshot_node(mut a, flat.Node{ kind: .global_decl }, [global])
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.collect_types()
info := t.structs['model.Record'] or { panic('missing record') }
assert info.fields.len == 1
assert info.fields[0].name == 'value'
assert info.fields[0].typ == flat.source_type_int
assert info.fields[0].default_expr == default_value
assert t.sum_types.type_ids('model.Choice') or { panic('missing choice') } == [flat.source_type_int]
assert t.enum_types['model.Kind'] == ['first', 'second']
assert t.enum_explicit_first_values['model.Kind']
assert t.globals.type_id('model.counter') or { panic('missing global') } == flat.source_type_i64
}
fn test_collected_generic_sum_preserves_payload_parameter_spelling() {
mut a := flat.FlatAst.new()
a.add_val(.module_decl, 'model')
generic := a.add_val(.ident, 'T')
concrete := a.add_val(.ident, 'Item')
pointer := a.add_val(.ident, '&T')
generic_snapshot_node(mut a, flat.Node{
kind: .type_decl
value: 'GenericChoice'
payload: flat.node_payload(['T'])
}, [generic, concrete, pointer])
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.collect_types()
variants := t.sum_types.type_ids('model.GenericChoice') or { panic('missing generic choice') }
assert variants.map(flat.source_type_name(it)) == ['T', 'model.Item', '&T']
}
+79
View File
@@ -0,0 +1,79 @@
module transform
import v.flat
import v.types
fn test_callee_signature_keeps_raw_binding_precedence_and_checked_fallback() {
mut a := flat.FlatAst.new()
callback := a.add_val(.ident, 'callback')
mut tc := types.TypeChecker.new(&a)
tc.type_aliases.set_id('RawCallback', flat.source_type_id('fn () string'))
tc.register_synth_type(callback, tc.parse_type(flat.source_type_id('fn () bool')))
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.set_var_type_with_raw('callback', flat.source_type_id('fn () int'), flat.source_type_id('RawCallback'))
raw_signature := t.call_callee_fn_type(callback) or { panic('missing raw callback') }
assert raw_signature.return_type is types.String
// An unusable raw spelling falls through to the live binding, then the checker.
t.set_var_type_with_raw('callback', flat.source_type_id('fn () int'), flat.source_type_int)
live_signature := t.call_callee_fn_type(callback) or { panic('missing live callback') }
assert types.semantic_types_equal(*live_signature.return_type, types.Type(types.int_))
t.unset_var_type('callback')
checked_signature := t.call_callee_fn_type(callback) or { panic('missing checked callback') }
assert types.semantic_types_equal(*checked_signature.return_type, types.Type(types.bool_))
}
fn test_generic_callback_detection_keeps_each_type_fallback() {
mut a := flat.FlatAst.new()
callback := a.add_val(.ident, 'callback')
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
callback_type := flat.source_type_id('fn (int) int')
t.set_var_type_with_raw('callback', flat.source_type_int, callback_type)
assert t.generic_call_base_is_fn_value(callback, a.node(callback))
t.set_var_type_with_raw('callback', callback_type, flat.source_type_int)
assert t.generic_call_base_is_fn_value(callback, a.node(callback))
t.unset_var_type('callback')
t.set_node_typ(int(callback), callback_type)
assert t.generic_call_base_is_fn_value(callback, a.node(callback))
t.set_node_typ(int(callback), 0)
tc.register_synth_type(callback, tc.parse_type(callback_type))
assert t.generic_call_base_is_fn_value(callback, a.node(callback))
other := a.add_val(.ident, 'ordinary')
t.set_var_type('ordinary', flat.source_type_int)
assert !t.generic_call_base_is_fn_value(other, a.node(other))
}
fn test_known_callback_views_do_not_resolve_later_type_candidates() {
mut a := flat.FlatAst.new()
callback := a.add_node(flat.Node{
kind: .ident
value: 'callback'
typ: flat.source_type_id('fn () int')
})
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.begin_node_type_memo(0, int(callback))
assert t.call_arg_is_fn_pointer_value(callback, a.node(callback))
assert t.node_type_memo.filled[int(callback)] == 0
t.set_var_type('callback', flat.source_type_id('fn () string'))
assert t.generic_call_base_is_fn_value(callback, a.node(callback))
assert t.node_type_memo.filled[int(callback)] == 0
}
fn test_resolved_receiver_keeps_raw_alias_and_pointer_views() {
for spelling in ['Thing', '&Thing', 'AliasThing'] {
mut a := flat.FlatAst.new()
base := a.add_node(flat.Node{ kind: .ident, value: 'object', typ: flat.source_type_id(spelling) })
mut tc := types.TypeChecker.new(&a)
tc.type_aliases.set_id('AliasThing', flat.source_type_id('Thing'))
tc.structs['Thing'] = []types.StructField{}
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.structs['Thing'] = StructInfo{ name: flat.source_type_id('Thing') }
t.set_var_type_with_raw('object', flat.source_type_id(spelling), flat.source_type_id(spelling))
result := t.receiver_base_for_resolved_method(base, 'Thing.run', [], flat.source_type_id(spelling))
assert a.node(result).kind == .ident
assert a.node(result).value == 'object'
assert t.expected_expr_type == 0
assert t.pending_stmts.len == 0
}
}
+3 -3
View File
@@ -6540,7 +6540,7 @@ fn (mut t Transformer) concrete_generic_call_param_types(id flat.NodeId, node fl
}
decl_key := t.generic_call_decl_key(id, node, t.cur_module, decls) or { return none }
decl := decls[decl_key] or { return none }
callee := t.a.child_node(&node, 0)
callee := t.a.node(t.a.child(&node, 0))
mut args := []u32{}
if callee.kind == .ident {
if exact_source_ids := t.recorded_generic_specialization_args(callee.value_text()) {
@@ -6571,7 +6571,7 @@ fn (mut t Transformer) concrete_generic_call_param_types(id flat.NodeId, node fl
params := t.generic_fn_param_names(decl.node, decl.module)
mut result := []types.Type{}
for i in 0 .. decl.node.children_count {
child := t.a.child_node(&decl.node, i)
child := t.a.node(t.a.child(&decl.node, i))
if child.kind != .param {
if t.prefix_param_scan {
break
@@ -9357,7 +9357,7 @@ fn (t &Transformer) explicit_generic_call_args(node flat.Node, module_name strin
if node.children_count == 0 {
return none
}
fn_node := t.a.child_node(&node, 0)
fn_node := t.a.node(t.a.child(&node, 0))
if fn_node.kind != .index || fn_node.children_count < 2 || fn_node.value == 'range' {
return none
}
+103
View File
@@ -0,0 +1,103 @@
module transform
import v.flat
import v.types
fn test_terminal_node_types_ignore_annotations_and_checker_guesses() {
mut a := flat.FlatAst.new()
kinds := [flat.NodeKind.bool_literal, .float_literal, .int_literal, .string_literal,
.string_interp, .nil_literal, .char_literal, .sizeof_expr, .offsetof_expr]
expected := [flat.source_type_bool, flat.source_type_f64, flat.source_type_int,
flat.source_type_string, flat.source_type_string, flat.source_type_voidptr,
flat.source_type_rune, flat.source_type_u32, flat.source_type_u32]
for kind in kinds {
a.add_node(flat.Node{ kind: kind, value: 'value', typ: flat.source_type_id('Guess') })
}
mut tc := types.TypeChecker.new(&a)
tc.structs['Guess'] = []types.StructField{}
for i in 0 .. kinds.len {
tc.register_synth_type(flat.NodeId(i), types.Type(types.Struct{ name_id: flat.source_type_id('Guess') }))
}
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.begin_node_type_memo(0, a.nodes.len - 1)
for building_v in [false, true] {
t.building_v = building_v
for skip_generics in [false, true] {
t.skip_generics = skip_generics
for i, result in expected {
id := flat.NodeId(i)
assert t.node_type_uncached(id) == result, '${kinds[i]}'
assert t.node_type(id) == result, '${kinds[i]}'
}
}
}
assert t.node_type(flat.NodeId(-1)) == 0
}
fn test_terminal_queries_preserve_contextual_resolution_and_rewrites() {
mut a := flat.FlatAst.new()
value := a.add_val(.ident, 'value')
character := a.add_val(.char_literal, 'c:hello')
optional := a.add_node(flat.Node{ kind: .none_expr, typ: flat.source_type_id('?string') })
mut tc := types.TypeChecker.new(&a)
tc.type_aliases.set_id('one.Item', flat.source_type_int)
tc.type_aliases.set_id('two.Item', flat.source_type_bool)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.cur_module = 'one'
t.set_var_type('value', flat.source_type_id('Item'))
t.begin_node_type_memo(0, a.nodes.len - 1)
assert t.node_type(value) == flat.source_type_int
assert t.node_type(character) == flat.source_type_id('&u8')
t.cur_module = 'two'
t.expected_expr_type = flat.source_type_string
t.in_const_init = true
assert t.node_type(character) == flat.source_type_id('&u8')
// A terminal query can defer the memo context check; the next contextual
// query must still observe the new module and resolve its alias again.
assert t.node_type(value) == flat.source_type_bool
t.set_node_value(int(character), 'hello')
assert t.node_type(character) == flat.source_type_rune
// `none` retains the annotation fallback for its unresolved `?void` type.
assert t.node_type(optional) == flat.source_type_id('?string')
a.nodes[int(character)].kind = .ident
t.set_node_value(int(character), 'value')
assert t.node_type(character) == flat.source_type_bool
}
fn test_boolean_operator_node_types_preserve_operand_count_guards() {
mut a := flat.FlatAst.new()
left := a.add_val(.ident, 'left')
right := a.add_val(.ident, 'right')
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
for op in [flat.Op.eq, .ne, .lt, .gt, .le, .ge, .logical_and, .logical_or] {
start := a.begin_children()
a.add_child(left)
a.add_child(right)
id := a.add_node(flat.Node{
kind: .infix
op: op
typ: flat.source_type_f64
children_start: i32(start)
children_count: 2
})
assert t.node_type_uncached(id) == flat.source_type_bool
assert t.node_type(id) == flat.source_type_bool
// Incomplete expressions retain the annotation fallback.
a.nodes[int(id)].children_count = 1
assert t.node_type(id) == flat.source_type_f64
}
start := a.begin_children()
a.add_child(left)
negated := a.add_node(flat.Node{
kind: .prefix
op: .not
typ: flat.source_type_f64
children_start: i32(start)
children_count: 1
})
assert t.node_type_uncached(negated) == flat.source_type_bool
assert t.node_type(negated) == flat.source_type_bool
a.nodes[int(negated)].children_count = 0
assert t.node_type(negated) == flat.source_type_f64
}
+71
View File
@@ -0,0 +1,71 @@
module transform
import v.flat
import v.types
// EscapeCallStorageBorrow holds a call's already-proven storage-only summary.
// The borrowed slice belongs to the transformer's immutable summary table.
struct EscapeCallStorageBorrow {
summary []bool
offset int
}
fn (b EscapeCallStorageBorrow) borrows(child_index int) bool {
if child_index == 0 && b.offset != 1 {
return false
}
index := if child_index == 0 { 0 } else { child_index - 1 + b.offset }
return index >= 0 && index < b.summary.len && b.summary[index]
}
// escape_call_storage_borrow uses the existing storage-only proof for an
// exactly aligned direct call. It does not prove that the value's payload is
// unrelated to the result: aggregate copies can carry pointers elsewhere.
fn (t &Transformer) escape_call_storage_borrow(id flat.NodeId, call flat.Node) EscapeCallStorageBorrow {
if isnil(t.tc) || call.children_count == 0 {
return EscapeCallStorageBorrow{}
}
callee := t.a.child_node(&call, 0)
// Wrapped generic/function-value callees need an independent identity proof.
if callee.kind !in [.ident, .selector] {
return EscapeCallStorageBorrow{}
}
name := t.call_name_for_node(id, call)
decl := t.fixed_array_call_param_ref(name, call, 0) or { return EscapeCallStorageBorrow{} }
summary := t.fixed_array_borrow_params[decl.decl] or { return EscapeCallStorageBorrow{} }
params := t.call_param_types_decl_cache[decl.decl] or { return EscapeCallStorageBorrow{} }
offset := t.call_param_offset(name, call, params)
// Optional/context arguments and named or spread forms need their own ABI
// alignment proof. Leave those calls conservative.
if call.children_count - 1 != params.len - offset {
return EscapeCallStorageBorrow{}
}
for i in 1 .. call.children_count {
arg := t.a.node(t.a.child(&call, i))
if arg.kind == .field_init || t.call_arg_is_spread(t.a.child(&call, i)) {
return EscapeCallStorageBorrow{}
}
}
if callee.kind == .selector && callee.children_count > 0 {
base_id := t.a.child(callee, 0)
base := types.unalias_type(types.unwrap_all_pointers(t.tc.resolve_type(base_id)))
if base is types.Interface {
return EscapeCallStorageBorrow{}
}
base_type := t.normalize_type_alias(t.trim_pointer_type(t.lvalue_type(base_id)))
if t.receiver_selector_is_fn_field(base_type, callee.value) {
return EscapeCallStorageBorrow{}
}
}
return EscapeCallStorageBorrow{
summary: summary
offset: offset
}
}
// escape_storage_borrow_ident restricts the new storage projection to locals
// whose complete recorded payload provenance is available by name. Dereferences
// and scalar fields/indexes need their original conservative address provenance.
fn (t &Transformer) escape_storage_borrow_ident(id flat.NodeId) bool {
return t.a.node(t.fixed_array_unwrap_arg(id)).kind == .ident
}
@@ -0,0 +1,283 @@
module transform
import os
import v.flat
import v.gen.c as cgen
import v.parser
import v.pref
import v.types
fn storage_borrow_fixture(mut t Transformer, name string, params []types.Type, summary []bool) {
decl := t.a.add_node(flat.Node{ kind: .fn_decl, value: name })
t.call_param_types_index_ready = true
t.call_param_types_decl_index[name] = FnParamDeclRef{ idx: int(decl) }
t.call_param_types_decl_cache[int(decl)] = params
t.fixed_array_borrow_params[int(decl)] = summary
t.tc.fn_param_types[name] = params
t.tc.fn_ret_types[name] = types.Type(types.Pointer{ base_type: types.Type(types.int_) })
}
fn test_borrowed_storage_address_keeps_payload_aliases_and_self_references() {
for proven in [false, true] {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
ptr := types.Type(types.Pointer{ base_type: types.Type(types.int_) })
storage_borrow_fixture(mut t, 'borrow', [ptr], [proven])
local := t.make_ident('local')
tc.register_synth_type(local, types.Type(types.int_))
address := t.make_prefix(.amp, local)
tc.register_synth_type(address, ptr)
call := t.make_call_typed('borrow', [address], flat.source_type_id('&int'))
tc.register_synth_type(call, ptr)
assert t.escape_aggregate_address_sources(call, map[string][]string{}, map[string]string{}) == if proven {
[]string{}
} else {
['local']
}
// A copied aggregate can forward an embedded pointer despite borrowing its header.
assert t.escape_aggregate_address_sources(call, {
'local': ['pointee']
}, map[string]string{}) == if proven {
['pointee']
} else {
['pointee', 'local']
}
assert t.escape_aggregate_address_sources(call, {
'local': ['local']
}, map[string]string{}) == ['local']
}
}
fn test_borrowed_storage_only_strips_the_explicit_address_layer() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
ptr := types.Type(types.Pointer{ base_type: types.Type(types.int_) })
storage_borrow_fixture(mut t, 'borrow', [ptr], [true])
owner := t.make_ident('owner')
tc.register_synth_type(owner, types.Type(types.Struct{ name_id: flat.source_type_id('Owner') }))
field := t.make_selector(owner, 'pointer', flat.source_type_id('&int'))
tc.register_synth_type(field, ptr)
address := t.make_prefix(.amp, field)
tc.register_synth_type(address, types.Type(types.Pointer{ base_type: ptr }))
call := t.make_call_typed('borrow', [address], flat.source_type_id('&int'))
tc.register_synth_type(call, ptr)
assert t.escape_aggregate_address_sources(call, {
'owner': ['pointee']
}, map[string]string{}) == ['pointee', 'owner']
// A pointer alias is not an explicit address. Keep its existing conservative provenance.
pointer := t.make_ident('p')
tc.register_synth_type(pointer, ptr)
alias_call := t.make_call_typed('borrow', [pointer], flat.source_type_id('&int'))
tc.register_synth_type(alias_call, ptr)
assert t.escape_aggregate_address_sources(alias_call, {
'p': ['local']
}, map[string]string{}) == ['local']
}
fn test_borrowed_storage_dereference_and_scalar_fields_keep_original_provenance() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
ptr := types.Type(types.Pointer{ base_type: types.Type(types.int_) })
storage_borrow_fixture(mut t, 'borrow', [ptr], [true])
pointer := t.make_ident('pointer')
tc.register_synth_type(pointer, ptr)
deref := t.make_prefix(.mul, pointer)
tc.register_synth_type(deref, types.Type(types.int_))
owner := t.make_ident('owner')
tc.register_synth_type(owner, types.Type(types.Struct{ name_id: flat.source_type_id('Owner') }))
field := t.make_selector(owner, 'encoded', flat.source_type_id('int'))
tc.register_synth_type(field, types.Type(types.int_))
for operand in [deref, field] {
address := t.make_prefix(.amp, operand)
tc.register_synth_type(address, ptr)
call := t.make_call_typed('borrow', [address], flat.source_type_id('&int'))
tc.register_synth_type(call, ptr)
assert t.escape_aggregate_address_sources(call, {
'pointer': ['stack']
'owner': ['stack']
}, map[string]string{}) == if operand == deref {
['stack']
} else {
['stack', 'owner']
}
}
// An identifier's recorded provenance also covers pointer-encoded scalars.
encoded := t.make_ident('encoded')
tc.register_synth_type(encoded, types.Type(types.int_))
address := t.make_prefix(.amp, encoded)
tc.register_synth_type(address, ptr)
call := t.make_call_typed('borrow', [address], flat.source_type_id('&int'))
tc.register_synth_type(call, ptr)
assert t.escape_aggregate_address_sources(call, {
'encoded': ['stack']
}, map[string]string{}) == ['stack']
}
fn test_unknown_function_values_arity_and_nested_calls_stay_conservative() {
for mode in ['known', 'unknown', 'fn_value', 'lexical_shadow', 'arity', 'nested'] {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
ptr := types.Type(types.Pointer{ base_type: types.Type(types.int_) })
if mode != 'unknown' {
storage_borrow_fixture(mut t, 'borrow', [ptr], [true])
}
if mode == 'fn_value' {
t.set_var_type('borrow', flat.source_type_id('fn (&int) &int'))
}
local := t.make_ident('local')
tc.register_synth_type(local, types.Type(types.int_))
address := t.make_prefix(.amp, local)
tc.register_synth_type(address, ptr)
mut args := [address]
if mode == 'arity' { args << address }
mut call := t.make_call_typed('borrow', args, flat.source_type_id('&int'))
tc.register_synth_type(call, ptr)
if mode == 'lexical_shadow' {
shadow := t.make_decl_assign('borrow', t.make_ident('callback'))
body := t.make_block([shadow, call])
start := a.children.len
a.children << body
a.add_node(flat.Node{ kind: .fn_decl, value: 'caller', children_start: start, children_count: 1 })
}
if mode == 'nested' {
call = t.make_call_typed('unknown_outer', [call], flat.source_type_id('&int'))
tc.register_synth_type(call, ptr)
}
assert t.escape_aggregate_address_sources(call, map[string][]string{}, map[string]string{}) == if mode in [
'known',
'nested',
] {
[]string{}
} else {
['local']
}, mode
}
}
fn test_reference_receiver_borrows_only_its_storage_and_dynamic_interfaces_are_conservative() {
for is_interface in [false, true] {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
receiver_type := if is_interface {
types.Type(types.Interface{ name_id: flat.source_type_id('Owner') })
} else {
types.Type(types.Struct{ name_id: flat.source_type_id('Owner') })
}
ptr := types.Type(types.Pointer{ base_type: types.Type(types.int_) })
storage_borrow_fixture(mut t, 'Owner.read', [
types.Type(types.Pointer{ base_type: receiver_type }),
ptr,
], [true, true])
receiver := t.make_ident('owner')
t.set_var_type('owner', flat.source_type_id('Owner'))
tc.register_synth_type(receiver, receiver_type)
callee := t.make_selector(receiver, 'read', 0)
local := t.make_ident('local')
tc.register_synth_type(local, types.Type(types.int_))
address := t.make_prefix(.amp, local)
tc.register_synth_type(address, ptr)
start := a.children.len
a.children << [callee, address]
call := a.add_node(flat.Node{ kind: .call, children_start: start, children_count: 2 })
tc.register_synth_type(call, ptr)
t.set_resolved_call_entry(int(call), 'Owner.read')
assert t.escape_aggregate_address_sources(call, map[string][]string{}, map[string]string{}) == if is_interface {
['local']
} else {
[]string{}
}
assert t.escape_aggregate_address_sources(call, {
'owner': ['payload']
}, map[string]string{}) == if is_interface {
['payload', 'local']
} else {
['payload']
}
}
}
const storage_borrow_source = 'module main
struct Entry { value int }
struct Tree { entries []Entry }
struct Payload { pointer &int }
struct Holder { inner Payload }
fn extract(copy Payload) &int { return copy.pointer }
fn (holder &Holder) copy_payload() &int { return extract(holder.inner) }
fn payload_snapshot(value int) &int {
local := value
holder := Holder{inner: Payload{pointer: &local}}
return holder.copy_payload()
}
fn (tree &Tree) index(node &Entry) int { return node.value }
fn (tree &Tree) child_node(node &Entry) &Entry {
id := tree.index(node)
return unsafe { &tree.entries[id] }
}
fn (tree &Tree) type_reference(node &Entry) &Entry {
if node.value == 0 { return unsafe { &tree.entries[0] } }
return unsafe { &tree.entries[1] }
}
fn (tree &Tree) copied_payload(node &Entry) &Entry {
_ = node.value
return tree.child_node(node)
}
fn identity(node &Entry) &Entry { return node }
fn inline_address(node &Entry) &int { return &node.value }
fn snapshot_lookup(tree &Tree, node Entry) &Entry { return tree.child_node(&node) }
fn snapshot_type(tree &Tree, node Entry) &Entry { return tree.type_reference(&node) }
fn snapshot_identity(node Entry) &Entry { return identity(&node) }
fn main() {}
'
fn test_borrowed_snapshot_pointer_results_remove_boxes_but_identity_remains_boxed() {
path := os.join_path(os.vtmp_dir(), 'return_storage_borrow_${os.getpid()}.v')
os.write_file(path, storage_borrow_source)!
defer { os.rm(path) or {} }
mut p := parser.Parser.new(pref.new_preferences())
mut a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.check_semantics()
assert tc.errors.len == 0, tc.errors.str()
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.skip_generics = true
t.cur_module = 'main'
t.cur_file = path
t.transformed_fns = []bool{len: a.nodes.len}
t.prepare()
mut used := map[string]bool{}
for i, node in a.nodes {
if node.kind == .fn_decl {
used[node.value] = true
if node.value in ['Tree.child_node', 'Tree.type_reference', 'Tree.copied_payload'] {
assert t.fixed_array_borrow_params[i] == [false, true], '${node.value}: ${t.fixed_array_borrow_params[i]}'
}
if node.value == 'Holder.copy_payload' {
assert t.fixed_array_borrow_params[i] == [true]
}
if node.value in ['identity', 'inline_address'] {
assert t.fixed_array_borrow_params[i].len == 0
}
}
}
t.transform_all()
assert t.monomorph_errors.len == 0, t.monomorph_errors.str()
mut gen := cgen.FlatGen.new()
generated := gen.gen_with_used(a, used, &tc)
for name in ['snapshot_lookup', 'snapshot_type'] {
assert generated.contains('${name}(')
body := generated.all_after_last('${name}(').all_after('{').all_before('\n}')
assert !body.contains('memdup('), '${name}: ${body}'
assert !body.contains('__heap_value_param'), '${name}: ${body}'
}
identity_body := generated.all_after_last('snapshot_identity(').all_after('{').all_before('\n}')
assert identity_body.contains('memdup('), identity_body
payload_body := generated.all_after_last('payload_snapshot(').all_after('{').all_before('\n}')
assert payload_body.contains('memdup('), payload_body
}
@@ -0,0 +1,54 @@
module transform
import v.flat
import v.types
fn test_scalar_child_lookup_preserves_empty_sentinels_and_snapshot_after_growth() {
mut a := flat.FlatAst.new()
child := a.add_node(flat.Node{ kind: .ident, value: 'saved' })
a.children << child
snapshot := flat.Node{ kind: .call, children_start: 0, children_count: 1 }
for i in [-1, 0, 1] {
assert a.node(a.child(&snapshot, i)).kind == a.child_node(&snapshot, i).kind
assert a.node(a.child(&snapshot, i)).value == a.child_node(&snapshot, i).value
}
for start in [-1, a.children.len, a.children.len + 100] {
invalid := flat.Node{ kind: .call, children_start: start, children_count: 1 }
assert a.node(a.child(&invalid, 0)).kind == a.child_node(&invalid, 0).kind
}
for id in [flat.NodeId(-1), flat.NodeId(a.nodes.len + 100)] {
start := a.children.len
a.children << id
invalid := flat.Node{ kind: .call, children_start: start, children_count: 1 }
assert a.node(a.child(&invalid, 0)).kind == a.child_node(&invalid, 0).kind
}
count := a.nodes.cap + 100
for _ in 0 .. count {
a.add_node(flat.Node{ kind: .ident, value: 'growth' })
a.children << flat.NodeId(-1)
}
assert a.node(a.child(&snapshot, 0)).value == 'saved'
assert snapshot.children_count == 1
}
fn test_call_type_queries_use_stable_snapshot_after_ast_growth() {
mut a := flat.FlatAst.new()
callee := a.add_node(flat.Node{ kind: .ident, value: 'known' })
start := a.children.len
a.children << callee
call := a.add_node(flat.Node{ kind: .call, children_start: start, children_count: 1 })
snapshot := a.nodes[int(call)]
mut tc := types.TypeChecker.new(&a)
tc.fn_ret_types['known'] = types.Type(types.bool_)
tc.fn_param_types['known'] = [types.Type(types.String{})]
mut t := new_transformer(mut a, &tc, map[string]bool{})
count := a.nodes.cap + 100
for _ in 0 .. count {
a.add_node(flat.Node{ kind: .ident, value: 'growth' })
a.children << flat.NodeId(-1)
}
assert t.get_call_return_type(call, snapshot) == flat.source_type_bool
params := t.call_param_types_for_node('known', snapshot)
assert params.len == 1
assert params[0] is types.String
}
@@ -0,0 +1,52 @@
module transform
import v.flat
import v.types
fn test_scoped_text_promotion_reuses_ast_text_and_deduplicates_unknown_text() {
mut a := flat.FlatAst.new()
_, canonical := a.intern_text('source identifier'.clone())
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.promote_text_cache = &PromoteTextCache{}
t.begin_promote_text_window()
source_copy := 'source identifier'.clone()
assert t.promote_scoped_result_text(source_copy) == canonical
assert t.scoped_promoted_texts.len == 0
first := t.promote_scoped_result_text('new identifier'.clone())
second := t.promote_scoped_result_text('new identifier'.clone())
assert first == second
assert first.str == second.str
assert t.scoped_promoted_texts.len == 1
t.end_promote_text_window()
assert !t.promote_text_cache.active
assert t.promote_scoped_result_text(source_copy) == canonical
}
fn test_scoped_text_local_clone_can_become_durable_ast_canonical_text() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
local := t.promote_scoped_result_text('later canonical identifier'.clone())
assert t.scoped_promoted_texts.len == 1
text_start := a.text_values.len
scratch := transform_worker_scope_begin(true)
a.intern_text('later canonical identifier'.clone())
transform_worker_scope_leave(scratch)
a.promote_transform_texts_from(text_start, scratch)
id := a.text_ids['later canonical identifier'] or { panic('missing canonical text') }
canonical := a.text(id)
t.promote_text_cache = &PromoteTextCache{}
t.begin_promote_text_window()
published := t.promote_scoped_result_text('later canonical identifier'.clone())
assert published == local
assert published.str == canonical.str
$if prealloc {
assert !transform_scope_owns(scratch, published.str)
}
t.end_promote_text_window()
transform_worker_scope_free(scratch)
assert published == 'later canonical identifier'
assert local == published
assert t.promote_scoped_result_text('later canonical identifier'.clone()) == published
}
@@ -0,0 +1,91 @@
module transform
import v.flat
import v.types
fn sql_guard_node(mut a flat.FlatAst, template flat.Node, children []flat.NodeId) flat.NodeId {
start := a.children.len
a.children << children
return a.add_node(flat.Node{
...template
children_start: start
children_count: flat.child_count(children.len)
})
}
fn test_sql_bookkeeping_is_conservative_before_program_scan() {
mut a := flat.FlatAst.new()
field := a.add_val(.field_init, 'name')
init := sql_guard_node(mut a, flat.Node{ kind: .struct_init }, [field])
data := a.add_val(.sql_expr, 'querydata select from records')
name := a.add_val(.ident, 'record')
other := a.add_val(.int_literal, '1')
assignment := sql_guard_node(mut a, flat.Node{ kind: .assign, op: .assign }, [
name,
other,
])
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
assert !t.sql_tracking_disabled
t.record_orm_initialized_fields('record', init)
assert t.sql_initialized_fields('record') == ['name']
t.record_sql_query_data_alias('record', data)
assert t.sql_query_data_aliases['record'] == ['select', 'from', 'records']
t.update_orm_initialized_fields_for_assignment(a.nodes[int(assignment)])
t.update_sql_query_data_aliases_for_assignment(a.nodes[int(assignment)])
assert 'record' !in t.orm_initialized_fields
assert 'record' !in t.sql_query_data_aliases
assert t.sql_initialized_fields('record') == ['*']
}
fn test_sql_free_program_disables_bookkeeping_and_workers_keep_the_proof() {
mut a := flat.FlatAst.new()
field := a.add_val(.field_init, 'name')
init := sql_guard_node(mut a, flat.Node{ kind: .struct_init }, [field])
name := a.add_val(.ident, 'record')
assignment := sql_guard_node(mut a, flat.Node{ kind: .assign, op: .assign }, [
name,
init,
])
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.collect_exclusive_closure_return_fns()
assert t.sql_tracking_disabled
t.record_orm_initialized_fields('record', init)
t.update_orm_initialized_fields_for_assignment(a.nodes[int(assignment)])
t.record_sql_query_data_alias('record', init)
t.update_sql_query_data_aliases_for_assignment(a.nodes[int(assignment)])
assert t.orm_initialized_fields.len == 0
assert t.sql_query_data_aliases.len == 0
mut worker := t.fork_program_view(&a, &tc, map[string]bool{}, false)
assert worker.sql_tracking_disabled
worker.record_orm_initialized_fields('record', init)
assert worker.orm_initialized_fields.len == 0
t.reset_var_types()
assert t.sql_tracking_disabled
}
fn test_sql_in_generic_or_unreachable_body_preserves_program_bookkeeping() {
mut a := flat.FlatAst.new()
field := a.add_val(.field_init, 'name')
init := sql_guard_node(mut a, flat.Node{ kind: .struct_init }, [field])
sql := a.add_val(.sql_expr, 'querydata select from records')
body := sql_guard_node(mut a, flat.Node{ kind: .comptime_if }, [sql])
generic_fn := sql_guard_node(mut a, flat.Node{ kind: .fn_decl, value: 'unreachable' }, [body])
a.nodes[int(generic_fn)].set_generic_params(['T'])
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.collect_exclusive_closure_return_fns()
assert !t.sql_tracking_disabled
t.record_orm_initialized_fields('record', init)
assert t.sql_initialized_fields('record') == ['name']
mut worker := t.fork_program_view(&a, &tc, map[string]bool{}, false)
assert !worker.sql_tracking_disabled
worker.record_orm_initialized_fields('record', init)
worker.record_sql_query_data_alias('query', sql)
assert worker.sql_initialized_fields('record') == ['name']
assert worker.sql_query_data_aliases['query'] == ['select', 'from', 'records']
// A second use of the completed prescan retains its immutable result.
t.collect_exclusive_closure_return_fns()
assert !t.sql_tracking_disabled
}
@@ -0,0 +1,157 @@
module transform
import os
import v.flat
import v.gen.c as cgen
import v.parser
import v.pref
import v.types
const storage_borrow_audit_source = 'module main
struct AuditPayload { pointer &int }
struct AuditEnvelope { inner AuditPayload }
struct AuditSink { mut: pointer &int = unsafe { nil } }
fn audit_extract(copy AuditPayload) &int { return copy.pointer }
fn audit_extract_optional(copy AuditPayload) ?&int { return copy.pointer }
fn audit_extract_result(copy AuditPayload) !&int { return copy.pointer }
fn audit_storage(input &AuditEnvelope) &int { return audit_extract(input.inner) }
fn audit_storage_optional(input &AuditEnvelope) ?&int { return audit_extract_optional(input.inner) }
fn audit_storage_result(input &AuditEnvelope) !&int { return audit_extract_result(input.inner) }
fn audit_storage_field(input &AuditEnvelope) &&int { return &input.inner.pointer }
fn audit_store_payload(input &AuditEnvelope, mut sink AuditSink) &int {
sink.pointer = input.inner.pointer
return sink.pointer
}
fn audit_direct(value int) &int {
local := value
payload := AuditEnvelope{inner: AuditPayload{pointer: &local}}
return audit_storage(&payload)
}
fn audit_optional(value int) ?&int {
local := value
payload := AuditEnvelope{inner: AuditPayload{pointer: &local}}
return audit_storage_optional(&payload)
}
fn audit_result(value int) !&int {
local := value
payload := AuditEnvelope{inner: AuditPayload{pointer: &local}}
return audit_storage_result(&payload)
}
fn audit_field(value int) &&int {
local := value
payload := AuditEnvelope{inner: AuditPayload{pointer: &local}}
return audit_storage_field(&payload)
}
fn audit_mutable_store(value int, mut sink AuditSink) &int {
local := value
payload := AuditEnvelope{inner: AuditPayload{pointer: &local}}
return audit_store_payload(&payload, mut sink)
}
fn main() {}
'
fn test_storage_only_summary_keeps_real_embedded_aliases_through_return_wrappers() {
path := os.join_path(os.vtmp_dir(), 'storage_borrow_audit_${os.getpid()}.v')
os.write_file(path, storage_borrow_audit_source)!
defer { os.rm(path) or {} }
mut p := parser.Parser.new(pref.new_preferences())
mut a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.check_semantics()
assert tc.errors.len == 0, tc.errors.str()
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.skip_generics = true
t.cur_module = 'main'
t.cur_file = path
t.transformed_fns = []bool{len: a.nodes.len}
t.prepare()
mut used := map[string]bool{}
for i, node in a.nodes {
if node.kind != .fn_decl {
continue
}
used[node.value] = true
if node.value in ['audit_storage', 'audit_storage_optional', 'audit_storage_result'] {
assert t.fixed_array_borrow_params[i] == [true], '${node.value}: ${t.fixed_array_borrow_params[i]}'
}
if node.value in ['audit_storage_field', 'audit_store_payload'] {
summary := t.fixed_array_borrow_params[i] or { []bool{} }
assert summary.len == 0 || !summary[0], '${node.value}: ${summary}'
}
}
t.transform_all()
assert t.monomorph_errors.len == 0, t.monomorph_errors.str()
mut gen := cgen.FlatGen.new()
generated := gen.gen_with_used(a, used, &tc)
for name in ['audit_direct', 'audit_optional', 'audit_result', 'audit_field', 'audit_mutable_store'] {
assert generated.contains('${name}('), name
body := generated.all_after_last('${name}(').all_after('{').all_before('\n}')
mut local_is_boxed := false
for line in body.split_into_lines() {
if line.contains('local =') && line.contains('memdup(') {
local_is_boxed = true
}
}
assert local_is_boxed, '${name}: ${body}'
}
proof := os.getenv('STORAGE_BORROW_AUDIT_C')
if proof.len > 0 {
os.write_file(proof, generated)!
}
}
fn test_storage_projection_rejects_nonidentifier_address_operands() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
value := t.make_ident('value')
assert t.escape_storage_borrow_ident(value)
paren := t.make_paren(value)
assert t.escape_storage_borrow_ident(paren)
field := t.make_selector(value, 'field', 0)
assert !t.escape_storage_borrow_ident(field)
deref := t.make_prefix(.mul, value)
assert !t.escape_storage_borrow_ident(deref)
}
fn test_storage_summary_rejects_wrapped_indirect_callees() {
for kind in [flat.NodeKind.index, .paren] {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})
ptr := types.Type(types.Pointer{ base_type: types.Type(types.int_) })
decl := a.add_node(flat.Node{ kind: .fn_decl, value: 'audit_borrow' })
t.call_param_types_index_ready = true
t.call_param_types_decl_index['audit_borrow'] = FnParamDeclRef{ idx: int(decl) }
t.call_param_types_decl_cache[int(decl)] = [ptr]
t.fixed_array_borrow_params[int(decl)] = [true]
tc.fn_param_types['audit_borrow'] = [ptr]
tc.fn_ret_types['audit_borrow'] = ptr
callback := t.make_ident('audit_borrow')
t.set_var_type('audit_borrow', flat.source_type_id('fn (&int) &int'))
wrapped_start := a.children.len
a.children << callback
wrapped := a.add_node(flat.Node{
kind: kind
children_start: wrapped_start
children_count: 1
})
local := t.make_ident('local')
tc.register_synth_type(local, types.Type(types.int_))
address := t.make_prefix(.amp, local)
tc.register_synth_type(address, ptr)
call_start := a.children.len
a.children << [wrapped, address]
call := a.add_node(flat.Node{
kind: .call
children_start: call_start
children_count: 2
})
tc.register_synth_type(call, ptr)
t.set_resolved_call_entry(int(call), 'audit_borrow')
assert !t.escape_call_storage_borrow(call, *a.node(call)).borrows(1), kind.str()
assert t.escape_aggregate_address_sources(call, map[string][]string{}, map[string]string{}) == ['local'], kind.str()
}
}
+112 -27
View File
@@ -210,6 +210,7 @@ mut:
receiver_method_suffix_index map[string]string
declared_fn_name_counts map[string]u8
variadic_suffix_index map[string]i8
const_selector_filter &ConstSelectorNameFilter = unsafe { nil }
const_suffixes map[string]string
source_parent_ids []i32
array_append_tree_factories map[string]bool // immutable freshness proof, shared by workers
@@ -245,6 +246,7 @@ mut:
// const_array_fixed_storage_cache avoids rescanning the complete AST for
// repeated uses of the same array constant in one transform worker.
const_array_fixed_storage_cache map[string]i8
enum_selector_filter &EnumSelectorNameFilter = unsafe { nil }
enum_types map[string][]string
enum_explicit_first_values map[string]bool // the first member has a value, maybe not 0
enum_backing_types flat.TypeTextBindings
@@ -283,6 +285,7 @@ mut:
fixed_array_value_rvalues map[string]bool
addr_lvalue_pointer_locals map[string]bool
orm_initialized_fields map[string][]string
sql_tracking_disabled bool // complete program scan found no SQL expressions
sql_query_data_aliases map[string][]string
sql_array_it_name string
bound_method_arrays map[string]BoundMethodArrayInfo
@@ -2532,12 +2535,18 @@ fn (mut t Transformer) mark_fn_used_name(name string) {
return
}
lowered := if isnil(t.tc) { c_name(name) } else { t.tc.cached_c_name(name) }
if !t.used_fn_contains_name(name) && !t.used_fn_contains_name(lowered)
name_used := t.used_fn_contains_name(name)
lowered_used := if lowered == name { name_used } else { t.used_fn_contains_name(lowered) }
if !name_used && !lowered_used
&& t.enum_method_name_shadows_field(name) {
return
}
t.mark_used_fn_key(name)
t.mark_used_fn_key(lowered)
if !name_used {
t.record_used_fn_key(name)
}
if lowered != name && !lowered_used {
t.record_used_fn_key(lowered)
}
if t.cur_module.len > 0 && t.cur_module != 'main' && t.cur_module != 'builtin' {
needs_module_prefix := !name.contains('.') || local_method_fn_name_needs_module_prefix(name)
if needs_module_prefix && !name.starts_with('${t.cur_module}.') {
@@ -2584,6 +2593,11 @@ fn (mut t Transformer) mark_used_fn_key(key string) {
if t.used_fn_contains_name(key) {
return
}
t.record_used_fn_key(key)
}
// record_used_fn_key records a spelling after the caller has checked all used sets.
fn (mut t Transformer) record_used_fn_key(key string) {
if t.used_fns_log_active {
t.used_fns_log << key
}
@@ -2646,6 +2660,7 @@ fn (mut t Transformer) prepare() {
}
mut psw := time.new_stopwatch()
t.collect_types()
t.prepare_enum_selector_filter()
t.rebuild_struct_autostr_recurse_index()
if !t.defer_pre_scan_indexes {
t.build_source_parent_index()
@@ -2660,6 +2675,7 @@ fn (mut t Transformer) prepare() {
t.collect_multi_return_fn_ret_types()
}
t.collect_const_suffixes()
t.prepare_const_selector_filter()
t.timing_profile(' [ttime] prep small idx ${f64(psw.elapsed().microseconds()) / 1000.0:7.2f} ms')
psw.restart()
// Alias normalization consults the type maps populated by collect_types.
@@ -3708,6 +3724,9 @@ fn (t &Transformer) declared_var_spelling(name string) ?string {
}
fn (mut t Transformer) record_orm_initialized_fields(name string, rhs_id flat.NodeId) {
if t.sql_tracking_disabled {
return
}
if name == '' || int(rhs_id) < 0 || int(rhs_id) >= t.a.nodes.len {
return
}
@@ -3727,6 +3746,9 @@ fn (mut t Transformer) record_orm_initialized_fields(name string, rhs_id flat.No
}
fn (mut t Transformer) record_sql_query_data_alias(name string, rhs_id flat.NodeId) {
if t.sql_tracking_disabled {
return
}
if name == '' || int(rhs_id) < 0 || int(rhs_id) >= t.a.nodes.len {
return
}
@@ -3744,6 +3766,9 @@ fn (mut t Transformer) record_sql_query_data_alias(name string, rhs_id flat.Node
}
fn (mut t Transformer) update_sql_query_data_aliases_for_assignment(node flat.Node) {
if t.sql_tracking_disabled {
return
}
if node.children_count < 2 {
return
}
@@ -3764,6 +3789,9 @@ fn (mut t Transformer) update_sql_query_data_aliases_for_assignment(node flat.No
}
fn (mut t Transformer) update_orm_initialized_fields_for_assignment(node flat.Node) {
if t.sql_tracking_disabled {
return
}
if node.children_count < 2 {
return
}
@@ -4038,7 +4066,7 @@ fn (mut t Transformer) collect_types() {
})
mut fields := []FieldInfo{}
for i in 0 .. node.children_count {
f := t.a.child_node(&node, i)
f := t.a.node(t.a.child(&node, i))
if f.kind != .field_decl {
continue
}
@@ -4065,10 +4093,10 @@ fn (mut t Transformer) collect_types() {
info := StructInfo{
name: node.source_value_id()
module: cur_mod
is_params: comma_list_has_item(node.typ_text(), 'params')
is_aligned: transform_struct_decl_alignment_is_set(node.typ_text())
is_c_anon: comma_list_has_item(node.typ_text(), 'c_anon')
alignment: transform_struct_decl_alignment_value(node.typ_text())
is_params: comma_list_has_item(flat.source_type_name(node.typ), 'params')
is_aligned: transform_struct_decl_alignment_is_set(flat.source_type_name(node.typ))
is_c_anon: comma_list_has_item(flat.source_type_name(node.typ), 'c_anon')
alignment: transform_struct_decl_alignment_value(flat.source_type_name(node.typ))
fields: fields
field_indices: struct_field_indices(fields)
}
@@ -4090,10 +4118,11 @@ fn (mut t Transformer) collect_types() {
}
.type_decl {
if node.children_count > 0 {
generic_params := node.generic_params()
mut variants := []u32{}
for i in 0 .. node.children_count {
v := t.a.child_node(&node, i)
variants << t.normalize_sum_variant_type(v.source_value_id(), cur_mod, node.generic_params())
variants << t.normalize_sum_variant_type(v.source_value_id(), cur_mod, generic_params)
}
decl_name := if cur_mod !in ['', 'main', 'builtin'] {
'${cur_mod}.${node.value_text()}'
@@ -4136,7 +4165,7 @@ fn (mut t Transformer) collect_types() {
mut field_names := []string{}
mut explicit_first_value := false
for i in 0 .. node.children_count {
f := t.a.child_node(&node, i)
f := t.a.node(t.a.child(&node, i))
if f.kind == .enum_field {
if field_names.len == 0 {
explicit_first_value = f.children_count > 0
@@ -4186,7 +4215,7 @@ fn (mut t Transformer) collect_types() {
t.cur_file = cur_file
t.cur_module = cur_mod
for i in 0 .. node.children_count {
f := t.a.child_node(&node, i)
f := t.a.node(t.a.child(&node, i))
mut typ := t.normalize_type_in_module(f.typ, cur_mod)
if typ == 0 && f.children_count > 0 {
typ = t.normalize_type_in_module(t.node_type(t.a.child(f, 0)), cur_mod)
@@ -5482,6 +5511,7 @@ fn (t &Transformer) fork_program_view(ast &flat.FlatAst, wtc &types.TypeChecker,
receiver_method_suffix_index: t.receiver_method_suffix_index
declared_fn_name_counts: t.declared_fn_name_counts
variadic_suffix_index: t.variadic_suffix_index
const_selector_filter: t.const_selector_filter
const_suffixes: t.const_suffixes
source_parent_ids: t.source_parent_ids
array_append_tree_factories: t.array_append_tree_factories
@@ -5497,6 +5527,7 @@ fn (t &Transformer) fork_program_view(ast &flat.FlatAst, wtc &types.TypeChecker,
} else {
map[string]i8{}
}
enum_selector_filter: t.enum_selector_filter
enum_types: t.enum_types
enum_explicit_first_values: t.enum_explicit_first_values
enum_backing_types: fork_prepared_type_bindings(t.enum_backing_types)
@@ -5572,6 +5603,7 @@ fn (t &Transformer) fork_program_view(ast &flat.FlatAst, wtc &types.TypeChecker,
pointer_value_rvalues: map[string]bool{}
fixed_array_value_rvalues: map[string]bool{}
orm_initialized_fields: map[string][]string{}
sql_tracking_disabled: t.sql_tracking_disabled
sql_query_data_aliases: map[string][]string{}
invalidated_smartcasts: map[string]bool{}
smartcast_invalidation_event_ids: map[string]int{}
@@ -7974,6 +8006,9 @@ fn (mut t Transformer) collect_exclusive_closure_return_fns() {
return
}
t.exclusive_closure_returns_done = true
// This existing whole-program scan also proves whether SQL bookkeeping can
// be skipped. Standalone transformers retain the conservative default.
t.sql_tracking_disabled = true
t.exclusive_closure_return_fns.clear()
old_module := t.cur_module
defer {
@@ -7987,6 +8022,9 @@ fn (mut t Transformer) collect_exclusive_closure_return_fns() {
for idx in 0 .. t.a.nodes.len {
// The AST arena is unchanged during this scan.
node := unsafe { &t.a.nodes[idx] }
if node.kind == .sql_expr {
t.sql_tracking_disabled = false
}
span_cost += match node.kind {
.call, .struct_init { 8 }
.selector { 6 }
@@ -10735,11 +10773,33 @@ fn (mut t Transformer) scan_escape_pass(id flat.NodeId, mut amp_ptrs map[string]
if int(id) < 0 || int(id) >= t.a.nodes.len {
return
}
kind := t.a.nodes[int(id)].kind
children_count := t.a.nodes[int(id)].children_count
// A childless node cannot introduce an alias or retain a local address.
if t.a.nodes[int(id)].children_count == 0 && t.a.nodes[int(id)].kind != .comptime_for {
if children_count == 0 && kind != .comptime_for {
return
}
t.scan_escape_pass_nonempty(id, mut amp_ptrs, mut amp_sources, mut ptr_aliases, mut method_value_receivers, mut closure_capture_aliases, mut interface_boxes, mut returned, mut local_stack_names, mut local_stack_added, mut reference_backing_sources, can_clear_interface_boxes)
needs_scan := match kind {
.comptime_for, .if_expr, .comptime_if, .match_stmt, .match_branch, .for_stmt,
.select_stmt, .select_branch, .block, .for_in_stmt, .fn_literal, .lambda_expr,
.fn_decl, .decl_assign, .assign, .return_stmt, .spawn_expr, .call,
.selector_assign, .index_assign {
true
}
.infix { t.a.nodes[int(id)].op == .left_shift && children_count == 2 }
.or_expr { children_count == 2 }
else { false }
}
if needs_scan {
t.scan_escape_pass_nonempty(id, mut amp_ptrs, mut amp_sources, mut ptr_aliases, mut method_value_receivers, mut closure_capture_aliases, mut interface_boxes, mut returned, mut local_stack_names, mut local_stack_added, mut reference_backing_sources, can_clear_interface_boxes)
return
}
// Value wrappers only forward their children. Keep scalar offsets, not a node
// header or child slice: reflected loops below them can grow both AST arrays.
children_start := t.a.nodes[int(id)].children_start
for i in 0 .. children_count {
t.scan_escape_pass(t.a.children[children_start + i], mut amp_ptrs, mut amp_sources, mut ptr_aliases, mut method_value_receivers, mut closure_capture_aliases, mut interface_boxes, mut returned, mut local_stack_names, mut local_stack_added, mut reference_backing_sources, can_clear_interface_boxes)
}
}
@[direct_array_access]
@@ -10804,7 +10864,7 @@ fn (mut t Transformer) scan_escape_pass_nonempty(id flat.NodeId, mut amp_ptrs ma
// The `else` of an `if x := res()` guard binds the error as `err`, which is only
// declared when the branch is lowered.
is_guard := node.kind == .if_expr && node.children_count > 0
&& t.a.child_node(node, 0).kind == .decl_assign
&& t.a.node(t.a.child(node, 0)).kind == .decl_assign
mut previous_err_depth := -1
if is_guard {
previous_err_depth = t.escape_decl_depths['err'] or { -1 }
@@ -10847,7 +10907,7 @@ fn (mut t Transformer) scan_escape_pass_nonempty(id flat.NodeId, mut amp_ptrs ma
}
mut body_start := if node.value == 'else' { 0 } else { 1 }
if node.children_count >= 2 {
second := t.a.child_node(node, 1)
second := t.a.node(t.a.child(node, 1))
if second.kind == .prefix && second.op == .arrow {
body_start = 2
}
@@ -11770,22 +11830,28 @@ fn (t &Transformer) escape_aggregate_address_sources(id flat.NodeId, amp_sources
return []string{}
}
mut sources := []string{}
mut storage_borrow := EscapeCallStorageBorrow{}
mut storage_borrow_ready := false
if node.children_count > 0 {
callee := t.a.child_node(node, 0)
if callee.kind == .selector && callee.children_count > 0 {
receiver_id := t.a.child(callee, 0)
if !t.callee_base_is_not_a_runtime_value(receiver_id)
&& t.method_receiver_is_reference(id, receiver_id, callee.value_text()) {
if t.escape_storage_borrow_ident(receiver_id) {
storage_borrow = t.escape_call_storage_borrow(id, *node)
storage_borrow_ready = true
}
// Interface dispatch borrows the header to read its object pointer;
// the concrete method receives the object, never the header's address.
// A receiver that already is a pointer (`node.parent.method()` with
// `parent &T`) is passed by value: only what it points to can flow
// through the call, never the address of the expression holding it.
receiver_sources := if !isnil(t.tc)
receiver_sources := if (!isnil(t.tc)
&& types.unalias_type(t.tc.resolve_type(receiver_id)) in [
types.Interface,
types.Pointer,
] {
]) || (t.escape_storage_borrow_ident(receiver_id) && storage_borrow.borrows(0)) {
t.escape_aggregate_address_sources(receiver_id, amp_sources, ptr_aliases)
} else {
t.escape_address_sources(receiver_id, amp_sources, ptr_aliases)
@@ -11800,7 +11866,22 @@ fn (t &Transformer) escape_aggregate_address_sources(id flat.NodeId, amp_sources
}
// Reference receivers and argument addresses can flow through the call result.
for i in 1 .. node.children_count {
for source_name in t.escape_aggregate_address_sources(t.a.child(node, i), amp_sources, ptr_aliases) {
arg_id := t.a.child(node, i)
arg := t.a.node(t.fixed_array_unwrap_arg(arg_id))
// A borrowed address cannot be returned, but pointers held inside the
// addressed value may still be copied into the result.
mut payload_id := arg_id
if arg.kind == .prefix && arg.op == .amp && arg.children_count == 1
&& t.escape_storage_borrow_ident(t.a.child(arg, 0)) {
if !storage_borrow_ready {
storage_borrow = t.escape_call_storage_borrow(id, *node)
storage_borrow_ready = true
}
if storage_borrow.borrows(i) {
payload_id = t.a.child(arg, 0)
}
}
for source_name in t.escape_aggregate_address_sources(payload_id, amp_sources, ptr_aliases) {
if source_name !in sources {
sources << source_name
}
@@ -19274,7 +19355,7 @@ fn (mut t Transformer) transform_decl_assign_stmt(id flat.NodeId, node flat.Node
}
panic('internal error: empty decl_assign child in ${t.cur_fn_name}: count=${node.children_count} typ=${node.typ_text()} value=${node.value_text()} children=${parts.join('|')}')
}
source_lhs := t.a.child_node(&node, 0)
source_lhs := t.a.node(t.a.child(&node, 0))
shadows_pointer_storage := source_single_decl && source_lhs.kind == .ident
&& (t.heaped_amp_locals[source_lhs.value_text()] || t.pointer_value_rvalues[source_lhs.value_text()]
|| t.pointer_value_lvalues[source_lhs.value_text()])
@@ -19512,7 +19593,7 @@ fn (mut t Transformer) transform_decl_assign_stmt(id flat.NodeId, node flat.Node
}
if typ != 0 {
mut raw_typ := u32(0)
for candidate in [t.a.child_node(&node, 0).typ, node.typ] {
for candidate in [t.a.node(t.a.child(&node, 0)).typ, node.typ] {
if candidate != 0
&& (t.generic_type_text_contains_alias(candidate, t.cur_module)
|| (t.enum_type_name_for_expected(candidate, t.cur_module)) != 0) {
@@ -19626,7 +19707,7 @@ fn (mut t Transformer) transform_decl_assign_stmt(id flat.NodeId, node flat.Node
for i in 0 .. node.children_count {
child_id := t.a.child(&node, i)
if i == 0 || (node.children_count > 2 && i > 1) {
child := t.a.child_node(&node, i)
child := t.a.node(t.a.child(&node, i))
if i == 0 && multi_match_decl_type_source_id != 0 && child.kind == .ident {
lhs := t.make_ident(child.value_text())
t.set_node_typ(int(lhs), multi_match_decl_type_source_id)
@@ -29490,10 +29571,14 @@ fn (mut t Transformer) new_global_temp(prefix string) string {
// make_ident builds make ident data for transform.
pub fn (mut t Transformer) make_ident(name string) flat.NodeId {
id := t.a.add_val(.ident, name)
typ_source_id_6 := t.var_type(name)
if typ_source_id_6 != 0 {
t.set_node_typ(int(id), typ_source_id_6)
typ := t.var_type(name)
if typ != 0 {
// This fresh leaf has no cached ancestors. Keep the body memo while
// preserving checker invalidation for recycled speculative node ids.
t.a.nodes[int(id)].typ = typ
if !isnil(t.tc) {
t.tc.invalidate_checked_expr_type(int(id))
}
}
return id
}
@@ -31147,7 +31232,7 @@ fn (t &Transformer) const_owned_expr_type(name string, node_typ_id u32) u32 {
// const_type_key supports const type key handling for Transformer.
fn (t &Transformer) const_type_key(name string) ?string {
if name == '' || isnil(t.tc) {
if name == '' || isnil(t.tc) || !t.const_selector_may_match(name) {
return none
}
if name in t.tc.const_types {
@@ -31162,7 +31247,7 @@ fn (t &Transformer) const_type_key(name string) ?string {
}
fn (t &Transformer) const_type_key_in_context(name string, module_name string, file string) ?string {
if name == '' || isnil(t.tc) {
if name == '' || isnil(t.tc) || !t.const_selector_may_match(name) {
return none
}
if !name.contains('.') {
@@ -2360,10 +2360,10 @@ fn (mut t Transformer) promote_scoped_result_text(value string) string {
// strings before adding a worker-local entry; generic specialization clones
// otherwise retain another copy of almost every source identifier and type.
mut canonical := ''
if promoted := t.scoped_promoted_texts[value] {
canonical = promoted
} else if id := t.a.text_ids[value] {
if id := t.a.text_ids[value] {
canonical = t.a.text_values[int(id) - 1]
} else if promoted := t.scoped_promoted_texts[value] {
canonical = promoted
} else {
publication_state := transform_stage_scope_suspend(t.merge_scratch_scope)
canonical = value.clone()
+58 -21
View File
@@ -756,38 +756,43 @@ fn (t &Transformer) resolve_selector_type_uncached(node flat.Node) u32 {
if field_name.len == 0 {
return u32(0)
}
base_selector_name := t.selector_expr_name(base_id)
if base_selector_name.len > 0 {
if enum_type_source_id := t.enum_type_name_from_selector_name(base_selector_name) {
enum_type_source_id_2 := enum_type_source_id
enum_type := flat.source_type_name(enum_type_source_id_2)
enum_may_match := t.enum_selector_may_match(field_name)
if enum_may_match {
base_selector_name := t.selector_expr_name(base_id)
if base_selector_name.len > 0 {
if enum_type_source_id := t.enum_type_name_from_selector_name(base_selector_name) {
enum_type_source_id_2 := enum_type_source_id
enum_type := flat.source_type_name(enum_type_source_id_2)
if fields := t.enum_types[enum_type] {
if field_name in fields {
return enum_type_source_id_2
if fields := t.enum_types[enum_type] {
if field_name in fields {
return enum_type_source_id_2
}
}
}
}
if type_name_source_id := t.qualified_enum_type_selector_name_from_selector_name(base_selector_name, field_name) {
type_name_source_id_2 := type_name_source_id
if type_name_source_id := t.qualified_enum_type_selector_name_from_selector_name(base_selector_name, field_name) {
type_name_source_id_2 := type_name_source_id
return type_name_source_id_2
return type_name_source_id_2
}
}
}
if base_node.kind == .ident {
if typ_source_id := t.const_type_name('${base_node.value}.${field_name}') {
return typ_source_id
}
if fields := t.enum_types[base_node.value] {
if field_name in fields {
return base_node.source_value_id()
}
}
if t.cur_module.len > 0 && t.cur_module != 'main' && t.cur_module != 'builtin' {
qenum := '${t.cur_module}.${base_node.value}'
if fields := t.enum_types[qenum] {
if enum_may_match {
if fields := t.enum_types[base_node.value] {
if field_name in fields {
return flat.source_type_id(qenum)
return base_node.source_value_id()
}
}
if t.cur_module.len > 0 && t.cur_module != 'main' && t.cur_module != 'builtin' {
qenum := '${t.cur_module}.${base_node.value}'
if fields := t.enum_types[qenum] {
if field_name in fields {
return flat.source_type_id(qenum)
}
}
}
}
@@ -2078,6 +2083,38 @@ fn (t &Transformer) normalized_node_type(id flat.NodeId) u32 {
@[direct_array_access; inline]
fn (t &Transformer) node_type(id flat.NodeId) u32 {
idx := int(id)
if idx < 0 {
return u32(0)
}
// These expression types cannot depend on annotations, bindings or the
// expected type. Resolve them before maintaining the contextual body memo.
match t.a.nodes[idx].kind {
.bool_literal { return flat.source_type_bool }
.float_literal { return flat.source_type_f64 }
.int_literal { return flat.source_type_int }
.string_literal, .string_interp { return flat.source_type_string }
.nil_literal { return flat.source_type_voidptr }
.char_literal {
return if t.a.nodes[idx].value.starts_with('c:') {
flat.source_type_id('&u8')
} else {
flat.source_type_rune
}
}
.sizeof_expr, .offsetof_expr { return flat.source_type_u32 }
.prefix {
if t.a.nodes[idx].children_count > 0 && t.a.nodes[idx].op == .not {
return flat.source_type_bool
}
}
.infix {
if t.a.nodes[idx].children_count >= 2
&& t.a.nodes[idx].op in [.eq, .ne, .lt, .gt, .le, .ge, .logical_and, .logical_or] {
return flat.source_type_bool
}
}
else {}
}
if !isnil(t.node_type_memo) {
mut memo := t.node_type_memo
if memo.active && idx >= memo.lo && idx <= memo.hi {
+75
View File
@@ -0,0 +1,75 @@
module transform
import v.flat
import v.types
fn used_name_reference(mut t Transformer, name string) {
if name == '' {
return
}
lowered := if isnil(t.tc) { c_name(name) } else { t.tc.cached_c_name(name) }
if !t.used_fn_contains_name(name) && !t.used_fn_contains_name(lowered)
&& t.enum_method_name_shadows_field(name) {
return
}
t.mark_used_fn_key(name)
t.mark_used_fn_key(lowered)
if t.cur_module.len > 0 && t.cur_module != 'main' && t.cur_module != 'builtin' {
needs_module_prefix := !name.contains('.') || local_method_fn_name_needs_module_prefix(name)
if needs_module_prefix && !name.starts_with('${t.cur_module}.') {
qname := '${t.cur_module}.${name}'
t.mark_used_fn_key(qname)
t.mark_used_fn_key(if isnil(t.tc) { c_name(qname) } else { t.tc.cached_c_name(qname) })
}
}
}
fn test_used_name_probes_preserve_spellings_overlays_and_log_order() {
for module_name in ['', 'main', 'builtin', 'demo'] {
for logging in [false, true] {
for state in 0 .. 8 {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut slow := new_transformer(mut a, &tc, map[string]bool{})
mut fast := new_transformer(mut a, &tc, map[string]bool{})
mut parent := map[string]bool{}
mut root := map[string]bool{}
if state & 1 != 0 {
parent['demo.call'] = true
parent['helper'] = true
}
if state & 2 != 0 {
root['demo__call'] = true
root['Color.red'] = true
}
if state & 4 != 0 {
slow.used_fns['malloc'] = true
fast.used_fns['malloc'] = true
}
slow.used_fns_parent = unsafe { &parent }
fast.used_fns_parent = unsafe { &parent }
slow.used_fns_root = unsafe { &root }
fast.used_fns_root = unsafe { &root }
slow.cur_module = module_name
fast.cur_module = module_name
slow.used_fns_log_active = logging
fast.used_fns_log_active = logging
slow.enum_types['Color'] = ['red', 'blue']
fast.enum_types['Color'] = ['red', 'blue']
for name in ['', 'helper', 'demo.call', 'malloc', 'int_str', 'exit', 'Color.red',
'Color.method', 'word__value', 'Box.+', 'helper'] {
used_name_reference(mut slow, name)
fast.mark_fn_used_name(name)
assert fast.used_fns == slow.used_fns, '${module_name}:${state}:${name}'
assert fast.used_fns_log == slow.used_fns_log, '${module_name}:${state}:${name}'
}
// A parent's additions remain visible on later invocations.
parent['later'] = true
used_name_reference(mut slow, 'later')
fast.mark_fn_used_name('later')
assert fast.used_fns == slow.used_fns
assert fast.used_fns_log == slow.used_fns_log
}
}
}
}
+5
View File
@@ -15,6 +15,11 @@ Dense cache entries retain their canonical references. Sparse cache and function
receive stable descriptor copies whose nested metadata stays shared. Keep either reference only
while its owning checker arena remains live; use `clone_owned_type` across arena boundaries.
`TypeChecker.resolve_type_reference(node)` returns `resolve_type`'s result as a canonical
reference. It observes the same checked-cache visibility and mixed-integer widening rules.
The reference remains valid while its owning checker arena is live; use `clone_owned_type`
when retaining the result beyond that arena.
Transform stages register generated return signatures with
`TypeChecker.register_generated_fn_return_type(name, ret)` and remove them with
`TypeChecker.unregister_generated_fn_return_type(name)`. Both increment the return-signature
+191 -41
View File
@@ -474,6 +474,7 @@ mut:
// SumVariantPatternFileEntries owns the module tables for one source file.
@[heap]
struct SumVariantPatternFileEntries {
file string
mut:
modules map[string]&SumVariantPatternModuleEntries
}
@@ -481,6 +482,8 @@ mut:
// SumVariantPatternModuleEntries owns the numeric source-type pairs in one context.
@[heap]
struct SumVariantPatternModuleEntries {
file string
module string
mut:
values map[u64]u32
}
@@ -584,6 +587,9 @@ mut:
source_error_embed_shared bool
ierror_impl_names flat.TypeTextList
ierror_impl_names_set bool
// Directory entries own these strings; a clear invalidates the recorded count.
sum_variant_pattern_recent &SumVariantPatternModuleEntries = unsafe { nil }
sum_variant_pattern_recent_count int
// short type name -> unique qualified name ('' = ambiguous); built lazily
// by unique_qualified_type_name from the five type-name maps.
short_type_name_index flat.TypeTextBindings
@@ -707,6 +713,12 @@ mut:
// Import-aware views keyed by source file, reused across every declaration
// type resolved in that file instead of forking a full checker per call.
scoped_by_file map[string]&TypeChecker
// Synchronous calls usually remain in one source file. The pointers belong to
// the same cache as the maps and are discarded with it at every phase reset.
recent_unscoped_file string
recent_unscoped &TypeChecker = unsafe { nil }
recent_scoped_file string
recent_scoped &TypeChecker = unsafe { nil }
}
// TypeCacheStats reports semantic cache effectiveness for compiler telemetry.
@@ -1232,6 +1244,7 @@ pub mut:
// Read-only post-check call graph for transform forks (see
// fork_for_parallel_transform); nil in the checker that owns the graph.
post_check_dependencies &DependencyGraphView = unsafe { nil }
global_store_cache &FnGlobalStoreCache = unsafe { nil }
// call_param_flows_to_callback results computed before transform lowers the
// bodies they read (see prepare_callback_param_flows); shared with forks.
callback_param_flows &CallbackParamFlows = unsafe { nil }
@@ -1289,10 +1302,14 @@ pub mut:
// node-indexed arrays (this checker is the range's only writer) and only
// out-of-range ids (consts, other decls' nodes) go through the private
// sparse maps that are merged after join.
check_range_lo int = -1
check_range_hi int = -1
sparse_resolved_call_names map[int]string
sparse_resolved_fn_values map[int]string // the only fn-value name store: a few dozen entries, a node-indexed array cost ~12 MB
check_range_lo int = -1
check_range_hi int = -1
sparse_resolved_call_names map[int]string
sparse_resolved_fn_values map[int]string // the only fn-value name store: a few dozen entries, a node-indexed array cost ~12 MB
// Only private transform/codegen snapshots enable this conservative filter.
// Mutable masters and storage probes keep supporting direct map writes.
sparse_fn_value_filter [4]u64
sparse_fn_value_filter_ready bool
fork_fn_value_writes map[int]string // a transform fork's own fn-value writes ('' = cleared); its sparse map stays a read-only snapshot and only these are merged
sparse_statement_nodes map[int]bool
sparse_expr_type_values map[int]Type
@@ -1340,6 +1357,8 @@ pub mut:
alias_short_names map[string]bool
alias_short_names_count int = -1
const_suffix_key_count int = -1
const_selector_bloom NameBloom // of const_types and const_suffixes
const_selector_suffix_count int = -1
const_expr_short_names map[string]bool
const_expr_short_names_count int = -1
global_name_bloom NameBloom // of global_names
@@ -1368,12 +1387,14 @@ pub mut:
import_line_indexed_files map[int]bool
source_texts_by_file map[string]string
source_texts_by_id map[int]string
ct_update_pos map[int]token.Pos
ct_update_indexed bool
insert_include_dirs_by_file map[string][]string
has_spawn_expr int = -1
inactive_top_level_node_ids []i32
selected_file_called_fns map[string]bool
// Zero means unindexed; one rules out the exceptional `cosnt` diagnostic.
malformed_const_source_states []u8
ct_update_pos map[int]token.Pos
ct_update_indexed bool
insert_include_dirs_by_file map[string][]string
has_spawn_expr int = -1
inactive_top_level_node_ids []i32
selected_file_called_fns map[string]bool
// Names newly inserted into selected_file_called_fns and not yet chased by
// the transitive closure in collect_selected_file_called_fns_transitively.
selected_file_worklist []string
@@ -1802,6 +1823,8 @@ fn (tc &TypeChecker) fork_program_view(ast &flat.FlatAst, direct_dependencies_by
const_files: tc.const_files
const_suffixes: tc.const_suffixes
const_suffix_key_count: tc.const_suffix_key_count
const_selector_bloom: tc.const_selector_bloom
const_selector_suffix_count: tc.const_selector_suffix_count
const_expr_short_names: tc.const_expr_short_names
const_expr_short_names_count: tc.const_expr_short_names_count
global_short_names: tc.global_short_names
@@ -1881,6 +1904,7 @@ fn (tc &TypeChecker) fork_program_view(ast &flat.FlatAst, direct_dependencies_by
multiple_module_import_lines: tc.multiple_module_import_lines
source_texts_by_file: tc.source_texts_by_file
source_texts_by_id: tc.source_texts_by_id
malformed_const_source_states: tc.malformed_const_source_states
ct_update_pos: tc.ct_update_pos
ct_update_indexed: tc.ct_update_indexed
has_spawn_expr: tc.has_spawn_expr
@@ -1973,6 +1997,7 @@ pub fn (tc &TypeChecker) fork_for_parallel_codegen() &TypeChecker {
// while cgen workers read (a type query on the declaration task can record
// a resolution), so each worker reads a private snapshot of its few entries.
forked.sparse_resolved_fn_values = tc.fn_value_snapshot()
forked.rebuild_sparse_fn_value_filter()
forked.visible_mutation_cache = unsafe { nil }
forked.inherit_ownership_codegen_metadata_from(tc)
forked.set_fresh_type_cache_based_on(tc, tc.type_cache_parse_enabled())
@@ -2046,6 +2071,8 @@ pub fn (tc &TypeChecker) fork_for_parallel_transform(ast &flat.FlatAst) &TypeChe
// Visible-mutation analysis only runs during checking. Do not allocate its
// three private maps for the many short-lived transform forks.
forked.visible_mutation_cache = unsafe { nil }
// Completed root queries are private to this immutable graph view.
forked.global_store_cache = &FnGlobalStoreCache{}
if check_memos_enabled() {
// Per-fork memos; the fork (and these caches) live inside one batch
// scope, so cached scratch-arena strings can never outlive their arena.
@@ -2068,6 +2095,7 @@ pub fn (tc &TypeChecker) fork_for_parallel_transform(ast &flat.FlatAst) &TypeChe
// Transformer.merge_worker / absorb_scoped_batch replay into the parent
// through apply_forked_fn_value.
forked.sparse_resolved_fn_values = tc.fn_value_snapshot()
forked.rebuild_sparse_fn_value_filter()
forked.fork_fn_value_writes = map[int]string{}
// Transform helpers allocate inside disposable arenas. A shared interner
// would let one helper publish map/array storage owned by its arena and leave
@@ -2083,8 +2111,9 @@ pub fn (tc &TypeChecker) fork_for_parallel_transform(ast &flat.FlatAst) &TypeChe
tc.post_check_dependencies
} else {
&DependencyGraphView{
by_fn: tc.direct_dependencies_by_fn
names: if isnil(tc.symbols) { []string{} } else { tc.symbols.names }
by_fn: tc.direct_dependencies_by_fn
names: if isnil(tc.symbols) { []string{} } else { tc.symbols.names }
declarations: tc.visible_mutation_cache
}
}
forked.type_cache = new_type_cache_with_base(if tc.type_cache != unsafe { nil } {
@@ -2106,6 +2135,10 @@ pub fn (tc &TypeChecker) fork_for_parallel_transform(ast &flat.FlatAst) &TypeChe
// transform worker writes them. The worker's private maps are merged while its
// disposable arena is still alive.
pub fn (mut tc TypeChecker) ensure_private_transform_signatures() {
if !isnil(tc.global_store_cache) {
mut cache := tc.global_store_cache
cache.values.clear()
}
if tc.transform_signature_maps_shared {
tc.fn_ret_types = tc.fn_ret_types.clone()
tc.fn_param_types = tc.fn_param_types.clone()
@@ -2391,6 +2424,7 @@ fn (mut tc TypeChecker) reset_node_caches(n int) {
// both the serial and the parallel cache initialization.
fn (mut tc TypeChecker) reset_sparse_fn_values() {
tc.sparse_resolved_fn_values.clear()
tc.sparse_fn_value_filter_ready = false
tc.fork_fn_value_writes.clear()
}
@@ -5773,6 +5807,16 @@ fn (mut tc TypeChecker) build_const_suffixes() {
}
}
tc.const_suffix_key_count = tc.const_types.len
// Include the complete existing suffix table, including ambiguous suffixes.
tc.const_selector_bloom = NameBloom{}
for name, _ in tc.const_types {
tc.const_selector_bloom.add(name)
}
for name, _ in tc.const_suffixes {
tc.const_selector_bloom.add(name)
}
tc.const_selector_bloom.count = u64(tc.const_types.len)
tc.const_selector_suffix_count = tc.const_suffixes.len
}
// prepare_check_name_filters takes short names of globals, constants, sum types and aliases
@@ -5916,8 +5960,14 @@ fn generic_base_view(name string) string {
// segment and no bare const key, so no selector with this field names a const.
@[inline]
fn (tc &TypeChecker) selector_names_no_const(field string) bool {
return tc.const_suffix_key_count == tc.const_types.len && field !in tc.const_suffixes
&& field !in tc.const_types
if tc.const_suffix_key_count != tc.const_types.len {
return false
}
if tc.const_selector_suffix_count == tc.const_suffixes.len
&& tc.const_selector_bloom.excludes(field, u64(tc.const_types.len)) {
return true
}
return field !in tc.const_suffixes && field !in tc.const_types
}
// selector_names_no_global reports that no global has the last name segment of
@@ -6884,12 +6934,19 @@ pub fn (tc &TypeChecker) parse_resolution_type(source_id u32) Type {
return *direct_view.parse_type(qualified_source_id)
}
mut views := unsafe { tc.resolution_type_views }
if !isnil(views.recent_unscoped) && views.recent_unscoped_file == tc.cur_file {
return *views.recent_unscoped.parse_type(qualified_source_id)
}
if cached := views.by_file[tc.cur_file] {
views.recent_unscoped_file = tc.cur_file
views.recent_unscoped = cached
return *cached.parse_type(qualified_source_id)
}
mut unscoped := tc.fork_type_parse_view(tc.cur_file, '')
unscoped.resolution_type_mode = false
views.by_file[tc.cur_file] = unscoped
views.recent_unscoped_file = tc.cur_file
views.recent_unscoped = unscoped
return *unscoped.parse_type(qualified_source_id)
}
@@ -6899,16 +6956,24 @@ pub fn (tc &TypeChecker) parse_resolution_type_in_file(source_id u32, file strin
if context_independent_type(source_id) {
return *tc.parse_type(source_id)
}
module_name := tc.file_modules[file] or { '' }
if !isnil(tc.resolution_type_views) {
mut views := unsafe { tc.resolution_type_views }
if !isnil(views.recent_scoped) && views.recent_scoped_file == file {
return views.recent_scoped.parse_resolution_type(source_id)
}
if cached := views.scoped_by_file[file] {
views.recent_scoped_file = file
views.recent_scoped = cached
return cached.parse_resolution_type(source_id)
}
module_name := tc.file_modules[file] or { '' }
mut scoped := tc.fork_type_parse_view(file, module_name)
views.scoped_by_file[file] = scoped
views.recent_scoped_file = file
views.recent_scoped = scoped
return scoped.parse_resolution_type(source_id)
}
module_name := tc.file_modules[file] or { '' }
mut scoped := tc.fork_type_parse_view(file, module_name)
return scoped.parse_resolution_type(source_id)
}
@@ -8272,16 +8337,17 @@ fn (tc &TypeChecker) type_symbol_known(name string) bool {
}
fn (tc &TypeChecker) unqualified_type_symbol_is_builtin(name string) bool {
if tc.unqualified_type_symbol_has_scoped_shadow(name) {
return false
}
if mod_name := tc.struct_modules[name] {
return mod_name == 'builtin'
if mod_name != 'builtin' {
return false
}
} else if name != 'IError' && name !in tc.enum_names && name !in tc.flag_enums {
return false
}
// Builtin enum declarations use their unqualified source name, just like
// builtin structs. Keep that name visible from imported modules unless a
// declaration in the active scope shadows it.
return name == 'IError' || name in tc.enum_names || name in tc.flag_enums
return !tc.unqualified_type_symbol_has_scoped_shadow(name)
}
fn (tc &TypeChecker) unqualified_type_symbol_has_scoped_shadow(name string) bool {
@@ -8315,8 +8381,8 @@ fn (tc &TypeChecker) unqualified_resolved_type_symbol_is_builtin(name string) bo
fn (tc &TypeChecker) type_from_known_symbol(name_id u32) ?Type {
name := flat.source_type_name(name_id)
if tc.type_aliases.contains(name) {
return tc.parse_alias_type(name_id, tc.type_aliases.value_id(name))
if target_id := tc.type_aliases.type_id(name) {
return tc.parse_alias_type(name_id, target_id)
}
if name in tc.structs {
return Type(Struct{
@@ -10204,7 +10270,7 @@ fn (tc &TypeChecker) builtin_array_annotation_expected_type(node flat.Node, arg_
if node.children_count == 0 {
return builtin_void_type
}
mut callee := tc.a.child_node(&node, 0)
mut callee := tc.a.node(tc.a.child(&node, 0))
if callee.kind == .index && callee.children_count > 0 {
callee = tc.a.child_node(callee, 0)
}
@@ -11097,8 +11163,7 @@ pub fn (tc &TypeChecker) resolved_call_name(id flat.NodeId) ?string {
// transitive callees belongs to a source file annotated with `@[has_globals]` or `@[translated]`.
pub fn (tc &TypeChecker) resolved_call_may_store_globally(id flat.NodeId) bool {
name := tc.cached_resolved_call(id) or { return false }
mut visiting := map[string]bool{}
return tc.fn_may_store_globally(name, mut visiting)
return tc.cached_fn_may_store_globally(name)
}
// fn_value_may_store_globally reports whether invoking a function value can reach a
@@ -11106,6 +11171,22 @@ pub fn (tc &TypeChecker) resolved_call_may_store_globally(id flat.NodeId) bool {
// value is conservative because its body is opaque at this call site.
pub fn (tc &TypeChecker) fn_value_may_store_globally(id flat.NodeId) bool {
name := tc.resolved_fn_value_name(id) or { return true }
return tc.cached_fn_may_store_globally(name)
}
fn (tc &TypeChecker) cached_fn_may_store_globally(name string) bool {
if !isnil(tc.post_check_dependencies) && !isnil(tc.global_store_cache) {
mut cache := tc.global_store_cache
if result := cache.values[name] {
return result
}
mut visiting := map[string]bool{}
result := tc.fn_may_store_globally(name, mut visiting)
// A recursive branch can be false only because an ancestor is
// already visiting. Cache completed root answers, never DFS branches.
cache.values[name] = result
return result
}
mut visiting := map[string]bool{}
return tc.fn_may_store_globally(name, mut visiting)
}
@@ -11120,7 +11201,7 @@ fn (tc &TypeChecker) fn_may_store_globally(name string, mut visiting map[string]
return true
}
decl_module := tc.fn_type_modules[name] or { '' }
decl := tc.visible_mutation_fn_decl(name, decl_module) or { return false }
decl := tc.global_store_fn_decl(name, decl_module) or { return false }
if !isnil(tc.post_check_dependencies) {
// A transform fork: other workers may be rewriting callee bodies in place,
// so follow only the call graph recorded while checking.
@@ -11149,6 +11230,25 @@ fn (tc &TypeChecker) fn_may_store_globally(name string, mut visiting map[string]
return false
}
fn (tc &TypeChecker) global_store_fn_decl(name string, fallback_mod string) ?VisibleMutationFnDecl {
if !isnil(tc.post_check_dependencies) {
index := tc.post_check_dependencies.declarations
if !isnil(index) && index.decl_index_ready {
lookup_name := visible_mutation_fn_lookup_name(name)
if decl := index.global_decls[lookup_name] {
if fallback_mod == '' || fallback_mod == decl.mod {
return decl
}
}
if decl := index.decls['${fallback_mod}\x01${lookup_name}'] {
return decl
}
}
}
// Generated declarations may not exist in the frozen source index.
return tc.visible_mutation_fn_decl(name, fallback_mod)
}
fn (tc &TypeChecker) node_calls_fn_that_may_store_globally(id flat.NodeId, caller_mod string, mut visiting map[string]bool) bool {
if int(id) < 0 || int(id) >= tc.a.nodes.len {
return false
@@ -11258,21 +11358,21 @@ fn (tc &TypeChecker) name_never_returns(name string) bool {
if name in tc.a.noreturn_fns {
return true
}
// Only dotted suffixes can match a qualified declaration. Probe those few
// names instead of scanning every noreturn declaration for every call.
mut has_dot := false
for i := name.len - 1; i >= 0; i-- {
if name[i] != `.` {
continue
// Only dotted suffixes can match a qualified declaration. Search complete
// words for each preceding dot instead of examining every name byte here.
mut end := name.last_index_u8(`.`)
for end > 0 {
// Both searches and the membership query only borrow these spans.
prefix := unsafe { name.substr_unsafe(0, end) }
dot := prefix.last_index_u8(`.`)
if dot < 0 {
break
}
if has_dot {
// The membership query does not retain the suffix.
suffix := unsafe { name.substr_unsafe(i + 1, name.len) }
if tc.a.noreturn_fns[suffix] {
return true
}
suffix := unsafe { name.substr_unsafe(dot + 1, name.len) }
if tc.a.noreturn_fns[suffix] {
return true
}
has_dot = true
end = dot
}
return false
}
@@ -11312,9 +11412,40 @@ pub fn (tc &TypeChecker) resolved_fn_value_name(id flat.NodeId) ?string {
return name
}
}
if tc.sparse_fn_value_filter_ready {
bit := sparse_fn_value_filter_bit(idx)
if tc.sparse_fn_value_filter[bit >> 6] & (u64(1) << (bit & 63)) == 0 {
return none
}
}
return tc.sparse_resolved_fn_values[idx] or { none }
}
@[inline]
fn sparse_fn_value_filter_bit(idx int) u32 {
key := u32(idx)
return (key ^ (key >> 8)) & 255
}
@[direct_array_access; inline]
fn (mut tc TypeChecker) record_sparse_fn_value_filter_key(idx int) {
if tc.sparse_fn_value_filter_ready {
bit := sparse_fn_value_filter_bit(idx)
tc.sparse_fn_value_filter[bit >> 6] |= u64(1) << (bit & 63)
}
}
// rebuild_sparse_fn_value_filter runs only after taking an owned snapshot.
// Fork source maps are otherwise read-only; later setters maintain the filter,
// and deletions may retain bits because a false positive still checks the map.
fn (mut tc TypeChecker) rebuild_sparse_fn_value_filter() {
tc.sparse_fn_value_filter = [4]u64{}
tc.sparse_fn_value_filter_ready = true
for idx, _ in tc.sparse_resolved_fn_values {
tc.record_sparse_fn_value_filter_key(idx)
}
}
// set_resolved_fn_value records the resolved function-value target of a node.
// A transform fork keeps the write private (its snapshot of the parent stays
// read-only) until the merge replays it.
@@ -11327,6 +11458,7 @@ pub fn (mut tc TypeChecker) set_resolved_fn_value(idx int, name string) {
return
}
tc.sparse_resolved_fn_values[idx] = name
tc.record_sparse_fn_value_filter_key(idx)
}
// clear_resolved_fn_value removes stale function-value metadata after a later
@@ -11403,8 +11535,15 @@ pub fn (mut tc TypeChecker) apply_forked_fn_value(idx int, name string) {
// DependencyGraphView is the checker's post-check call graph with the symbol
// names its ids refer to. Transform forks share it read-only.
struct DependencyGraphView {
by_fn map[int][]SymbolId
names []string
by_fn map[int][]SymbolId
names []string
declarations &VisibleMutationCache = unsafe { nil }
}
@[heap]
struct FnGlobalStoreCache {
mut:
values map[string]bool
}
// direct_dependency_ids returns the checker-resolved function dependency
@@ -13358,6 +13497,11 @@ fn (tc &TypeChecker) malformed_const_keyword_pos(id flat.NodeId) ?token.Pos {
return none
}
pos := tc.a.node(id).pos
file_id := int(pos.id)
if file_id >= 0 && file_id < tc.malformed_const_source_states.len
&& tc.malformed_const_source_states[file_id] == 1 {
return none
}
file := tc.a.source_files[pos.id] or { return none }
source := tc.source_texts_by_file[file.name] or { return none }
if pos.offset < 0 || pos.offset > source.len {
@@ -20758,6 +20902,12 @@ fn (tc &TypeChecker) match_branch_definitely_returns(branch &flat.Node) bool {
}
fn (tc &TypeChecker) stmt_sequence_definitely_returns(node &flat.Node, body_start int) bool {
if body_start < node.children_count {
tail_id := tc.a.child(node, int(node.children_count) - 1)
if tc.valid_node_id(tail_id) && tc.a.nodes[int(tail_id)].kind == .return_stmt {
return true
}
}
// Only the `smartcasts` binding needs isolation here (see
// branch_tail_never_returns); avoid the ~11KB full struct copy.
mut mtc := unsafe { &TypeChecker(voidptr(tc)) }
+1
View File
@@ -5714,6 +5714,7 @@ fn (mut tc TypeChecker) merge_parallel_check_worker_scoped(w &TypeChecker, scope
for idx, name in w.sparse_resolved_fn_values {
owned_name := if scoped { name.clone() } else { name }
tc.sparse_resolved_fn_values[idx] = owned_name
tc.record_sparse_fn_value_filter_key(idx)
}
for idx, _ in w.sparse_statement_nodes {
if tc.parallel_check_sparse {
+44 -12
View File
@@ -5610,7 +5610,7 @@ fn (tc &TypeChecker) translated_named_cast_call_name(id flat.NodeId, node flat.N
if node.children_count != 2 || !tc.node_is_in_translated_file(id) {
return none
}
callee := tc.a.child_node(&node, 0)
callee := tc.a.node(tc.a.child(&node, 0))
if callee.kind != .ident || tc.ident_resolves_to_value(callee.value) {
return none
}
@@ -6921,7 +6921,7 @@ fn (tc &TypeChecker) unknown_import_function_call_parts(node flat.Node) ?(flat.N
if node.kind != .call || node.children_count == 0 {
return none
}
callee := tc.a.child_node(&node, 0)
callee := tc.a.node(tc.a.child(&node, 0))
if callee.kind != .selector || callee.children_count == 0 {
return none
}
@@ -7381,7 +7381,7 @@ fn (tc &TypeChecker) explicit_generic_local_fn_value(node flat.Node) ?FnType {
if node.children_count == 0 {
return none
}
callee := tc.a.child_node(&node, 0)
callee := tc.a.node(tc.a.child(&node, 0))
if callee.kind != .index || callee.children_count < 2 {
return none
}
@@ -7477,7 +7477,7 @@ fn (tc &TypeChecker) deprecated_call_diagnostic_pos(node flat.Node, is_method bo
if !is_method || node.children_count == 0 {
return node.pos
}
selector := tc.a.child_node(&node, 0)
selector := tc.a.node(tc.a.child(&node, 0))
if selector.kind != .selector {
return node.pos
}
@@ -9087,6 +9087,7 @@ fn (mut tc TypeChecker) index_multiple_module_import_lines(a &flat.FlatAst) {
tc.multiple_module_import_lines = map[u64]bool{}
tc.source_texts_by_file = map[string]string{}
tc.source_texts_by_id = map[int]string{}
tc.malformed_const_source_states = []u8{}
tc.import_line_indexed_files = map[int]bool{}
tc.index_module_import_lines_of_new_files(a)
}
@@ -9095,11 +9096,18 @@ fn (mut tc TypeChecker) index_multiple_module_import_lines(a &flat.FlatAst) {
// not indexed yet.
fn (mut tc TypeChecker) index_module_import_lines_of_new_files(a &flat.FlatAst) {
mut scans := []ImportLineScan{}
mut dense_file_id := tc.malformed_const_source_states.len - 1
// Parser file ids are dense. Sparse ids supplied by tools retain the fallback
// rather than making this diagnostic filter allocate an oversized table.
dense_limit := a.source_files.len * 2 + 64
for file_id, file in a.source_files {
if tc.import_line_indexed_files[file_id] {
continue
}
tc.import_line_indexed_files[file_id] = true
if file_id >= 0 && file_id <= dense_limit && file_id > dense_file_id {
dense_file_id = file_id
}
// The parser may have kept the text it read (flat.FlatAst.source_texts).
has_source := file_id in a.source_texts
scans << ImportLineScan{
@@ -9109,6 +9117,9 @@ fn (mut tc TypeChecker) index_module_import_lines_of_new_files(a &flat.FlatAst)
source: if has_source { a.source_texts[file_id] } else { '' }
}
}
if dense_file_id >= tc.malformed_const_source_states.len {
tc.malformed_const_source_states << []u8{len: dense_file_id + 1 - tc.malformed_const_source_states.len}
}
// Reading and scanning one file does not depend on the others. The pool's
// threads keep what they allocate, so the texts outlive the batch.
pool := checker_worker_pool(a)
@@ -9137,16 +9148,37 @@ fn (mut tc TypeChecker) index_module_import_lines_of_new_files(a &flat.FlatAst)
scan.run()
}
}
mut duplicate_names := map[string]bool{}
for scan in scans {
if !scan.read {
continue
}
if scan.name in tc.source_texts_by_file {
duplicate_names[scan.name] = true
}
tc.source_texts_by_file[scan.name] = scan.source
tc.source_texts_by_id[scan.file_id] = scan.source
if scan.file_id >= 0 && scan.file_id < tc.malformed_const_source_states.len {
tc.malformed_const_source_states[scan.file_id] = if scan.source.contains('cosnt') {
u8(2)
} else {
u8(1)
}
}
for line in scan.lines {
tc.multiple_module_import_lines[multiple_module_import_line_key(scan.file_id, line)] = true
}
}
if duplicate_names.len > 0 {
// The diagnostic reads by filename. Different ids for one filename can
// therefore see another id's text, including an earlier indexed file.
for file_id, file in a.source_files {
if file_id >= 0 && file_id < tc.malformed_const_source_states.len
&& duplicate_names[file.name] {
tc.malformed_const_source_states[file_id] = 0
}
}
}
}
// ImportLineScan is the source text of one file and the lines of it that
@@ -9377,7 +9409,7 @@ fn (mut tc TypeChecker) resolve_call_info_uncached(id flat.NodeId, node flat.Nod
if node.children_count == 0 {
return none
}
fn_node := tc.a.child_node(&node, 0)
fn_node := tc.a.node(tc.a.child(&node, 0))
if fn_node.kind == .selector && '__v3_comptime_method_selector' in fn_node.generic_params()
&& fn_node.typ != 0 {
return CallInfo{
@@ -14924,7 +14956,7 @@ fn (tc &TypeChecker) visible_mutation_call_name(call_id flat.NodeId, call flat.N
if call.children_count == 0 {
return ''
}
mut fn_node := tc.a.child_node(&call, 0)
mut fn_node := tc.a.node(tc.a.child(&call, 0))
if fn_node.kind == .index && fn_node.children_count > 0 {
fn_node = tc.a.child_node(fn_node, 0)
}
@@ -17569,7 +17601,7 @@ fn (tc &TypeChecker) call_argument_target_name(node flat.Node, info CallInfo) st
return 'Map.delete'
}
if node.children_count > 0 {
mut callee := tc.a.child_node(&node, 0)
mut callee := tc.a.node(tc.a.child(&node, 0))
if callee.kind == .index && callee.children_count > 0 {
callee = tc.a.child_node(callee, 0)
}
@@ -17600,7 +17632,7 @@ fn (tc &TypeChecker) call_argument_target_name(node flat.Node, info CallInfo) st
if node.children_count == 0 {
return tc.call_display_name(node)
}
callee := tc.a.child_node(&node, 0)
callee := tc.a.node(tc.a.child(&node, 0))
if callee.kind in [.fn_literal, .lambda_expr] {
return 'anon'
}
@@ -21710,7 +21742,7 @@ fn (tc &TypeChecker) direct_call_return_type(node flat.Node) ?Type {
if node.children_count == 0 {
return none
}
fn_node := tc.a.child_node(&node, 0)
fn_node := tc.a.node(tc.a.child(&node, 0))
if fn_node.kind == .index && fn_node.children_count >= 2 && fn_node.value != 'range' {
base_node := tc.a.child_node(fn_node, 0)
name := tc.generic_call_base_name(base_node) or { return none }
@@ -21800,7 +21832,7 @@ fn (tc &TypeChecker) spawn_child_call_return_type(node flat.Node) ?Type {
if node.children_count == 0 {
return none
}
fn_node := tc.a.child_node(&node, 0)
fn_node := tc.a.node(tc.a.child(&node, 0))
if fn_node.kind == .selector && fn_node.children_count > 0 {
base_type := tc.resolve_type(tc.a.child(fn_node, 0))
clean_base := unwrap_all_pointers(base_type)
@@ -22618,7 +22650,7 @@ fn (tc &TypeChecker) c_va_macro_call_name(node flat.Node) string {
if node.children_count == 0 {
return ''
}
callee := tc.a.child_node(&node, 0)
callee := tc.a.node(tc.a.child(&node, 0))
if callee.kind == .selector && callee.children_count > 0 {
if tc.a.child_node(callee, 0).value != 'C' {
return ''
@@ -22646,7 +22678,7 @@ fn (tc &TypeChecker) call_display_name(node flat.Node) string {
if node.children_count == 0 {
return '<missing>'
}
fn_node := tc.a.child_node(&node, 0)
fn_node := tc.a.node(tc.a.child(&node, 0))
if fn_node.kind == .ident {
return fn_node.value_text()
}
+69 -28
View File
@@ -5656,7 +5656,7 @@ fn (tc &TypeChecker) bound_method_node_source_type_text(node flat.Node) string {
mut params := []string{}
mut skipped_receiver := false
for i in 0 .. node.children_count {
param := tc.a.child_node(&node, i)
param := tc.a.node(tc.a.child(&node, i))
if param.kind != .param {
continue
}
@@ -5685,7 +5685,7 @@ fn (tc &TypeChecker) bound_method_node_source_type_text(node flat.Node) string {
fn (tc &TypeChecker) fn_node_source_type_text(node flat.Node) string {
mut params := []string{}
for i in 0 .. node.children_count {
param := tc.a.child_node(&node, i)
param := tc.a.node(tc.a.child(&node, i))
if param.kind != .param {
continue
}
@@ -6258,7 +6258,7 @@ fn (tc &TypeChecker) source_struct_field_decls(struct_name_id u32) []SourceStruc
}
mut fields := []SourceStructFieldDecl{}
for i in 0 .. decl.children_count {
field := tc.a.child_node(&decl, i)
field := tc.a.node(tc.a.child(&decl, i))
if field.kind != .field_decl {
continue
}
@@ -7783,8 +7783,10 @@ fn (tc &TypeChecker) selector_type(_id flat.NodeId, node flat.Node) ?Type {
if base_node.kind == .ident && base_node.value == 'C' && c_int_selector_name(node.value) {
return builtin_int_type
}
if typ := tc.enum_selector_type(&node) {
return typ
if node.kind == .selector {
if typ := tc.enum_selector_type_from_base(base_id, node.value) {
return typ
}
}
if base_node.kind == .typeof_expr {
if node.value == 'name' {
@@ -9885,7 +9887,7 @@ fn (tc &TypeChecker) fn_value_key(node flat.Node) ?string {
return tc.selector_fn_value_key(node)
}
if node.kind in [.cast_expr, .paren, .expr_stmt] && node.children_count > 0 {
return tc.fn_value_key(tc.a.child_node(&node, 0))
return tc.fn_value_key(tc.a.node(tc.a.child(&node, 0)))
}
return none
}
@@ -10563,7 +10565,11 @@ fn (tc &TypeChecker) enum_selector_type(node &flat.Node) ?Type {
if node.kind != .selector || node.children_count == 0 {
return none
}
base := tc.a.child_node(node, 0)
return tc.enum_selector_type_from_base(tc.a.child(node, 0), node.value)
}
fn (tc &TypeChecker) enum_selector_type_from_base(base_id flat.NodeId, field string) ?Type {
base := tc.a.node(base_id)
mut enum_name := flat.source_type_id('')
if base.kind == .ident {
if !tc.ident_may_be_type(base.value) {
@@ -10577,7 +10583,7 @@ fn (tc &TypeChecker) enum_selector_type(node &flat.Node) ?Type {
enum_name = (tc.resolve_enum_name(flat.source_type_id('${mod_name}.${base.value}')) or { flat.source_type_id('') })
}
}
if enum_name == 0 || !tc.enum_has_field(enum_name, node.value) {
if enum_name == 0 || !tc.enum_has_field(enum_name, field) {
return none
}
return Type(Enum{
@@ -16142,27 +16148,39 @@ pub fn (tc &TypeChecker) sum_variant_type_for_pattern(sum_name_id u32, variant_n
}
mut cache := tc.type_cache
key := (u64(sum_name_id) << 32) | u64(variant_name_id)
if file_entries := cache.sum_variant_pattern_entries[tc.cur_file] {
if module_entries := file_entries.modules[tc.cur_module] {
if result := module_entries.values[key] {
if result == 0 { return none }
return result
mut context := cache.sum_variant_pattern_recent
if isnil(context) || cache.sum_variant_pattern_recent_count != cache.sum_variant_pattern_entries.len
|| context.file != tc.cur_file || context.module != tc.cur_module {
context = unsafe { nil }
if file_entries := cache.sum_variant_pattern_entries[tc.cur_file] {
if module_entries := file_entries.modules[tc.cur_module] {
context = module_entries
cache.sum_variant_pattern_recent = context
cache.sum_variant_pattern_recent_count = cache.sum_variant_pattern_entries.len
}
}
}
if !isnil(context) {
if result := context.values[key] {
if result == 0 { return none }
return result
}
}
result := tc.sum_variant_type_for_pattern_depth(sum_name_id, variant_name_id, 0) or { u32(0) }
// Resolution can populate this context recursively; acquire its current tables again.
mut file_entries := cache.sum_variant_pattern_entries[tc.cur_file] or {
created := &SumVariantPatternFileEntries{}
cache.sum_variant_pattern_entries[tc.cur_file.clone()] = created
created := &SumVariantPatternFileEntries{ file: tc.cur_file.clone() }
cache.sum_variant_pattern_entries[created.file] = created
created
}
mut module_entries := file_entries.modules[tc.cur_module] or {
created := &SumVariantPatternModuleEntries{}
file_entries.modules[tc.cur_module.clone()] = created
created := &SumVariantPatternModuleEntries{ file: file_entries.file, module: tc.cur_module.clone() }
file_entries.modules[created.module] = created
created
}
module_entries.values[key] = result
cache.sum_variant_pattern_recent = module_entries
cache.sum_variant_pattern_recent_count = cache.sum_variant_pattern_entries.len
if result == 0 { return none }
return result
}
@@ -19455,6 +19473,29 @@ pub fn (tc &TypeChecker) resolve_type(id flat.NodeId) Type {
return tc.resolve_type_slow(id)
}
// resolve_type_reference returns the canonical reference for an expression's resolved type.
// It follows resolve_type's checked-cache visibility and mixed-integer widening.
// The reference remains valid while the owning checker arena is live.
@[direct_array_access; inline]
pub fn (tc &TypeChecker) resolve_type_reference(id flat.NodeId) &Type {
if tc.trust_checked_expr_types && isnil(tc.storage_query_probe) {
tidx := int(id)
if tidx >= 0 && tidx < tc.expr_type_set.len && tc.expr_type_set[tidx]
&& (!tc.parallel_check_sparse || tc.in_check_range(tidx)) {
typ := tc.expr_type_values[tidx]
if !type_contains_unknown(typ) {
// Non-arithmetic nodes retain their existing canonical identity.
if !mixed_integer_expr_type_can_widen(typ) || tidx >= tc.a.nodes.len
|| tc.a.nodes[tidx].kind != .infix {
return typ
}
return tc.intern_type_reference(tc.widen_mixed_integer_infix_type(id, typ))
}
}
}
return tc.intern_type_reference(tc.resolve_type_slow(id))
}
@[direct_array_access]
fn (tc &TypeChecker) resolve_type_slow(id flat.NodeId) Type {
if tc.trust_checked_expr_types && !isnil(tc.storage_query_probe) {
@@ -20904,7 +20945,7 @@ fn (tc &TypeChecker) fn_literal_type(node flat.Node) Type {
mut reached_params := false
mut is_variadic := false
for i in 0 .. node.children_count {
child := tc.a.child_node(&node, i)
child := tc.a.node(tc.a.child(&node, i))
if child.kind != .param {
// Captures precede parameters in a function literal's flat children, so a
// prefix-only scan must skip those identifiers, then stop once the body begins.
@@ -21098,7 +21139,7 @@ fn (tc &TypeChecker) explicit_generic_fn_value_type(node flat.Node) ?Type {
if node.kind != .index || node.children_count < 2 || node.value == 'range' {
return none
}
base_node := tc.a.child_node(&node, 0)
base_node := tc.a.node(tc.a.child(&node, 0))
if base_node.kind == .ident && tc.ident_resolves_to_value(base_node.value) {
return none
}
@@ -22911,12 +22952,15 @@ fn (tc &TypeChecker) int_literal_promoted_infix_type(lit_id flat.NodeId, other_i
if tc.int_literal_value(other_id) != none {
return none
}
value := tc.implicit_integer_constant_value(lit_id, lit_type)?
clean_type := unalias_type(other_type)
// An untyped integer literal adopts a float operand's type too, including an
// alias of a float: 1 + f where is ype Fraction = f64 is Fraction,
// not int.
if clean_type.is_float() {
// Reject ineligible storage before recursively evaluating the constant.
is_float := clean_type.is_float()
if !is_float && (!clean_type.is_integer() || tc.type_name(clean_type) == flat.source_type_int) {
return none
}
value := tc.implicit_integer_constant_value(lit_id, lit_type)?
// Untyped integer constants also adopt a float operand, including aliases.
if is_float {
return other_type
}
if clean_type is Primitive && clean_type.props.has(.integer)
@@ -22929,10 +22973,7 @@ fn (tc &TypeChecker) int_literal_promoted_infix_type(lit_id flat.NodeId, other_i
// An untyped integer literal adopts the other integer operand's concrete storage
// type. In particular, `24 * time.hour` is `i64`, not `int`; map literal
// inference relies on that distinction when it chooses its value type.
if clean_type.is_integer() && tc.type_name(clean_type) != flat.source_type_int {
return *clean_type
}
return none
return *clean_type
}
fn infix_integer_promotion_type(lhs Type, rhs Type) ?Type {
@@ -0,0 +1,62 @@
module types
import v.flat
fn assert_const_selector_filter_matches_tables(tc &TypeChecker, names []string) {
for name in names {
expected := tc.const_suffix_key_count == tc.const_types.len
&& name !in tc.const_suffixes && name !in tc.const_types
assert tc.selector_names_no_const(name) == expected, name
}
}
fn test_const_selector_filter_preserves_bare_qualified_and_ambiguous_names() {
mut a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
for name in ['bare', 'one.limit', 'two.limit', 'deep.module.width', 'éclair', '_hidden'] {
tc.const_types[name] = builtin_int_type
}
// Existing suffix entries must remain visible even when not derived from this collection.
tc.const_suffixes['old_suffix'] = 'prior.old_suffix'
tc.build_const_suffixes()
assert tc.const_suffixes['limit'] == ''
assert !tc.selector_names_no_const('old_suffix')
for name, _ in tc.const_types {
assert !tc.selector_names_no_const(name), name
}
for name, _ in tc.const_suffixes {
assert !tc.selector_names_no_const(name), name
}
mut names := ['bare', 'limit', 'width', 'module.width', 'deep.module.width', 'one.limit',
'old_suffix', '', 'éclair', '_hidden', 'runtime_local']
for i in 0 .. 1000 {
names << 'absent_${i}'
}
assert_const_selector_filter_matches_tables(tc, names)
fork := tc.fork_for_parallel_codegen()
assert fork.const_selector_bloom.count == tc.const_selector_bloom.count
assert fork.const_selector_suffix_count == tc.const_suffixes.len
assert_const_selector_filter_matches_tables(fork, names)
}
fn test_const_selector_filter_falls_back_after_collection_changes() {
mut a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.const_types['pkg.first'] = builtin_int_type
tc.build_const_suffixes()
tc.const_suffixes['added_suffix'] = 'pkg.added_suffix'
assert !tc.selector_names_no_const('added_suffix')
assert_const_selector_filter_matches_tables(tc, ['first', 'added_suffix', 'missing'])
tc.const_types['pkg.second'] = builtin_int_type
assert !tc.selector_names_no_const('second')
assert !tc.selector_names_no_const('missing')
tc.build_const_suffixes()
assert_const_selector_filter_matches_tables(tc, ['first', 'second', 'added_suffix', 'missing'])
// Value changes preserve the key set and may continue to use the snapshot.
tc.const_types['pkg.first'] = builtin_string_type
assert_const_selector_filter_matches_tables(tc, ['first', 'second', 'missing'])
tc.const_types.delete('pkg.second')
assert !tc.selector_names_no_const('missing')
tc.build_const_suffixes()
assert_const_selector_filter_matches_tables(tc, ['first', 'second', 'added_suffix', 'missing'])
}
+44
View File
@@ -0,0 +1,44 @@
module types
import v.flat
fn test_source_struct_field_decls_keeps_field_names_and_source_types() {
mut a := flat.FlatAst.new()
file := a.add_val(.file, 'model.v')
mod := a.add_val(.module_decl, 'model')
field := a.add_node(flat.Node{ kind: .field_decl, value: 'value', typ: flat.source_type_int })
non_field := a.add_val(.ident, 'ignored')
a.children << field
a.children << non_field
decl := a.add_node(flat.Node{ kind: .struct_decl, value: 'Item', children_start: 0, children_count: 2 })
mut tc := TypeChecker.new(&a)
tc.cur_module = 'model'
tc.top_level_idx = [int(file), int(mod), int(decl)]
tc.struct_files['model.Item'] = 'model.v'
fields := tc.source_struct_field_decls(flat.source_type_id('model.Item'))
assert fields.len == 1
assert fields[0].name == 'value'
assert fields[0].typ == flat.source_type_int
assert tc.source_struct_field_decls(flat.source_type_id('Missing')).len == 0
}
fn test_enum_selector_keeps_saved_header_after_ast_reallocation() {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'Color')
a.children << base
selector := a.add_node(flat.Node{ kind: .selector, value: 'red', children_start: 0, children_count: 1 })
snapshot := a.nodes[int(selector)]
a.nodes[int(selector)].children_count = 0
growth := a.nodes.cap + 100
for _ in 0 .. growth {
a.add_val(.ident, 'padding')
}
mut tc := TypeChecker.new(&a)
tc.enum_names['Color'] = true
tc.enum_fields['Color'] = ['red']
from_pointer := tc.enum_selector_type(&snapshot) or { panic('missing enum selector') }
from_value := tc.selector_type(selector, snapshot) or { panic('missing saved selector') }
assert from_pointer.source_type_id() == flat.source_type_id('Color')
assert from_value.source_type_id() == flat.source_type_id('Color')
assert snapshot.children_count == 1
}
+117
View File
@@ -0,0 +1,117 @@
module types
import v.flat
fn test_fn_value_filter_snapshot_hits_misses_collisions_and_empty_names() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.set_resolved_fn_value(3, 'main.callback')
tc.set_resolved_fn_value(5, '')
forked := tc.fork_for_parallel_codegen()
assert forked.sparse_fn_value_filter_ready
assert forked.resolved_fn_value_name(3)? == 'main.callback'
assert forked.resolved_fn_value_name(5)? == ''
assert forked.resolved_fn_value_name(4) == none
// 258 hashes to the same bit as 3; the final map lookup rejects it.
assert sparse_fn_value_filter_bit(258) == sparse_fn_value_filter_bit(3)
assert forked.resolved_fn_value_name(258) == none
assert forked.resolved_fn_value_name(-1) == none
}
fn test_fn_value_filter_stays_disabled_on_mutable_master_direct_writes() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
assert !tc.sparse_fn_value_filter_ready
tc.sparse_resolved_fn_values[3] = 'main.first'
assert tc.resolved_fn_value_name(3)? == 'main.first'
tc.sparse_resolved_fn_values.delete(3)
tc.sparse_resolved_fn_values[4] = 'main.replacement'
assert tc.resolved_fn_value_name(3) == none
assert tc.resolved_fn_value_name(4)? == 'main.replacement'
}
fn test_fn_value_filter_codegen_set_clear_and_reset_keep_exact_membership() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.set_resolved_fn_value(3, 'main.original')
mut forked := tc.fork_for_parallel_codegen()
assert forked.resolved_fn_value_name(4) == none
forked.set_resolved_fn_value(4, 'main.later')
assert forked.resolved_fn_value_name(4)? == 'main.later'
forked.clear_resolved_fn_value(4)
assert forked.resolved_fn_value_name(4) == none
forked.set_resolved_fn_value(4, 'main.again')
assert forked.resolved_fn_value_name(4)? == 'main.again'
forked.reset_sparse_fn_values()
assert !forked.sparse_fn_value_filter_ready
assert forked.resolved_fn_value_name(3) == none
assert forked.resolved_fn_value_name(4) == none
forked.sparse_resolved_fn_values[7] = 'main.new_ast'
assert forked.resolved_fn_value_name(7)? == 'main.new_ast'
assert tc.resolved_fn_value_name(3)? == 'main.original'
assert tc.resolved_fn_value_name(4) == none
}
fn test_fn_value_filter_transform_writes_tombstones_and_nested_snapshots() {
mut a := flat.FlatAst.new()
for _ in 0 .. 8 {
a.add(.ident)
}
mut tc := TypeChecker.new(&a)
tc.set_resolved_fn_value(3, 'main.original')
mut forked := tc.fork_for_parallel_transform(&a)
assert forked.sparse_fn_value_filter_ready
assert forked.resolved_fn_value_name(3)? == 'main.original'
forked.set_resolved_fn_value(4, 'main.private')
assert forked.resolved_fn_value_name(4)? == 'main.private'
forked.clear_resolved_fn_value(3)
assert forked.resolved_fn_value_name(3) == none
// The nested snapshot must include private writes and omit tombstones,
// even when the parent's snapshot filter did not contain the new key.
nested := forked.fork_for_parallel_transform(&a)
assert nested.sparse_fn_value_filter_ready
assert nested.resolved_fn_value_name(3) == none
assert nested.resolved_fn_value_name(4)? == 'main.private'
forked.fork_overlay.resolved_fn_values[9] = 'main.generated'
assert forked.resolved_fn_value_name(9)? == 'main.generated'
assert tc.resolved_fn_value_name(3)? == 'main.original'
assert tc.resolved_fn_value_name(4) == none
}
fn test_fn_value_filter_parallel_worker_merge_records_new_snapshot_keys() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
mut forked := tc.fork_for_parallel_codegen()
mut worker := tc.fork_for_parallel_check()
assert !worker.sparse_fn_value_filter_ready
worker.sparse_resolved_fn_values[7] = 'main.worker'
forked.merge_parallel_check_worker_scoped(worker, true)
assert forked.sparse_fn_value_filter_ready
assert forked.resolved_fn_value_name(7)? == 'main.worker'
}
fn test_fn_value_filter_storage_clones_and_nested_observers_stay_unfiltered() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.set_resolved_fn_value(3, 'main.original')
forked := tc.fork_for_parallel_codegen()
mut exact := forked.fork_storage_query_view()
assert !exact.sparse_fn_value_filter_ready
exact.sparse_resolved_fn_values.delete(3)
exact.sparse_resolved_fn_values[4] = 'main.private_exact'
assert exact.resolved_fn_value_name(3) == none
assert exact.resolved_fn_value_name(4)? == 'main.private_exact'
mut outer := forked.fork_storage_observation_view()
assert !outer.sparse_fn_value_filter_ready
assert outer.resolved_fn_value_name(3)? == 'main.original'
outer.sparse_resolved_fn_values[4] = 'main.private_observer'
assert outer.resolved_fn_value_name(4)? == 'main.private_observer'
mut nested := outer.fork_storage_observation_view()
assert !nested.sparse_fn_value_filter_ready
assert nested.resolved_fn_value_name(3)? == 'main.original'
assert nested.resolved_fn_value_name(4)? == 'main.private_observer'
nested.clear_resolved_fn_value(3)
assert nested.resolved_fn_value_name(3) == none
assert outer.resolved_fn_value_name(3)? == 'main.original'
assert forked.resolved_fn_value_name(4) == none
}
+117
View File
@@ -0,0 +1,117 @@
module types
import v.flat
fn global_store_query_checker() &TypeChecker {
mut a := flat.FlatAst.new()
names := ['root', 'cycle', 'sink', 'clean', 'leaf', 'translated']
for name in names {
a.add_node(flat.Node{ kind: .fn_decl, value: name })
}
mut tc := TypeChecker.new(&a)
for i, name in names {
tc.register_visible_mutation_fn_decl(i, 'main', name, name)
tc.fn_type_files[name] = '${name}.v'
tc.fn_type_modules[name] = 'main'
}
tc.has_globals_files['sink.v'] = true
tc.translated_files['translated.v'] = true
root_id, _ := tc.intern_symbol('root')
cycle_id, _ := tc.intern_symbol('cycle')
sink_id, _ := tc.intern_symbol('sink')
leaf_id, _ := tc.intern_symbol('leaf')
tc.direct_dependencies_by_fn[0] = [cycle_id, sink_id]
tc.direct_dependencies_by_fn[1] = [root_id]
tc.direct_dependencies_by_fn[3] = [leaf_id]
tc.visible_mutation_cache.decl_index_ready = true
tc.remember_resolved_call(flat.NodeId(0), 'root')
tc.set_resolved_fn_value(1, 'cycle')
return tc
}
fn test_frozen_global_store_queries_keep_recursive_roots_independent() {
tc := global_store_query_checker()
forked := tc.fork_for_parallel_transform(tc.a)
// root -> cycle -> root and root -> sink. A negative answer reached while
// root is visiting must not become the answer to a later cycle root query.
for _ in 0 .. 3 {
assert forked.resolved_call_may_store_globally(flat.NodeId(0))
assert forked.fn_value_may_store_globally(flat.NodeId(1))
assert !forked.cached_fn_may_store_globally('clean')
assert forked.cached_fn_may_store_globally('translated')
}
assert forked.fn_value_may_store_globally(flat.NodeId(-1))
assert !forked.resolved_call_may_store_globally(flat.NodeId(-1))
}
fn test_frozen_global_store_cache_is_private_and_invalidated_by_signature_changes() {
tc := global_store_query_checker()
mut first := tc.fork_for_parallel_transform(tc.a)
second := tc.fork_for_parallel_transform(tc.a)
assert !first.cached_fn_may_store_globally('clean')
assert !second.cached_fn_may_store_globally('clean')
first.ensure_private_transform_signatures()
first.fn_type_files['leaf'] = 'sink.v'
assert first.cached_fn_may_store_globally('clean')
assert !second.cached_fn_may_store_globally('clean')
// Later edits must invalidate as well as the initial detachment.
first.ensure_private_transform_signatures()
first.fn_type_files['leaf'] = 'leaf.v'
assert !first.cached_fn_may_store_globally('clean')
assert !second.cached_fn_may_store_globally('clean')
nested := first.fork_for_parallel_transform(first.a)
assert !nested.cached_fn_may_store_globally('clean')
}
fn test_live_global_store_queries_do_not_reuse_post_check_results() {
mut tc := global_store_query_checker()
assert !tc.cached_fn_may_store_globally('clean')
tc.fn_type_files['leaf'] = 'sink.v'
assert tc.cached_fn_may_store_globally('clean')
tc.fn_type_files['leaf'] = 'leaf.v'
assert !tc.cached_fn_may_store_globally('clean')
}
fn test_frozen_global_store_declaration_index_preserves_module_priority() {
mut a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.register_visible_mutation_fn_decl_with_lowered(10, 'alpha', 'alpha.helper', 'helper',
'alpha__helper', 'helper')
tc.register_visible_mutation_fn_decl_with_lowered(20, 'beta', 'beta.helper', 'helper',
'beta__helper', 'helper')
tc.register_visible_mutation_fn_decl_with_lowered(30, 'alpha', 'alpha.Box[T].method',
'Box[T].method', 'alpha__Box_T__method', 'Box_T__method')
tc.visible_mutation_cache.decl_index_ready = true
forked := tc.fork_for_parallel_transform(&a)
for module_name, expected in {
'alpha': 10
'beta': 20
'': 10
} {
decl := forked.global_store_fn_decl('helper', module_name) or { panic('missing helper') }
assert decl.idx == expected
}
assert forked.global_store_fn_decl('helper', 'missing') == none
for name in ['Box[int].method', 'alpha.Box[string].method', 'Box.method'] {
decl := forked.global_store_fn_decl(name, 'alpha') or { panic('missing generic method') }
assert decl.idx == 30
}
alias_decl := forked.global_store_fn_decl('beta__helper', 'beta') or { panic('missing alias') }
assert alias_decl.idx == 20
}
fn test_frozen_global_store_declaration_misses_fall_back_to_current_ast() {
mut a := flat.FlatAst.new()
module_id := a.add_node(flat.Node{ kind: .module_decl, value: 'main' })
generated_id := a.add_node(flat.Node{ kind: .fn_decl, value: 'generated' })
mut tc := TypeChecker.new(&a)
tc.top_level_idx = [i32(module_id), i32(generated_id)]
tc.visible_mutation_cache.decl_index_ready = true
forked := tc.fork_for_parallel_transform(&a)
decl := forked.global_store_fn_decl('generated', 'main') or { panic('missing generated declaration') }
assert decl.idx == int(generated_id)
assert decl.mod == 'main'
}
@@ -0,0 +1,92 @@
module types
import v.flat
struct IntegerPromotionBoundaryCase {
value string
other Type
expected string
}
fn test_integer_promotion_guard_preserves_storage_and_small_integer_boundaries() {
cases := [
IntegerPromotionBoundaryCase{'127', Primitive{ props: .integer, size: 8 }, 'i8'},
IntegerPromotionBoundaryCase{'128', Primitive{ props: .integer, size: 8 }, 'int'},
IntegerPromotionBoundaryCase{'-128', Primitive{ props: .integer, size: 8 }, 'i8'},
IntegerPromotionBoundaryCase{'-129', Primitive{ props: .integer, size: 8 }, 'int'},
IntegerPromotionBoundaryCase{'255', Primitive{ props: .integer | .unsigned, size: 8 }, 'u8'},
IntegerPromotionBoundaryCase{'256', Primitive{ props: .integer | .unsigned, size: 8 }, 'int'},
IntegerPromotionBoundaryCase{'-1', Primitive{ props: .integer | .unsigned, size: 8 }, 'int'},
IntegerPromotionBoundaryCase{'32767', Primitive{ props: .integer, size: 16 }, 'i16'},
IntegerPromotionBoundaryCase{'32768', Primitive{ props: .integer, size: 16 }, 'int'},
IntegerPromotionBoundaryCase{'-32768', Primitive{ props: .integer, size: 16 }, 'i16'},
IntegerPromotionBoundaryCase{'-32769', Primitive{ props: .integer, size: 16 }, 'int'},
IntegerPromotionBoundaryCase{'65535', Primitive{ props: .integer | .unsigned, size: 16 }, 'u16'},
IntegerPromotionBoundaryCase{'65536', Primitive{ props: .integer | .unsigned, size: 16 }, 'int'},
IntegerPromotionBoundaryCase{'-1', Primitive{ props: .integer | .unsigned, size: 16 }, 'int'},
IntegerPromotionBoundaryCase{'42', Primitive{ props: .integer, size: 32 }, 'i32'},
IntegerPromotionBoundaryCase{'42', Primitive{ props: .integer, size: 64 }, 'i64'},
IntegerPromotionBoundaryCase{'42', Primitive{ props: .integer | .unsigned, size: 32 }, 'u32'},
IntegerPromotionBoundaryCase{'42', Primitive{ props: .integer | .unsigned, size: 64 }, 'u64'},
IntegerPromotionBoundaryCase{'42', Rune{}, 'rune'},
IntegerPromotionBoundaryCase{'42', ISize{}, 'isize'},
IntegerPromotionBoundaryCase{'42', USize{}, 'uint'},
IntegerPromotionBoundaryCase{'42', Alias{ name_id: flat.source_type_id('Count'), base_type: &Type(i64_) }, 'i64'},
]
for c in cases {
mut a := flat.FlatAst.new()
mut literal := a.add_val(.int_literal, c.value.trim_left('-'))
if c.value.starts_with('-') {
start := a.children.len
a.children << literal
literal = a.add_node(flat.Node{ kind: .prefix, op: .minus, children_start: start, children_count: 1 })
}
other := a.add_val(.ident, 'operand')
tc := TypeChecker.new(&a)
actual := tc.int_literal_promoted_infix_type(literal, other, builtin_int_type, c.other) or {
panic('missing ${c.value} promotion to ${c.other.name()}')
}
assert actual.name() == c.expected, '${c.value}, ${c.other.name()}'
}
}
fn test_integer_promotion_guard_rejects_ineligible_operands_and_preserves_constant_expressions() {
mut a := flat.FlatAst.new()
left := a.add_val(.int_literal, '20')
right := a.add_val(.int_literal, '22')
a.children << left
a.children << right
expr := a.add_node(flat.Node{ kind: .infix, op: .plus, children_start: 0, children_count: 2 })
constant := a.add_val(.ident, 'limit')
unknown := a.add_val(.ident, 'missing')
other := a.add_val(.ident, 'operand')
mut tc := TypeChecker.new(&a)
tc.const_exprs['limit'] = expr
for id in [expr, constant] {
actual := tc.int_literal_promoted_infix_type(id, other, builtin_int_type, builtin_i64_type) or {
panic('missing constant expression promotion')
}
assert actual.name() == 'i64'
for typ in [Type(int_), String{}, Pointer{ base_type: &Type(int_) },
Array{ elem_type: &Type(int_) }, OptionType{ base_type: &Type(int_) },
Struct{ name_id: flat.source_type_id('Item') },
Alias{ name_id: flat.source_type_id('Plain'), base_type: &Type(int_) }] {
assert tc.int_literal_promoted_infix_type(id, other, builtin_int_type, typ) == none
}
}
float_type := Type(Primitive{ props: .float, size: 64 })
float_alias := Type(Alias{ name_id: flat.source_type_id('Fraction'), base_type: &float_type })
for typ in [float_type, float_alias] {
for id in [left, expr, constant] {
actual := tc.int_literal_promoted_infix_type(id, other, builtin_int_type, typ) or {
panic('missing constant promotion to ${typ.name()}')
}
assert actual.source_type_id() == typ.source_type_id()
}
assert tc.int_literal_promoted_infix_type(unknown, other, builtin_int_type, typ) == none
assert tc.int_literal_promoted_infix_type(left, other, builtin_i64_type, typ) == none
}
assert tc.int_literal_promoted_infix_type(left, right, builtin_int_type, builtin_i64_type) == none
assert tc.int_literal_promoted_infix_type(unknown, other, builtin_int_type, builtin_i64_type) == none
assert tc.int_literal_promoted_infix_type(left, other, builtin_i64_type, builtin_i64_type) == none
}
@@ -0,0 +1,29 @@
module types
import v.flat
fn test_known_symbol_alias_keeps_present_empty_binding() {
mut a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
assert tc.type_from_known_symbol(flat.source_type_id('missing')) == none
tc.type_aliases.set_id('Empty', 0)
typ := tc.type_from_known_symbol(flat.source_type_id('Empty')) or { panic('lost present empty alias') }
assert typ is Alias
assert typ.name() == 'Empty'
if typ is Alias {
assert typ.base_type is Void
}
}
fn test_known_symbol_alias_precedes_other_type_kinds() {
mut a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.type_aliases.set('Counter', 'int')
tc.structs['Counter'] = []StructField{}
typ := tc.type_from_known_symbol(flat.source_type_id('Counter')) or { panic('missing integer alias') }
assert typ is Alias
assert typ.name() == 'Counter'
if typ is Alias {
assert typ.base_type.name() == 'int'
}
}
+118
View File
@@ -0,0 +1,118 @@
module types
import v.flat
import v.token
struct MalformedConstQueryCase {
source string
offset int
keyword_offset int = -1
}
fn malformed_const_query_checker(source string, file_id int, offset int) (&TypeChecker, flat.NodeId) {
mut a := flat.FlatAst.new()
mut files := token.FileSet.new()
a.source_files[file_id] = files.add_file('typo.v', source.len)
a.source_texts[file_id] = source
id := a.add_node(flat.Node{
kind: .ident
pos: token.new_pos(file_id, offset)
})
mut tc := TypeChecker.new(&a)
tc.source_texts_by_file['typo.v'] = source
return tc, id
}
fn test_malformed_const_query_preserves_line_and_keyword_boundaries() {
cases := [
MalformedConstQueryCase{ source: '\t cosnt amount = 1\n', offset: 10, keyword_offset: 2 },
MalformedConstQueryCase{ source: 'module main\n\tcosnt\tamount = 1', offset: 20, keyword_offset: 13 },
MalformedConstQueryCase{ source: 'cosnt', offset: 0, keyword_offset: 0 },
MalformedConstQueryCase{ source: 'cosnt\n', offset: 0, keyword_offset: 0 },
MalformedConstQueryCase{ source: 'cosntamount = 1\n', offset: 0 },
MalformedConstQueryCase{ source: '// cosnt amount = 1\n', offset: 10 },
MalformedConstQueryCase{ source: 'const amount = "cosnt"\n', offset: 10 },
MalformedConstQueryCase{ source: 'const amount = 1\n', offset: 10 },
MalformedConstQueryCase{ source: 'cosnt amount = 1\namount = 2\n', offset: 18 },
MalformedConstQueryCase{ source: 'cosnt amount = 1', offset: -1 },
MalformedConstQueryCase{ source: 'cosnt amount = 1', offset: 100 },
]
for indexed in [false, true] {
for item in cases {
mut tc, id := malformed_const_query_checker(item.source, 1, item.offset)
if indexed {
tc.index_multiple_module_import_lines(tc.a)
}
if pos := tc.malformed_const_keyword_pos(id) {
assert item.keyword_offset >= 0
assert pos == token.new_span(1, item.keyword_offset, item.keyword_offset + 5)
} else {
assert item.keyword_offset == -1
}
}
}
}
fn test_malformed_const_query_reindexes_changed_sources_without_mutating_forks() {
mut tc, id := malformed_const_query_checker('const amount = 1', 1, 10)
tc.index_multiple_module_import_lines(tc.a)
forked := tc.fork_for_parallel_check()
assert tc.malformed_const_keyword_pos(id) == none
assert forked.malformed_const_keyword_pos(id) == none
tc.a.source_texts[1] = 'cosnt amount = 1'
tc.index_multiple_module_import_lines(tc.a)
assert (tc.malformed_const_keyword_pos(id) or { panic('missing typo after source reset') }).offset == 0
assert forked.malformed_const_keyword_pos(id) == none
}
fn test_malformed_const_query_indexes_added_files_and_retains_sparse_fallback() {
mut tc, _ := malformed_const_query_checker('const amount = 1', 1, 10)
tc.index_multiple_module_import_lines(tc.a)
mut files := token.FileSet.new()
for file_id in [2, 1000000000] {
source := '\tcosnt amount = 1'
tc.a.source_files[file_id] = files.add_file('typo_${file_id}.v', source.len)
tc.a.source_texts[file_id] = source
id := tc.a.add_node(flat.Node{
kind: .ident
pos: token.new_pos(file_id, 10)
})
tc.index_module_import_lines_of_new_files(tc.a)
pos := tc.malformed_const_keyword_pos(id) or { panic('missing added-file typo') }
assert pos == token.new_span(file_id, 1, 6)
}
assert tc.malformed_const_source_states.len < 128
}
fn test_malformed_const_query_retains_filename_fallback_for_duplicate_ids() {
for incremental in [false, true] {
mut tc, first := malformed_const_query_checker('const amount = 1', 1, 10)
if incremental {
tc.index_multiple_module_import_lines(tc.a)
} else {
tc.source_texts_by_file.clear()
}
mut files := token.FileSet.new()
tc.a.source_files[2] = files.add_file('typo.v', 16)
tc.a.source_texts[2] = 'cosnt amount = 1'
second := tc.a.add_node(flat.Node{
kind: .ident
pos: token.new_pos(2, 10)
})
tc.index_module_import_lines_of_new_files(tc.a)
assert tc.malformed_const_source_states[1] == 0
assert tc.malformed_const_source_states[2] == 0
has_typo := tc.source_texts_by_file['typo.v'].starts_with('cosnt')
if incremental {
assert has_typo
}
for id in [first, second] {
if pos := tc.malformed_const_keyword_pos(id) {
assert has_typo
assert pos == token.new_span(tc.a.nodes[int(id)].pos.id, 0, 5)
} else {
assert !has_typo
}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
module types
import v.flat
fn noreturn_byte_search_reference(tc &TypeChecker, name string) bool {
if resolved_name_never_returns(name) {
return true
}
if name in tc.a.noreturn_fns {
return true
}
mut has_dot := false
for i := name.len - 1; i >= 0; i-- {
if name[i] == `.` {
if has_dot && tc.a.noreturn_fns[name[i + 1..]] {
return true
}
has_dot = true
}
}
return false
}
fn test_noreturn_word_search_matches_byte_search_with_long_spans() {
mut a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
for declarations in [map[string]bool{}, {
'pkg.fail': true
'pkg.disabled': false
'.fail': true
'fail': true
'': true
}] {
a.noreturn_fns = declarations.clone()
for padding in 0 .. 48 {
stem := 'a'.repeat(padding)
for tail in ['', '.', '..', 'pkg.fail', 'pkg.disabled', 'notpkg.fail', '.fail', 'fail',
'panic', 'exit', 'C.exit'] {
for prefix in ['', '.', '..', stem, stem + '.', stem + '..', stem + '.long_module.'] {
name := prefix + tail
assert tc.name_never_returns(name) == noreturn_byte_search_reference(tc, name), name
}
}
}
}
}
@@ -0,0 +1,41 @@
module types
import v.flat
fn test_resolution_type_views_preserve_file_context_across_recent_hits_and_file_switches() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_file = 'caller.v'
tc.cur_module = 'caller'
tc.structs['left.Item'] = []StructField{}
tc.structs['right.Item'] = []StructField{}
tc.file_modules['left.v'] = 'left'
tc.file_modules['right.v'] = 'right'
for file in ['left.v', 'left.v'.clone(), 'right.v', 'right.v'.clone(), 'left.v'] {
module_name := tc.file_modules[file] or { panic('missing file module') }
for source in ['[]Item', '?Item', '&Item'] {
fresh := tc.fork_type_parse_view(file, module_name)
expected := fresh.parse_resolution_type(flat.source_type_id(source))
assert tc.parse_resolution_type_in_file(flat.source_type_id(source), file) == expected
}
}
assert tc.cur_file == 'caller.v'
assert tc.cur_module == 'caller'
}
fn test_reset_resolution_type_views_discards_recent_file_context() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.structs['left.Item'] = []StructField{}
tc.structs['right.Item'] = []StructField{}
tc.file_modules['source.v'] = 'left'
source := flat.source_type_id('[]Item')
assert tc.parse_resolution_type_in_file(source, 'source.v').name() == '[]left.Item'
tc.file_modules['source.v'] = 'right'
tc.reset_resolution_type_view_cache()
assert tc.parse_resolution_type_in_file(source, 'source.v').name() == '[]right.Item'
tc.disable_resolution_type_view_cache()
tc.file_modules['source.v'] = 'left'
assert tc.parse_resolution_type_in_file(source, 'source.v').name() == '[]left.Item'
assert isnil(tc.resolution_type_views)
}
@@ -0,0 +1,79 @@
module types
import v.flat
fn test_resolved_references_keep_checked_storage_and_invalidation() {
mut a := flat.FlatAst.new()
id := a.add_val(.ident, 'item')
mut tc := TypeChecker.new(&a)
tc.reset_node_caches(a.nodes.len)
tc.trust_checked_expr_types = true
tc.remember_expr_type(id, Type(Array{ elem_type: &Type(int_) }))
first := tc.resolve_type_reference(id)
assert voidptr(first) == voidptr(tc.expr_type_values[int(id)])
assert *first == tc.resolve_type(id)
tc.invalidate_checked_expr_type(int(id))
tc.remember_expr_type(id, Type(string_))
assert *tc.resolve_type_reference(id) == Type(string_)
assert first.name() == '[]int'
}
fn test_resolved_references_preserve_sparse_and_probe_precedence() {
mut a := flat.FlatAst.new()
id := a.add_val(.int_literal, '1')
mut tc := TypeChecker.new(&a)
tc.reset_node_caches(a.nodes.len)
tc.trust_checked_expr_types = true
tc.remember_expr_type(id, Type(string_))
tc.parallel_check_sparse = true
tc.check_range_lo = 1
tc.check_range_hi = 1
assert *tc.resolve_type_reference(id) == tc.resolve_type(id)
assert *tc.resolve_type_reference(id) == Type(int_)
tc.sparse_expr_type_values[int(id)] = Type(bool_)
mut probe := tc.fork_storage_observation_view()
assert *probe.resolve_type_reference(id) == Type(bool_)
probe.sparse_expr_type_values[int(id)] = Type(u64_)
assert *probe.resolve_type_reference(id) == probe.resolve_type(id)
assert *probe.resolve_type_reference(id) == Type(u64_)
}
fn test_resolved_references_keep_integer_widening_and_shift_rules() {
for op in [flat.Op.plus, .eq, .logical_and, .left_shift, .right_shift] {
mut a := flat.FlatAst.new()
left := a.add_val(.ident, 'left')
right := a.add_val(.ident, 'right')
a.children << [left, right]
expr := a.add_node(flat.Node{ kind: .infix, op: op, children_count: 2 })
mut tc := TypeChecker.new(&a)
tc.reset_node_caches(a.nodes.len)
tc.trust_checked_expr_types = true
tc.remember_expr_type(left, Type(u64_))
tc.remember_expr_type(right, Type(u128_))
tc.remember_expr_type(expr, Type(u64_))
expected := if op == .plus { Type(u128_) } else { Type(u64_) }
assert *tc.resolve_type_reference(expr) == tc.resolve_type(expr)
assert *tc.resolve_type_reference(expr) == expected
if op in [.left_shift, .right_shift] {
tc.remember_expr_type(left, Type(i128_))
assert *tc.resolve_type_reference(expr) == Type(i128_)
}
}
}
fn test_resolved_references_do_not_publish_provisional_unknowns() {
mut a := flat.FlatAst.new()
id := a.add_val(.ident, 'missing')
mut tc := TypeChecker.new(&a)
tc.reset_node_caches(a.nodes.len)
tc.trust_checked_expr_types = true
tc.remember_expr_type(id, Type(Array{ elem_type: &Type(unknown_type('T')) }))
tc.arm_body_resolve_memo(0, a.nodes.len - 1)
first := tc.resolve_type_reference(id)
assert *first == tc.resolve_type(id)
assert tc.body_resolve_memo.filled[int(id)] == 0
tc.invalidate_checked_expr_type(int(id))
tc.cur_scope.insert('missing', Type(bool_))
assert *tc.resolve_type_reference(id) == Type(bool_)
assert first.name() != 'bool'
}
+37
View File
@@ -0,0 +1,37 @@
module types
import os
import v.parser
import v.pref
fn test_return_analysis_preserves_local_noreturn_shadowing() {
root := os.join_path(os.vtmp_dir(), 'v3_return_tail_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
path := os.join_path(root, 'main.v')
os.write_file(path, 'fn implicit_exit() { value := 1; panic("stop") }
fn shadowed_exit() { panic := fn (_ string) {}; panic("stop") }
fn explicit_return() int { panic := fn (_ string) {}; panic("stop"); return 7 }
')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := TypeChecker.new(a)
tc.collect(a)
tc.smartcasts['sentinel'] = builtin_int_type
expected := {
'implicit_exit': true
'shadowed_exit': false
'explicit_return': true
}
mut checked := 0
for node in a.nodes {
if node.kind != .fn_decl { continue }
returns := expected[node.value] or { continue }
assert tc.stmt_sequence_definitely_returns(&node, 0) == returns, node.value
assert tc.smartcasts.len == 1
assert 'sentinel' in tc.smartcasts
checked++
}
assert checked == expected.len
}
+82
View File
@@ -0,0 +1,82 @@
module types
import v.flat
fn test_scalar_child_call_helpers_preserve_saved_header_after_ast_growth() {
mut a := flat.FlatAst.new()
callee := a.add_val(.ident, 'saved')
a.children << callee
call := a.add_node(flat.Node{ kind: .call, children_start: 0, children_count: 1 })
snapshot := a.nodes[int(call)]
// Querying the saved header must not reload a header changed by a nested check.
a.nodes[int(call)].children_count = 0
growth := a.nodes.cap + 100
for _ in 0 .. growth {
a.add_val(.ident, 'padding')
}
mut tc := TypeChecker.new(&a)
tc.fn_ret_types['saved'] = builtin_int_type
assert tc.call_display_name(snapshot) == 'saved'
assert tc.call_argument_target_name(snapshot, CallInfo{}) == 'saved'
assert tc.c_va_macro_call_name(snapshot) == ''
direct := tc.direct_call_return_type(snapshot) or { panic('missing direct return type') }
spawned := tc.spawn_child_call_return_type(snapshot) or { panic('missing spawn return type') }
assert direct.source_type_id() == flat.source_type_int
assert spawned.source_type_id() == flat.source_type_int
assert tc.builtin_array_annotation_expected_type(snapshot, 1) is Void
assert snapshot.children_count == 1
}
fn test_scalar_child_function_wrappers_keep_saved_header_and_empty_sentinel() {
mut a := flat.FlatAst.new()
ident := a.add_val(.ident, 'saved')
a.children << ident
paren := a.add_node(flat.Node{ kind: .paren, children_start: 0, children_count: 1 })
a.children << paren
cast := a.add_node(flat.Node{ kind: .cast_expr, children_start: 1, children_count: 1 })
snapshot := a.nodes[int(cast)]
a.nodes[int(cast)].children_count = 0
growth := a.nodes.cap + 100
for _ in 0 .. growth {
a.add_val(.ident, 'padding')
}
mut tc := TypeChecker.new(&a)
tc.fn_ret_types['saved'] = builtin_int_type
tc.fn_param_types['saved'] = []Type{}
assert tc.fn_value_key(snapshot) or { panic('missing wrapped function key') } == 'saved'
for start in [-1, a.children.len, a.children.len + 100] {
invalid := flat.Node{ kind: .call, children_start: start, children_count: 1 }
assert tc.call_display_name(invalid) == ''
assert tc.direct_call_return_type(invalid) == none
}
assert tc.call_display_name(flat.Node{ kind: .call }) == '<missing>'
}
fn test_scalar_child_c_macro_and_signature_queries_keep_saved_headers() {
mut a := flat.FlatAst.new()
base := a.add_val(.ident, 'C')
a.children << base
selector := a.add_node(flat.Node{ kind: .selector, value: 'va_arg', children_start: 0, children_count: 1 })
a.children << selector
call := flat.Node{ kind: .call, children_start: 1, children_count: 1 }
param := a.add_node(flat.Node{ kind: .param, value: 'arg', typ: flat.source_type_int })
a.children << param
decl := a.add_node(flat.Node{ kind: .fn_literal, typ: flat.source_type_bool, children_start: 2, children_count: 1 })
signature := a.nodes[int(decl)]
a.nodes[int(decl)].children_count = 0
growth := a.nodes.cap + 100
for _ in 0 .. growth {
a.add_val(.ident, 'padding')
}
tc := TypeChecker.new(&a)
assert tc.c_va_macro_call_name(call) == 'C.va_arg'
assert tc.call_display_name(call) == 'C.va_arg'
assert tc.fn_node_source_type_text(signature) == 'fn (arg int) bool'
typ := tc.fn_literal_type(signature)
assert typ is FnType
if typ is FnType {
assert typ.params.len == 1
assert typ.params[0].typ.source_type_id() == flat.source_type_int
assert typ.return_type.source_type_id() == flat.source_type_bool
}
}
@@ -0,0 +1,42 @@
module types
import v.flat
fn selector_header_test_add(mut a flat.FlatAst, base flat.NodeId, name string) flat.NodeId {
start := a.children.len
a.children << base
return a.add_node(flat.Node{ kind: .selector, value: name, children_start: start, children_count: 1 })
}
fn test_enum_selector_base_helper_keeps_qualified_names_and_header_guards() {
mut a := flat.FlatAst.new()
module_name := a.add_val(.ident, 'dep')
enum_name := selector_header_test_add(mut a, module_name, 'Color')
selector := selector_header_test_add(mut a, enum_name, 'red')
c_name := a.add_val(.ident, 'C')
c_selector := selector_header_test_add(mut a, c_name, 'errno')
mut tc := TypeChecker.new(&a)
tc.cur_file = 'main.v'
tc.cur_module = 'main'
tc.register_file_import('dep', 'one')
tc.enum_names['one.Color'] = true
tc.enum_fields['one.Color'] = ['red']
header := a.nodes[int(selector)]
wrapped := tc.enum_selector_type(&header) or { panic('qualified wrapper missing') }
assert wrapped.source_type_id() == flat.source_type_id('one.Color')
direct := tc.selector_type(selector, header) or { panic('qualified direct missing') }
assert direct.source_type_id() == flat.source_type_id('one.Color')
mut invalid_kind := header
invalid_kind.kind = .ident
assert tc.enum_selector_type(&invalid_kind) == none
mut invalid_count := header
invalid_count.children_count = 0
assert tc.enum_selector_type(&invalid_count) == none
assert tc.selector_type(selector, invalid_count) == none
mut missing := header
missing.value = 'missing'
assert tc.enum_selector_type(&missing) == none
assert tc.selector_type(selector, missing) == none
c_type := tc.selector_type(c_selector, a.nodes[int(c_selector)]) or { panic('C errno missing') }
assert c_type.source_type_id() == flat.source_type_int
}
+65
View File
@@ -0,0 +1,65 @@
module types
import v.flat
fn test_recent_sum_pattern_context_is_invalidated_by_clear() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_file = 'main.v'
tc.cur_module = 'main'
tc.sum_types.set('Choice', ['string'])
sum_id := flat.source_type_id('Choice')
variant_id := flat.source_type_int
assert tc.sum_variant_type_for_pattern(sum_id, variant_id) == none
assert !isnil(tc.type_cache.sum_variant_pattern_recent)
tc.sum_types.set('Choice', ['int', 'string'])
tc.type_cache.sum_variant_pattern_entries.clear()
assert tc.sum_variant_type_for_pattern(sum_id, variant_id)? == variant_id
assert tc.type_cache.sum_variant_pattern_recent_count == 1
}
fn test_recent_sum_pattern_context_clear_and_repopulate_preserves_other_files() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_module = 'main'
tc.sum_types.set('Choice', ['string'])
sum_id := flat.source_type_id('Choice')
variant_id := flat.source_type_int
tc.cur_file = 'first.v'
assert tc.sum_variant_type_for_pattern(sum_id, variant_id) == none
tc.sum_types.set('Choice', ['int', 'string'])
tc.type_cache.sum_variant_pattern_entries.clear()
tc.cur_file = 'second.v'
assert tc.sum_variant_type_for_pattern(sum_id, variant_id)? == variant_id
assert tc.type_cache.sum_variant_pattern_entries.len == 1
tc.cur_file = 'first.v'
assert tc.sum_variant_type_for_pattern(sum_id, variant_id)? == variant_id
assert tc.type_cache.sum_variant_pattern_entries.len == 2
}
fn test_recent_sum_pattern_context_owns_file_and_module_guard_strings() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
mut file := 'first.v'.clone()
mut mod_name := 'one'.clone()
tc.cur_file = file
tc.cur_module = mod_name
tc.structs['one.Item'] = []StructField{}
tc.structs['two.Item'] = []StructField{}
tc.sum_types.set('one.Choice', ['one.Item'])
tc.sum_types.set('two.Choice', ['two.Item'])
sum_id := flat.source_type_id('Choice')
variant_id := flat.source_type_id('Item')
assert tc.sum_variant_type_for_pattern(sum_id, variant_id)? == flat.source_type_id('one.Item')
// The directory owns the guard strings independently of the caller's spelling storage.
unsafe {
file.str[0] = `s`
mod_name.str[0] = `t`
mod_name.str[1] = `w`
mod_name.str[2] = `o`
}
assert tc.sum_variant_type_for_pattern(sum_id, variant_id)? == flat.source_type_id('two.Item')
assert tc.type_cache.sum_variant_pattern_entries.len == 2
assert tc.type_cache.sum_variant_pattern_recent.file == 'sirst.v'
assert tc.type_cache.sum_variant_pattern_recent.module == 'two'
}
@@ -0,0 +1,79 @@
module types
import v.flat
fn test_unqualified_builtin_type_candidates_keep_their_declared_owners() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_file = 'consumer.v'
tc.cur_module = 'consumer'
tc.struct_modules['Error'] = 'builtin'
tc.struct_modules['User'] = 'other'
tc.enum_names['BuiltinKind'] = true
tc.flag_enums['BuiltinFlags'] = true
assert tc.unqualified_type_symbol_is_builtin('Error')
assert tc.unqualified_type_symbol_is_builtin('IError')
assert tc.unqualified_type_symbol_is_builtin('BuiltinKind')
assert tc.unqualified_type_symbol_is_builtin('BuiltinFlags')
assert !tc.unqualified_type_symbol_is_builtin('User')
assert !tc.unqualified_type_symbol_is_builtin('Unknown')
assert !tc.unqualified_type_symbol_is_builtin('')
// A non-builtin struct owner wins even if another table has the bare name.
tc.enum_names['User'] = true
assert !tc.unqualified_type_symbol_is_builtin('User')
}
fn test_unqualified_builtin_types_are_shadowed_by_every_local_type_kind() {
for kind in 0 .. 6 {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_file = 'consumer.v'
tc.cur_module = 'consumer'
tc.struct_modules['Error'] = 'builtin'
assert tc.unqualified_type_symbol_is_builtin('Error')
match kind {
0 { tc.type_aliases.set('consumer.Error', 'int') }
1 { tc.structs['consumer.Error'] = []StructField{} }
2 { tc.interface_names['consumer.Error'] = true }
3 { tc.flag_enums['consumer.Error'] = true }
4 { tc.enum_names['consumer.Error'] = true }
5 { tc.sum_types.set('consumer.Error', ['int', 'string']) }
else {}
}
assert !tc.unqualified_type_symbol_is_builtin('Error'), 'type kind ${kind}'
}
}
fn test_unqualified_builtin_type_source_shadow_tracks_the_active_file() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_file = 'consumer.v'
tc.cur_module = 'consumer'
tc.struct_modules['Error'] = 'builtin'
tc.declared_type_scope_keys[scope_type_key('consumer.v', 'consumer', 'Error')] = true
assert !tc.unqualified_type_symbol_is_builtin('Error')
tc.cur_file = 'other.v'
assert tc.unqualified_type_symbol_is_builtin('Error')
}
fn test_unqualified_builtin_type_selective_imports_keep_shadow_priority() {
for owner in ['other', 'builtin', 'ambiguous'] {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_file = 'consumer.v'
tc.cur_module = 'consumer'
tc.struct_modules['Error'] = 'builtin'
candidates := if owner == 'ambiguous' { []string{} } else { ['${owner}.Error'] }
tc.file_selective_imports[file_import_key(tc.cur_file, 'Error')] = candidates
tc.file_imports_by_file[tc.cur_file] = &FileImportInfo{
selective_imports: {
'Error': candidates
}
}
if owner != 'ambiguous' {
tc.structs['${owner}.Error'] = []StructField{}
tc.struct_modules['${owner}.Error'] = owner
}
assert tc.unqualified_type_symbol_is_builtin('Error') == (owner == 'builtin'), owner
}
}
+82
View File
@@ -4,6 +4,88 @@ Every compiler fix must preserve or improve self-compilation efficiency. Agents
retired instructions before and after compiler changes and resolve regressions before merging.
Keep correctness tests and generated-output checks alongside performance measurements.
## Follow-up optimization, 2026-10-26
The pinned follow-up uses **8.0317% fewer retired instructions**,
exceeding the accepted 7.5% target. This compares baseline
`f7e9ee96c22ca6db47b56d28332e7553715c659f` with measured source revision
`e71269ceddda4651e29b0363cae5f88bda0d4362`. The later integration with upstream
`c2eda292429ae843a501180a9ad32eca7360e380` is validated separately and has not been
remeasured; these numbers do not claim a reduction against that newer base.
The changes reuse immutable node snapshots, type references, and names; defer unnecessary
checker, transform, markused, and C generation queries; and accelerate map, short-string, builder,
and scanner operations. Regression tests cover cache lifetimes, custom map callbacks, overlapping
builder writes, and borrowed storage. No language or CLI behavior change is intended.
| Median metric | Before | After |
| --- | ---: | ---: |
| Retired instructions | 59,509,416,234.5 | 54,729,825,042 |
| Instructions/parsed line | 106,268.71 | 97,733.58 |
| Elapsed time, ms | 15,262.58 | 12,667.81 |
| Peak resident memory, MiB | 1,122.44 | 1,095.13 |
| CPU cycles, billions | 28.2215 | 26.2491 |
One warmup per role precedes ten alternating measured pairs. All 22 runs succeed with exactly one
positive retired-instruction counter each; warmups are excluded. **0/10 pairs regress**. The median
paired instruction difference is `-4,629,942,000.5`.
Elapsed time and cycles are contextual measurements under shared host load, without a controlled
latency claim.
The fixture remains `2d1367e589ed30c03325e586a8ddfd3bc96c8c3d`, with all **8,498 file hashes
unchanged**. Separate verbose inventories match: **559,990 physical lines in 562 parsed `.v`
files**, no `.vh` files. Both roles use the same fixture, executable, and C output paths.
The fixture executable is restored after the comparison.
Both compilers use the same compatible seed, then their own-source self build, then production,
with Apple Clang 21 and **no PGO**. Restore each role to the same build path. Clear inherited
`VFLAGS`, `VEXE`, `LLVM_PROFILE_FILE`, `CC`, `CFLAGS`, and `LDFLAGS`; use the environment below
for `./v -cc /usr/bin/clang -gc none self` and the production build:
```sh
env -u VFLAGS -u VEXE -u LLVM_PROFILE_FILE -u CC -u CFLAGS -u LDFLAGS \
VJOBS=4 SOURCE_DATE_EPOCH=0 V_MACOS_V3_NO_FALLBACK=1 \
V_MACOS_V3_VHASH=f7e9ee96c22ca6db47b56d28332e7553715c659f \
./v -prod -cc /usr/bin/clang -gc none -no-memory-limit -prealloc \
-compile-backend fastc -nocache -o ./v-perf cmd/v
```
Install each production binary as `./v` in the fixed fixture, then measure serially:
```sh
env -u VFLAGS -u VEXE -u LLVM_PROFILE_FILE -u CC -u CFLAGS -u LDFLAGS \
VJOBS=1 SOURCE_DATE_EPOCH=0 V_MACOS_V3_NO_FALLBACK=1 \
V_MACOS_V3_VHASH=2d1367e589ed30c03325e586a8ddfd3bc96c8c3d \
/usr/bin/time -l -o "$run_log" \
./v -nocache -gc none -prealloc -compile-backend fastc -building-v \
-o /Users/alex/code/v-speed-next10-20261025/output.c cmd/v
```
The common seed SHA-256 is
`a17d929dbba4205a49f5fb0e0d178e70b3902b60074478765b37749a4bfb8d6d`.
The baseline production SHA-256 is
`e4e2c6b42f757a99a6159b21ebda12bd15af0aa927aaccd645e1dee762db6369`;
the candidate production SHA-256 is
`5d12d2dd03dab024837cf41d6ef4bd15f3967681f572d7ccf89342cfa0c8bec8`.
Generated C intentionally differs in audited local storage representations. Every sample matches
its approved role output: baseline SHA-256
`35f76ab8160d9172bfced3fea28a4970226b71f3e2da5e8445695c6e3ada8d32`, candidate
`1fcc487b2a39c089dea4e511dd4d14c6878b00e7046acaceef038ddd90c191a3`.
Both outputs pass Clang syntax checks and native linking. Both linked compilers generate
byte-identical second-generation C, SHA-256
`4b3998af7880683073a01a21ff3f726c61f2058e6c933bd8bf7f50838f46cb27`.
Evidence remains under `/Users/alex/code/v-speed-next10-20261025/`:
`c8-integrated-next/final-ten-pair/` contains all raw logs, commands, environment, inventories,
samples, summary, and integrity checks; its sibling build and output directories retain binaries
and generated C. Baseline build records are in `c8-integrated-original/`.
`c9-final-validation/` pins the measured source commit and records 26 passing build/execution
commands, with 70 test-function invocations across final-source and exact-production checks.
Latest-base integration checks are recorded separately in `pr-integration-evidence/` and the
`pr-integration-validation-*` directories. The full suite and `test-all` are skipped at the user's
explicit request. Historical broader validation below belongs to its recorded revisions.
## Upstream integration, 2026-10-25
A fresh comparison of the merged implementation against upstream