Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,124 @@
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
|
||||
fn test_parse_fn_signature_and_return_generic_params_with_space() {
|
||||
info := parse_fn_signature_and_return('pub fn run_at[A, X](mut global_app A, params RunParams) ! {') or {
|
||||
assert false
|
||||
return
|
||||
}
|
||||
assert info.signature == 'pub fn run_at[A, X](mut global_app A, params RunParams)'
|
||||
assert info.return_type == '!'
|
||||
}
|
||||
|
||||
fn test_sanitize_staged_c_source_addresses_channel_semaphore_waits() {
|
||||
source := [
|
||||
'sync__Semaphore__wait(ch->writesem);',
|
||||
'sync__Semaphore__wait(&ch->readsem);',
|
||||
].join('\n')
|
||||
sanitized := sanitize_staged_c_source(source)
|
||||
assert sanitized.contains('sync__Semaphore__wait(&ch->writesem);')
|
||||
assert sanitized.contains('sync__Semaphore__wait(&ch->readsem);')
|
||||
assert !sanitized.contains('sync__Semaphore__wait(ch->writesem);')
|
||||
assert !sanitized.contains('sync__Semaphore__wait(&&ch->readsem);')
|
||||
}
|
||||
|
||||
fn test_sanitize_cached_main_c_source_addresses_channel_semaphore_waits() {
|
||||
source := [
|
||||
'sync__Semaphore__wait(ch->writesem);',
|
||||
'sync__Semaphore__wait(&ch->readsem);',
|
||||
].join('\n')
|
||||
sanitized := sanitize_cached_main_c_source(source)
|
||||
assert sanitized.contains('sync__Semaphore__wait(&ch->writesem);')
|
||||
assert sanitized.contains('sync__Semaphore__wait(&ch->readsem);')
|
||||
assert !sanitized.contains('sync__Semaphore__wait(ch->writesem);')
|
||||
assert !sanitized.contains('sync__Semaphore__wait(&&ch->readsem);')
|
||||
}
|
||||
|
||||
fn test_sanitize_cached_main_c_source_uses_c_typedef_names_for_sync_fields() {
|
||||
source := [
|
||||
'struct atomic_uintptr_t write_adr;',
|
||||
'struct pthread_rwlockattr_t attr;',
|
||||
'struct pthread_condattr_t attr;',
|
||||
].join('\n')
|
||||
sanitized := sanitize_cached_main_c_source(source)
|
||||
assert sanitized.contains('atomic_uintptr_t write_adr;')
|
||||
assert sanitized.contains('pthread_rwlockattr_t attr;')
|
||||
assert sanitized.contains('pthread_condattr_t attr;')
|
||||
assert !sanitized.contains('struct atomic_uintptr_t write_adr;')
|
||||
assert !sanitized.contains('struct pthread_rwlockattr_t attr;')
|
||||
assert !sanitized.contains('struct pthread_condattr_t attr;')
|
||||
}
|
||||
|
||||
fn test_sanitize_cached_object_c_source_uses_c_typedef_names_for_sync_fields() {
|
||||
source := [
|
||||
'struct atomic_uintptr_t write_adr;',
|
||||
'struct atomic_uintptr_t read_adr;',
|
||||
].join('\n')
|
||||
sanitized := sanitize_cached_object_c_source(source)
|
||||
assert sanitized.contains('atomic_uintptr_t write_adr;')
|
||||
assert sanitized.contains('atomic_uintptr_t read_adr;')
|
||||
assert !sanitized.contains('struct atomic_uintptr_t write_adr;')
|
||||
assert !sanitized.contains('struct atomic_uintptr_t read_adr;')
|
||||
}
|
||||
|
||||
fn test_sanitize_cached_source_guards_stdatomic_preamble_for_tcc_compat_header() {
|
||||
source := [
|
||||
'#include <stdint.h>',
|
||||
'#include <stdatomic.h>',
|
||||
'#include "/tmp/vroot/thirdparty/stdatomic/nix/atomic.h"',
|
||||
].join('\n')
|
||||
sanitized := sanitize_cached_object_c_source(source)
|
||||
assert sanitized.contains('#ifndef __TINYC__\n#include <stdatomic.h>\n#endif')
|
||||
assert sanitized.contains('#define extern static\n#endif\n#include "/tmp/vroot/thirdparty/stdatomic/nix/atomic.h"')
|
||||
assert sanitized.contains('#undef extern')
|
||||
assert sanitized.contains('#include "/tmp/vroot/thirdparty/stdatomic/nix/atomic.h"')
|
||||
}
|
||||
|
||||
fn test_sanitize_cached_source_keeps_stdatomic_preamble_without_compat_header() {
|
||||
source := [
|
||||
'#include <stdint.h>',
|
||||
'#include <stdatomic.h>',
|
||||
].join('\n')
|
||||
sanitized := sanitize_cached_object_c_source(source)
|
||||
assert sanitized == source
|
||||
}
|
||||
|
||||
fn test_join_flag_strings_deduplicates_partial_overlap() {
|
||||
flags := join_flag_strings('/tmp/sqlite3.c -lm -lpthread',
|
||||
'/tmp/sqlite3.c -lm -lssl -lcrypto -L/tmp/ssl -lpthread')
|
||||
assert flags == '/tmp/sqlite3.c -lm -lpthread -lssl -lcrypto -L/tmp/ssl'
|
||||
}
|
||||
|
||||
fn test_join_flag_strings_keeps_distinct_two_token_flags() {
|
||||
flags := join_flag_strings('-I /tmp/a -framework Cocoa',
|
||||
'-I /tmp/a -I /tmp/b -framework Foundation')
|
||||
assert flags == '-I /tmp/a -framework Cocoa -I /tmp/b -framework Foundation'
|
||||
}
|
||||
|
||||
fn test_user_entry_stamp_files_expands_directory_to_parsed_files() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_cache_entry_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
source_file := os.join_path(tmp_dir, 'main.v')
|
||||
header_file := os.join_path(tmp_dir, 'main.vh')
|
||||
mut b := Builder{
|
||||
pref: unsafe { nil }
|
||||
user_files: [tmp_dir]
|
||||
files: [
|
||||
ast.File{
|
||||
name: source_file
|
||||
},
|
||||
ast.File{
|
||||
name: header_file
|
||||
},
|
||||
ast.File{},
|
||||
]
|
||||
}
|
||||
files := b.user_entry_stamp_files()
|
||||
assert files == [os.norm_path(source_file)]
|
||||
assert os.norm_path(tmp_dir) !in files
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,59 @@
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.pref
|
||||
|
||||
// Concern 1: the pre-parse self-host fast relink must never fire for a request
|
||||
// that gen_cleanc() treats as "generation only" — otherwise a warm-cache
|
||||
// `-o foo.c cmd/v2/v2.v` links an executable into foo.c instead of writing C.
|
||||
fn test_fast_relink_skips_generation_only_outputs() {
|
||||
// A `.c` output is generation-only regardless of how it would be built.
|
||||
b_c := new_builder(&pref.Preferences{ backend: .cleanc })
|
||||
assert b_c.fast_relink_output_is_generation_only('/tmp/v2.c')
|
||||
assert b_c.fast_relink_output_is_generation_only('out.c')
|
||||
|
||||
// A shared library is generation-only.
|
||||
b_shared := new_builder(&pref.Preferences{ backend: .cleanc, is_shared_lib: true })
|
||||
assert b_shared.fast_relink_output_is_generation_only('/tmp/lib')
|
||||
|
||||
// A normal local executable build is NOT generation-only — the fast relink
|
||||
// is allowed to proceed (subject to its cache/freshness checks).
|
||||
b_exe := new_builder(&pref.Preferences{ backend: .cleanc })
|
||||
assert b_exe.can_compile_cleanc_locally() // sanity: host target compiles locally
|
||||
assert !b_exe.fast_relink_output_is_generation_only('/tmp/v3')
|
||||
}
|
||||
|
||||
// Concern 2: the fast relink trusts the cc/cc_flags recorded in main.stamp, so a
|
||||
// pre-parse fingerprint of the flag inputs that do NOT need parsing must change
|
||||
// when the compiler / prod-shared mode / env CFLAGS change, and stay stable
|
||||
// otherwise. A changed fingerprint is what invalidates a would-be stale relink.
|
||||
fn test_preparse_flag_fingerprint_tracks_flag_settings() {
|
||||
base := new_builder(&pref.Preferences{ backend: .cleanc })
|
||||
fp0 := base.preparse_flag_fingerprint()
|
||||
|
||||
// Stable for identical settings.
|
||||
assert new_builder(&pref.Preferences{ backend: .cleanc }).preparse_flag_fingerprint() == fp0
|
||||
|
||||
// A different C compiler changes it.
|
||||
b_cc := new_builder(&pref.Preferences{ backend: .cleanc, ccompiler: 'some-other-cc' })
|
||||
assert b_cc.preparse_flag_fingerprint() != fp0
|
||||
|
||||
// -prod changes it (different optimization flags).
|
||||
b_prod := new_builder(&pref.Preferences{ backend: .cleanc, is_prod: true })
|
||||
assert b_prod.preparse_flag_fingerprint() != fp0
|
||||
|
||||
// -shared changes it (no -flto, different link mode).
|
||||
b_shared := new_builder(&pref.Preferences{ backend: .cleanc, is_shared_lib: true })
|
||||
assert b_shared.preparse_flag_fingerprint() != fp0
|
||||
|
||||
// Env CFLAGS (V2CFLAGS) change it.
|
||||
prev := os.getenv('V2CFLAGS')
|
||||
os.setenv('V2CFLAGS', '-DV2_FAST_RELINK_TEST', true)
|
||||
fp_env := new_builder(&pref.Preferences{ backend: .cleanc }).preparse_flag_fingerprint()
|
||||
if prev == '' {
|
||||
os.unsetenv('V2CFLAGS')
|
||||
} else {
|
||||
os.setenv('V2CFLAGS', prev, true)
|
||||
}
|
||||
assert fp_env != fp0
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
import v2.pref
|
||||
|
||||
fn collect_cflags_for_test_source(source string, mut prefs pref.Preferences) string {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_flag_target_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
source_path := os.join_path(tmp_dir, 'main.v')
|
||||
os.write_file(source_path, source) or { panic(err) }
|
||||
prefs.skip_builtin = true
|
||||
mut b := new_builder(&prefs)
|
||||
b.files = [
|
||||
ast.File{
|
||||
name: source_path
|
||||
},
|
||||
]
|
||||
return b.collect_cflags_from_sources()
|
||||
}
|
||||
|
||||
fn collect_cflags_for_flat_only_test_source(source string, mut prefs pref.Preferences) string {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_flag_target_flat_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
source_path := os.join_path(tmp_dir, 'main.v')
|
||||
os.write_file(source_path, source) or { panic(err) }
|
||||
prefs.skip_builtin = true
|
||||
mut b := new_builder(&prefs)
|
||||
b.flat = ast.flatten_files([
|
||||
ast.File{
|
||||
name: source_path
|
||||
mod: 'main'
|
||||
},
|
||||
])
|
||||
b.files = []ast.File{}
|
||||
return b.collect_cflags_from_sources()
|
||||
}
|
||||
|
||||
fn freestanding_test_call_name(name string, user_defines []string) string {
|
||||
return freestanding_test_call_name_with_hooks(name, user_defines, [])
|
||||
}
|
||||
|
||||
fn freestanding_test_call_name_with_hooks(name string, user_defines []string, hooks []string) string {
|
||||
return freestanding_restricted_call_in_expr(ast.Expr(ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: name
|
||||
})
|
||||
}), FreestandingScanContext{
|
||||
user_defines: user_defines
|
||||
target_os: 'linux'
|
||||
freestanding_hooks: hooks
|
||||
})
|
||||
}
|
||||
|
||||
fn test_flag_directives_match_cross_and_freestanding_preferences() {
|
||||
mut cross_prefs := pref.new_preferences()
|
||||
cross_prefs.target_os = 'cross'
|
||||
cross_prefs.user_defines = ['cross']
|
||||
cross_flag := parse_flag_directive_line_with_pref('#flag cross -DCROSS_DIRECT',
|
||||
'/tmp/source.v', &cross_prefs) or { '' }
|
||||
linux_flag := parse_flag_directive_line_with_pref('#flag linux -DLINUX_DIRECT',
|
||||
'/tmp/source.v', &cross_prefs) or { '' }
|
||||
assert cross_flag == '-DCROSS_DIRECT'
|
||||
assert linux_flag == ''
|
||||
|
||||
mut free_prefs := pref.new_preferences()
|
||||
free_prefs.target_os = 'linux'
|
||||
free_prefs.user_defines = ['freestanding']
|
||||
free_flag := parse_flag_directive_line_with_pref('#flag freestanding -DFREE_DIRECT',
|
||||
'/tmp/source.v', &free_prefs) or { '' }
|
||||
linux_in_free_flag := parse_flag_directive_line_with_pref('#flag linux -DLINUX_DIRECT',
|
||||
'/tmp/source.v', &free_prefs) or { '' }
|
||||
assert free_flag == '-DFREE_DIRECT'
|
||||
assert linux_in_free_flag == '-DLINUX_DIRECT'
|
||||
}
|
||||
|
||||
fn test_comptime_flag_blocks_match_cross_and_freestanding_preferences() {
|
||||
mut cross_prefs := pref.new_preferences()
|
||||
cross_prefs.target_os = 'cross'
|
||||
cross_prefs.user_defines = ['cross']
|
||||
assert comptime_cond_matches_with_pref('cross', &cross_prefs)
|
||||
assert !comptime_cond_matches_with_pref('linux', &cross_prefs)
|
||||
assert !comptime_cond_matches_with_pref('linux ?', &cross_prefs)
|
||||
assert comptime_cond_matches_with_pref('cross && !linux', &cross_prefs)
|
||||
|
||||
mut optional_linux_prefs := pref.new_preferences()
|
||||
optional_linux_prefs.target_os = 'cross'
|
||||
optional_linux_prefs.user_defines = ['cross', 'linux']
|
||||
optional_linux_prefs.explicit_user_defines = ['linux']
|
||||
assert comptime_cond_matches_with_pref('linux ?', &optional_linux_prefs)
|
||||
|
||||
mut linux_prefs := pref.new_preferences()
|
||||
linux_prefs.target_os = 'linux'
|
||||
assert comptime_cond_matches_with_pref('linux', &linux_prefs)
|
||||
assert !comptime_cond_matches_with_pref('linux ?', &linux_prefs)
|
||||
assert !comptime_cond_matches_with_pref('linux ? || windows', &linux_prefs)
|
||||
linux_prefs.user_defines = ['linux']
|
||||
linux_prefs.explicit_user_defines = ['linux']
|
||||
assert comptime_cond_matches_with_pref('linux ?', &linux_prefs)
|
||||
assert comptime_cond_matches_with_pref('linux ? || windows', &linux_prefs)
|
||||
|
||||
mut termux_prefs := pref.new_preferences()
|
||||
termux_prefs.target_os = 'termux'
|
||||
assert comptime_cond_matches_with_pref('termux', &termux_prefs)
|
||||
assert comptime_cond_matches_with_pref('linux || termux', &termux_prefs)
|
||||
assert !comptime_cond_matches_with_pref('android', &termux_prefs)
|
||||
assert !comptime_cond_matches_with_pref('android && !termux', &termux_prefs)
|
||||
|
||||
mut free_prefs := pref.new_preferences()
|
||||
free_prefs.target_os = 'linux'
|
||||
free_prefs.user_defines = ['freestanding']
|
||||
assert comptime_cond_matches_with_pref('freestanding', &free_prefs)
|
||||
assert !comptime_cond_matches_with_pref('freestanding ?', &free_prefs)
|
||||
assert comptime_cond_matches_with_pref('linux', &free_prefs)
|
||||
assert comptime_cond_matches_with_pref('freestanding && linux', &free_prefs)
|
||||
}
|
||||
|
||||
fn test_comptime_pkgconfig_condition_matches_available_package() {
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.target_os = os.user_os()
|
||||
assert !comptime_cond_matches_with_pref(r"$pkgconfig('__v2_definitely_missing_pkgconfig_test_package__')",
|
||||
&prefs)
|
||||
if pref.comptime_pkgconfig_value('sqlite3') {
|
||||
assert comptime_cond_matches_with_pref(r"$pkgconfig('sqlite3')", &prefs)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_pkgconfig_directive_collects_flags_for_active_native_branch() {
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.target_os = os.user_os()
|
||||
if pref.comptime_pkgconfig_value('sqlite3') {
|
||||
flag := parse_flag_directive_line_with_pref('#pkgconfig sqlite3', '/tmp/source.v', &prefs) or {
|
||||
''
|
||||
}
|
||||
assert flag.contains('sqlite3')
|
||||
}
|
||||
|
||||
mut cross_prefs := pref.new_preferences()
|
||||
cross_prefs.target_os = 'cross'
|
||||
cross_prefs.output_cross_c = true
|
||||
flag := parse_flag_directive_line_with_pref('#pkgconfig sqlite3', '/tmp/source.v', &cross_prefs) or {
|
||||
''
|
||||
}
|
||||
assert flag == ''
|
||||
}
|
||||
|
||||
fn test_comptime_optional_target_mode_flags_require_explicit_define() {
|
||||
mut cross_prefs := pref.new_preferences()
|
||||
cross_prefs.target_os = 'cross'
|
||||
cross_prefs.user_defines = ['cross']
|
||||
assert comptime_cond_matches_with_pref('cross', &cross_prefs)
|
||||
assert !comptime_cond_matches_with_pref('cross ?', &cross_prefs)
|
||||
cross_prefs.explicit_user_defines = ['cross']
|
||||
assert comptime_cond_matches_with_pref('cross ?', &cross_prefs)
|
||||
|
||||
mut free_prefs := pref.new_preferences()
|
||||
free_prefs.target_os = 'linux'
|
||||
free_prefs.user_defines = ['freestanding']
|
||||
assert comptime_cond_matches_with_pref('freestanding', &free_prefs)
|
||||
assert !comptime_cond_matches_with_pref('freestanding ?', &free_prefs)
|
||||
free_prefs.explicit_user_defines = ['freestanding']
|
||||
assert comptime_cond_matches_with_pref('freestanding ?', &free_prefs)
|
||||
|
||||
mut none_prefs := pref.new_preferences()
|
||||
none_prefs.target_os = 'none'
|
||||
none_prefs.user_defines = ['freestanding']
|
||||
assert comptime_cond_matches_with_pref('none', &none_prefs)
|
||||
assert !comptime_cond_matches_with_pref('none ?', &none_prefs)
|
||||
none_prefs.explicit_user_defines = ['none']
|
||||
assert comptime_cond_matches_with_pref('none ?', &none_prefs)
|
||||
}
|
||||
|
||||
fn test_comptime_flag_blocks_use_or_and_not_precedence() {
|
||||
mut linux_prefs := pref.new_preferences()
|
||||
linux_prefs.target_os = 'linux'
|
||||
assert comptime_cond_matches_with_pref('!windows && linux', &linux_prefs)
|
||||
assert comptime_cond_matches_with_pref('linux || windows && macos', &linux_prefs)
|
||||
|
||||
mut macos_prefs := pref.new_preferences()
|
||||
macos_prefs.target_os = 'macos'
|
||||
assert !comptime_cond_matches_with_pref('!windows && linux', &macos_prefs)
|
||||
assert !comptime_cond_matches_with_pref('linux || windows && macos', &macos_prefs)
|
||||
|
||||
mut windows_prefs := pref.new_preferences()
|
||||
windows_prefs.target_os = 'windows'
|
||||
assert !comptime_cond_matches_with_pref('!windows && linux', &windows_prefs)
|
||||
assert !comptime_cond_matches_with_pref('linux || windows && macos', &windows_prefs)
|
||||
}
|
||||
|
||||
fn test_parser_level_comptime_flags_match_cross_and_accepted_targets() {
|
||||
assert ast_comptime_flag_matches('cross', [], 'cross')
|
||||
assert ast_comptime_flag_matches('solaris', [], 'solaris')
|
||||
assert ast_comptime_flag_matches('qnx', [], 'qnx')
|
||||
assert ast_comptime_flag_matches('serenity', [], 'serenity')
|
||||
assert ast_comptime_flag_matches('plan9', [], 'plan9')
|
||||
assert ast_comptime_flag_matches('vinix', [], 'vinix')
|
||||
assert ast_comptime_flag_matches('ios', [], 'ios')
|
||||
assert ast_comptime_flag_matches('termux', [], 'termux')
|
||||
assert ast_comptime_flag_matches('none', [], 'none')
|
||||
assert !ast_comptime_flag_matches('linux', [], 'cross')
|
||||
assert !ast_comptime_flag_matches('linux', ['linux'], 'windows')
|
||||
assert !ast_comptime_flag_matches('none', ['none'], 'linux')
|
||||
}
|
||||
|
||||
fn test_flag_directives_match_all_accepted_target_os_names() {
|
||||
for target in ['android', 'termux', 'ios', 'solaris', 'qnx', 'serenity', 'plan9', 'vinix'] {
|
||||
flag := parse_flag_directive_line('#flag ${target} -D${target.to_upper()}',
|
||||
'/tmp/source.v', target) or { '' }
|
||||
assert flag == '-D${target.to_upper()}'
|
||||
assert comptime_cond_matches(target, target)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_collect_cflags_from_sources_matches_cross_and_freestanding_blocks() {
|
||||
cross_source := 'module main
|
||||
|
||||
$if cross {
|
||||
#flag -DCROSS_BLOCK
|
||||
}
|
||||
$if cross ? {
|
||||
#flag -DOPTIONAL_CROSS_BLOCK
|
||||
}
|
||||
$if linux {
|
||||
#flag -DLINUX_BLOCK
|
||||
}
|
||||
$if linux ? {
|
||||
#flag -DOPTIONAL_LINUX_BLOCK
|
||||
}
|
||||
$if macos {
|
||||
#flag -DMACOS_BLOCK
|
||||
}
|
||||
$if windows {
|
||||
#flag -DWINDOWS_BLOCK
|
||||
}
|
||||
#flag cross -DCROSS_DIRECT
|
||||
#flag linux -DLINUX_DIRECT
|
||||
#flag macos -DMACOS_DIRECT
|
||||
#flag windows -DWINDOWS_DIRECT
|
||||
'
|
||||
mut cross_prefs := pref.new_preferences()
|
||||
cross_prefs.target_os = 'cross'
|
||||
cross_prefs.user_defines = ['cross']
|
||||
cross_flags := collect_cflags_for_test_source(cross_source, mut cross_prefs)
|
||||
assert cross_flags.contains('-DCROSS_BLOCK')
|
||||
assert cross_flags.contains('-DCROSS_DIRECT')
|
||||
assert !cross_flags.contains('-DOPTIONAL_CROSS_BLOCK')
|
||||
assert !cross_flags.contains('-DOPTIONAL_LINUX_BLOCK')
|
||||
host_os := normalize_target_os_name(os.user_os())
|
||||
if host_os in ['linux', 'macos', 'windows'] {
|
||||
assert cross_flags.contains(match host_os {
|
||||
'macos' { '-DMACOS_BLOCK' }
|
||||
'windows' { '-DWINDOWS_BLOCK' }
|
||||
else { '-DLINUX_BLOCK' }
|
||||
})
|
||||
assert cross_flags.contains(match host_os {
|
||||
'macos' { '-DMACOS_DIRECT' }
|
||||
'windows' { '-DWINDOWS_DIRECT' }
|
||||
else { '-DLINUX_DIRECT' }
|
||||
})
|
||||
}
|
||||
if host_os != 'linux' {
|
||||
assert !cross_flags.contains('-DLINUX_BLOCK')
|
||||
assert !cross_flags.contains('-DLINUX_DIRECT')
|
||||
}
|
||||
if host_os != 'macos' {
|
||||
assert !cross_flags.contains('-DMACOS_BLOCK')
|
||||
assert !cross_flags.contains('-DMACOS_DIRECT')
|
||||
}
|
||||
if host_os != 'windows' {
|
||||
assert !cross_flags.contains('-DWINDOWS_BLOCK')
|
||||
assert !cross_flags.contains('-DWINDOWS_DIRECT')
|
||||
}
|
||||
mut cross_with_optional_linux_prefs := pref.new_preferences()
|
||||
cross_with_optional_linux_prefs.target_os = 'cross'
|
||||
cross_with_optional_linux_prefs.user_defines = ['cross', 'linux']
|
||||
cross_with_optional_linux_prefs.explicit_user_defines = ['linux']
|
||||
cross_with_optional_linux_flags := collect_cflags_for_test_source(cross_source, mut
|
||||
cross_with_optional_linux_prefs)
|
||||
assert cross_with_optional_linux_flags.contains('-DOPTIONAL_LINUX_BLOCK')
|
||||
assert !cross_with_optional_linux_flags.contains('-DOPTIONAL_CROSS_BLOCK')
|
||||
|
||||
mut cross_explicit_prefs := pref.new_preferences()
|
||||
cross_explicit_prefs.target_os = 'cross'
|
||||
cross_explicit_prefs.user_defines = ['cross']
|
||||
cross_explicit_prefs.explicit_user_defines = ['cross']
|
||||
cross_explicit_flags := collect_cflags_for_test_source(cross_source, mut cross_explicit_prefs)
|
||||
assert cross_explicit_flags.contains('-DOPTIONAL_CROSS_BLOCK')
|
||||
|
||||
free_source := 'module main
|
||||
|
||||
$if freestanding {
|
||||
#flag -DFREE_BLOCK
|
||||
}
|
||||
$if freestanding ? {
|
||||
#flag -DOPTIONAL_FREE_BLOCK
|
||||
}
|
||||
$if linux {
|
||||
#flag -DLINUX_BLOCK
|
||||
}
|
||||
#flag freestanding -DFREE_DIRECT
|
||||
#flag linux -DLINUX_DIRECT
|
||||
'
|
||||
mut free_prefs := pref.new_preferences()
|
||||
free_prefs.target_os = 'linux'
|
||||
free_prefs.user_defines = ['freestanding']
|
||||
free_flags := collect_cflags_for_test_source(free_source, mut free_prefs)
|
||||
assert free_flags.contains('-DFREE_BLOCK')
|
||||
assert !free_flags.contains('-DOPTIONAL_FREE_BLOCK')
|
||||
assert free_flags.contains('-DFREE_DIRECT')
|
||||
assert free_flags.contains('-DLINUX_BLOCK')
|
||||
assert free_flags.contains('-DLINUX_DIRECT')
|
||||
|
||||
mut free_explicit_prefs := pref.new_preferences()
|
||||
free_explicit_prefs.target_os = 'linux'
|
||||
free_explicit_prefs.user_defines = ['freestanding']
|
||||
free_explicit_prefs.explicit_user_defines = ['freestanding']
|
||||
free_explicit_flags := collect_cflags_for_test_source(free_source, mut free_explicit_prefs)
|
||||
assert free_explicit_flags.contains('-DOPTIONAL_FREE_BLOCK')
|
||||
}
|
||||
|
||||
fn test_collect_cflags_from_sources_reads_flat_file_names() {
|
||||
source := 'module main
|
||||
|
||||
#flag -DFLAT_ONLY_FLAG
|
||||
'
|
||||
mut prefs := pref.new_preferences()
|
||||
flags := collect_cflags_for_flat_only_test_source(source, mut prefs)
|
||||
assert flags.contains('-DFLAT_ONLY_FLAG')
|
||||
}
|
||||
|
||||
fn test_collect_cflags_from_sources_keeps_optional_os_flags_custom_only() {
|
||||
source := 'module main
|
||||
|
||||
$if linux {
|
||||
#flag -DLINUX_BLOCK
|
||||
}
|
||||
$if linux ? {
|
||||
#flag -DOPTIONAL_LINUX_BLOCK
|
||||
}
|
||||
$if linux ? || windows {
|
||||
#flag -DOPTIONAL_LINUX_OR_WINDOWS_BLOCK
|
||||
}
|
||||
'
|
||||
mut linux_prefs := pref.new_preferences()
|
||||
linux_prefs.target_os = 'linux'
|
||||
linux_flags := collect_cflags_for_test_source(source, mut linux_prefs)
|
||||
assert linux_flags.contains('-DLINUX_BLOCK')
|
||||
assert !linux_flags.contains('-DOPTIONAL_LINUX_BLOCK')
|
||||
assert !linux_flags.contains('-DOPTIONAL_LINUX_OR_WINDOWS_BLOCK')
|
||||
|
||||
mut linux_defined_prefs := pref.new_preferences()
|
||||
linux_defined_prefs.target_os = 'linux'
|
||||
linux_defined_prefs.user_defines = ['linux']
|
||||
linux_defined_prefs.explicit_user_defines = ['linux']
|
||||
linux_defined_flags := collect_cflags_for_test_source(source, mut linux_defined_prefs)
|
||||
assert linux_defined_flags.contains('-DLINUX_BLOCK')
|
||||
assert linux_defined_flags.contains('-DOPTIONAL_LINUX_BLOCK')
|
||||
assert linux_defined_flags.contains('-DOPTIONAL_LINUX_OR_WINDOWS_BLOCK')
|
||||
|
||||
mut windows_prefs := pref.new_preferences()
|
||||
windows_prefs.target_os = 'windows'
|
||||
windows_flags := collect_cflags_for_test_source(source, mut windows_prefs)
|
||||
assert !windows_flags.contains('-DLINUX_BLOCK')
|
||||
assert !windows_flags.contains('-DOPTIONAL_LINUX_BLOCK')
|
||||
assert windows_flags.contains('-DOPTIONAL_LINUX_OR_WINDOWS_BLOCK')
|
||||
}
|
||||
|
||||
fn test_collect_cflags_from_sources_keeps_optional_none_custom_only() {
|
||||
source := 'module main
|
||||
|
||||
$if none {
|
||||
#flag -DNONE_BLOCK
|
||||
}
|
||||
$if none ? {
|
||||
#flag -DOPTIONAL_NONE_BLOCK
|
||||
}
|
||||
'
|
||||
mut none_prefs := pref.new_preferences()
|
||||
none_prefs.target_os = 'none'
|
||||
none_prefs.user_defines = ['freestanding']
|
||||
none_flags := collect_cflags_for_test_source(source, mut none_prefs)
|
||||
assert none_flags.contains('-DNONE_BLOCK')
|
||||
assert !none_flags.contains('-DOPTIONAL_NONE_BLOCK')
|
||||
|
||||
mut none_explicit_prefs := pref.new_preferences()
|
||||
none_explicit_prefs.target_os = 'none'
|
||||
none_explicit_prefs.user_defines = ['freestanding']
|
||||
none_explicit_prefs.explicit_user_defines = ['none']
|
||||
none_explicit_flags := collect_cflags_for_test_source(source, mut none_explicit_prefs)
|
||||
assert none_explicit_flags.contains('-DOPTIONAL_NONE_BLOCK')
|
||||
}
|
||||
|
||||
fn test_collect_cflags_from_sources_matches_termux_blocks_and_directives() {
|
||||
source := 'module main
|
||||
|
||||
$if termux {
|
||||
#flag -DTERMUX_BLOCK
|
||||
}
|
||||
$if linux || termux {
|
||||
#flag -DTERMUX_OR_LINUX_BLOCK
|
||||
}
|
||||
$if android && !termux {
|
||||
#flag -DANDROID_OUTSIDE_TERMUX_BLOCK
|
||||
}
|
||||
#flag termux -DTERMUX_DIRECT
|
||||
#flag android -DANDROID_DIRECT
|
||||
'
|
||||
mut termux_prefs := pref.new_preferences()
|
||||
termux_prefs.target_os = 'termux'
|
||||
flags := collect_cflags_for_test_source(source, mut termux_prefs)
|
||||
assert flags.contains('-DTERMUX_BLOCK')
|
||||
assert flags.contains('-DTERMUX_OR_LINUX_BLOCK')
|
||||
assert flags.contains('-DTERMUX_DIRECT')
|
||||
assert !flags.contains('-DANDROID_OUTSIDE_TERMUX_BLOCK')
|
||||
assert !flags.contains('-DANDROID_DIRECT')
|
||||
}
|
||||
|
||||
fn test_freestanding_diagnostics_gate_direct_builtins_by_capability() {
|
||||
for name in ['print', 'println', 'eprint', 'eprintln'] {
|
||||
assert freestanding_test_call_name(name, ['freestanding_hooks']) == name
|
||||
assert freestanding_test_call_name_with_hooks(name, ['freestanding_hooks',
|
||||
'freestanding_output'], ['output']) == ''
|
||||
}
|
||||
assert freestanding_test_call_name('panic', ['freestanding_hooks']) == 'panic'
|
||||
assert freestanding_test_call_name_with_hooks('panic', ['freestanding_hooks',
|
||||
'freestanding_panic'], ['panic']) == ''
|
||||
}
|
||||
|
||||
fn test_freestanding_diagnostics_leave_runtime_helpers_to_cleanc() {
|
||||
for name in ['_write_buf_to_fd', '_writeln_to_fd', 'flush_stdout', 'flush_stderr'] {
|
||||
assert freestanding_test_call_name(name, ['freestanding_hooks']) == ''
|
||||
}
|
||||
|
||||
assert freestanding_test_call_name_with_hooks('arguments', ['freestanding_hooks',
|
||||
'freestanding_output'], ['output']) == ''
|
||||
assert freestanding_test_call_name_with_hooks('arguments', ['freestanding_hooks',
|
||||
'freestanding_alloc'], ['alloc']) == ''
|
||||
assert freestanding_test_call_name('malloc_noscan',
|
||||
['freestanding_hooks', 'freestanding_alloc']) == ''
|
||||
assert freestanding_test_call_name_with_hooks('malloc_noscan', [
|
||||
'freestanding_hooks',
|
||||
'freestanding_alloc',
|
||||
], ['alloc']) == ''
|
||||
assert freestanding_restricted_call_in_expr(ast.Expr(ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'println'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.BasicLiteral{
|
||||
kind: .number
|
||||
value: '123'
|
||||
}),
|
||||
]
|
||||
}), FreestandingScanContext{
|
||||
user_defines: ['freestanding_hooks', 'freestanding_output']
|
||||
target_os: 'linux'
|
||||
freestanding_hooks: ['output']
|
||||
}) == ''
|
||||
assert freestanding_restricted_call_in_expr(ast.Expr(ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'println'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.BasicLiteral{
|
||||
kind: .number
|
||||
value: '123'
|
||||
}),
|
||||
]
|
||||
}), FreestandingScanContext{
|
||||
user_defines: ['freestanding_hooks', 'freestanding_output', 'freestanding_alloc']
|
||||
target_os: 'linux'
|
||||
freestanding_hooks: ['output', 'alloc']
|
||||
}) == ''
|
||||
}
|
||||
|
||||
fn test_freestanding_diagnostics_skip_inactive_conditional_functions() {
|
||||
ctx := FreestandingScanContext{
|
||||
user_defines: ['freestanding']
|
||||
target_os: 'linux'
|
||||
}
|
||||
hosted_call := ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.Expr(ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'println'
|
||||
})
|
||||
})
|
||||
})
|
||||
inactive_windows_fn := ast.Stmt(ast.FnDecl{
|
||||
attributes: [
|
||||
ast.Attribute{
|
||||
comptime_cond: ast.Expr(ast.Ident{
|
||||
name: 'windows'
|
||||
})
|
||||
},
|
||||
]
|
||||
stmts: [hosted_call]
|
||||
})
|
||||
inactive_not_freestanding_fn := ast.Stmt(ast.FnDecl{
|
||||
attributes: [
|
||||
ast.Attribute{
|
||||
comptime_cond: ast.Expr(ast.PrefixExpr{
|
||||
op: .not
|
||||
expr: ast.Expr(ast.Ident{
|
||||
name: 'freestanding'
|
||||
})
|
||||
})
|
||||
},
|
||||
]
|
||||
stmts: [hosted_call]
|
||||
})
|
||||
active_fn := ast.Stmt(ast.FnDecl{
|
||||
attributes: [
|
||||
ast.Attribute{
|
||||
comptime_cond: ast.Expr(ast.Ident{
|
||||
name: 'freestanding'
|
||||
})
|
||||
},
|
||||
]
|
||||
stmts: [hosted_call]
|
||||
})
|
||||
assert freestanding_restricted_call_in_stmt(inactive_windows_fn, ctx) == ''
|
||||
assert freestanding_restricted_call_in_stmt(inactive_not_freestanding_fn, ctx) == ''
|
||||
assert freestanding_restricted_call_in_stmt(active_fn, ctx) == 'println'
|
||||
}
|
||||
|
||||
fn test_freestanding_diagnostics_leave_array_allocation_to_cleanc() {
|
||||
ctx := FreestandingScanContext{
|
||||
user_defines: ['freestanding']
|
||||
target_os: 'linux'
|
||||
}
|
||||
fixed_array := ast.Expr(ast.ArrayInitExpr{
|
||||
typ: ast.Expr(ast.Type(ast.ArrayFixedType{
|
||||
len: ast.Expr(ast.BasicLiteral{
|
||||
kind: .number
|
||||
value: '3'
|
||||
})
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
}))
|
||||
})
|
||||
dynamic_array := ast.Expr(ast.ArrayInitExpr{
|
||||
typ: ast.Expr(ast.Type(ast.ArrayType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
}))
|
||||
})
|
||||
fixed_literal_array := ast.Expr(ast.ArrayInitExpr{
|
||||
exprs: [
|
||||
ast.Expr(ast.BasicLiteral{
|
||||
kind: .number
|
||||
value: '1'
|
||||
}),
|
||||
ast.Expr(ast.BasicLiteral{
|
||||
kind: .number
|
||||
value: '2'
|
||||
}),
|
||||
ast.Expr(ast.BasicLiteral{
|
||||
kind: .number
|
||||
value: '3'
|
||||
}),
|
||||
]
|
||||
len: ast.Expr(ast.PostfixExpr{
|
||||
op: .not
|
||||
expr: ast.empty_expr
|
||||
})
|
||||
})
|
||||
assert freestanding_restricted_call_in_expr(fixed_array, ctx) == ''
|
||||
assert freestanding_restricted_call_in_expr(fixed_literal_array, ctx) == ''
|
||||
assert freestanding_restricted_call_in_expr(dynamic_array, ctx) == ''
|
||||
}
|
||||
|
||||
fn test_collect_cflags_from_sources_uses_or_and_not_precedence() {
|
||||
source := 'module main
|
||||
|
||||
$if !windows && linux {
|
||||
#flag -DNEGATED_AND_LINUX
|
||||
}
|
||||
|
||||
$if linux || windows && macos {
|
||||
#flag -DOR_AND_LINUX
|
||||
}
|
||||
'
|
||||
mut linux_prefs := pref.new_preferences()
|
||||
linux_prefs.target_os = 'linux'
|
||||
linux_flags := collect_cflags_for_test_source(source, mut linux_prefs)
|
||||
assert linux_flags.contains('-DNEGATED_AND_LINUX')
|
||||
assert linux_flags.contains('-DOR_AND_LINUX')
|
||||
|
||||
mut macos_prefs := pref.new_preferences()
|
||||
macos_prefs.target_os = 'macos'
|
||||
macos_flags := collect_cflags_for_test_source(source, mut macos_prefs)
|
||||
assert !macos_flags.contains('-DNEGATED_AND_LINUX')
|
||||
assert !macos_flags.contains('-DOR_AND_LINUX')
|
||||
|
||||
mut windows_prefs := pref.new_preferences()
|
||||
windows_prefs.target_os = 'windows'
|
||||
windows_flags := collect_cflags_for_test_source(source, mut windows_prefs)
|
||||
assert !windows_flags.contains('-DNEGATED_AND_LINUX')
|
||||
assert !windows_flags.contains('-DOR_AND_LINUX')
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// vtest build: macos
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
import v2.parser
|
||||
import v2.pref
|
||||
import v2.token
|
||||
|
||||
fn parse_source_for_flat(src string) []ast.File {
|
||||
tmp := '/tmp/v2_flat_roundtrip_${os.getpid()}.v'
|
||||
os.write_file(tmp, src) or { panic('write_file: ${err}') }
|
||||
defer {
|
||||
os.rm(tmp) or {}
|
||||
}
|
||||
p := &pref.Preferences{}
|
||||
mut fs := token.FileSet.new()
|
||||
mut par := parser.Parser.new(p)
|
||||
return par.parse_files([tmp], mut fs)
|
||||
}
|
||||
|
||||
fn assert_roundtrip(src string) {
|
||||
files := parse_source_for_flat(src)
|
||||
flat_a := ast.flatten_files(files)
|
||||
rt_files := flat_a.to_files()
|
||||
flat_b := ast.flatten_files(rt_files)
|
||||
sig_a := flat_a.signature()
|
||||
sig_b := flat_b.signature()
|
||||
if sig_a != sig_b {
|
||||
mut diff_at := 0
|
||||
for diff_at < sig_a.len && diff_at < sig_b.len && sig_a[diff_at] == sig_b[diff_at] {
|
||||
diff_at++
|
||||
}
|
||||
from := if diff_at > 80 { diff_at - 80 } else { 0 }
|
||||
end_a := if diff_at + 200 < sig_a.len { diff_at + 200 } else { sig_a.len }
|
||||
end_b := if diff_at + 200 < sig_b.len { diff_at + 200 } else { sig_b.len }
|
||||
eprintln('signature mismatch at offset ${diff_at}')
|
||||
eprintln('A: ${sig_a[from..end_a]}')
|
||||
eprintln('B: ${sig_b[from..end_b]}')
|
||||
}
|
||||
assert sig_a == sig_b, 'round-trip signature mismatch for source:\n${src}'
|
||||
}
|
||||
|
||||
fn test_roundtrip_module_only() {
|
||||
assert_roundtrip('module foo\n')
|
||||
}
|
||||
|
||||
fn test_roundtrip_simple_fn() {
|
||||
src := 'module main
|
||||
|
||||
fn add(a int, b int) int {
|
||||
return a + b
|
||||
}
|
||||
'
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_struct_and_const() {
|
||||
src := "module main
|
||||
|
||||
pub const greeting = 'hello'
|
||||
|
||||
pub struct Point {
|
||||
pub mut:
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
fn (p Point) sum() int {
|
||||
return p.x + p.y
|
||||
}
|
||||
"
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_control_flow() {
|
||||
src := 'module main
|
||||
|
||||
fn run(n int) int {
|
||||
mut total := 0
|
||||
for i := 0; i < n; i++ {
|
||||
if i % 2 == 0 {
|
||||
total += i
|
||||
} else {
|
||||
total -= i
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
'
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_string_inter() {
|
||||
src := "module main
|
||||
|
||||
fn show(name string, age int) string {
|
||||
return 'name=\${name} age=\${age:04d}'
|
||||
}
|
||||
"
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_match_and_array() {
|
||||
src := 'module main
|
||||
|
||||
fn classify(x int) string {
|
||||
return match x {
|
||||
0 { "zero" }
|
||||
1, 2, 3 { "small" }
|
||||
else { "big" }
|
||||
}
|
||||
}
|
||||
|
||||
fn sum_array() int {
|
||||
nums := [1, 2, 3, 4, 5]
|
||||
mut total := 0
|
||||
for n in nums {
|
||||
total += n
|
||||
}
|
||||
return total
|
||||
}
|
||||
'
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_map_and_or() {
|
||||
src := "module main
|
||||
|
||||
fn lookup() int {
|
||||
m := {'a': 1, 'b': 2}
|
||||
v := m['a'] or { -1 }
|
||||
return v
|
||||
}
|
||||
"
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_enum_and_attrs() {
|
||||
src := 'module main
|
||||
|
||||
@[flag]
|
||||
pub enum Color {
|
||||
red
|
||||
green
|
||||
blue
|
||||
}
|
||||
'
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_interface() {
|
||||
src := 'module main
|
||||
|
||||
pub interface Animal {
|
||||
name() string
|
||||
mut:
|
||||
age int
|
||||
}
|
||||
'
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_generic_fn() {
|
||||
src := 'module main
|
||||
|
||||
fn max[T](a T, b T) T {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
'
|
||||
assert_roundtrip(src)
|
||||
}
|
||||
|
||||
fn test_roundtrip_real_source_file() {
|
||||
// Round-trip a real V2 source file end-to-end. This exercises the full
|
||||
// surface of parser-produced AST variants.
|
||||
path := os.real_path('vlib/v2_toberemoved/ast/ast.v')
|
||||
if !os.exists(path) {
|
||||
return
|
||||
}
|
||||
src := os.read_file(path) or { return }
|
||||
files := parse_source_for_flat(src)
|
||||
flat_a := ast.flatten_files(files)
|
||||
rt_files := flat_a.to_files()
|
||||
flat_b := ast.flatten_files(rt_files)
|
||||
assert flat_a.signature() == flat_b.signature(), 'round-trip mismatch for ${path}'
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// vtest build: macos
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
import v2.parser
|
||||
import v2.pref
|
||||
import v2.token
|
||||
|
||||
// Phase 2 regression: Parser.parse_files_to_flat must produce a FlatAst whose
|
||||
// canonical signature is identical to ast.flatten_files(Parser.parse_files()).
|
||||
// This guards the streaming path while downstream consumers are migrated.
|
||||
|
||||
fn write_tmp_file(name string, src string) string {
|
||||
path := '/tmp/v2_flat_stream_${os.getpid()}_${name}.v'
|
||||
os.write_file(path, src) or { panic('write_file: ${err}') }
|
||||
return path
|
||||
}
|
||||
|
||||
fn assert_streaming_matches(srcs map[string]string) {
|
||||
mut paths := []string{cap: srcs.len}
|
||||
for name, src in srcs {
|
||||
paths << write_tmp_file(name, src)
|
||||
}
|
||||
defer {
|
||||
for p in paths {
|
||||
os.rm(p) or {}
|
||||
}
|
||||
}
|
||||
|
||||
prefs := &pref.Preferences{}
|
||||
|
||||
mut fs_batch := token.FileSet.new()
|
||||
mut p_batch := parser.Parser.new(prefs)
|
||||
legacy_files := p_batch.parse_files(paths, mut fs_batch)
|
||||
flat_batch := ast.flatten_files(legacy_files)
|
||||
|
||||
mut fs_stream := token.FileSet.new()
|
||||
mut p_stream := parser.Parser.new(prefs)
|
||||
flat_stream := p_stream.parse_files_to_flat(paths, mut fs_stream)
|
||||
|
||||
sig_batch := flat_batch.signature()
|
||||
sig_stream := flat_stream.signature()
|
||||
if sig_batch != sig_stream {
|
||||
mut diff_at := 0
|
||||
for diff_at < sig_batch.len && diff_at < sig_stream.len
|
||||
&& sig_batch[diff_at] == sig_stream[diff_at] {
|
||||
diff_at++
|
||||
}
|
||||
from := if diff_at > 80 { diff_at - 80 } else { 0 }
|
||||
end_b := if diff_at + 200 < sig_batch.len { diff_at + 200 } else { sig_batch.len }
|
||||
end_s := if diff_at + 200 < sig_stream.len { diff_at + 200 } else { sig_stream.len }
|
||||
eprintln('signature mismatch at offset ${diff_at}')
|
||||
eprintln('batch: ${sig_batch[from..end_b]}')
|
||||
eprintln('stream: ${sig_stream[from..end_s]}')
|
||||
}
|
||||
assert sig_batch == sig_stream, 'streaming flat AST diverges from batch flatten'
|
||||
}
|
||||
|
||||
fn test_streaming_single_file() {
|
||||
assert_streaming_matches({
|
||||
'single': 'module main
|
||||
|
||||
fn add(a int, b int) int {
|
||||
return a + b
|
||||
}
|
||||
'
|
||||
})
|
||||
}
|
||||
|
||||
fn test_streaming_multi_file_order() {
|
||||
// Two files should produce node ids in parse order regardless of whether
|
||||
// the conversion is batched or streamed. Strings interned by file 0 must
|
||||
// be reused by file 1 in both cases.
|
||||
a := 'module main
|
||||
|
||||
pub const greeting = "hello"
|
||||
|
||||
fn say() string {
|
||||
return greeting
|
||||
}
|
||||
'
|
||||
b := 'module main
|
||||
|
||||
pub struct Point { x int y int }
|
||||
|
||||
fn (p Point) sum() int { return p.x + p.y }
|
||||
'
|
||||
assert_streaming_matches({
|
||||
'a': a
|
||||
'b': b
|
||||
})
|
||||
}
|
||||
|
||||
fn test_streaming_real_source_file() {
|
||||
path := os.real_path('vlib/v2_toberemoved/ast/ast.v')
|
||||
if !os.exists(path) {
|
||||
return
|
||||
}
|
||||
prefs := &pref.Preferences{}
|
||||
|
||||
mut fs_batch := token.FileSet.new()
|
||||
mut p_batch := parser.Parser.new(prefs)
|
||||
legacy := p_batch.parse_files([path], mut fs_batch)
|
||||
flat_batch := ast.flatten_files(legacy)
|
||||
|
||||
mut fs_stream := token.FileSet.new()
|
||||
mut p_stream := parser.Parser.new(prefs)
|
||||
flat_stream := p_stream.parse_files_to_flat([path], mut fs_stream)
|
||||
|
||||
assert flat_batch.signature() == flat_stream.signature(), 'real-source streaming mismatch'
|
||||
}
|
||||
|
||||
fn test_default_flat_parallel_transform_keeps_user_files() {
|
||||
path := write_tmp_file('parallel_transform_main', 'module main
|
||||
|
||||
fn main() {
|
||||
println("flat parallel ok")
|
||||
}
|
||||
')
|
||||
out_path := '/tmp/v2_flat_parallel_transform_${os.getpid()}.out'
|
||||
defer {
|
||||
os.rm(path) or {}
|
||||
os.rm(out_path) or {}
|
||||
}
|
||||
cmd := '${os.quoted_path(@VEXE)} -v2 -nocache -o ${os.quoted_path(out_path)} ${os.quoted_path(path)} 2>&1'
|
||||
res := os.execute(cmd)
|
||||
assert res.exit_code == 0, res.output
|
||||
run_res := os.execute(os.quoted_path(out_path))
|
||||
assert run_res.exit_code == 0, run_res.output
|
||||
assert run_res.output.trim_space() == 'flat parallel ok', run_res.output
|
||||
}
|
||||
|
||||
fn test_default_parallel_transform_monomorphizes_generic_calls_for_native_backend() {
|
||||
path := write_tmp_file('parallel_transform_generic_main', 'module main
|
||||
|
||||
fn id[T](x T) T {
|
||||
return x
|
||||
}
|
||||
|
||||
fn main() {
|
||||
_ = id(41)
|
||||
}
|
||||
')
|
||||
out_path := '/tmp/v2_parallel_generic_transform_${os.getpid()}.out'
|
||||
defer {
|
||||
os.rm(path) or {}
|
||||
os.rm(out_path) or {}
|
||||
}
|
||||
cmd := '${os.quoted_path(@VEXE)} -v2 -nocache -backend arm64 -gc none -o ${os.quoted_path(out_path)} ${os.quoted_path(path)} 2>&1'
|
||||
res := os.execute(cmd)
|
||||
assert !res.output.contains('_id_T_int'), res.output
|
||||
assert res.exit_code == 0, res.output
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
import runtime
|
||||
import v2.gen.arm64
|
||||
|
||||
$if !windows {
|
||||
struct GenARM64ChunkArgs {
|
||||
worker voidptr // &arm64.Gen — pre-cloned worker (created on main thread)
|
||||
start_idx int
|
||||
end_idx int
|
||||
}
|
||||
|
||||
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
|
||||
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
|
||||
fn C.pthread_attr_init(attr voidptr) int
|
||||
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
|
||||
fn C.pthread_attr_destroy(attr voidptr) int
|
||||
|
||||
fn gen_arm64_chunk_thread(arg voidptr) voidptr {
|
||||
a := unsafe { &GenARM64ChunkArgs(arg) }
|
||||
mut w := unsafe { &arm64.Gen(a.worker) }
|
||||
for fi := a.start_idx; fi < a.end_idx; fi++ {
|
||||
w.gen_func(fi)
|
||||
}
|
||||
return unsafe { nil }
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut b Builder) gen_arm64_parallel(mut gen arm64.Gen) {
|
||||
gen.gen_pre_pass()
|
||||
|
||||
n_funcs := gen.mod.funcs.len
|
||||
n_jobs := runtime.nr_jobs()
|
||||
|
||||
$if windows {
|
||||
for fi := 0; fi < n_funcs; fi++ {
|
||||
gen.gen_func(fi)
|
||||
}
|
||||
gen.gen_post_pass()
|
||||
return
|
||||
} $else {
|
||||
if n_funcs <= 1 || n_jobs <= 1 {
|
||||
// Fallback to sequential
|
||||
for fi := 0; fi < n_funcs; fi++ {
|
||||
gen.gen_func(fi)
|
||||
}
|
||||
gen.gen_post_pass()
|
||||
return
|
||||
}
|
||||
|
||||
// Split functions into chunks
|
||||
chunk_size := (n_funcs + n_jobs - 1) / n_jobs
|
||||
mut thread_ids := []C.pthread_t{len: n_jobs}
|
||||
mut args := []GenARM64ChunkArgs{cap: n_jobs}
|
||||
|
||||
// Pre-create all workers on the main thread to avoid concurrent .clone() races.
|
||||
// Each worker gets its own deep copy of maps/arrays.
|
||||
mut workers := []voidptr{cap: n_jobs}
|
||||
|
||||
mut chunk_idx := 0
|
||||
mut i := 0
|
||||
for i < n_funcs {
|
||||
end := if i + chunk_size < n_funcs { i + chunk_size } else { n_funcs }
|
||||
w := gen.new_worker_clone()
|
||||
workers << voidptr(w)
|
||||
args << GenARM64ChunkArgs{
|
||||
worker: voidptr(w)
|
||||
start_idx: i
|
||||
end_idx: end
|
||||
}
|
||||
i = end
|
||||
chunk_idx++
|
||||
}
|
||||
|
||||
attr_buf := [64]u8{}
|
||||
attr := unsafe { voidptr(&attr_buf[0]) }
|
||||
C.pthread_attr_init(attr)
|
||||
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
|
||||
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
C.pthread_create(unsafe { &thread_ids[ci] }, attr, gen_arm64_chunk_thread,
|
||||
unsafe { voidptr(&args[ci]) })
|
||||
}
|
||||
C.pthread_attr_destroy(attr)
|
||||
|
||||
// Wait for all workers
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
C.pthread_join(thread_ids[ci], unsafe { nil })
|
||||
}
|
||||
|
||||
// Merge worker results in order
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
w := unsafe { &arm64.Gen(workers[ci]) }
|
||||
gen.merge_worker(w)
|
||||
}
|
||||
|
||||
gen.gen_post_pass()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
import runtime
|
||||
import time
|
||||
import v2.gen.cleanc
|
||||
|
||||
const max_cleanc_pass5_jobs = 16
|
||||
|
||||
$if !windows {
|
||||
struct GenCleancChunkArgs {
|
||||
worker voidptr // &cleanc.Gen — pre-cloned worker
|
||||
work_items_ptr voidptr // &[]cleanc.Pass5WorkItem — work items to process
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
struct C.pthread_t {}
|
||||
|
||||
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
|
||||
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
|
||||
fn C.pthread_attr_init(attr voidptr) int
|
||||
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
|
||||
fn C.pthread_attr_destroy(attr voidptr) int
|
||||
|
||||
fn gen_cleanc_chunk_thread(arg voidptr) voidptr {
|
||||
a := unsafe { &GenCleancChunkArgs(arg) }
|
||||
mut w := unsafe { &cleanc.Gen(a.worker) }
|
||||
items := unsafe { &[]cleanc.Pass5WorkItem(a.work_items_ptr) }
|
||||
w.gen_pass5_work_items(*items)
|
||||
return unsafe { nil }
|
||||
}
|
||||
}
|
||||
|
||||
fn print_cleanc_parallel_step_time(stats_enabled bool, step string, elapsed time.Duration) {
|
||||
if !stats_enabled {
|
||||
return
|
||||
}
|
||||
println(' - C Gen/full ${step}: ${elapsed.milliseconds()}ms')
|
||||
}
|
||||
|
||||
fn mark_cleanc_parallel_step(stats_enabled bool, mut sw time.StopWatch, stage_start time.Duration, step string) time.Duration {
|
||||
if !stats_enabled {
|
||||
return stage_start
|
||||
}
|
||||
now := sw.elapsed()
|
||||
print_cleanc_parallel_step_time(true, step, time.Duration(now - stage_start))
|
||||
return now
|
||||
}
|
||||
|
||||
fn (mut b Builder) gen_cleanc_parallel(mut gen cleanc.Gen) {
|
||||
stats_enabled := b.pref != unsafe { nil } && b.pref.stats
|
||||
mut stats_sw := time.new_stopwatch()
|
||||
mut stage_start := stats_sw.elapsed()
|
||||
emit_indices := gen.gen_pass5_pre()
|
||||
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 pre')
|
||||
|
||||
// Split large files into sub-file work items so no single huge file
|
||||
// (e.g. ssa/builder.v) pins the whole parallel phase.
|
||||
work_items := gen.build_pass5_work_items(emit_indices)
|
||||
n_items := work_items.len
|
||||
n_runtime_jobs := runtime.nr_jobs()
|
||||
n_jobs := cleanc_parallel_pass5_job_count(n_runtime_jobs, n_items)
|
||||
|
||||
$if windows {
|
||||
gen.gen_pass5_files(emit_indices)
|
||||
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
|
||||
'pass 5 files')
|
||||
gen.gen_pass5_post()
|
||||
_ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post')
|
||||
return
|
||||
} $else {
|
||||
if n_items <= 1 || n_jobs <= 1 {
|
||||
// Fallback to sequential
|
||||
gen.gen_pass5_files(emit_indices)
|
||||
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
|
||||
'pass 5 files')
|
||||
gen.gen_pass5_post()
|
||||
_ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post')
|
||||
return
|
||||
}
|
||||
|
||||
mut thread_ids := []C.pthread_t{len: n_jobs}
|
||||
mut args := []GenCleancChunkArgs{cap: n_jobs}
|
||||
mut workers := []voidptr{cap: n_jobs}
|
||||
// chunk_items: work items assigned to each worker.
|
||||
// chunk_indices: unique file indices each worker touches (for
|
||||
// new_pass5_worker's owned-file / cross-worker dedup bookkeeping).
|
||||
mut chunk_items := [][]cleanc.Pass5WorkItem{cap: n_jobs}
|
||||
mut chunk_indices := [][]int{cap: n_jobs}
|
||||
mut chunk_file_seen := []map[int]bool{cap: n_jobs}
|
||||
mut chunk_costs := []int{cap: n_jobs}
|
||||
|
||||
mut chunk_idx := n_jobs
|
||||
if chunk_idx > n_items {
|
||||
chunk_idx = n_items
|
||||
}
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
chunk_items << []cleanc.Pass5WorkItem{}
|
||||
chunk_indices << []int{}
|
||||
chunk_file_seen << map[int]bool{}
|
||||
chunk_costs << 0
|
||||
}
|
||||
mut sorted_items := work_items.clone()
|
||||
for i := 1; i < sorted_items.len; i++ {
|
||||
mut j := i
|
||||
for j > 0 && sorted_items[j - 1].cost < sorted_items[j].cost {
|
||||
sorted_items[j - 1], sorted_items[j] = sorted_items[j], sorted_items[j - 1]
|
||||
j--
|
||||
}
|
||||
}
|
||||
for item in sorted_items {
|
||||
mut target := 0
|
||||
for ci := 1; ci < chunk_idx; ci++ {
|
||||
if chunk_costs[ci] < chunk_costs[target] {
|
||||
target = ci
|
||||
}
|
||||
}
|
||||
chunk_items[target] << item
|
||||
// Only the worker that emits a file's globals (a whole-file item, or the
|
||||
// first slice of a split file — both carry emit_globals) takes file-level
|
||||
// dedup ownership. A split file's later slices deliberately do NOT, so the
|
||||
// file's lazily/transitively emitted fns stay blocked in those workers and
|
||||
// only the owning worker can emit them; the explicit slice still emits via
|
||||
// the owner-scoped bypass in gen_file_range. This closes the duplicate /
|
||||
// reordered-emission hole that file-level ownership left open for files
|
||||
// split across workers.
|
||||
if item.emit_globals && item.file_idx !in chunk_file_seen[target] {
|
||||
chunk_file_seen[target][item.file_idx] = true
|
||||
chunk_indices[target] << item.file_idx
|
||||
}
|
||||
chunk_costs[target] += item.cost
|
||||
}
|
||||
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
|
||||
'pass 5 chunk split')
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
w := gen.new_pass5_worker(chunk_indices[ci], ci)
|
||||
workers << voidptr(w)
|
||||
}
|
||||
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
|
||||
'pass 5 worker setup')
|
||||
|
||||
// Set up args after all chunk_items are stable
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
args << GenCleancChunkArgs{
|
||||
worker: workers[ci]
|
||||
work_items_ptr: unsafe { voidptr(&chunk_items[ci]) }
|
||||
}
|
||||
}
|
||||
|
||||
attr_buf := [64]u8{}
|
||||
attr := unsafe { voidptr(&attr_buf[0]) }
|
||||
C.pthread_attr_init(attr)
|
||||
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
|
||||
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
C.pthread_create(unsafe { &thread_ids[ci] }, attr, gen_cleanc_chunk_thread,
|
||||
unsafe { voidptr(&args[ci]) })
|
||||
}
|
||||
C.pthread_attr_destroy(attr)
|
||||
|
||||
// Wait for all workers
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
C.pthread_join(thread_ids[ci], unsafe { nil })
|
||||
}
|
||||
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
|
||||
'pass 5 worker run')
|
||||
|
||||
// Merge worker results in order
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
w := unsafe { &cleanc.Gen(workers[ci]) }
|
||||
gen.merge_pass5_worker(w)
|
||||
}
|
||||
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
|
||||
'pass 5 merge')
|
||||
gen.print_pass5_file_times(8)
|
||||
|
||||
gen.gen_pass5_post()
|
||||
_ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post')
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanc_parallel_pass5_job_count(n_runtime_jobs int, n_files int) int {
|
||||
if n_runtime_jobs <= 0 || n_files <= 0 {
|
||||
return 0
|
||||
}
|
||||
mut n_jobs := n_runtime_jobs
|
||||
if n_jobs > max_cleanc_pass5_jobs {
|
||||
n_jobs = max_cleanc_pass5_jobs
|
||||
}
|
||||
if n_jobs > n_files {
|
||||
n_jobs = n_files
|
||||
}
|
||||
return n_jobs
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module builder
|
||||
|
||||
fn test_cleanc_parallel_pass5_job_count_caps_runtime_jobs() {
|
||||
assert cleanc_parallel_pass5_job_count(128, 1000) == max_cleanc_pass5_jobs
|
||||
assert cleanc_parallel_pass5_job_count(64, 3) == 3
|
||||
assert cleanc_parallel_pass5_job_count(2, 1000) == 2
|
||||
assert cleanc_parallel_pass5_job_count(4, 0) == 0
|
||||
assert cleanc_parallel_pass5_job_count(0, 4) == 0
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
#include <malloc/malloc.h>
|
||||
|
||||
struct C.malloc_statistics_t {
|
||||
blocks_in_use u32
|
||||
size_in_use usize
|
||||
max_size_in_use usize
|
||||
size_allocated usize
|
||||
}
|
||||
|
||||
fn C.malloc_default_zone() voidptr
|
||||
fn C.malloc_zone_statistics(zone voidptr, stats &C.malloc_statistics_t)
|
||||
|
||||
// darwin_live_malloc_bytes returns (current live malloc bytes, peak live bytes)
|
||||
// from the default malloc zone. Under `-gc none` (no frees) the current value
|
||||
// is monotonic across phases, so per-phase deltas are the exact number of bytes
|
||||
// each phase allocated and never released. This is the reliable counterpart to
|
||||
// runtime.used_memory(), whose resident_size reading is distorted by OS paging.
|
||||
fn darwin_live_malloc_bytes() (u64, u64) {
|
||||
mut st := C.malloc_statistics_t{}
|
||||
C.malloc_zone_statistics(C.malloc_default_zone(), &st)
|
||||
return u64(st.size_in_use), u64(st.max_size_in_use)
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
import v2.gen.v as gen_v
|
||||
import v2.parser
|
||||
import v2.pref
|
||||
import v2.token
|
||||
|
||||
fn module_storage_cache_tmp_dir(label string) string {
|
||||
return os.join_path(os.temp_dir(), 'v2_module_storage_cache_${label}_${os.getpid()}')
|
||||
}
|
||||
|
||||
fn module_storage_cached_header_source_for_test() string {
|
||||
tmp_dir := module_storage_cache_tmp_dir('source')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
source_file := os.join_path(tmp_dir, 'state', 'state.v')
|
||||
os.write_file(source_file, 'module state
|
||||
|
||||
pub __global mut total = 0
|
||||
__global mut hidden = 0
|
||||
') or {
|
||||
panic(err)
|
||||
}
|
||||
mut prefs := pref.new_preferences()
|
||||
mut b := new_builder(&prefs)
|
||||
source_files := b.parse_source_files_for_headers([source_file])
|
||||
header_ast := b.build_module_header_ast(source_files, 'state') or {
|
||||
panic('missing state header')
|
||||
}
|
||||
mut gen := gen_v.new_gen(b.pref)
|
||||
gen.gen(header_ast)
|
||||
return sanitize_header_source(gen.output_string(), map[string]string{})
|
||||
}
|
||||
|
||||
fn module_mut_cached_header_source_for_test() string {
|
||||
tmp_dir := module_storage_cache_tmp_dir('module_mut_source')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
source_file := os.join_path(tmp_dir, 'state', 'state.v')
|
||||
os.write_file(source_file, 'module state
|
||||
|
||||
pub struct Counter {
|
||||
pub module_mut:
|
||||
value int
|
||||
pub mut:
|
||||
open int
|
||||
}
|
||||
') or {
|
||||
panic(err)
|
||||
}
|
||||
mut prefs := pref.new_preferences()
|
||||
mut b := new_builder(&prefs)
|
||||
source_files := b.parse_source_files_for_headers([source_file])
|
||||
header_ast := b.build_module_header_ast(source_files, 'state') or {
|
||||
panic('missing state header')
|
||||
}
|
||||
mut gen := gen_v.new_gen(b.pref)
|
||||
gen.gen(header_ast)
|
||||
return sanitize_header_source(gen.output_string(), map[string]string{})
|
||||
}
|
||||
|
||||
fn module_mut_interface_cached_header_source_for_test() string {
|
||||
return module_mut_interface_cached_header_source_for_test_with_source_decls(false)
|
||||
}
|
||||
|
||||
fn module_mut_interface_merged_cached_header_source_for_test() string {
|
||||
return module_mut_interface_cached_header_source_for_test_with_source_decls(true)
|
||||
}
|
||||
|
||||
fn module_mut_interface_cached_header_source_for_test_with_source_decls(include_source_decls bool) string {
|
||||
tmp_dir := module_storage_cache_tmp_dir('module_mut_interface_source')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
source_file := os.join_path(tmp_dir, 'state', 'state.v')
|
||||
os.write_file(source_file, 'module state
|
||||
|
||||
pub type Handler = fn (int) bool
|
||||
|
||||
pub interface Mutator {
|
||||
handler Handler
|
||||
direct fn (int) bool
|
||||
handle(int) bool
|
||||
mut:
|
||||
bump()
|
||||
reset()
|
||||
}
|
||||
|
||||
pub struct Counter {
|
||||
pub module_mut:
|
||||
iface Mutator
|
||||
}
|
||||
') or {
|
||||
panic(err)
|
||||
}
|
||||
mut prefs := pref.new_preferences()
|
||||
mut b := new_builder(&prefs)
|
||||
source_files := b.parse_source_files_for_headers([source_file])
|
||||
header_ast := b.build_module_header_ast(source_files, 'state') or {
|
||||
panic('missing state header')
|
||||
}
|
||||
mut gen := gen_v.new_gen(b.pref)
|
||||
gen.gen(header_ast)
|
||||
mut header_source := sanitize_header_source(gen.output_string(), map[string]string{})
|
||||
if include_source_decls {
|
||||
source_fn_decls := b.source_fn_decls_for_files([source_file])
|
||||
header_source = merge_missing_source_fn_decls(header_source, source_fn_decls)
|
||||
}
|
||||
return header_source
|
||||
}
|
||||
|
||||
fn module_storage_cached_header_global(header_source string, name string) ast.FieldDecl {
|
||||
tmp_dir := module_storage_cache_tmp_dir('parse')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
header_path := os.join_path(tmp_dir, 'state.vh')
|
||||
os.write_file(header_path, header_source) or { panic(err) }
|
||||
mut prefs := pref.new_preferences()
|
||||
mut file_set := token.FileSet.new()
|
||||
mut par := parser.Parser.new(&prefs)
|
||||
files := par.parse_files([header_path], mut file_set)
|
||||
for stmt in files[0].stmts {
|
||||
if stmt is ast.GlobalDecl {
|
||||
for field in stmt.fields {
|
||||
if field.name == name {
|
||||
return field
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panic('missing cached header field ${name}')
|
||||
}
|
||||
|
||||
fn module_storage_cached_header_struct(header_source string, name string) ast.StructDecl {
|
||||
tmp_dir := module_storage_cache_tmp_dir('parse_struct')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
header_path := os.join_path(tmp_dir, 'state.vh')
|
||||
os.write_file(header_path, header_source) or { panic(err) }
|
||||
mut prefs := pref.new_preferences()
|
||||
mut file_set := token.FileSet.new()
|
||||
mut par := parser.Parser.new(&prefs)
|
||||
files := par.parse_files([header_path], mut file_set)
|
||||
for stmt in files[0].stmts {
|
||||
if stmt is ast.StructDecl {
|
||||
if stmt.name == name {
|
||||
return stmt
|
||||
}
|
||||
}
|
||||
}
|
||||
panic('missing cached header struct ${name}')
|
||||
}
|
||||
|
||||
fn module_storage_cached_header_interface(header_source string, name string) ast.InterfaceDecl {
|
||||
tmp_dir := module_storage_cache_tmp_dir('parse_interface')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
header_path := os.join_path(tmp_dir, 'state.vh')
|
||||
os.write_file(header_path, header_source) or { panic(err) }
|
||||
mut prefs := pref.new_preferences()
|
||||
mut file_set := token.FileSet.new()
|
||||
mut par := parser.Parser.new(&prefs)
|
||||
files := par.parse_files([header_path], mut file_set)
|
||||
for stmt in files[0].stmts {
|
||||
if stmt is ast.InterfaceDecl {
|
||||
if stmt.name == name {
|
||||
return stmt
|
||||
}
|
||||
}
|
||||
}
|
||||
panic('missing cached header interface ${name}')
|
||||
}
|
||||
|
||||
fn module_storage_run_cached_header_check(label string, header_source string, main_source string) (int, string) {
|
||||
tmp_dir := module_storage_cache_tmp_dir(label)
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
os.write_file(os.join_path(tmp_dir, 'state', 'state.v'), header_source) or { panic(err) }
|
||||
main_path := os.join_path(tmp_dir, 'main.v')
|
||||
os.write_file(main_path, main_source) or { panic(err) }
|
||||
out_path := os.join_path(tmp_dir, 'out.txt')
|
||||
cmd := '${os.quoted_path(@VEXE)} -v2 -backend v -o ${os.quoted_path(out_path)} ${os.quoted_path(main_path)} 2>&1'
|
||||
res := os.execute(cmd)
|
||||
return res.exit_code, res.output
|
||||
}
|
||||
|
||||
fn test_module_storage_cached_header_preserves_flags() {
|
||||
header_source := module_storage_cached_header_source_for_test()
|
||||
assert header_source.contains('pub __global (')
|
||||
assert header_source.contains('mut total int')
|
||||
assert header_source.contains('__global (')
|
||||
assert header_source.contains('mut hidden int')
|
||||
public_field := module_storage_cached_header_global(header_source, 'total')
|
||||
assert public_field.is_public
|
||||
assert public_field.is_mut
|
||||
private_field := module_storage_cached_header_global(header_source, 'hidden')
|
||||
assert !private_field.is_public
|
||||
assert private_field.is_mut
|
||||
}
|
||||
|
||||
fn test_module_storage_cached_header_preserves_module_mut_struct_fields() {
|
||||
header_source := module_mut_cached_header_source_for_test()
|
||||
assert header_source.contains('pub module_mut:')
|
||||
assert !header_source.contains('@[module_mut]')
|
||||
counter := module_storage_cached_header_struct(header_source, 'Counter')
|
||||
assert counter.fields[0].name == 'value'
|
||||
assert counter.fields[0].is_public
|
||||
assert counter.fields[0].is_mut
|
||||
assert counter.fields[0].is_module_mut
|
||||
assert counter.fields[1].name == 'open'
|
||||
assert counter.fields[1].is_public
|
||||
assert counter.fields[1].is_mut
|
||||
assert !counter.fields[1].is_module_mut
|
||||
}
|
||||
|
||||
fn test_module_storage_cached_header_preserves_mut_interface_methods_for_module_mut_field() {
|
||||
header_source := module_mut_interface_cached_header_source_for_test()
|
||||
assert header_source.contains('pub interface Mutator {')
|
||||
assert header_source.contains('handler Handler')
|
||||
assert header_source.contains('direct fn(int) bool')
|
||||
assert header_source.contains('handle(int) bool')
|
||||
assert header_source.contains('mut:')
|
||||
assert header_source.contains('bump()')
|
||||
assert !header_source.contains('direct(int) bool')
|
||||
assert !header_source.contains('bump fn(')
|
||||
mutator := module_storage_cached_header_interface(header_source, 'Mutator')
|
||||
assert mutator.fields[0].name == 'handler'
|
||||
assert !mutator.fields[0].is_interface_method
|
||||
assert mutator.fields[1].name == 'direct'
|
||||
assert !mutator.fields[1].is_interface_method
|
||||
assert mutator.fields[2].name == 'handle'
|
||||
assert mutator.fields[2].is_interface_method
|
||||
assert mutator.fields[3].name == 'bump'
|
||||
assert mutator.fields[3].is_mut
|
||||
assert mutator.fields[3].is_interface_method
|
||||
assert mutator.fields[4].name == 'reset'
|
||||
assert mutator.fields[4].is_mut
|
||||
assert mutator.fields[4].is_interface_method
|
||||
|
||||
module_mut_code, module_mut_output := module_storage_run_cached_header_check('module_mut_interface_method',
|
||||
header_source, 'module main
|
||||
|
||||
import state
|
||||
|
||||
fn main() {
|
||||
mut c := state.Counter{}
|
||||
c.iface.bump()
|
||||
}
|
||||
')
|
||||
assert module_mut_code != 0, 'cached module_mut interface method mutation should fail'
|
||||
assert module_mut_output.contains('cannot mutate module-mutable field `state.Counter.iface` outside module `state`'), module_mut_output
|
||||
}
|
||||
|
||||
fn test_module_storage_cached_header_does_not_synthesize_interface_fn_field_decls() {
|
||||
header_source := module_mut_interface_merged_cached_header_source_for_test()
|
||||
assert header_source.contains('pub interface Mutator {')
|
||||
assert header_source.contains('direct fn(int) bool')
|
||||
assert header_source.contains('handle(int) bool')
|
||||
assert header_source.contains('pub fn (it Mutator) handle(int) bool')
|
||||
assert header_source.contains('pub fn (mut it Mutator) bump()')
|
||||
assert header_source.contains('pub fn (mut it Mutator) reset()')
|
||||
assert !header_source.contains('pub fn (it Mutator) direct fn')
|
||||
assert !header_source.contains('fn (it Mutator) direct fn')
|
||||
assert !header_source.contains('pub fn (it Mutator) bump()')
|
||||
|
||||
module_mut_code, module_mut_output := module_storage_run_cached_header_check('module_mut_interface_merged_method',
|
||||
header_source, 'module main
|
||||
|
||||
import state
|
||||
|
||||
fn main() {
|
||||
mut c := state.Counter{}
|
||||
c.iface.bump()
|
||||
}
|
||||
')
|
||||
assert module_mut_code != 0, 'merged cached module_mut interface method mutation should fail'
|
||||
assert module_mut_output.contains('cannot mutate module-mutable field `state.Counter.iface` outside module `state`'), module_mut_output
|
||||
}
|
||||
|
||||
fn test_module_storage_cached_header_visibility_matches_source_module() {
|
||||
header_source := module_storage_cached_header_source_for_test()
|
||||
public_code, public_output := module_storage_run_cached_header_check('public', header_source, 'module main
|
||||
|
||||
import state
|
||||
|
||||
fn main() {
|
||||
state.total += 1
|
||||
}
|
||||
')
|
||||
assert public_code == 0, public_output
|
||||
|
||||
private_code, private_output := module_storage_run_cached_header_check('private',
|
||||
header_source, 'module main
|
||||
|
||||
import state
|
||||
|
||||
fn main() {
|
||||
state.hidden += 1
|
||||
}
|
||||
')
|
||||
assert private_code != 0, 'cached private module storage should fail'
|
||||
assert private_output.contains('module global `state.hidden` is private'), private_output
|
||||
}
|
||||
|
||||
fn test_module_storage_cached_header_module_mut_visibility_matches_source_module() {
|
||||
header_source := module_mut_cached_header_source_for_test()
|
||||
public_code, public_output := module_storage_run_cached_header_check('module_mut_public',
|
||||
header_source, 'module main
|
||||
|
||||
import state
|
||||
|
||||
fn main() {
|
||||
mut c := state.Counter{}
|
||||
println(c.value)
|
||||
c.open++
|
||||
}
|
||||
')
|
||||
assert public_code == 0, public_output
|
||||
|
||||
module_mut_code, module_mut_output := module_storage_run_cached_header_check('module_mut_private',
|
||||
header_source, 'module main
|
||||
|
||||
import state
|
||||
|
||||
fn main() {
|
||||
mut c := state.Counter{}
|
||||
c.value++
|
||||
}
|
||||
')
|
||||
assert module_mut_code != 0, 'cached module_mut field mutation should fail'
|
||||
assert module_mut_output.contains('cannot mutate module-mutable field `state.Counter.value` outside module `state`'), module_mut_output
|
||||
}
|
||||
|
||||
fn test_module_storage_c_extern_import_cache_uses_raw_symbol() {
|
||||
tmp_dir := module_storage_cache_tmp_dir('c_extern_import')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'ext')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
mut prefs := pref.new_preferences()
|
||||
mut cache_builder := new_builder(&prefs)
|
||||
cache_dir := cache_builder.core_cache_dir()
|
||||
os.rmdir_all(cache_dir) or {}
|
||||
c_path := os.join_path(tmp_dir, 'raw_status.c')
|
||||
os.write_file(c_path, 'int raw_status = 7;\n') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'ext', 'ext.v'), 'module ext
|
||||
|
||||
@[c_extern]
|
||||
pub __global raw_status int
|
||||
|
||||
pub fn read_status() int {
|
||||
return raw_status
|
||||
}
|
||||
') or {
|
||||
panic(err)
|
||||
}
|
||||
main_path := os.join_path(tmp_dir, 'main.v')
|
||||
os.write_file(main_path, 'module main
|
||||
|
||||
import ext
|
||||
|
||||
#flag ${c_path}
|
||||
|
||||
fn main() {
|
||||
_ = ext.raw_status
|
||||
_ = ext.read_status()
|
||||
}
|
||||
') or {
|
||||
panic(err)
|
||||
}
|
||||
out_path := os.join_path(tmp_dir, 'out')
|
||||
cmd := '${os.quoted_path(@VEXE)} -v -v2 -keepc -o ${os.quoted_path(out_path)} ${os.quoted_path(main_path)} 2>&1'
|
||||
res := os.execute(cmd)
|
||||
assert res.exit_code == 0, res.output
|
||||
ext_header := os.read_file(cache_builder.core_header_path('ext')) or { panic(err) }
|
||||
assert ext_header.contains('@[c_extern]'), ext_header
|
||||
imports_c := os.read_file(os.join_path(cache_dir, 'imports.c')) or { panic(err) }
|
||||
assert imports_c.contains('return raw_status;'), imports_c
|
||||
assert !imports_c.contains('ext__raw_status'), imports_c
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.abi
|
||||
import v2.ast
|
||||
import v2.gen.x64
|
||||
import v2.pref
|
||||
|
||||
fn test_native_x64_windows_selects_coff_and_windows_abi() {
|
||||
assert is_windows_x64_native_target(.x64, 'windows')
|
||||
assert native_x64_object_format_for_os('windows') == x64.ObjectFormat.coff
|
||||
assert native_x64_codegen_abi_for_os('windows') == x64.X64Abi.windows
|
||||
assert native_x64_lowering_abi_for_os('windows') == abi.X64Abi.windows
|
||||
}
|
||||
|
||||
fn test_native_x64_non_windows_keeps_existing_object_formats_and_sysv() {
|
||||
assert !is_windows_x64_native_target(.x64, 'linux')
|
||||
assert native_x64_object_format_for_os('linux') == x64.ObjectFormat.elf
|
||||
assert native_x64_codegen_abi_for_os('linux') == x64.X64Abi.sysv
|
||||
assert native_x64_lowering_abi_for_os('linux') == abi.X64Abi.sysv
|
||||
|
||||
assert !is_windows_x64_native_target(.x64, 'macos')
|
||||
assert native_x64_object_format_for_os('macos') == x64.ObjectFormat.macho
|
||||
assert native_x64_codegen_abi_for_os('macos') == x64.X64Abi.sysv
|
||||
assert native_x64_lowering_abi_for_os('macos') == abi.X64Abi.sysv
|
||||
assert is_macos_native_target('macos')
|
||||
assert is_macos_native_target('darwin')
|
||||
assert native_x64_object_format_for_os('darwin') == x64.ObjectFormat.macho
|
||||
assert native_x64_codegen_abi_for_os('darwin') == x64.X64Abi.sysv
|
||||
assert native_x64_lowering_abi_for_os('darwin') == abi.X64Abi.sysv
|
||||
}
|
||||
|
||||
fn test_minimal_x64_runtime_and_macos_tiny_object_targets() {
|
||||
old_linux_tiny := os.getenv_opt('V2_X64_LINUX_TINY')
|
||||
os.unsetenv('V2_X64_LINUX_TINY')
|
||||
defer {
|
||||
if old := old_linux_tiny {
|
||||
os.setenv('V2_X64_LINUX_TINY', old, true)
|
||||
} else {
|
||||
os.unsetenv('V2_X64_LINUX_TINY')
|
||||
}
|
||||
}
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.backend = .x64
|
||||
prefs.arch = .x64
|
||||
|
||||
prefs.target_os = 'linux'
|
||||
linux_builder := new_builder(&prefs)
|
||||
assert !linux_builder.uses_minimal_windows_x64_runtime()
|
||||
assert linux_builder.uses_minimal_linux_x64_runtime()
|
||||
assert linux_builder.uses_minimal_x64_runtime()
|
||||
assert !linux_builder.uses_minimal_linux_x64_runtime_roots()
|
||||
assert !linux_builder.uses_minimal_x64_runtime_roots()
|
||||
|
||||
os.setenv('V2_X64_LINUX_TINY', '1', true)
|
||||
linux_tiny_builder := new_builder(&prefs)
|
||||
assert linux_tiny_builder.uses_minimal_linux_x64_runtime()
|
||||
assert linux_tiny_builder.uses_minimal_linux_x64_runtime_roots()
|
||||
assert linux_tiny_builder.uses_minimal_x64_runtime_roots()
|
||||
os.unsetenv('V2_X64_LINUX_TINY')
|
||||
|
||||
prefs.target_os = 'macos'
|
||||
macos_builder := new_builder(&prefs)
|
||||
assert !macos_builder.uses_minimal_windows_x64_runtime()
|
||||
assert !macos_builder.uses_minimal_linux_x64_runtime()
|
||||
assert !macos_builder.uses_minimal_x64_runtime()
|
||||
assert !macos_builder.uses_minimal_x64_runtime_roots()
|
||||
assert macos_builder.uses_macos_x64_tiny_object(.x64)
|
||||
assert !macos_builder.uses_macos_x64_tiny_object(.arm64)
|
||||
|
||||
macos_tiny_builder := new_builder(&prefs)
|
||||
assert !macos_tiny_builder.uses_minimal_x64_runtime()
|
||||
assert !macos_tiny_builder.uses_minimal_x64_runtime_roots()
|
||||
assert macos_tiny_builder.uses_macos_x64_tiny_object(.x64)
|
||||
assert !macos_tiny_builder.uses_macos_x64_tiny_object(.arm64)
|
||||
|
||||
prefs.macos_tiny = false
|
||||
macos_no_tiny_builder := new_builder(&prefs)
|
||||
assert !macos_no_tiny_builder.uses_macos_x64_tiny_object(.x64)
|
||||
|
||||
prefs.macos_tiny = true
|
||||
prefs.arch = .auto
|
||||
macos_auto_arch_builder := new_builder(&prefs)
|
||||
assert !macos_auto_arch_builder.uses_minimal_x64_runtime_roots()
|
||||
assert macos_auto_arch_builder.uses_macos_x64_tiny_object(.x64)
|
||||
assert !macos_auto_arch_builder.uses_macos_x64_tiny_object(.arm64)
|
||||
prefs.arch = .x64
|
||||
|
||||
prefs.target_os = 'darwin'
|
||||
darwin_builder := new_builder(&prefs)
|
||||
assert !darwin_builder.uses_minimal_windows_x64_runtime()
|
||||
assert !darwin_builder.uses_minimal_linux_x64_runtime()
|
||||
assert !darwin_builder.uses_minimal_x64_runtime()
|
||||
assert !darwin_builder.uses_minimal_x64_runtime_roots()
|
||||
assert darwin_builder.uses_macos_x64_tiny_object(.x64)
|
||||
|
||||
prefs.target_os = 'windows'
|
||||
windows_builder := new_builder(&prefs)
|
||||
assert windows_builder.uses_minimal_windows_x64_runtime()
|
||||
assert !windows_builder.uses_minimal_linux_x64_runtime()
|
||||
assert windows_builder.uses_minimal_x64_runtime()
|
||||
assert windows_builder.uses_minimal_x64_runtime_roots()
|
||||
assert !windows_builder.uses_macos_x64_tiny_object(.x64)
|
||||
}
|
||||
|
||||
fn test_macos_tiny_candidate_source_snapshot_is_auto_by_default_with_opt_out() {
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.backend = .x64
|
||||
prefs.arch = .auto
|
||||
prefs.target_os = 'macos'
|
||||
|
||||
mut hosted_builder := new_builder(&prefs)
|
||||
hosted_builder.files = [
|
||||
ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
},
|
||||
]
|
||||
hosted_builder.prepare_macos_tiny_candidate_source_files()
|
||||
assert hosted_builder.macos_tiny_candidate_source_files.len == 1
|
||||
assert hosted_builder.macos_tiny_candidate_source_files[0].name == 'main.v'
|
||||
|
||||
prefs.macos_tiny = false
|
||||
mut opt_out_builder := new_builder(&prefs)
|
||||
opt_out_builder.files = [
|
||||
ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
},
|
||||
]
|
||||
opt_out_builder.prepare_macos_tiny_candidate_source_files()
|
||||
assert opt_out_builder.macos_tiny_candidate_source_files.len == 0
|
||||
assert !opt_out_builder.uses_minimal_x64_runtime_roots()
|
||||
}
|
||||
|
||||
fn test_macos_tiny_candidate_source_snapshot_uses_flat_when_enabled() {
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.backend = .x64
|
||||
prefs.arch = .auto
|
||||
prefs.target_os = 'macos'
|
||||
|
||||
source_files := [
|
||||
ast.File{
|
||||
mod: 'main'
|
||||
name: 'flat_main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.FnDecl{
|
||||
name: 'main'
|
||||
}),
|
||||
]
|
||||
},
|
||||
]
|
||||
mut tiny_builder := new_builder(&prefs)
|
||||
tiny_builder.files = [
|
||||
ast.File{
|
||||
mod: 'stale'
|
||||
name: 'stale.v'
|
||||
},
|
||||
]
|
||||
tiny_builder.flat = ast.flatten_files(source_files)
|
||||
tiny_builder.prepare_macos_tiny_candidate_source_files()
|
||||
assert tiny_builder.macos_tiny_candidate_source_files.len == 0
|
||||
assert tiny_builder.macos_tiny_candidate_source_flat.files.len == source_files.len
|
||||
assert tiny_builder.macos_tiny_candidate_source_flat.file_name(tiny_builder.macos_tiny_candidate_source_flat.files[0]) == 'flat_main.v'
|
||||
assert tiny_builder.macos_tiny_candidate_source_flat.string_at(tiny_builder.macos_tiny_candidate_source_flat.files[0].mod_idx) == 'main'
|
||||
}
|
||||
|
||||
fn test_macos_tiny_candidate_native_mir_build_policy_is_sequential_only_for_candidate() {
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.backend = .x64
|
||||
prefs.arch = .x64
|
||||
prefs.target_os = 'macos'
|
||||
prefs.no_parallel = false
|
||||
prefs.hot_fn = ''
|
||||
builder := new_builder(&prefs)
|
||||
|
||||
assert !builder.native_mir_build_sequential('')
|
||||
assert builder.native_mir_build_sequential(macos_tiny_candidate_graph_label)
|
||||
|
||||
prefs.no_parallel = true
|
||||
no_parallel_builder := new_builder(&prefs)
|
||||
assert no_parallel_builder.native_mir_build_sequential('')
|
||||
}
|
||||
|
||||
fn test_macos_tiny_link_command_adds_hygiene_only_for_tiny_object() {
|
||||
normal := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false, '')
|
||||
tiny := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', true, '')
|
||||
|
||||
assert normal == 'ld -o ${os.quoted_path('/tmp/out')} ${os.quoted_path('main.o')} -lSystem -syslibroot ${os.quoted_path('/SDK Path')} -e _main -arch x86_64 -platform_version macos 11.0.0 11.0.0'
|
||||
assert tiny == '${normal} -dead_strip -x -S'
|
||||
assert !normal.contains('-dead_strip')
|
||||
assert !normal.contains(' -x')
|
||||
assert !normal.contains(' -S')
|
||||
}
|
||||
|
||||
fn test_macos_sdk_path_from_xcrun_output_accepts_simple_sdk_path() {
|
||||
output := '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk\n'
|
||||
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk'
|
||||
}
|
||||
|
||||
fn test_macos_sdk_path_from_xcrun_output_ignores_diagnostics_before_sdk_path() {
|
||||
output :=
|
||||
'2026-06-07 16:19:11.428 xcodebuild[1002:7567] Requested but did not find extension point\n' +
|
||||
'2026-06-07 16:19:14.880 xcodebuild[1004:7681] Requested but did not find extension point\n' +
|
||||
'/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk\n'
|
||||
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk'
|
||||
}
|
||||
|
||||
fn test_macos_sdk_path_from_xcrun_output_handles_empty_lines_spaces_and_crlf() {
|
||||
output := ' \r\n\t\r\n /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk \r\n'
|
||||
assert macos_sdk_path_from_xcrun_output(output)! == '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk'
|
||||
}
|
||||
|
||||
fn test_macos_sdk_path_from_xcrun_output_rejects_missing_sdk_path() {
|
||||
path := macos_sdk_path_from_xcrun_output('xcodebuild diagnostic only\nnot a path\n') or {
|
||||
assert err.msg().contains('could not find a clean macOS SDK path')
|
||||
return
|
||||
}
|
||||
assert false, 'expected missing SDK path error, got ${path}'
|
||||
}
|
||||
|
||||
fn test_macos_sdk_path_from_xcrun_output_preserves_spaces_inside_path() {
|
||||
output := '/Applications/Xcode With Spaces.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk\n'
|
||||
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/Xcode With Spaces.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk'
|
||||
}
|
||||
|
||||
fn test_macos_sdk_path_from_xcrun_output_rejects_relative_or_non_macos_sdk_lines() {
|
||||
output := 'relative/MacOSX12.3.sdk\n/Applications/Xcode.app/Contents/Developer/SDKs/iPhoneOS.sdk\n/Applications/Xcode.app/Contents/Developer/SDKs/NotMacOSX.sdk\n'
|
||||
path := macos_sdk_path_from_xcrun_output(output) or {
|
||||
assert err.msg().contains('could not find a clean macOS SDK path')
|
||||
return
|
||||
}
|
||||
assert false, 'expected invalid SDK path lines to be rejected, got ${path}'
|
||||
}
|
||||
|
||||
fn test_macos_sdk_path_from_xcrun_output_uses_last_valid_sdk_line() {
|
||||
output := '/Applications/Old.app/Contents/Developer/SDKs/MacOSX11.0.sdk\n' +
|
||||
'/Applications/New.app/Contents/Developer/SDKs/MacOSX12.3.sdk\n'
|
||||
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/New.app/Contents/Developer/SDKs/MacOSX12.3.sdk'
|
||||
}
|
||||
|
||||
fn test_validate_macos_sdk_path_for_native_link_accepts_libsystem_tbd() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_macos_sdk_tbd_${os.getpid()}')
|
||||
sdk_path := os.join_path(tmp_dir, 'MacOSX.test.sdk')
|
||||
lib_dir := os.join_path(sdk_path, 'usr', 'lib')
|
||||
os.mkdir_all(lib_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
os.write_file(os.join_path(lib_dir, 'libSystem.tbd'), '') or { panic(err) }
|
||||
validate_macos_sdk_path_for_native_link(sdk_path)!
|
||||
}
|
||||
|
||||
fn test_validate_macos_sdk_path_for_native_link_accepts_libsystem_dylib() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_macos_sdk_dylib_${os.getpid()}')
|
||||
sdk_path := os.join_path(tmp_dir, 'MacOSX.test.sdk')
|
||||
lib_dir := os.join_path(sdk_path, 'usr', 'lib')
|
||||
os.mkdir_all(lib_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
os.write_file(os.join_path(lib_dir, 'libSystem.dylib'), '') or { panic(err) }
|
||||
validate_macos_sdk_path_for_native_link(sdk_path)!
|
||||
}
|
||||
|
||||
fn test_validate_macos_sdk_path_for_native_link_rejects_sdk_without_libsystem() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_macos_sdk_missing_libsystem_${os.getpid()}')
|
||||
sdk_path := os.join_path(tmp_dir, 'MacOSX.test.sdk')
|
||||
os.mkdir_all(os.join_path(sdk_path, 'usr', 'lib')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
validate_macos_sdk_path_for_native_link(sdk_path) or {
|
||||
assert err.msg().contains('missing usr/lib/libSystem')
|
||||
return
|
||||
}
|
||||
assert false, 'expected SDK without libSystem to be rejected'
|
||||
}
|
||||
|
||||
fn test_validate_macos_sdk_path_for_native_link_rejects_missing_sdk_dir() {
|
||||
missing_sdk := os.join_path(os.vtmp_dir(), 'v2_missing_macos_sdk_${os.getpid()}',
|
||||
'MacOSX.test.sdk')
|
||||
validate_macos_sdk_path_for_native_link(missing_sdk) or {
|
||||
assert err.msg().contains('does not exist')
|
||||
return
|
||||
}
|
||||
assert false, 'expected missing SDK directory to be rejected'
|
||||
}
|
||||
|
||||
fn test_native_link_commands_append_directive_link_flags() {
|
||||
linux := linux_native_link_command('cc', '/tmp/out', 'main.o', '-lm')
|
||||
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false, '-lm')
|
||||
|
||||
assert linux == 'cc ${os.quoted_path('main.o')} -o ${os.quoted_path('/tmp/out')} -no-pie -lm'
|
||||
assert macos == 'ld -o ${os.quoted_path('/tmp/out')} ${os.quoted_path('main.o')} -lm -lSystem -syslibroot ${os.quoted_path('/SDK Path')} -e _main -arch x86_64 -platform_version macos 11.0.0 11.0.0'
|
||||
}
|
||||
|
||||
fn test_linux_native_link_command_uses_configured_driver_and_link_flags() {
|
||||
cmd := linux_native_link_command('custom-cc', '/tmp/out', 'main.o', '-fopenmp')
|
||||
|
||||
assert cmd.starts_with('custom-cc ${os.quoted_path('main.o')}'), cmd
|
||||
assert cmd.contains('-fopenmp'), cmd
|
||||
}
|
||||
|
||||
fn test_native_link_commands_keep_wl_for_linux_and_translate_for_macos_ld() {
|
||||
linux := linux_native_link_command('cc', '/tmp/out', 'main.o', '-Wl,-rpath,/tmp/lib')
|
||||
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false,
|
||||
'-Wl,-rpath,@loader_path/lib')
|
||||
|
||||
assert linux.contains('-Wl,-rpath,/tmp/lib'), linux
|
||||
assert macos.contains(' -rpath @loader_path/lib '), macos
|
||||
assert !macos.contains('-Wl,'), macos
|
||||
}
|
||||
|
||||
fn test_native_link_commands_translate_xlinker_for_macos_ld() {
|
||||
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false,
|
||||
'-Xlinker -rpath -Xlinker @loader_path/lib')
|
||||
|
||||
assert macos.contains(' -rpath @loader_path/lib '), macos
|
||||
assert !macos.contains('-Xlinker'), macos
|
||||
}
|
||||
|
||||
fn test_macos_native_link_command_keeps_framework_search_paths() {
|
||||
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false,
|
||||
'-F /tmp/Fw -F/opt/Fw -framework Foo')
|
||||
|
||||
assert macos.contains(' -F /tmp/Fw -F/opt/Fw -framework Foo '), macos
|
||||
}
|
||||
|
||||
fn test_split_compile_and_link_flags_duplicates_dual_use_driver_flags() {
|
||||
compile_flags, link_flags :=
|
||||
split_compile_and_link_flags('-I include -fopenmp -fopenmp=libomp -pthread helper.c')
|
||||
|
||||
assert compile_flags == '-I include -fopenmp -fopenmp=libomp -pthread'
|
||||
assert link_flags == '-fopenmp -fopenmp=libomp -pthread helper.c'
|
||||
}
|
||||
|
||||
fn test_split_compile_and_link_flags_keeps_framework_search_paths_dual_use() {
|
||||
compile_flags, link_flags :=
|
||||
split_compile_and_link_flags('-F /tmp/Fw -F/opt/Fw -framework Foo helper.m')
|
||||
|
||||
assert compile_flags == '-F /tmp/Fw -F/opt/Fw'
|
||||
assert link_flags == '-F /tmp/Fw -F/opt/Fw -framework Foo helper.m'
|
||||
}
|
||||
|
||||
fn test_macos_native_ld_rejects_dual_use_driver_link_flags() {
|
||||
validate_macos_native_ld_link_flags('-lm')!
|
||||
validate_macos_native_ld_link_flags('-fopenmp') or {
|
||||
assert err.msg().contains('macOS native ld cannot consume driver linker flags'), err.msg()
|
||||
return
|
||||
}
|
||||
assert false, 'expected macOS native ld driver flag diagnostic'
|
||||
}
|
||||
|
||||
fn test_native_linux_tiny_link_allows_runtime_system_libs_when_user_flags_are_empty() {
|
||||
assert native_link_flags_allow_builtin_linux_tiny('', '')
|
||||
assert native_link_flags_allow_builtin_linux_tiny(' ', '')
|
||||
assert native_link_flags_allow_builtin_linux_tiny('-lpthread -lm -ldl -lc', '')
|
||||
assert native_link_flags_allow_builtin_linux_tiny('-l pthread -l m -l dl -l c', '')
|
||||
}
|
||||
|
||||
fn test_native_linux_tiny_link_blocks_user_external_link_flags() {
|
||||
for user_link_flags in ['-lm', '-Wl,-rpath,/tmp/lib', '/tmp/helper.c', '-fopenmp', '-pthread',
|
||||
'-F/tmp/Frameworks'] {
|
||||
assert !native_link_flags_allow_builtin_linux_tiny('-lpthread -lm -ldl', user_link_flags), user_link_flags
|
||||
}
|
||||
}
|
||||
|
||||
fn test_native_linux_tiny_link_blocks_external_link_inputs_in_global_flags() {
|
||||
assert !native_link_flags_allow_builtin_linux_tiny('-Xlinker -rpath', '')
|
||||
assert !native_link_flags_allow_builtin_linux_tiny('-L/tmp', '')
|
||||
assert !native_link_flags_allow_builtin_linux_tiny('/tmp/helper.o', '')
|
||||
assert !native_link_flags_allow_builtin_linux_tiny('/tmp/libhelper.a', '')
|
||||
assert !native_link_flags_allow_builtin_linux_tiny('-framework Foundation', '')
|
||||
assert !native_link_flags_allow_builtin_linux_tiny('-lfoo', '')
|
||||
}
|
||||
|
||||
fn test_native_external_link_inputs_replace_sources_with_objects_in_order() {
|
||||
inputs := native_external_link_inputs('-L/tmp/lib /tmp/foo.c -lfoo /tmp/bar.m /tmp/baz.o',
|
||||
'/tmp/out')!
|
||||
|
||||
assert inputs.source_files == ['/tmp/foo.c', '/tmp/bar.m']
|
||||
assert inputs.object_files.len == 2
|
||||
assert inputs.link_flags == '-L/tmp/lib ${os.quoted_path(inputs.object_files[0])} -lfoo ${os.quoted_path(inputs.object_files[1])} /tmp/baz.o'
|
||||
assert !inputs.link_flags.contains('/tmp/foo.c')
|
||||
assert !inputs.link_flags.contains('/tmp/bar.m')
|
||||
}
|
||||
|
||||
fn test_native_external_link_inputs_reject_unsupported_c_family_sources() {
|
||||
for source in ['/tmp/foo.cc', '/tmp/foo.cpp', '/tmp/foo.cxx', '/tmp/foo.mm'] {
|
||||
native_external_link_inputs(source, '/tmp/out') or {
|
||||
assert err.msg().starts_with('x64: unsupported backend feature: '), err.msg()
|
||||
continue
|
||||
}
|
||||
assert false, 'expected ${source} source to be rejected'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_native_external_link_inputs_reject_quoted_source_tokens() {
|
||||
native_external_link_inputs('"foo.c"', '/tmp/out') or {
|
||||
assert err.msg().starts_with('x64: unsupported backend feature: quoted #flag source path'), err.msg()
|
||||
|
||||
return
|
||||
}
|
||||
assert false, 'expected quoted source token to be rejected'
|
||||
}
|
||||
|
||||
fn test_macos_native_link_command_uses_rewritten_external_objects() {
|
||||
tmp_dir := os.real_path(os.vtmp_dir())
|
||||
foo_c := os.join_path(tmp_dir, 'foo.c')
|
||||
bar_m := os.join_path(tmp_dir, 'bar.m')
|
||||
out_path := os.join_path(tmp_dir, 'out')
|
||||
inputs := native_external_link_inputs('${foo_c} -framework Foundation ${bar_m}', out_path)!
|
||||
cmd := macos_native_link_command(out_path, 'main.o', '/SDK Path', 'x86_64', false,
|
||||
inputs.link_flags)
|
||||
|
||||
assert cmd.contains(os.quoted_path(inputs.object_files[0])), cmd
|
||||
assert cmd.contains(os.quoted_path(inputs.object_files[1])), cmd
|
||||
assert cmd.contains('-framework Foundation'), cmd
|
||||
assert !cmd.contains(foo_c), cmd
|
||||
assert !cmd.contains(bar_m), cmd
|
||||
}
|
||||
|
||||
fn test_native_external_source_compile_command_adds_macos_sdk_and_arch() {
|
||||
cmd := native_external_source_compile_command('cc', '/tmp/foo.c', '/tmp/foo.o',
|
||||
'-I /tmp/include -DHELPER', '/SDK Path', 'x86_64', 'macos')
|
||||
|
||||
assert cmd.starts_with('cc -c '), cmd
|
||||
assert cmd.contains('-isysroot ${os.quoted_path('/SDK Path')}'), cmd
|
||||
assert cmd.contains('-arch x86_64'), cmd
|
||||
assert cmd.contains('-I /tmp/include -DHELPER'), cmd
|
||||
assert cmd.contains(os.quoted_path('/tmp/foo.c')), cmd
|
||||
assert cmd.contains('-o ${os.quoted_path('/tmp/foo.o')}'), cmd
|
||||
}
|
||||
|
||||
fn test_native_external_source_compile_command_keeps_linux_compile_flags() {
|
||||
cmd := native_external_source_compile_command('cc', '/tmp/foo.c', '/tmp/foo.o',
|
||||
'-I /tmp/include -DHELPER', '', '', 'linux')
|
||||
|
||||
assert cmd.starts_with('cc -c '), cmd
|
||||
assert !cmd.contains('-isysroot'), cmd
|
||||
assert !cmd.contains('-arch'), cmd
|
||||
assert cmd.contains('-I /tmp/include -DHELPER'), cmd
|
||||
assert cmd.contains(os.quoted_path('/tmp/foo.c')), cmd
|
||||
assert cmd.contains('-o ${os.quoted_path('/tmp/foo.o')}'), cmd
|
||||
}
|
||||
|
||||
fn test_native_external_source_compiler_uses_pref_ccompiler() {
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.ccompiler = 'custom-native-cc'
|
||||
mut b := new_builder(&prefs)
|
||||
|
||||
assert b.native_external_source_compiler('macos') == 'custom-native-cc'
|
||||
}
|
||||
|
||||
fn test_native_external_source_compiler_uses_v2cc_when_pref_is_empty() {
|
||||
old_v2cc := os.getenv_opt('V2CC')
|
||||
os.setenv('V2CC', 'env-native-cc', true)
|
||||
defer {
|
||||
if old := old_v2cc {
|
||||
os.setenv('V2CC', old, true)
|
||||
} else {
|
||||
os.unsetenv('V2CC')
|
||||
}
|
||||
}
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.ccompiler = ''
|
||||
mut b := new_builder(&prefs)
|
||||
|
||||
assert b.native_external_source_compiler('macos') == 'env-native-cc'
|
||||
}
|
||||
|
||||
fn test_native_external_source_compiler_defaults_to_cc_for_macos() {
|
||||
old_v2cc := os.getenv_opt('V2CC')
|
||||
os.unsetenv('V2CC')
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_external_cc_${os.getpid()}')
|
||||
tcc_dir := os.join_path(tmp_dir, 'thirdparty', 'tcc')
|
||||
os.mkdir_all(tcc_dir) or { panic(err) }
|
||||
os.write_file(os.join_path(tcc_dir, 'tcc.exe'), '') or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
if old := old_v2cc {
|
||||
os.setenv('V2CC', old, true)
|
||||
} else {
|
||||
os.unsetenv('V2CC')
|
||||
}
|
||||
}
|
||||
|
||||
mut prefs := pref.Preferences{
|
||||
vroot: tmp_dir
|
||||
}
|
||||
mut b := new_builder(&prefs)
|
||||
|
||||
assert b.native_external_source_compiler('macos') == 'cc'
|
||||
assert b.native_external_source_compiler('darwin') == 'cc'
|
||||
}
|
||||
|
||||
fn test_native_linux_hosted_link_compiler_pref_v2cc_default_order() {
|
||||
old_v2cc := os.getenv_opt('V2CC')
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_linux_hosted_cc_${os.getpid()}')
|
||||
tcc_dir := os.join_path(tmp_dir, 'thirdparty', 'tcc')
|
||||
os.mkdir_all(tcc_dir) or { panic(err) }
|
||||
os.write_file(os.join_path(tcc_dir, 'tcc.exe'), '') or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
if old := old_v2cc {
|
||||
os.setenv('V2CC', old, true)
|
||||
} else {
|
||||
os.unsetenv('V2CC')
|
||||
}
|
||||
}
|
||||
|
||||
os.setenv('V2CC', 'env-hosted-cc', true)
|
||||
mut pref_prefs := pref.new_preferences()
|
||||
pref_prefs.ccompiler = 'pref-hosted-cc'
|
||||
pref_builder := new_builder(&pref_prefs)
|
||||
assert pref_builder.native_linux_hosted_link_compiler() == 'pref-hosted-cc'
|
||||
|
||||
mut env_prefs := pref.new_preferences()
|
||||
env_prefs.ccompiler = ''
|
||||
env_builder := new_builder(&env_prefs)
|
||||
assert env_builder.native_linux_hosted_link_compiler() == 'env-hosted-cc'
|
||||
|
||||
os.unsetenv('V2CC')
|
||||
mut default_prefs := pref.Preferences{
|
||||
vroot: tmp_dir
|
||||
}
|
||||
default_builder := new_builder(&default_prefs)
|
||||
assert default_builder.native_linux_hosted_link_compiler() == 'cc'
|
||||
}
|
||||
|
||||
fn test_native_link_flags_from_sources_are_not_global() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_link_flags_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
no_flag_path := os.join_path(tmp_dir, 'no_flag.v')
|
||||
math_flag_path := os.join_path(tmp_dir, 'math_flag.v')
|
||||
os.write_file(no_flag_path, 'module main\n') or { panic(err) }
|
||||
os.write_file(math_flag_path, 'module main\n#flag -lm\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.backend = .x64
|
||||
prefs.arch = .x64
|
||||
prefs.target_os = 'linux'
|
||||
prefs.skip_builtin = true
|
||||
|
||||
mut no_flag_builder := new_builder(&prefs)
|
||||
no_flag_builder.files = [
|
||||
ast.File{
|
||||
name: no_flag_path
|
||||
},
|
||||
]
|
||||
assert no_flag_builder.native_link_flags_from_sources() == ''
|
||||
|
||||
mut math_flag_builder := new_builder(&prefs)
|
||||
math_flag_builder.files = [
|
||||
ast.File{
|
||||
name: math_flag_path
|
||||
},
|
||||
]
|
||||
assert math_flag_builder.native_link_flags_from_sources() == '-lm'
|
||||
}
|
||||
|
||||
fn test_native_user_link_flags_ignore_internal_vlib_runtime_flags() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_user_link_flags_${os.getpid()}')
|
||||
vlib_os_dir := os.join_path(tmp_dir, 'vlib', 'os')
|
||||
os.mkdir_all(vlib_os_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
main_path := os.join_path(tmp_dir, 'main.v')
|
||||
internal_path := os.join_path(vlib_os_dir, 'signal_linux.c.v')
|
||||
os.write_file(main_path, 'module main\n#flag -lm\n') or { panic(err) }
|
||||
os.write_file(internal_path, 'module os\n#flag -lpthread\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.Preferences{
|
||||
backend: .x64
|
||||
arch: .x64
|
||||
target_os: 'linux'
|
||||
vroot: tmp_dir
|
||||
skip_builtin: true
|
||||
}
|
||||
|
||||
mut b := new_builder(&prefs)
|
||||
b.user_files = [main_path]
|
||||
b.files = [
|
||||
ast.File{
|
||||
name: main_path
|
||||
},
|
||||
ast.File{
|
||||
name: internal_path
|
||||
},
|
||||
]
|
||||
_, all_link_flags := b.native_compile_and_link_flags_from_sources()
|
||||
_, user_link_flags := b.native_user_compile_and_link_flags_from_sources()
|
||||
|
||||
assert all_link_flags.fields().sorted() == ['-lm', '-lpthread']
|
||||
assert user_link_flags == '-lm'
|
||||
assert !native_link_flags_allow_builtin_linux_tiny(all_link_flags, user_link_flags)
|
||||
|
||||
b.user_files = []
|
||||
b.files = [
|
||||
ast.File{
|
||||
name: internal_path
|
||||
},
|
||||
]
|
||||
_, internal_only_link_flags := b.native_compile_and_link_flags_from_sources()
|
||||
_, no_user_link_flags := b.native_user_compile_and_link_flags_from_sources()
|
||||
|
||||
assert internal_only_link_flags == '-lpthread'
|
||||
assert no_user_link_flags == ''
|
||||
assert native_link_flags_allow_builtin_linux_tiny(internal_only_link_flags, no_user_link_flags)
|
||||
}
|
||||
|
||||
fn test_native_compile_and_link_flags_from_sources_keep_compile_flags_for_source_inputs() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_source_flags_${os.getpid()}')
|
||||
include_dir := os.join_path(tmp_dir, 'include')
|
||||
os.mkdir_all(include_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
main_path := os.join_path(tmp_dir, 'main.v')
|
||||
c_path := os.join_path(tmp_dir, 'helper.c')
|
||||
os.write_file(c_path, 'int helper(void) { return 7; }\n') or { panic(err) }
|
||||
os.write_file(main_path, 'module main
|
||||
#flag -I include -DHELPER -fopenmp -pthread helper.c -Wl,-rpath,/tmp/native-lib -lm
|
||||
') or {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.backend = .x64
|
||||
prefs.arch = .x64
|
||||
prefs.target_os = 'linux'
|
||||
prefs.skip_builtin = true
|
||||
|
||||
mut b := new_builder(&prefs)
|
||||
b.files = [
|
||||
ast.File{
|
||||
name: main_path
|
||||
},
|
||||
]
|
||||
compile_flags, link_flags := b.native_compile_and_link_flags_from_sources()
|
||||
expected_include_dir := os.real_path(include_dir)
|
||||
expected_c_path := os.real_path(c_path)
|
||||
|
||||
assert compile_flags == '-I ${expected_include_dir} -DHELPER -fopenmp -pthread'
|
||||
assert link_flags == '-fopenmp -pthread ${expected_c_path} -Wl,-rpath,/tmp/native-lib -lm'
|
||||
}
|
||||
|
||||
fn test_native_x64_requires_ssa_optimization() {
|
||||
mut prefs := pref.new_preferences()
|
||||
assert prefs.no_optimize
|
||||
builder := new_builder(&prefs)
|
||||
assert builder.native_backend_requires_ssa_optimization(.x64)
|
||||
assert !builder.native_backend_requires_ssa_optimization(.arm64)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
import v2.ast
|
||||
|
||||
fn (mut b Builder) parse_files_parallel(files []string) []ast.File {
|
||||
// FlatAst mode is the only builder parse pipeline. Keep `-parallel`
|
||||
// parsing serial for now: token.FileSet shares position counters and file
|
||||
// slices that are not concurrency-safe, and selector_names are keyed by
|
||||
// those position ids.
|
||||
return b.parse_files(files)
|
||||
}
|
||||
@@ -0,0 +1,795 @@
|
||||
// vtest build: macos
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
import v2.pref
|
||||
|
||||
fn parse_files_for_parse_test(mut b Builder, paths []string) []ast.File {
|
||||
b.parse_files(paths)
|
||||
b.flat = b.flat_builder.flat
|
||||
return b.flat.to_files()
|
||||
}
|
||||
|
||||
fn test_parse_files_keeps_single_file_inputs_isolated() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_single_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(tmp_dir, 'hello.v')
|
||||
sibling_file := os.join_path(tmp_dir, 'extra.v')
|
||||
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(sibling_file, 'module main\nfn extra() {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [entry_file])
|
||||
|
||||
assert files.len == 1
|
||||
}
|
||||
|
||||
fn test_parse_files_expands_directory_inputs() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(tmp_dir, 'hello.v')
|
||||
sibling_file := os.join_path(tmp_dir, 'extra.v')
|
||||
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(sibling_file, 'module main\nfn extra() {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [tmp_dir])
|
||||
|
||||
assert files.len == 2
|
||||
}
|
||||
|
||||
fn test_parse_files_expands_same_module_subdirectories() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_recursive_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'user')) or { panic(err) }
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'api')) or { panic(err) }
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'nested_app')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(tmp_dir, 'main.v')
|
||||
user_file := os.join_path(tmp_dir, 'user', 'user.v')
|
||||
api_file := os.join_path(tmp_dir, 'api', 'api.v')
|
||||
nested_file := os.join_path(tmp_dir, 'nested_app', 'main.v')
|
||||
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(user_file, 'module main\nstruct User {}\n') or { panic(err) }
|
||||
os.write_file(api_file, 'module api\nstruct Payload {}\n') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'nested_app', 'v.mod'), 'Module { name: "nested" }\n') or {
|
||||
panic(err)
|
||||
}
|
||||
os.write_file(nested_file, 'module main\nfn nested_main() {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [tmp_dir])
|
||||
names := files.map(os.file_name(it.name))
|
||||
|
||||
assert files.len == 2
|
||||
assert 'main.v' in names
|
||||
assert 'user.v' in names
|
||||
}
|
||||
|
||||
fn test_parse_files_expands_implicit_main_subdirectories() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_implicit_main_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'heroes')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(tmp_dir, 'main.v')
|
||||
hero_file := os.join_path(tmp_dir, 'heroes', 'hero.v')
|
||||
os.write_file(entry_file, 'fn main() {}\n') or { panic(err) }
|
||||
os.write_file(hero_file, 'struct HeroEffect {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [tmp_dir])
|
||||
names := files.map(os.file_name(it.name))
|
||||
|
||||
assert files.len == 2
|
||||
assert 'main.v' in names
|
||||
assert 'hero.v' in names
|
||||
}
|
||||
|
||||
fn test_parse_files_skips_block_commented_non_main_subdirectories() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_block_comment_mod_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'api')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(tmp_dir, 'main.v')
|
||||
api_file := os.join_path(tmp_dir, 'api', 'api.v')
|
||||
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(api_file, '/* license header */\nmodule api\nstruct Payload {}\n') or {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [tmp_dir])
|
||||
names := files.map(os.file_name(it.name))
|
||||
|
||||
assert files.len == 1
|
||||
assert 'main.v' in names
|
||||
assert 'api.v' !in names
|
||||
}
|
||||
|
||||
fn test_virtual_main_modules_skip_groups_with_executable_main_from_paths() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_virtual_paths_main_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'admin')) or { panic(err) }
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'tests')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
main_file := os.join_path(tmp_dir, 'main.v')
|
||||
admin_file := os.join_path(tmp_dir, 'admin', 'admin.v')
|
||||
test_file := os.join_path(tmp_dir, 'tests', 'first_run.v')
|
||||
os.write_file(main_file, 'module main\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(admin_file, 'module main\nfn admin_route() {}\n') or { panic(err) }
|
||||
os.write_file(test_file, 'module main\nfn main() {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
b.user_files = [tmp_dir]
|
||||
groups := b.collect_virtual_main_modules_from_paths([main_file, admin_file, test_file])
|
||||
names := groups.map(it.name)
|
||||
|
||||
assert names == ['admin']
|
||||
}
|
||||
|
||||
fn test_virtual_main_modules_skip_groups_with_attributed_executable_main_from_paths() {
|
||||
tmp_dir := os.join_path(os.temp_dir(),
|
||||
'v2_builder_virtual_paths_attributed_main_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'admin')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
main_file := os.join_path(tmp_dir, 'main.v')
|
||||
admin_file := os.join_path(tmp_dir, 'admin', 'admin.v')
|
||||
os.write_file(main_file, 'module main\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(admin_file, 'module main\n@[console] fn main() {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
b.user_files = [tmp_dir]
|
||||
groups := b.collect_virtual_main_modules_from_paths([main_file, admin_file])
|
||||
|
||||
assert groups.len == 0
|
||||
}
|
||||
|
||||
fn test_virtual_main_modules_skip_groups_with_executable_main_from_ast() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_virtual_ast_main_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'admin')) or { panic(err) }
|
||||
os.mkdir_all(os.join_path(tmp_dir, 'tests')) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
os.write_file(os.join_path(tmp_dir, 'main.v'), 'module main\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'admin', 'admin.v'), 'module main\nfn admin_route() {}\n') or {
|
||||
panic(err)
|
||||
}
|
||||
os.write_file(os.join_path(tmp_dir, 'tests', 'first_run.v'), 'module main\nfn main() {}\n') or {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
b.user_files = [tmp_dir]
|
||||
b.files = parse_files_for_parse_test(mut b, [tmp_dir])
|
||||
groups := b.collect_virtual_main_modules()
|
||||
names := groups.map(it.name)
|
||||
|
||||
assert names == ['admin']
|
||||
}
|
||||
|
||||
fn test_parse_files_expands_non_main_module_files() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_module_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(tmp_dir, 'math_test.v')
|
||||
sibling_file := os.join_path(tmp_dir, 'math.v')
|
||||
os.write_file(entry_file, 'module math\nconst x = twice(21)\n') or { panic(err) }
|
||||
os.write_file(sibling_file, 'module math\nfn twice(v int) int { return v * 2 }\n') or {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
prefs.skip_imports = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [entry_file])
|
||||
|
||||
assert files.len == 2
|
||||
}
|
||||
|
||||
fn test_parse_files_resolves_project_root_sibling_import_from_nested_module_file() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_nested_import_${os.getpid()}')
|
||||
flags_dir := os.join_path(tmp_dir, 'core', 'flags')
|
||||
ignore_dir := os.join_path(tmp_dir, 'ignore')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(flags_dir) or { panic(err) }
|
||||
os.mkdir_all(ignore_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(flags_dir, 'complete_test.v')
|
||||
ignore_file := os.join_path(ignore_dir, 'lib.v')
|
||||
os.write_file(os.join_path(tmp_dir, 'v.mod'), "Module { name: 'nested_import' }\n") or {
|
||||
panic(err)
|
||||
}
|
||||
os.write_file(entry_file, 'module flags\nimport ignore\nfn test_use_import() {}\n') or {
|
||||
panic(err)
|
||||
}
|
||||
os.write_file(ignore_file, 'module ignore\npub struct Marker {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [entry_file])
|
||||
names := files.map(os.file_name(it.name))
|
||||
|
||||
assert 'complete_test.v' in names
|
||||
assert 'lib.v' in names
|
||||
}
|
||||
|
||||
fn test_parse_files_prefers_project_root_module_over_vlib_module() {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_local_shadow_${os.getpid()}')
|
||||
app_dir := os.join_path(tmp_dir, 'app')
|
||||
regex_dir := os.join_path(tmp_dir, 'regex')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(app_dir) or { panic(err) }
|
||||
os.mkdir_all(regex_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
|
||||
entry_file := os.join_path(app_dir, 'main.v')
|
||||
local_regex_file := os.join_path(regex_dir, 'local_regex.v')
|
||||
os.write_file(os.join_path(tmp_dir, 'v.mod'), "Module { name: 'local_shadow' }\n") or {
|
||||
panic(err)
|
||||
}
|
||||
os.write_file(entry_file, 'module main\nimport regex\nfn main() {}\n') or { panic(err) }
|
||||
os.write_file(local_regex_file, 'module regex\npub struct RegexMatcher {}\n') or { panic(err) }
|
||||
|
||||
mut prefs := pref.new_preferences()
|
||||
prefs.skip_builtin = true
|
||||
mut b := new_builder(&prefs)
|
||||
files := parse_files_for_parse_test(mut b, [entry_file])
|
||||
|
||||
assert files.any(it.name == local_regex_file)
|
||||
}
|
||||
|
||||
fn test_active_file_imports_follow_comptime_else_branch() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.ComptimeExpr{
|
||||
expr: ast.IfExpr{
|
||||
cond: ast.Ident{
|
||||
name: 'linux'
|
||||
}
|
||||
stmts: [
|
||||
ast.Stmt(ast.ImportStmt{
|
||||
name: 'net.openssl'
|
||||
alias: 'openssl'
|
||||
}),
|
||||
]
|
||||
else_expr: ast.IfExpr{
|
||||
cond: ast.empty_expr
|
||||
stmts: [
|
||||
ast.Stmt(ast.ImportStmt{
|
||||
name: 'net.mbedtls'
|
||||
alias: 'mbedtls'
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.len == 1
|
||||
assert imports[0].name == 'net.mbedtls'
|
||||
}
|
||||
|
||||
fn test_active_file_imports_skips_sync_for_channel_in_inactive_comptime_branch() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.ComptimeExpr{
|
||||
expr: ast.IfExpr{
|
||||
cond: ast.Ident{
|
||||
name: 'windows'
|
||||
}
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
linux_imports := active_file_imports(file, [], 'linux').map(it.name)
|
||||
windows_imports := active_file_imports(file, [], 'windows').map(it.name)
|
||||
|
||||
assert 'sync' !in linux_imports
|
||||
assert 'sync' in windows_imports
|
||||
}
|
||||
|
||||
fn test_active_file_imports_skips_sync_for_channel_in_sync_module() {
|
||||
file := ast.File{
|
||||
mod: 'sync'
|
||||
name: 'cond.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ModuleStmt{
|
||||
name: 'sync'
|
||||
}),
|
||||
ast.Stmt(ast.StructDecl{
|
||||
name: 'Cond'
|
||||
fields: [
|
||||
ast.FieldDecl{
|
||||
name: 'waiters'
|
||||
typ: ast.Type(ast.ArrayType{
|
||||
elem_type: ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'bool'
|
||||
})
|
||||
}))
|
||||
})
|
||||
},
|
||||
]
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac').map(it.name)
|
||||
mut flat_builder := ast.new_flat_builder()
|
||||
flat_builder.append_file(file)
|
||||
flat_imports := active_file_imports_from_flat(&flat_builder.flat, flat_builder.flat.files[0],
|
||||
[], [], 'mac').map(it.name)
|
||||
|
||||
assert 'sync' !in imports
|
||||
assert 'sync' !in flat_imports
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_if_condition() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.IfExpr{
|
||||
cond: ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'takes_chan'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_for_condition() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ForStmt{
|
||||
cond: ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'takes_chan'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_assert_expr() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.AssertStmt{
|
||||
expr: ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'takes_chan'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_array_literal() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.ArrayInitExpr{
|
||||
exprs: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_match_condition() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.MatchExpr{
|
||||
expr: ast.Expr(ast.Ident{
|
||||
name: 'value'
|
||||
})
|
||||
branches: [
|
||||
ast.MatchBranch{
|
||||
cond: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_nested_channel_in_infix_expr() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.IfExpr{
|
||||
cond: ast.InfixExpr{
|
||||
op: .and
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'ok'
|
||||
})
|
||||
rhs: ast.Expr(ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'takes_chan'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_call_callee_generic_args() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.CallExpr{
|
||||
lhs: ast.Expr(ast.GenericArgs{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'make_value'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
})
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_modifier_expr_operand() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'use'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.ModifierExpr{
|
||||
kind: .key_mut
|
||||
expr: ast.IndexExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'arr'
|
||||
})
|
||||
expr: ast.Expr(ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'idx'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
})
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_assign_lhs() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.AssignStmt{
|
||||
lhs: [
|
||||
ast.Expr(ast.IndexExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'arr'
|
||||
})
|
||||
expr: ast.Expr(ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'idx'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
})
|
||||
}),
|
||||
]
|
||||
rhs: [
|
||||
ast.Expr(ast.BasicLiteral{
|
||||
kind: .number
|
||||
value: '0'
|
||||
}),
|
||||
]
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_adds_sync_for_channel_in_labelled_loop() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.LabelStmt{
|
||||
name: 'outer'
|
||||
stmt: ast.Stmt(ast.ForStmt{
|
||||
stmts: [
|
||||
ast.Stmt(ast.AssignStmt{
|
||||
lhs: [
|
||||
ast.Expr(ast.Ident{
|
||||
name: '_'
|
||||
}),
|
||||
]
|
||||
rhs: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
}),
|
||||
]
|
||||
})
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_ignores_channel_in_inactive_attributed_fn_body() {
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.FnDecl{
|
||||
name: 'debug'
|
||||
typ: ast.FnType{}
|
||||
attributes: [
|
||||
ast.Attribute{
|
||||
name: 'if'
|
||||
comptime_cond: ast.Expr(ast.Ident{
|
||||
name: 'windows'
|
||||
})
|
||||
},
|
||||
]
|
||||
stmts: [
|
||||
ast.Stmt(ast.AssignStmt{
|
||||
lhs: [
|
||||
ast.Expr(ast.Ident{
|
||||
name: '_'
|
||||
}),
|
||||
]
|
||||
rhs: [
|
||||
ast.Expr(ast.Type(ast.ChannelType{
|
||||
elem_type: ast.Expr(ast.Ident{
|
||||
name: 'int'
|
||||
})
|
||||
})),
|
||||
]
|
||||
}),
|
||||
]
|
||||
}),
|
||||
]
|
||||
}
|
||||
imports := active_file_imports(file, [], 'mac')
|
||||
|
||||
assert !imports.any(it.name == 'sync')
|
||||
}
|
||||
|
||||
fn test_active_file_imports_skip_pkgconfig_branch_when_disabled() {
|
||||
if !pref.comptime_pkgconfig_value('sqlite3') {
|
||||
return
|
||||
}
|
||||
file := ast.File{
|
||||
mod: 'main'
|
||||
name: 'main.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ExprStmt{
|
||||
expr: ast.ComptimeExpr{
|
||||
expr: ast.IfExpr{
|
||||
cond: ast.CallExpr{
|
||||
lhs: ast.Expr(ast.Ident{
|
||||
name: 'pkgconfig'
|
||||
})
|
||||
args: [
|
||||
ast.Expr(ast.StringLiteral{
|
||||
kind: .v
|
||||
value: "'sqlite3'"
|
||||
}),
|
||||
]
|
||||
}
|
||||
stmts: [
|
||||
ast.Stmt(ast.ImportStmt{
|
||||
name: 'db.sqlite'
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
native_imports := active_file_imports_with_options(file, [], [], 'mac', true).map(it.name)
|
||||
cross_imports := active_file_imports_with_options(file, [], [], 'mac', false).map(it.name)
|
||||
|
||||
assert 'db.sqlite' in native_imports
|
||||
assert 'db.sqlite' !in cross_imports
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
import runtime
|
||||
import v2.ast
|
||||
import v2.ssa
|
||||
|
||||
// FnDeclRef references a function declaration within the files array.
|
||||
struct FnDeclRef {
|
||||
file_idx int
|
||||
stmt_idx int
|
||||
mod_name string
|
||||
selective_import_fn_names map[string]string
|
||||
selective_import_fn_candidates map[string][]string
|
||||
module_import_aliases map[string]string
|
||||
}
|
||||
|
||||
$if !windows {
|
||||
struct SSABuildChunkArgs {
|
||||
worker voidptr // &ssa.Builder (pre-created worker builder)
|
||||
files voidptr // &[]ast.File
|
||||
fn_refs voidptr // &[]FnDeclRef
|
||||
start_idx int
|
||||
end_idx int
|
||||
}
|
||||
|
||||
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
|
||||
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
|
||||
fn C.pthread_attr_init(attr voidptr) int
|
||||
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
|
||||
fn C.pthread_attr_destroy(attr voidptr) int
|
||||
|
||||
fn ssa_build_chunk_thread(arg voidptr) voidptr {
|
||||
a := unsafe { &SSABuildChunkArgs(arg) }
|
||||
mut worker_b := unsafe { &ssa.Builder(a.worker) }
|
||||
files := unsafe { &[]ast.File(a.files) }
|
||||
fn_refs := unsafe { &[]FnDeclRef(a.fn_refs) }
|
||||
|
||||
// Build assigned functions.
|
||||
// Avoid chained array access like files[i].stmts[j] — in ARM64-compiled
|
||||
// binaries, chained indexing returns copies with potentially corrupted fields.
|
||||
// Instead, copy to a local first, then access fields.
|
||||
for fi := a.start_idx; fi < a.end_idx; fi++ {
|
||||
ref := unsafe { fn_refs[fi] }
|
||||
worker_b.cur_module = ref.mod_name
|
||||
worker_b.set_selective_import_fn_names(ref.selective_import_fn_names)
|
||||
worker_b.set_selective_import_fn_candidates(ref.selective_import_fn_candidates)
|
||||
worker_b.set_module_import_aliases(ref.module_import_aliases)
|
||||
file := unsafe { (*files)[ref.file_idx] }
|
||||
stmt := file.stmts[ref.stmt_idx]
|
||||
decl := stmt as ast.FnDecl
|
||||
worker_b.build_fn(decl)
|
||||
}
|
||||
return unsafe { nil }
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut b Builder) ssa_build_parallel(mut ssa_builder ssa.Builder, files []ast.File) {
|
||||
n_jobs := runtime.nr_jobs()
|
||||
mut mod := ssa_builder.mod
|
||||
|
||||
// Collect all function declarations that need building.
|
||||
// Avoid chained array access like files[fi].stmts[si] — copy to locals first.
|
||||
has_markused := ssa_builder.used_fn_keys.len > 0
|
||||
mut fn_refs := []FnDeclRef{cap: 4096}
|
||||
for fi in 0 .. files.len {
|
||||
file := files[fi]
|
||||
mod_name := ssa.file_module_name(file)
|
||||
selective_import_fn_names := ssa.selective_import_fn_names_from_imports(file.imports)
|
||||
selective_import_fn_candidates :=
|
||||
ssa.selective_import_fn_candidates_from_imports(file.imports)
|
||||
module_import_aliases := ssa.module_import_aliases_from_imports(file.imports)
|
||||
nstmts := file.stmts.len
|
||||
for si in 0 .. nstmts {
|
||||
stmt := file.stmts[si]
|
||||
if stmt is ast.FnDecl {
|
||||
decl := stmt as ast.FnDecl
|
||||
if decl.language == .c && decl.stmts.len == 0 {
|
||||
continue
|
||||
}
|
||||
if decl.typ.generic_params.len > 0 {
|
||||
continue
|
||||
}
|
||||
// Dead code elimination: skip unreachable functions
|
||||
if has_markused {
|
||||
ssa_builder.cur_module = mod_name
|
||||
if !ssa_builder.should_build_fn(file.name, decl) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
fn_refs << FnDeclRef{
|
||||
file_idx: fi
|
||||
stmt_idx: si
|
||||
mod_name: mod_name
|
||||
selective_import_fn_names: selective_import_fn_names.clone()
|
||||
selective_import_fn_candidates: selective_import_fn_candidates.clone()
|
||||
module_import_aliases: module_import_aliases.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
n_fns := fn_refs.len
|
||||
$if windows {
|
||||
ssa_builder.build_all_fn_bodies(files)
|
||||
ssa_builder.generate_referenced_synthetic_runtime_stubs()
|
||||
return
|
||||
} $else {
|
||||
if n_fns <= 1 || n_jobs <= 1 {
|
||||
// Fallback to sequential
|
||||
ssa_builder.build_all_fn_bodies(files)
|
||||
ssa_builder.generate_referenced_synthetic_runtime_stubs()
|
||||
return
|
||||
}
|
||||
|
||||
// Pre-create all worker modules and builders on the main thread
|
||||
// to avoid COW races on shared data structures.
|
||||
chunk_size := (n_fns + n_jobs - 1) / n_jobs
|
||||
mut actual_chunks := 0
|
||||
mut i := 0
|
||||
for i < n_fns {
|
||||
actual_chunks++
|
||||
i += chunk_size
|
||||
}
|
||||
|
||||
// Record seed lengths for merge — workers' new data starts beyond these.
|
||||
seed_values := mod.values.len
|
||||
seed_instrs := mod.instrs.len
|
||||
seed_blocks := mod.blocks.len
|
||||
seed_types := mod.type_store.types.len
|
||||
seed_funcs := mod.funcs.len
|
||||
|
||||
mut workers := []voidptr{cap: actual_chunks}
|
||||
for ci := 0; ci < actual_chunks; ci++ {
|
||||
mut worker_mod := mod.new_worker_module()
|
||||
mut worker_b := ssa_builder.new_worker_clone(worker_mod, ci)
|
||||
workers << voidptr(worker_b)
|
||||
}
|
||||
|
||||
// Spawn worker threads
|
||||
mut thread_ids := []C.pthread_t{len: actual_chunks}
|
||||
mut args := []SSABuildChunkArgs{cap: actual_chunks}
|
||||
|
||||
attr_buf := [64]u8{}
|
||||
attr := unsafe { voidptr(&attr_buf[0]) }
|
||||
C.pthread_attr_init(attr)
|
||||
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
|
||||
|
||||
mut chunk_idx := 0
|
||||
i = 0
|
||||
for i < n_fns {
|
||||
end := if i + chunk_size < n_fns { i + chunk_size } else { n_fns }
|
||||
args << SSABuildChunkArgs{
|
||||
worker: workers[chunk_idx]
|
||||
files: unsafe { voidptr(&files) }
|
||||
fn_refs: unsafe { voidptr(&fn_refs) }
|
||||
start_idx: i
|
||||
end_idx: end
|
||||
}
|
||||
C.pthread_create(unsafe { &thread_ids[chunk_idx] }, attr, ssa_build_chunk_thread,
|
||||
unsafe { voidptr(&args[chunk_idx]) })
|
||||
i = end
|
||||
chunk_idx++
|
||||
}
|
||||
C.pthread_attr_destroy(attr)
|
||||
|
||||
// Wait for all workers
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
C.pthread_join(thread_ids[ci], unsafe { nil })
|
||||
}
|
||||
|
||||
// Merge worker results in order
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
w := unsafe { &ssa.Builder(workers[ci]) }
|
||||
w_mod := w.mod
|
||||
// Collect func_data from worker's modified funcs[].
|
||||
// Only include functions that were actually built by this worker,
|
||||
// not pre-seeded synthetic functions (array__eq, wyhash*, etc.)
|
||||
// whose blocks already exist in the main module from Phase 3.5.
|
||||
mut func_data := []ssa.FuncSSAData{cap: 512}
|
||||
for fi2 := 0; fi2 < w_mod.funcs.len; fi2++ {
|
||||
wf := w_mod.funcs[fi2]
|
||||
if wf.blocks.len == 0 {
|
||||
continue
|
||||
}
|
||||
// Seeded funcs (fi2 < seed_funcs) that already had blocks before
|
||||
// workers started are not new work — skip them.
|
||||
if fi2 < seed_funcs && wf.blocks[0] < seed_blocks {
|
||||
continue
|
||||
}
|
||||
func_data << ssa.FuncSSAData{
|
||||
func_idx: fi2
|
||||
blocks: wf.blocks
|
||||
params: wf.params
|
||||
}
|
||||
}
|
||||
mod.merge_worker_module(w_mod, func_data, seed_values, seed_instrs, seed_blocks,
|
||||
seed_types, seed_funcs)
|
||||
}
|
||||
ssa_builder.generate_referenced_synthetic_runtime_stubs()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// vtest build: !windows
|
||||
module builder
|
||||
|
||||
import os
|
||||
import runtime
|
||||
import v2.parser
|
||||
import v2.pref
|
||||
import v2.ssa
|
||||
import v2.token
|
||||
import v2.transformer
|
||||
import v2.types
|
||||
|
||||
fn parallel_ssa_alias_tmp_dir(label string) string {
|
||||
return os.join_path(os.temp_dir(), 'v2_parallel_ssa_alias_${label}_${os.getpid()}')
|
||||
}
|
||||
|
||||
struct ParallelSSAAliasTestSource {
|
||||
rel_path string
|
||||
code string
|
||||
}
|
||||
|
||||
fn parallel_ssa_module_for_test_sources(label string, sources []ParallelSSAAliasTestSource) &ssa.Module {
|
||||
tmp_dir := parallel_ssa_alias_tmp_dir(label)
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic('cannot create ${tmp_dir}') }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
mut paths := []string{}
|
||||
for source in sources {
|
||||
path := os.join_path(tmp_dir, source.rel_path)
|
||||
os.mkdir_all(os.dir(path)) or { panic('cannot create ${os.dir(path)}') }
|
||||
os.write_file(path, source.code) or { panic('cannot write ${path}') }
|
||||
paths << path
|
||||
}
|
||||
prefs := &pref.Preferences{
|
||||
backend: .x64
|
||||
arch: .x64
|
||||
}
|
||||
mut file_set := token.FileSet.new()
|
||||
mut par := parser.Parser.new(prefs)
|
||||
files := par.parse_files(paths, mut file_set)
|
||||
env := types.Environment.new()
|
||||
mut checker := types.Checker.new(prefs, file_set, env)
|
||||
checker.check_files(files)
|
||||
mut trans := transformer.Transformer.new_with_pref(env, prefs)
|
||||
trans.set_file_set(file_set)
|
||||
transformed_files := trans.transform_files(files)
|
||||
mut mod := ssa.Module.new('parallel_alias_test')
|
||||
mut ssa_builder := ssa.Builder.new_with_env(mod, env)
|
||||
ssa_builder.skip_fn_bodies = true
|
||||
ssa_builder.build_all(transformed_files)
|
||||
ssa_builder.skip_fn_bodies = false
|
||||
|
||||
old_vjobs := os.getenv_opt('VJOBS')
|
||||
os.setenv('VJOBS', '2', true)
|
||||
defer {
|
||||
if old := old_vjobs {
|
||||
os.setenv('VJOBS', old, true)
|
||||
} else {
|
||||
os.unsetenv('VJOBS')
|
||||
}
|
||||
}
|
||||
assert runtime.nr_jobs() == 2
|
||||
mut outer_builder := new_builder(prefs)
|
||||
outer_builder.ssa_build_parallel(mut ssa_builder, transformed_files)
|
||||
return mod
|
||||
}
|
||||
|
||||
fn parallel_ssa_func_names(m &ssa.Module) []string {
|
||||
mut names := []string{}
|
||||
for func in m.funcs {
|
||||
names << func.name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
fn test_parallel_ssa_worker_restores_module_import_aliases() {
|
||||
m := parallel_ssa_module_for_test_sources('module_alias', [
|
||||
ParallelSSAAliasTestSource{
|
||||
rel_path: 'left/left.v'
|
||||
code: 'module left
|
||||
|
||||
pub struct Box {
|
||||
pub:
|
||||
value int
|
||||
}
|
||||
'
|
||||
},
|
||||
ParallelSSAAliasTestSource{
|
||||
rel_path: 'right/right.v'
|
||||
code: 'module right
|
||||
|
||||
pub struct Box {
|
||||
pub:
|
||||
value int
|
||||
}
|
||||
'
|
||||
},
|
||||
ParallelSSAAliasTestSource{
|
||||
rel_path: 'a.v'
|
||||
code: 'module main
|
||||
|
||||
import left as dep
|
||||
|
||||
fn bump_left() int {
|
||||
box := dep.Box{
|
||||
value: 1
|
||||
}
|
||||
return keep[dep.Box](box).value
|
||||
}
|
||||
'
|
||||
},
|
||||
ParallelSSAAliasTestSource{
|
||||
rel_path: 'b.v'
|
||||
code: 'module main
|
||||
|
||||
import right as dep
|
||||
|
||||
fn bump_right() int {
|
||||
box := dep.Box{
|
||||
value: 2
|
||||
}
|
||||
return keep[dep.Box](box).value
|
||||
}
|
||||
'
|
||||
},
|
||||
ParallelSSAAliasTestSource{
|
||||
rel_path: 'main.v'
|
||||
code: 'module main
|
||||
|
||||
fn keep[T](value T) T {
|
||||
return value
|
||||
}
|
||||
|
||||
fn filler0() int { return 0 }
|
||||
fn filler1() int { return 1 }
|
||||
fn filler2() int { return 2 }
|
||||
fn filler3() int { return 3 }
|
||||
|
||||
fn main() {
|
||||
_ = bump_left()
|
||||
_ = bump_right()
|
||||
}
|
||||
'
|
||||
},
|
||||
])
|
||||
|
||||
func_names := parallel_ssa_func_names(m)
|
||||
assert 'keep_T_left_Box' in func_names, func_names.str()
|
||||
assert 'keep_T_right_Box' in func_names, func_names.str()
|
||||
assert 'keep_T_dep_Box' !in func_names, func_names.str()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
import v2.ast
|
||||
import v2.transformer
|
||||
import runtime
|
||||
|
||||
$if !windows {
|
||||
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
|
||||
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
|
||||
fn C.pthread_attr_init(attr voidptr) int
|
||||
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
|
||||
fn C.pthread_attr_destroy(attr voidptr) int
|
||||
|
||||
// TransformChunkFlatArgs carries one worker's slice of the parallel flat
|
||||
// transform: the worker emits its file range cursor-native into its own
|
||||
// FlatBuilder (no legacy ast.File rehydrate) and hands back the resulting
|
||||
// FlatAst, which the main thread merges via `FlatBuilder.append_flat`.
|
||||
struct TransformChunkFlatArgs {
|
||||
t voidptr // &transformer.Transformer
|
||||
flat &ast.FlatAst = unsafe { nil }
|
||||
flat_extra_stmts [][]ast.Stmt
|
||||
flat_start int
|
||||
flat_end int
|
||||
result_ptr voidptr // &ast.FlatAst
|
||||
worker_ptr voidptr
|
||||
worker_idx int
|
||||
}
|
||||
|
||||
fn transform_chunk_flat_thread(arg voidptr) voidptr {
|
||||
a := unsafe { &TransformChunkFlatArgs(arg) }
|
||||
t := unsafe { &transformer.Transformer(a.t) }
|
||||
mut w := t.new_worker_clone(a.worker_idx)
|
||||
// Cursor-native: transform one file at a time straight into this
|
||||
// worker's FlatBuilder. Peak per worker is the cumulative flat for its
|
||||
// chunk (no legacy ast.File ever materialises).
|
||||
mut wb := ast.new_flat_builder()
|
||||
for fi := a.flat_start; fi < a.flat_end; fi++ {
|
||||
extra := if fi >= 0 && fi < a.flat_extra_stmts.len {
|
||||
a.flat_extra_stmts[fi]
|
||||
} else {
|
||||
[]ast.Stmt{}
|
||||
}
|
||||
w.transform_file_index_with_extra_to_flat(a.flat, fi, extra, mut wb)
|
||||
}
|
||||
unsafe {
|
||||
*(&ast.FlatAst(a.result_ptr)) = wb.flat
|
||||
*(&voidptr(a.worker_ptr)) = voidptr(w)
|
||||
}
|
||||
return unsafe { nil }
|
||||
}
|
||||
}
|
||||
|
||||
fn flat_extra_stmts_by_file(extra_stmts map[int][]ast.Stmt, n_files int) [][]ast.Stmt {
|
||||
mut out := [][]ast.Stmt{cap: n_files}
|
||||
for _ in 0 .. n_files {
|
||||
out << []ast.Stmt{}
|
||||
}
|
||||
for fi, stmts in extra_stmts {
|
||||
if fi >= 0 && fi < n_files {
|
||||
out[fi] = stmts
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// transform_files_parallel_flat_direct is the flat-native parallel transform
|
||||
// used by every backend. It mirrors the sequential
|
||||
// `transform_flat_to_flat_direct` (pre_pass -> prepare -> per-file cursor
|
||||
// transform -> post_pass tail) but fans the per-file loop across worker threads:
|
||||
// each worker emits its contiguous file range cursor-native into its OWN
|
||||
// FlatBuilder, then the main thread concatenates them in file order via
|
||||
// `FlatBuilder.append_flat`. No legacy ast.File is materialised; backends that
|
||||
// still consume []ast.File (.v/eval) rehydrate from the transformed flat at
|
||||
// the codegen boundary in builder.v.
|
||||
//
|
||||
// Per-worker synth-position disjointness is handled by `new_worker_clone`
|
||||
// (it offsets synth_pos_counter by `-worker_idx * 100_000`); worker state is
|
||||
// merged back via `merge_worker` after the join, exactly as the legacy path.
|
||||
fn (mut b Builder) transform_files_parallel_flat_direct(mut trans transformer.Transformer) ast.FlatAst {
|
||||
// Pre-pass + generic preparation: sequential, once (same as the legacy path).
|
||||
trans.pre_pass_from_flat(&b.flat)
|
||||
mut flat_extra_stmts := [][]ast.Stmt{}
|
||||
if trans.needs_full_files_for_transform() {
|
||||
extra_stmts := trans.prepare_flat_for_transform(&b.flat)
|
||||
flat_extra_stmts = flat_extra_stmts_by_file(extra_stmts, b.flat.files.len)
|
||||
}
|
||||
|
||||
n_jobs := runtime.nr_jobs()
|
||||
n_files := b.flat.files.len
|
||||
mut out := ast.new_flat_builder()
|
||||
$if windows {
|
||||
for fi in 0 .. n_files {
|
||||
extra := if fi < flat_extra_stmts.len { flat_extra_stmts[fi] } else { []ast.Stmt{} }
|
||||
trans.transform_file_index_with_extra_to_flat(&b.flat, fi, extra, mut out)
|
||||
}
|
||||
} $else {
|
||||
if n_files <= 1 || n_jobs <= 1 {
|
||||
for fi in 0 .. n_files {
|
||||
extra := if fi < flat_extra_stmts.len { flat_extra_stmts[fi] } else { []ast.Stmt{} }
|
||||
trans.transform_file_index_with_extra_to_flat(&b.flat, fi, extra, mut out)
|
||||
}
|
||||
} else {
|
||||
// Contiguous [start,end) file ranges, one per worker. Merging the
|
||||
// worker outputs in spawn order preserves original file order.
|
||||
mut bucket_indices := [][]int{len: n_jobs}
|
||||
chunk_size := (n_files + n_jobs - 1) / n_jobs
|
||||
mut i := 0
|
||||
mut bw := 0
|
||||
for i < n_files {
|
||||
end := if i + chunk_size < n_files { i + chunk_size } else { n_files }
|
||||
for j in i .. end {
|
||||
bucket_indices[bw] << j
|
||||
}
|
||||
i = end
|
||||
bw++
|
||||
}
|
||||
|
||||
mut chunk_flats := []ast.FlatAst{len: n_jobs}
|
||||
mut worker_ptrs := []voidptr{len: n_jobs, init: unsafe { nil }}
|
||||
mut thread_ids := []C.pthread_t{len: n_jobs}
|
||||
mut args := []TransformChunkFlatArgs{cap: n_jobs}
|
||||
|
||||
// 64 MB worker stacks: ARM64-compiled transform recursion is stack-heavy.
|
||||
attr_buf := [64]u8{}
|
||||
attr := unsafe { voidptr(&attr_buf[0]) }
|
||||
C.pthread_attr_init(attr)
|
||||
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
|
||||
|
||||
mut chunk_idx := 0
|
||||
for w in 0 .. n_jobs {
|
||||
idxs := bucket_indices[w]
|
||||
if idxs.len == 0 {
|
||||
continue
|
||||
}
|
||||
args << TransformChunkFlatArgs{
|
||||
t: unsafe { voidptr(trans) }
|
||||
flat: unsafe { &b.flat }
|
||||
flat_extra_stmts: flat_extra_stmts
|
||||
flat_start: idxs[0]
|
||||
flat_end: idxs[idxs.len - 1] + 1
|
||||
result_ptr: unsafe { voidptr(&chunk_flats[chunk_idx]) }
|
||||
worker_ptr: unsafe { voidptr(&worker_ptrs[chunk_idx]) }
|
||||
worker_idx: chunk_idx
|
||||
}
|
||||
C.pthread_create(unsafe { &thread_ids[chunk_idx] }, attr,
|
||||
transform_chunk_flat_thread, unsafe { voidptr(&args[chunk_idx]) })
|
||||
chunk_idx++
|
||||
}
|
||||
C.pthread_attr_destroy(attr)
|
||||
for ci := 0; ci < chunk_idx; ci++ {
|
||||
C.pthread_join(thread_ids[ci], unsafe { nil })
|
||||
}
|
||||
|
||||
// Concatenate worker flats in spawn (= file) order, merging worker state.
|
||||
mut ci := 0
|
||||
for w in 0 .. n_jobs {
|
||||
if bucket_indices[w].len == 0 {
|
||||
continue
|
||||
}
|
||||
out.append_flat(chunk_flats[ci])
|
||||
worker := unsafe { &transformer.Transformer(worker_ptrs[ci]) }
|
||||
trans.merge_worker(worker)
|
||||
ci++
|
||||
}
|
||||
// Move synth_pos_counter past all worker ranges for the post_pass.
|
||||
trans.set_synth_pos_counter(-(chunk_idx * 100_000) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Post-pass tail: identical to transform_flat_to_flat_direct's tail.
|
||||
generated_parts := trans.generated_fns_parts_from_flat(&out.flat)
|
||||
trans.post_pass_to_flat(mut out, generated_parts)
|
||||
trans.apply_post_pass_tail_from_flat(&out.flat)
|
||||
return out.flat
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module builder
|
||||
|
||||
import v2.types
|
||||
|
||||
fn (mut b Builder) type_check_files() &types.Environment {
|
||||
env := types.Environment.new()
|
||||
mut checker := types.Checker.new(b.pref, b.file_set, env)
|
||||
checker.check_flat(&b.flat)
|
||||
return env
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
import v2.types
|
||||
|
||||
fn (mut b Builder) type_check_files_parallel() &types.Environment {
|
||||
// FlatAst-driven type check is sequential today (check_flat). A parallel
|
||||
// flat pipeline is future work.
|
||||
return b.type_check_files()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
module builder
|
||||
|
||||
import v2.types
|
||||
|
||||
// Default build (no `-d parallel`): type-checking is sequential. The real
|
||||
// parallel implementation lives in type_check_d_parallel.v and is only
|
||||
// included when `-d parallel` is set.
|
||||
fn (mut b Builder) type_check_files_parallel() &types.Environment {
|
||||
// Type checking is sequential: the bulk of the time (~90%) is in check_file
|
||||
// which has complex interdependencies (pending fn bodies, struct fields, etc.)
|
||||
// that prevent safe parallelization. The pre-registration phases (scopes, types)
|
||||
// are only ~6% of total time and share mutable Scope objects, making them
|
||||
// unsafe for concurrent access without major restructuring.
|
||||
return b.type_check_files()
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// vtest build: macos
|
||||
module builder
|
||||
|
||||
import os
|
||||
|
||||
fn run_parallel_type_check_fixture(label string, code string) (int, string) {
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v2_parallel_type_check_${label}_${os.getpid()}')
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
os.mkdir_all(tmp_dir) or { panic('cannot create ${tmp_dir}') }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
source_path := os.join_path(tmp_dir, 'main.v')
|
||||
helper_path := os.join_path(tmp_dir, 'helper.v')
|
||||
os.write_file(source_path, code) or { panic('cannot write fixture source') }
|
||||
os.write_file(helper_path, 'import v2.builder
|
||||
import v2.pref
|
||||
|
||||
fn main() {
|
||||
prefs := &pref.Preferences{
|
||||
backend: .v
|
||||
no_cache: true
|
||||
}
|
||||
mut b := builder.new_builder(prefs)
|
||||
b.build(["${source_path}"])
|
||||
}
|
||||
') or {
|
||||
panic('cannot write helper source')
|
||||
}
|
||||
cmd := '${os.quoted_path(@VEXE)} -d parallel -gc none run ${os.quoted_path(helper_path)} 2>&1'
|
||||
res := os.execute(cmd)
|
||||
return res.exit_code, res.output
|
||||
}
|
||||
|
||||
fn test_parallel_type_check_runs_struct_field_default_checks() {
|
||||
code, output := run_parallel_type_check_fixture('struct_default', 'module main
|
||||
|
||||
struct Config {
|
||||
value int = missing_default()
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
')
|
||||
assert code != 0, output
|
||||
assert output.contains('missing_default'), output
|
||||
}
|
||||
|
||||
fn test_parallel_type_check_runs_enum_value_checks() {
|
||||
code, output := run_parallel_type_check_fixture('enum_value', 'module main
|
||||
|
||||
enum Mode {
|
||||
ready = missing_enum_value()
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
')
|
||||
assert code != 0, output
|
||||
assert output.contains('missing_enum_value'), output
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.pref
|
||||
|
||||
fn list_dir_entries(path string) []string {
|
||||
mut entries := os.ls(path) or { []string{} }
|
||||
entries.sort()
|
||||
return entries
|
||||
}
|
||||
|
||||
pub fn get_v_files_from_dir(dir string, user_defines []string, target_os string) []string {
|
||||
if dir == '' {
|
||||
return []string{}
|
||||
}
|
||||
mod_files := list_dir_entries(dir)
|
||||
mut v_files := []string{}
|
||||
mut defaults := []string{}
|
||||
for file in mod_files {
|
||||
if file == '' {
|
||||
continue
|
||||
}
|
||||
// Include .v files (including .c.v), exclude .js.v and test files
|
||||
if !file.ends_with('.v') || file.ends_with('.js.v') || file.contains('_test.') {
|
||||
continue
|
||||
}
|
||||
// skip platform-specific files
|
||||
if file.contains('.arm64.') || file.contains('.arm32.') || file.contains('.amd64.') {
|
||||
continue
|
||||
}
|
||||
// Skip files specialized for a different target OS before parsing/type checking.
|
||||
if pref.file_has_incompatible_os_suffix(file, target_os) {
|
||||
continue
|
||||
}
|
||||
if file.ends_with('prealloc.c.v') && 'prealloc' !in user_defines {
|
||||
continue
|
||||
}
|
||||
// Conditional compilation files: _d_<feature> included when -d <feature> is set,
|
||||
// _notd_<feature> included when -d <feature> is NOT set.
|
||||
if file.contains('_notd_') {
|
||||
feature := extract_define_feature(file, '_notd_')
|
||||
if feature.len > 0 && feature in user_defines {
|
||||
continue
|
||||
}
|
||||
} else if file.contains('_d_') {
|
||||
feature := extract_define_feature(file, '_d_')
|
||||
if feature.len == 0 || feature !in user_defines {
|
||||
continue
|
||||
}
|
||||
}
|
||||
path := os.join_path(dir, file)
|
||||
if path == '' {
|
||||
continue
|
||||
}
|
||||
// Collect _default.c.v files separately; they are only included when
|
||||
// no platform-specific variant exists (e.g. _darwin.c.v, _linux.c.v).
|
||||
if file.contains('default.c.v') {
|
||||
defaults << path
|
||||
} else {
|
||||
v_files << path
|
||||
}
|
||||
}
|
||||
// Add _default.c.v files only when no platform-specific variant was selected.
|
||||
for dfile in defaults {
|
||||
no_postfix := fname_without_platform_postfix(dfile)
|
||||
mut has_specialized := false
|
||||
for vf in v_files {
|
||||
if fname_without_platform_postfix(vf) == no_postfix {
|
||||
has_specialized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !has_specialized {
|
||||
v_files << dfile
|
||||
}
|
||||
}
|
||||
return v_files
|
||||
}
|
||||
|
||||
// fname_without_platform_postfix strips the platform-specific suffix from a file path,
|
||||
// so that e.g. "free_memory_impl_darwin.c.v" and "free_memory_impl_default.c.v" both
|
||||
// map to the same key, allowing detection of specialized vs default variants.
|
||||
fn fname_without_platform_postfix(file string) string {
|
||||
return file.replace_each([
|
||||
'default.c.v',
|
||||
'_',
|
||||
'windows.c.v',
|
||||
'_',
|
||||
'linux.c.v',
|
||||
'_',
|
||||
'darwin.c.v',
|
||||
'_',
|
||||
'macos.c.v',
|
||||
'_',
|
||||
'android.c.v',
|
||||
'_',
|
||||
'android_outside_termux.c.v',
|
||||
'_',
|
||||
'termux.c.v',
|
||||
'_',
|
||||
'ios.c.v',
|
||||
'_',
|
||||
'freebsd.c.v',
|
||||
'_',
|
||||
'openbsd.c.v',
|
||||
'_',
|
||||
'netbsd.c.v',
|
||||
'_',
|
||||
'dragonfly.c.v',
|
||||
'_',
|
||||
'bsd.c.v',
|
||||
'_',
|
||||
'solaris.c.v',
|
||||
'_',
|
||||
'qnx.c.v',
|
||||
'_',
|
||||
'serenity.c.v',
|
||||
'_',
|
||||
'plan9.c.v',
|
||||
'_',
|
||||
'vinix.c.v',
|
||||
'_',
|
||||
'nix.c.v',
|
||||
'_',
|
||||
])
|
||||
}
|
||||
|
||||
// extract_define_feature extracts the feature name from a _d_ or _notd_ filename.
|
||||
// e.g. "parse_d_parallel.v" with marker "_d_" returns "parallel"
|
||||
// e.g. "array_notd_gcboehm_opt.v" with marker "_notd_" returns "gcboehm_opt"
|
||||
fn extract_define_feature(file string, marker string) string {
|
||||
idx := file.index(marker) or { return '' }
|
||||
rest := file[idx + marker.len..]
|
||||
// Strip .v or .c.v suffix
|
||||
feature := if rest.ends_with('.c.v') {
|
||||
rest[..rest.len - 4]
|
||||
} else if rest.ends_with('.v') {
|
||||
rest[..rest.len - 2]
|
||||
} else {
|
||||
rest
|
||||
}
|
||||
return feature
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module builder
|
||||
|
||||
#include <dirent.h>
|
||||
|
||||
fn C.opendir(path &char) &C.DIR
|
||||
fn C.readdir(dir &C.DIR) &C.dirent
|
||||
fn C.closedir(dir &C.DIR) int
|
||||
|
||||
// list_dir_entries_nix returns one-level directory entries without using
|
||||
// result-or blocks, so SSA/C bootstrap does not depend on option guard lowering.
|
||||
@[manualfree]
|
||||
fn list_dir_entries_nix(path string) []string {
|
||||
if path.len == 0 {
|
||||
return []string{}
|
||||
}
|
||||
mut res := []string{cap: 50}
|
||||
dir := unsafe { C.opendir(path.str) }
|
||||
if isnil(dir) {
|
||||
return res
|
||||
}
|
||||
for {
|
||||
ent := C.readdir(dir)
|
||||
if isnil(ent) {
|
||||
break
|
||||
}
|
||||
unsafe {
|
||||
bptr := &u8(&ent.d_name[0])
|
||||
if bptr[0] == 0 || (bptr[0] == `.` && bptr[1] == 0)
|
||||
|| (bptr[0] == `.` && bptr[1] == `.` && bptr[2] == 0) {
|
||||
continue
|
||||
}
|
||||
res << tos_clone(bptr)
|
||||
}
|
||||
}
|
||||
C.closedir(dir)
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// vtest build: macos
|
||||
module builder
|
||||
|
||||
import os
|
||||
import v2.ast
|
||||
import v2.pref
|
||||
|
||||
fn test_get_v_files_from_dir_uses_target_os() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_target_os_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
os.write_file(os.join_path(tmp_dir, 'common.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'platform_linux.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'platform_macos.v'), 'module test') or { panic(err) }
|
||||
|
||||
macos_files := get_v_files_from_dir(tmp_dir, []string{}, 'mac')
|
||||
macos_names := macos_files.map(os.file_name(it))
|
||||
assert 'common.v' in macos_names
|
||||
assert 'platform_macos.v' in macos_names
|
||||
assert 'platform_linux.v' !in macos_names
|
||||
|
||||
linux_files := get_v_files_from_dir(tmp_dir, []string{}, 'linux')
|
||||
linux_names := linux_files.map(os.file_name(it))
|
||||
assert 'common.v' in linux_names
|
||||
assert 'platform_linux.v' in linux_names
|
||||
assert 'platform_macos.v' !in linux_names
|
||||
}
|
||||
|
||||
fn test_get_v_files_from_dir_uses_d_and_notd_suffixes() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_define_suffix_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
os.write_file(os.join_path(tmp_dir, 'common.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'ssl_d_use_openssl.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'ssl_notd_use_openssl.v'), 'module test') or { panic(err) }
|
||||
|
||||
default_names := get_v_files_from_dir(tmp_dir, []string{}, 'mac').map(os.file_name(it))
|
||||
assert 'common.v' in default_names
|
||||
assert 'ssl_d_use_openssl.v' !in default_names
|
||||
assert 'ssl_notd_use_openssl.v' in default_names
|
||||
|
||||
openssl_names := get_v_files_from_dir(tmp_dir, ['use_openssl'], 'mac').map(os.file_name(it))
|
||||
assert 'common.v' in openssl_names
|
||||
assert 'ssl_d_use_openssl.v' in openssl_names
|
||||
assert 'ssl_notd_use_openssl.v' !in openssl_names
|
||||
}
|
||||
|
||||
fn test_get_v_files_from_dir_skips_prealloc_without_prealloc_flag() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_prealloc_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
os.write_file(os.join_path(tmp_dir, 'common.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'prealloc.c.v'), 'module test') or { panic(err) }
|
||||
|
||||
default_names := get_v_files_from_dir(tmp_dir, []string{}, 'mac').map(os.file_name(it))
|
||||
assert 'common.v' in default_names
|
||||
assert 'prealloc.c.v' !in default_names
|
||||
|
||||
prealloc_names := get_v_files_from_dir(tmp_dir, ['prealloc'], 'mac').map(os.file_name(it))
|
||||
assert 'common.v' in prealloc_names
|
||||
assert 'prealloc.c.v' in prealloc_names
|
||||
}
|
||||
|
||||
fn test_get_v_files_from_dir_returns_lexically_sorted_files() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_sorted_v_files_${os.getpid()}')
|
||||
os.mkdir_all(tmp_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
os.write_file(os.join_path(tmp_dir, 'foo.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'foo.c.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'bar.v'), 'module test') or { panic(err) }
|
||||
os.write_file(os.join_path(tmp_dir, 'ignore.txt'), 'not v') or { panic(err) }
|
||||
|
||||
names := get_v_files_from_dir(tmp_dir, []string{}, 'linux').map(os.file_name(it))
|
||||
assert names == ['bar.v', 'foo.c.v', 'foo.v']
|
||||
assert names.index('foo.c.v') or { -1 } < names.index('foo.v') or { -1 }
|
||||
}
|
||||
|
||||
fn test_flag_helpers_use_target_os() {
|
||||
assert flag_os_matches('macos', 'mac')
|
||||
assert flag_os_matches('bsd', 'mac')
|
||||
assert flag_os_matches('bsd', 'freebsd')
|
||||
assert !flag_os_matches('linux', 'mac')
|
||||
assert comptime_cond_matches('macos', 'mac')
|
||||
assert comptime_cond_matches('linux || bsd', 'mac')
|
||||
assert !comptime_cond_matches('linux', 'mac')
|
||||
assert comptime_cond_matches('macos && !linux', 'mac')
|
||||
macos_flag := parse_flag_directive_line('#flag macos -framework Cocoa', '/tmp/source.v', 'mac') or {
|
||||
''
|
||||
}
|
||||
linux_flag := parse_flag_directive_line('#flag linux -lm', '/tmp/source.v', 'mac') or { '' }
|
||||
assert macos_flag == '-framework Cocoa'
|
||||
assert linux_flag == ''
|
||||
}
|
||||
|
||||
fn test_flag_helpers_expand_when_first_existing() {
|
||||
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_when_first_${os.getpid()}')
|
||||
existing_dir := os.join_path(tmp_dir, 'openssl_include')
|
||||
os.mkdir_all(existing_dir) or { panic(err) }
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {}
|
||||
}
|
||||
missing_dir := os.join_path(tmp_dir, 'missing')
|
||||
flag := parse_flag_directive_line("#flag darwin -I\$when_first_existing('${missing_dir}','${existing_dir}')", os.join_path(tmp_dir,
|
||||
'source.v'), 'mac') or { '' }
|
||||
assert flag == '-I${existing_dir}'
|
||||
none_flag := parse_flag_directive_line("#flag darwin -I\$when_first_existing('${missing_dir}')", os.join_path(tmp_dir,
|
||||
'source.v'), 'mac') or { '' }
|
||||
assert none_flag == ''
|
||||
}
|
||||
|
||||
fn test_split_compile_and_link_flags_moves_c_sources_to_link_step() {
|
||||
compile_flags, link_flags :=
|
||||
split_compile_and_link_flags('-I /tmp/include -DMYFLAG thirdparty/sqlite/sqlite3.c -L/tmp/lib -lsqlite3 foo.o')
|
||||
assert compile_flags == '-I /tmp/include -DMYFLAG'
|
||||
assert link_flags == 'thirdparty/sqlite/sqlite3.c -L/tmp/lib -lsqlite3 foo.o'
|
||||
}
|
||||
|
||||
fn test_sanitize_header_source_preserves_public_module_storage() {
|
||||
source := 'module state
|
||||
|
||||
pub __global (
|
||||
mut count int
|
||||
pub mut shared int
|
||||
)
|
||||
|
||||
pub __global mut total int
|
||||
'
|
||||
sanitized := sanitize_header_source(source, map[string]string{})
|
||||
assert sanitized.contains('pub __global (')
|
||||
assert sanitized.contains('mut count int')
|
||||
assert sanitized.contains('pub mut shared int')
|
||||
assert sanitized.contains('pub __global mut total int')
|
||||
}
|
||||
|
||||
fn test_build_module_header_ast_preserves_module_storage_flags() {
|
||||
mut prefs := pref.new_preferences()
|
||||
mut b := new_builder(&prefs)
|
||||
source_file := ast.File{
|
||||
mod: 'state'
|
||||
name: 'state.v'
|
||||
stmts: [
|
||||
ast.Stmt(ast.ModuleStmt{
|
||||
name: 'state'
|
||||
}),
|
||||
ast.Stmt(ast.GlobalDecl{
|
||||
is_public: true
|
||||
fields: [
|
||||
ast.FieldDecl{
|
||||
name: 'private_count'
|
||||
typ: ast.Ident{
|
||||
name: 'int'
|
||||
}
|
||||
is_mut: true
|
||||
is_public: false
|
||||
},
|
||||
ast.FieldDecl{
|
||||
name: 'shared_count'
|
||||
typ: ast.Ident{
|
||||
name: 'int'
|
||||
}
|
||||
is_mut: true
|
||||
is_public: true
|
||||
},
|
||||
]
|
||||
}),
|
||||
]
|
||||
}
|
||||
header := b.build_module_header_ast([source_file], 'state') or {
|
||||
panic('missing module storage header')
|
||||
}
|
||||
mut found := false
|
||||
for stmt in header.stmts {
|
||||
if stmt is ast.GlobalDecl {
|
||||
found = true
|
||||
assert stmt.is_public
|
||||
assert stmt.fields.len == 2
|
||||
assert stmt.fields[0].name == 'private_count'
|
||||
assert stmt.fields[0].is_mut
|
||||
assert !stmt.fields[0].is_public
|
||||
assert stmt.fields[1].name == 'shared_count'
|
||||
assert stmt.fields[1].is_mut
|
||||
assert stmt.fields[1].is_public
|
||||
}
|
||||
}
|
||||
assert found
|
||||
}
|
||||
|
||||
fn test_cflags_need_objc_mode_only_for_objc_inputs() {
|
||||
assert cflags_need_objc_mode('-framework Cocoa')
|
||||
assert cflags_need_objc_mode('/tmp/clipboard_darwin.m -framework Foundation')
|
||||
assert !cflags_need_objc_mode('-I /tmp/include -lssl -lcrypto /tmp/sqlite3.c')
|
||||
assert !cflags_need_objc_mode('-I /tmp/project.m/include')
|
||||
}
|
||||
|
||||
fn test_file_has_incompatible_os_suffix_windows() {
|
||||
assert pref.file_has_incompatible_os_suffix('time_solaris.c.v', 'windows')
|
||||
assert pref.file_has_incompatible_os_suffix('time_freebsd.c.v', 'windows')
|
||||
assert pref.file_has_incompatible_os_suffix('time_openbsd.c.v', 'windows')
|
||||
assert pref.file_has_incompatible_os_suffix('time_netbsd.c.v', 'windows')
|
||||
assert pref.file_has_incompatible_os_suffix('time_dragonfly.c.v', 'windows')
|
||||
assert pref.file_has_incompatible_os_suffix('time_nix.c.v', 'windows')
|
||||
assert !pref.file_has_incompatible_os_suffix('time_windows.c.v', 'windows')
|
||||
}
|
||||
|
||||
fn test_file_has_incompatible_os_suffix_non_windows_targets() {
|
||||
assert !pref.file_has_incompatible_os_suffix('time_solaris.c.v', 'solaris')
|
||||
assert !pref.file_has_incompatible_os_suffix('time_freebsd.c.v', 'freebsd')
|
||||
assert !pref.file_has_incompatible_os_suffix('time_bsd.c.v', 'macos')
|
||||
assert pref.file_has_incompatible_os_suffix('time_bsd.c.v', 'linux')
|
||||
assert !pref.file_has_incompatible_os_suffix('time_darwin.c.v', 'macos')
|
||||
assert !pref.file_has_incompatible_os_suffix('time_macos.c.v', 'darwin')
|
||||
assert !pref.file_has_incompatible_os_suffix('time_android_outside_termux.c.v', 'android')
|
||||
assert pref.file_has_incompatible_os_suffix('time_windows.c.v', 'linux')
|
||||
assert pref.file_has_incompatible_os_suffix('time_android_outside_termux.c.v', 'linux')
|
||||
}
|
||||
|
||||
fn test_default_cc_uses_tcc_when_available() {
|
||||
vroot := os.dir(os.dir(@FILE))
|
||||
tcc_path := os.join_path(vroot, 'thirdparty', 'tcc', 'tcc.exe')
|
||||
cc := default_cc(vroot)
|
||||
if os.exists(tcc_path) {
|
||||
assert cc.contains('tcc')
|
||||
} else {
|
||||
assert cc == 'cc'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_cc_recompile_flags_from_cmd_keeps_include_paths() {
|
||||
cmd := 'cc -I /tmp/include -I/tmp/other -Dfoo=1 -x objective-c -std=gnu11 -fwrapv "/tmp/main.c" -x none "/tmp/lib.o" -L/tmp/lib -lm -o "app"'
|
||||
flags := cc_recompile_flags_from_cmd(cmd)
|
||||
assert flags.contains('-I /tmp/include')
|
||||
assert flags.contains('-I/tmp/other')
|
||||
assert flags.contains('-Dfoo=1')
|
||||
assert flags.contains('-x objective-c')
|
||||
assert flags.contains('-std=gnu11')
|
||||
assert flags.contains('-fwrapv')
|
||||
assert !flags.contains('/tmp/lib.o')
|
||||
assert !flags.contains('-L/tmp/lib')
|
||||
assert !flags.contains('-lm')
|
||||
assert !flags.contains('-o')
|
||||
}
|
||||
Reference in new issue
Block a user