cache: reuse bootstrap builtin objects in test builds (#31219)

* cache: retain builtin objects across test compilations

* checker: recognize builtin lifecycle attributes

* cache: stabilize ignored local names across source reparsing

* transform: preserve generic struct map return types

* cache: preserve error dispatch IDs and portable make warmups
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-27 11:46:33 +03:00
1 parent 7ec48941f8
commit 75365168ad
14 files changed
+300 -28

No files matched your search

+6
View File
@@ -291,6 +291,12 @@ endif
@echo "V has been successfully built"
@$(VEXE)$(EXE_EXT) -version
@$(VEXE)$(EXE_EXT) $(POST_BOOTSTRAP_CCOMPILER_VFLAG) -new-compiler -gc none run .github/problem-matchers/register_all.vsh
@set -e; \
cache_gcflags=; \
if [ ! -f "$(TMPTCC)/lib/libgc.a" ] && [ ! -f "$(TMPTCC)/lib/libgc.dylib" ]; then \
cache_gcflags='-gc none'; \
fi; \
$(VEXE)$(EXE_EXT) $$cache_gcflags $(VFLAGS) $(POST_BOOTSTRAP_CCOMPILER_VFLAG) -new-compiler cmd/tools/cache_basic_modules_test.v
v1:
ifdef LEGACY
+6 -1
View File
@@ -148,7 +148,12 @@ v:
./v -new-compiler run ./cmd/tools/detect_tcc.v; \
echo "V has been successfully built"; \
./v version; \
./v -new-compiler run .github/problem-matchers/register_all.vsh
./v -new-compiler run .github/problem-matchers/register_all.vsh; \
cache_gcflags=; \
if [ ! -f thirdparty/tcc/lib/libgc.a ] && [ ! -f thirdparty/tcc/lib/libgc.dylib ]; then \
cache_gcflags='-gc none'; \
fi; \
./v $$cache_gcflags $(VFLAGS) -new-compiler cmd/tools/cache_basic_modules_test.v
v1:
@set -e; \
+10
View File
@@ -0,0 +1,10 @@
// `make` runs this file to populate the default test configuration as well as
// the program configuration used by its bootstrap helpers. Tests can then link
// the builtin, strings, strconv, hash and bits bundle without rebuilding it.
import strconv
import strings
fn test_cached_basic_modules() {
assert strconv.atoi('42')! == 42
assert strings.repeat(120, 2) == 'xx'
}
+14 -1
View File
@@ -69,6 +69,17 @@ Different build flags, program or test modes, and import sequences can require
separate checks. Pass `v up -v` to see each checked configuration.
Cached module headers preserve struct field attribute groups, so valid deprecation
attributes do not introduce warnings during refresh.
`make` also warms the default test configuration's builtin module bundle, including
`strings`, `strconv`, `hash` and `bits`. Compatible test builds reuse these objects
when the program introduces new error types, retaining builtin error dispatch ID dependencies.
Dispatch hash collisions retain the full error-type dependency. Without a bundled collector,
the warmup uses `-gc none`; an explicit `-gc` setting takes precedence.
A missing object for another module rebuilds that module while retaining compatible cached
modules. Ignored local names remain stable when cached headers replace source files, so
reparsed modules can reuse identical C objects. Compiler flags, source changes and dependency
changes still invalidate the affected objects.
Ordinary builds report cache progress on standard error. Refreshes retain absolute
module search paths, resolved C compiler commands, and build modes. They omit
linker-only flags, including operands embedded in `-cflags`. Relative `V3CACHE`
@@ -1590,6 +1601,7 @@ println(upper_fn) // ['HELLO', 'WORLD']
`it` is a builtin variable which refers to the element currently being
processed in filter/map methods.
User-defined struct methods named `map` keep their declared return type, including when chained.
Additionally, `.any()` and `.all()` can be used to conveniently test
for elements that satisfy a condition.
@@ -8335,7 +8347,8 @@ Call-style attributes can also use named arguments.
The C backend emits `@[_constructor]` and `@[_destructor]` as GNU function attributes before the
function declarator, so definitions compile with GCC and Clang. When supported by the C compiler,
these hooks run before `main` and after it returns. They must not depend on V's runtime being
initialized. MSVC ignores these attributes.
initialized. Both are builtin attributes and require no user-defined attribute schema.
MSVC ignores these attributes.
```v
// @[flag] enables Enum types to be used as bitfields
@@ -0,0 +1,88 @@
import os
import time
import v.cmdexec
fn test_cached_builtin_survives_new_error_implementers_and_module_misses() {
$if windows {
return
}
root := os.join_path(os.vtmp_dir(), 'v_cache_builtin_reuse_${os.getpid()}_${time.now().unix_nano()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
// Keep the compiler configuration and imported modules fixed while changing
// the program's error implementations. Each file is a fresh test program.
for index in 0 .. 3 {
name := 'PayloadError${index}'
source := os.join_path(root, 'program_${index}_test.v')
os.write_file(source, '
import os
import strconv
struct ${name} {
Error
value int
}
fn (e ${name}) msg() string { return "payload " + e.value.str() }
fn (e ${name}) code() int { return e.value }
fn test_error_dispatch() {
err := IError(${name}{value: 42})
assert err.msg() == "payload 42"
assert err.code() == 42
assert err.str().contains("payload 42")
assert strconv.atoi("42")! == 42
assert os.getpid() > 0
}
')!
result := cmdexec.run_in('env', ['-u', 'VEXE', '-u', 'VFLAGS', 'V3_CACHE_TRACE=1',
'V3CACHE=${root}/cache', @VEXE, '-new-compiler', '-v', source], root)
assert result.exit_code == 0, result.output
if index == 0 {
assert result.output.contains('Caching module builtin...'), result.output
} else {
assert !result.output.contains('Caching module builtin...'), result.output
assert result.output.contains('object miss: module=os'), result.output
// An imported object's interface signature can miss; that retry must
// still parse the builtin declaration header rather than its sources.
assert result.output.contains('.vh files:'), result.output
assert !result.output.contains('/builtin/array.v'), result.output
}
}
}
fn test_cached_builtin_invalidates_for_colliding_error_dispatch_ids() {
$if windows {
return
}
root := os.join_path(os.vtmp_dir(), 'v_cache_error_collision_${os.getpid()}_${time.now().unix_nano()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
// The added names collide with Error, MessageError and None__, respectively.
for index, declarations in ['',
'
struct ABuecGy { Error }
struct A8n7fFM { Error }
struct AH8ogMV { Error }
'] {
source := os.join_path(root, 'collision_${index}_test.v')
os.write_file(source, declarations + '
struct ErrorHolder { err IError }
fn test_builtin_error_dispatch() {
plain := IError(Error{})
assert plain is Error
assert plain.msg() == ""
assert plain.code() == 0
message := error_with_code("detail", 37)
assert message is MessageError
assert message.str() == "detail; code: 37"
assert message.msg() == "detail"
assert message.code() == 37
none_error := ErrorHolder{err: none}
assert none_error.err.str() == "none"
}
')!
result := cmdexec.run_in('env', ['-u', 'VEXE', '-u', 'VFLAGS', 'V3CACHE=${root}/cache',
@VEXE, '-new-compiler', '-v', source], root)
assert result.exit_code == 0, result.output
assert result.output.contains('Caching module builtin...'), result.output
}
}
@@ -1,6 +1,7 @@
module driver
import v.flat
import v.types
// The object of a cached module is found under the implementer lists that the
// code of the module can depend on. These tests give module_signature() the
@@ -68,6 +69,49 @@ fn test_module_signature_changes_with_an_implementer_of_a_visible_interface() {
assert scopes.module_signature(visible_modules('io')) != other.module_signature(visible_modules('io'))
}
fn test_builtin_object_signature_does_not_depend_on_error_implementers() {
signature := scope_signature + '\n#reach MyErr=Shape'
scopes := parse_v3_interface_scopes(signature)
other := parse_v3_interface_scopes(signature.replace('IError=Error,MessageError,io.Eof,MyErr',
'IError=Error,MessageError,io.Eof,OtherErr').replace('Shape=Circle,Square',
'Shape=Circle,Square,Triangle') + '\n#module OtherErr=main\n#reach OtherErr=Shape')
assert scopes.object_signature(visible_modules(), true) == '#builtin IError=Error=1938660593,MessageError=71273906'
assert scopes.object_signature(visible_modules(), true) == other.object_signature(visible_modules(), true)
// Imported module objects can inspect error payloads and retain their scope.
assert scopes.module_signature(visible_modules()) != other.module_signature(visible_modules())
// Other visible interfaces still affect the bundle's generated helpers.
changed_reader := parse_v3_interface_scopes(signature.replace('io.Reader=io.BufferedReader,os.File,Feed',
'io.Reader=io.BufferedReader,os.File,Feed,OtherReader'))
assert scopes.object_signature(visible_modules('io'), true) != changed_reader.object_signature(visible_modules('io'), true)
// A project module named `hash` is not the builtin bundle.
state := V3ModuleCacheState{}
assert v3_cached_object_alias_signature('', scopes, state, ['hash'], []) != v3_cached_object_alias_signature('', other, state, [
'hash',
], [])
}
fn test_builtin_object_signature_retains_error_dispatch_collisions() {
// These names have the same dispatch hashes as Error, MessageError and None__.
for builtin_name, colliding_name in {
'Error': 'ABuecGy'
'MessageError': 'A8n7fFM'
'None__': 'AH8ogMV'
} {
builtin_id := flat.source_type_id(builtin_name)
collision_id := flat.source_type_id(colliding_name)
ids := types.stable_interface_type_ids([collision_id, builtin_id])
assert ids[builtin_id] != types.stable_interface_type_ids([builtin_id])[builtin_id]
original := 'IError=${builtin_name}\n#module ${builtin_name}=builtin\n#module IError=builtin'
scopes := parse_v3_interface_scopes(original)
changed := parse_v3_interface_scopes(original.replace('IError=${builtin_name}',
'IError=${colliding_name},${builtin_name}') + '\n#module ${colliding_name}=main')
assert scopes.object_signature(visible_modules(), true) != changed.object_signature(visible_modules(), true)
assert changed.object_signature(visible_modules(), true) == changed.module_signature(visible_modules())
}
unknown := parse_v3_interface_scopes('IError=Mystery\n#module IError=builtin')
assert unknown.object_signature(visible_modules(), true) == 'IError=Mystery'
}
// An implementer that the module cannot name brings the interfaces among its fields
// with it: the code that tells the implementers of `io.Reader` apart, to compare or
// to print them, handles the `Shape` inside a `Feed` as well.
+64 -12
View File
@@ -191,6 +191,7 @@ mut:
objects_only bool // the program unit is compiled and linked with the objects directly: no dylib, no program plan
no_program_declarations bool // nothing reads the declarations of the program unit on its own: no dev dylib, no generic or incremental plan
force_source bool
force_source_modules []string
bundle_valid bool
calls_recover bool // the code of a module of this build calls `recover()`, see cache_module_calls_recover
module_sources map[string][]string
@@ -13184,6 +13185,7 @@ pub fn run(args []string) {
bundle_sources: bundle_sources
bundle_source_paths: module_cache_source_path_set(p.a, bundle_sources)
force_source: force_cache_source
force_source_modules: os.getenv(v3_internal_cache_force_source_env).split_into_lines()
module_sources: map[string][]string{}
module_import_paths: map[string]string{}
module_dependencies: map[string][]string{}
@@ -13208,7 +13210,7 @@ pub fn run(args []string) {
cache_state.module_sources['builtin'] = builtin_files
mut files := []string{}
mut loaded_cached_bundle := false
if !force_cache_source {
if !force_cache_source && 'builtin' !in cache_state.force_source_modules {
if bundle_object := cache_manager.valid_object('builtin', bundle_sources) {
if builtin_header := cache_manager.valid_header('builtin', builtin_files) {
record_v3_cached_source_digests(mut cache_state, builtin_header.source_digests)
@@ -16578,7 +16580,6 @@ pub fn run(args []string) {
compile_signature += program_suffix
objects := cache_state.manager.valid_cgen_prepared_objects(cgen_cache_entry, compile_signature) or {
if resolve_flag_specific_cache_objects(mut cache_state, a, object_flags_signature + program_suffix, parse_v3_interface_scopes(interface_impl_signature)) {
os.setenv(v3_internal_cache_force_source_env, '1', true)
restart_v3_after_cache_invalidation(cc_dir)
}
resolved_objects := cache_object_paths(cache_state.objects)
@@ -18380,7 +18381,6 @@ fn v3_incremental_program_main_source(cached_prefix string, body_source string)
fn prepare_v3_incremental_cached_body(body_path string, prefix_path string, tcc_declarations_path string, cached_prefix string, object_base string, interface_impl_signature string, a &flat.FlatAst, mut state V3ModuleCacheState) !V3PreparedModuleCache {
if resolve_flag_specific_cache_objects(mut state, a, object_base, parse_v3_interface_scopes(interface_impl_signature)) {
os.setenv(v3_internal_cache_force_source_env, '1', true)
restart_v3_after_cache_invalidation(state.c_build_dir)
}
objects := cache_object_paths(state.objects)
@@ -18404,7 +18404,6 @@ fn prepare_v3_cached_generic_body(generated_source string, cached_prefix string,
return error('v3 module cache directory is unavailable')
}
if resolve_flag_specific_cache_objects(mut state, a, object_base, parse_v3_interface_scopes(interface_impl_signature)) {
os.setenv(v3_internal_cache_force_source_env, '1', true)
restart_v3_after_cache_invalidation(state.c_build_dir)
}
incremental_c_function_sections(generated_source) or {
@@ -18452,7 +18451,6 @@ fn prepare_v3_module_cache(generated_source string, cache_used_fns &map[string]b
mut has_declarations := false
scopes := parse_v3_interface_scopes(interface_impl_signature)
if resolve_flag_specific_cache_objects(mut state, tc.a, object_base, scopes) {
os.setenv(v3_internal_cache_force_source_env, '1', true)
restart_v3_after_cache_invalidation(state.c_build_dir)
}
main_body := split.modules['main'] or { '' }
@@ -18637,7 +18635,6 @@ fn prepare_v3_module_cache(generated_source string, cache_used_fns &map[string]b
// Refresh stops before the program link could discover that mismatch.
state.objects = object_paths.clone()
if resolve_flag_specific_cache_objects(mut state, tc.a, object_base, scopes) {
os.setenv(v3_internal_cache_force_source_env, '1', true)
restart_v3_after_cache_invalidation(state.c_build_dir)
}
}
@@ -20037,9 +20034,44 @@ fn (scopes &V3InterfaceScopes) declaring_module(name_id u32) string {
// An interface or an implementer that the signature says too little about makes
// the module depend on every list, as all of them did before.
fn (scopes &V3InterfaceScopes) module_signature(visible map[string]bool) string {
return scopes.object_signature(visible, false)
}
fn (scopes &V3InterfaceScopes) builtin_error_signature(name u32) ?string {
impls := scopes.impls[name]
ids := types.stable_interface_type_ids(impls)
mut builtin_ids := []string{}
for impl in impls {
// Retain the full dependency when hashes collide. The signature can also
// contain open generic types that code generation leaves out of dispatch.
seed := types.stable_interface_type_ids([impl])
if ids[impl] != seed[impl] {
return none
}
owner := scopes.declaring_module(impl)
if owner == '?' {
return none
}
if owner == 'builtin' {
builtin_ids << '${flat.source_type_name(impl)}=${ids[impl]}'
}
}
return '#builtin ${flat.source_type_name(name)}=${builtin_ids.join(',')}'
}
fn (scopes &V3InterfaceScopes) object_signature(visible map[string]bool, builtin_bundle bool) string {
mut in_scope := map[u32]bool{}
mut pending := []u32{cap: scopes.names.len}
mut lines := []string{cap: scopes.names.len}
for i, name in scopes.names {
// Dispatch stubs belong to the program, but builtin error boxing and the
// None__ check embed builtin dispatch IDs. Preserve those dependencies.
if builtin_bundle && flat.source_type_name(name) in ['IError', 'builtin.IError'] {
if signature := scopes.builtin_error_signature(name) {
lines << signature
continue
}
}
owner := scopes.owners[i]
// The code of a module cannot name an interface of the program.
if owner == 'main' {
@@ -20079,7 +20111,6 @@ fn (scopes &V3InterfaceScopes) module_signature(visible map[string]bool) string
}
}
}
mut lines := []string{cap: pending.len}
for name in scopes.names {
if in_scope[name] {
lines << scopes.lines[name]
@@ -20127,7 +20158,8 @@ fn cache_object_visible_modules(state &V3ModuleCacheState, roots []string, commo
// module from its header finds the object under this signature.
fn v3_cached_object_alias_signature(object_base string, scopes &V3InterfaceScopes, state &V3ModuleCacheState, roots []string, common []string) string {
visible := cache_object_visible_modules(state, roots, common)
return '${object_base}\ninterfaces=${scopes.module_signature(visible)}'
builtin_bundle := 'builtin' in roots && roots.all(it in modulecache.builtin_bundle_modules)
return '${object_base}\ninterfaces=${scopes.object_signature(visible, builtin_bundle)}'
}
// v3_cached_object_content_signature identifies a module object by how it is
@@ -20448,6 +20480,7 @@ fn resolve_flag_specific_cache_objects(mut state V3ModuleCacheState, a &flat.Fla
return false
}
common := cache_common_visible_modules(state, a)
mut missing := false
for object_name in state.objects.keys() {
roots := if object_name == 'builtin' {
cache_builtin_bundle_roots(state, a)
@@ -20464,7 +20497,9 @@ fn resolve_flag_specific_cache_objects(mut state V3ModuleCacheState, a &flat.Fla
if os.getenv('V3_CACHE_TRACE') != '' {
eprintln(' V3 module cache object miss: module=${object_name} reason=no object for these C compilation flags and interface implementations')
}
return true
force_v3_cache_object_sources(object_name)
missing = true
continue
}
dependency_inputs := cache_object_dependency_signatures(state, a, roots)
if entry := state.manager.valid_object_for_compile_signature(object_name, source_files, v3_cached_object_content_signature(object_base, content_key), dependency_inputs) {
@@ -20473,10 +20508,26 @@ fn resolve_flag_specific_cache_objects(mut state V3ModuleCacheState, a &flat.Fla
if os.getenv('V3_CACHE_TRACE') != '' {
eprintln(' V3 module cache object miss: module=${object_name}')
}
return true
force_v3_cache_object_sources(object_name)
missing = true
continue
}
}
return false
return missing
}
// force_v3_cache_object_sources reparses only the objects whose configuration
// changed. Other cached modules keep their headers and objects on the retry.
fn force_v3_cache_object_sources(object_name string) {
current := os.getenv(v3_internal_cache_force_source_env)
if current == '1' {
return
}
mut modules := current.split_into_lines().filter(it.len > 0)
if object_name !in modules {
modules << object_name
}
os.setenv(v3_internal_cache_force_source_env, modules.join('\n'), true)
}
fn (state &V3ModuleCacheState) print_module_cache_notice(module_name string) {
@@ -25713,7 +25764,8 @@ fn resolve_imports(mut a flat.FlatAst, mut p parser.Parser, prefs &pref.Preferen
cache_state.parsed_from_source[cache_module] = true
cache_state.source_body_modules[cache_module] = true
}
} else if !cache_state.force_source {
} else if !cache_state.force_source
&& cache_module !in cache_state.force_source_modules {
if cached := cache_state.manager.valid_entry_with_metadata_cache(cache_module, mod_files, mut cache_state.dependency_metadata) {
record_v3_cached_source_digests(mut cache_state, cached.source_digests)
cache_module_calls_recover(mut cache_state, modulecache.header_calls_recover(cached))
+14
View File
@@ -1,6 +1,20 @@
module flat
import os
import hash
import v.token
// discard_name identifies an ignored local by its source file and byte span.
// File table indices change when cached headers replace parsed source files.
// Synthetic positions without a named source retain a separate index namespace.
pub fn (a &FlatAst) discard_name(pos token.Pos) string {
if file := a.source_files[pos.id] {
if file.name.len > 0 {
return '__discard_${hash.sum64_string(file.name, 0)}_${pos.offset}_${pos.end}'
}
}
return '__discard_synthetic_${pos.id}_${pos.offset}_${pos.end}'
}
// record_source_path returns os.real_path(path) through the table of resolved
// source paths. Until resolve_source_paths freezes the table it also adds the
+24
View File
@@ -3,6 +3,30 @@ module flat
import os
import v.token
fn test_discard_names_survive_source_file_renumbering() {
mut file_set := token.FileSet.new()
mut a := FlatAst.new()
a.source_files[1] = file_set.add_file('library/first.v', 20)
a.source_files[2] = file_set.add_file('library/second.v', 20)
first := a.discard_name(token.new_span(1, 4, 5))
second := a.discard_name(token.new_span(2, 4, 5))
assert first != second
assert first != a.discard_name(token.new_span(1, 6, 7))
assert first != a.discard_name(token.new_span(1, 4, 6))
a.source_files.clear()
// A warm compilation can replace earlier files with a single cached header.
a.source_files[17] = file_set.add_file('library/first.v', 20)
a.source_files[3] = file_set.add_file('library/second.v', 20)
assert first == a.discard_name(token.new_span(17, 4, 5))
assert second == a.discard_name(token.new_span(3, 4, 5))
// Missing and unnamed source entries use the same explicit fallback.
missing := a.discard_name(token.new_span(9, 4, 5))
a.source_files[9] = file_set.add_file('', 20)
assert missing == a.discard_name(token.new_span(9, 4, 5))
assert missing != a.discard_name(token.new_span(10, 4, 5))
assert missing != first
}
// real_source_path must answer exactly like os.real_path, from the table for
// parsed sources and directly for anything else, and must never add to the
// table: threads read it concurrently once it is built.
+3 -4
View File
@@ -10361,11 +10361,10 @@ fn (mut g FlatGen) decl_lhs_str(id flat.NodeId) string {
return g.expr_to_string(id)
}
// discard_name derives ignored-local names from their source span. NodeIds for
// generated AST nodes depend on parallel append-region sizes and are not stable.
// discard_name derives ignored-local names from their source file and span.
// NodeIds and file table indices both change when cached modules replace sources.
fn (g &FlatGen) discard_name(id flat.NodeId) string {
pos := g.a.nodes[int(id)].pos
return '__discard_${pos.id}_${pos.offset}_${pos.end}'
return g.a.discard_name(g.a.nodes[int(id)].pos)
}
// The name a current parameter is read under. The identifier path reads one that
@@ -30,3 +30,17 @@ fn test_generics_method_chaining_call() {
println(s)
assert s.ar == [3, 4]
}
fn test_generics_method_chaining_changes_receiver_and_result_types() {
s := Seq[int]{[1, 22]}
.map[string](fn (element int) string {
return element.str()
})
.map[bool](fn (element string) bool {
return element.len > 1
})
.map[string](fn (element bool) string {
return if element { 'long' } else { 'short' }
})
assert s.ar == ['short', 'long']
}
+1 -1
View File
@@ -1066,7 +1066,7 @@ fn (mut t Transformer) lower_range_for_in(id flat.NodeId, node flat.Node, key_id
loop_name := if escaping_value {
t.new_temp('range_index')
} else if key.value == '_' {
'__discard_${key.pos.id}_${key.pos.offset}_${key.pos.end}'
t.a.discard_name(key.pos)
} else {
key.value
}
+10 -9
View File
@@ -2462,6 +2462,16 @@ fn (t &Transformer) array_map_call_type_name(id flat.NodeId, node flat.Node) ?u3
if fn_node.kind != .selector || fn_node.value != 'map' || fn_node.children_count == 0 {
return none
}
// User-defined map methods retain their declared return type.
base_id := t.a.child(fn_node, 0)
mut base_type0 := t.node_type(base_id)
if !flat.source_type_shape(t.trim_pointer_type(base_type0)).has(.array_type) {
base_type0 = t.original_expr_type(base_id)
}
base_type := t.trim_pointer_type(base_type0)
if !flat.source_type_shape(base_type).has(.array_type) {
return none
}
map_expr_id := t.a.child(&node, 1)
if callback_ret_source_id := t.array_map_callback_return_type_name(map_expr_id) {
callback_ret := flat.source_type_name(callback_ret_source_id)
@@ -2475,15 +2485,6 @@ fn (t &Transformer) array_map_call_type_name(id flat.NodeId, node flat.Node) ?u3
return checker_type_source_id
}
}
base_id := t.a.child(fn_node, 0)
mut base_type0 := t.node_type(base_id)
if !flat.source_type_shape(t.trim_pointer_type(base_type0)).has(.array_type) {
base_type0 = t.original_expr_type(base_id)
}
base_type := t.trim_pointer_type(base_type0)
if !flat.source_type_shape(base_type).has(.array_type) {
return none
}
mut elem_type_source_id := if callback_ret_source_id := t.array_map_callback_return_type_name(map_expr_id) {
callback_ret_source_id
} else if checker_type_source_id := t.checker_expr_type_name(map_expr_id) {
+2
View File
@@ -18216,6 +18216,8 @@ fn is_attribute_target_name(name string) bool {
// exact and case-sensitive: a name differing only in casing is unknown.
const builtin_attribute_names = [
'_allow_multiple_values',
'_constructor',
'_destructor',
'_linker_section',
'_naked',
'_pack',