cgen: move closure C code to V code under vlib/builtin/closure/ (#24912)

This commit is contained in:
kbkpbot 2025-07-27 22:44:06 +08:00 committed by GitHub
parent a08ea74167
commit 2d87ac4837
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 719 additions and 311 deletions

278
bench/bench_closure.v Normal file
View file

@ -0,0 +1,278 @@
module main
import time
import sync
import os
import runtime
import v.util.version
// Define closure type alias
type ClosureFN = fn () int
// Test closures with different capture sizes
fn create_closure_small() ClosureFN {
a := 0
return fn [a] () int {
return a
}
}
fn create_closure_medium() ClosureFN {
a, b, c, d := 1, 2, 3, 4
return fn [a, b, c, d] () int {
return a + b - c * d
}
}
struct LargeData {
array [10]int
}
fn create_closure_large() ClosureFN {
data := LargeData{
array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]!
}
return fn [data] () int {
mut sum := 0
for i in 0 .. 10 {
sum += data.array[i]
}
return sum
}
}
// Result structs
struct TestResult {
test_name string
iterations int
time_ms i64
ops_per_sec f64 // Operations per second
notes string
}
struct MemoryResult {
test_name string
count int
start_mem_kb int
end_mem_kb int
delta_kb int
bytes_per_closure int
check_sum int
}
// Benchmark group - returns result structs
fn benchmark_closure_creation(iterations int) []TestResult {
mut results := []TestResult{}
// Test small closure creation
mut start := time.ticks()
for _ in 0 .. iterations {
_ = create_closure_small()
}
small_time := time.ticks() - start
mut ops_per_sec := f64(iterations) * 1000.0 / f64(small_time)
results << TestResult{'Small Closure Creation', iterations, small_time, ops_per_sec, ''}
// Test medium closure creation
start = time.ticks()
for _ in 0 .. iterations {
_ = create_closure_medium()
}
medium_time := time.ticks() - start
ops_per_sec = f64(iterations) * 1000.0 / f64(medium_time)
results << TestResult{'Medium Closure Creation', iterations, medium_time, ops_per_sec, ''}
// Test large closure creation
large_iter := iterations / 10
start = time.ticks()
for _ in 0 .. large_iter {
_ = create_closure_large()
}
large_time := time.ticks() - start
ops_per_sec = f64(large_iter) * 1000.0 / f64(large_time)
results << TestResult{'Large Closure Creation', large_iter, large_time, ops_per_sec, ''} //, "Equivalent iterations: ${iterations/10}"}
return results
}
fn benchmark_closure_call(iterations int) []TestResult {
mut results := []TestResult{}
closure_small := create_closure_small()
closure_medium := create_closure_medium()
closure_large := create_closure_large()
// Test small closure call
mut start := time.ticks()
for _ in 0 .. iterations {
_ = closure_small()
}
small_time := time.ticks() - start
mut ops_per_sec := f64(iterations) * 1000.0 / f64(small_time)
results << TestResult{'Small Closure Call', iterations, small_time, ops_per_sec, ''}
// Test medium closure call
start = time.ticks()
for _ in 0 .. iterations {
_ = closure_medium()
}
medium_time := time.ticks() - start
ops_per_sec = f64(iterations) * 1000.0 / f64(medium_time)
results << TestResult{'Medium Closure Call', iterations, medium_time, ops_per_sec, ''}
// Test large closure call
large_iter := iterations / 10
start = time.ticks()
for _ in 0 .. large_iter {
_ = closure_large()
}
large_time := time.ticks() - start
ops_per_sec = f64(large_iter) * 1000.0 / f64(large_time)
results << TestResult{'Large Closure Call', large_iter, large_time, ops_per_sec, ''}
return results
}
fn benchmark_threaded_creation(threads int, iterations_per_thread int) TestResult {
total_iterations := threads * iterations_per_thread
mut wg := sync.new_waitgroup()
wg.add(threads)
start := time.ticks()
for _ in 0 .. threads {
go fn [mut wg, iterations_per_thread] () {
defer { wg.done() }
for _ in 0 .. iterations_per_thread {
_ = create_closure_medium()
}
}()
}
wg.wait()
elapsed := time.ticks() - start
ops_per_sec := f64(total_iterations) * 1000.0 / f64(elapsed)
return TestResult{
test_name: 'Multi-threaded Creation'
iterations: total_iterations
time_ms: elapsed
ops_per_sec: ops_per_sec
notes: 'Threads: ${threads} Iterations per thread: ${iterations_per_thread}'
}
}
fn baseline_call_performance(iterations int) TestResult {
start := time.ticks()
for _ in 0 .. iterations {
_ = normal_function()
}
elapsed := time.ticks() - start
ops_per_sec := f64(iterations) * 1000.0 / f64(elapsed)
return TestResult{
test_name: 'Normal Function Call'
iterations: iterations
time_ms: elapsed
ops_per_sec: ops_per_sec
notes: 'Baseline'
}
}
fn benchmark_memory_usage(count int) MemoryResult {
mut closures := []ClosureFN{}
start_mem := runtime.used_memory() or { panic(err) }
for i in 0 .. count {
the_closure := create_closure_medium()
closures << the_closure
if i % 1000 == 0 {
_ = the_closure()
}
}
end_mem := runtime.used_memory() or { panic(err) }
delta := int(end_mem) - int(start_mem)
bytes_per_closure := delta / count
// Calculate verification sum
mut check_sum := 0
n := if closures.len < 100 { closures.len } else { 100 }
for idx in 0 .. n {
check_sum += closures[idx]()
}
return MemoryResult{
test_name: 'Closure Memory Overhead'
count: count
start_mem_kb: int(start_mem / 1024)
end_mem_kb: int(end_mem / 1024)
delta_kb: delta / 1024
bytes_per_closure: bytes_per_closure
check_sum: check_sum
}
}
fn normal_function() int {
return 42
}
// Format performance data for readability
fn format_perf(ops_per_sec f64) string {
if ops_per_sec >= 1_000_000 {
return '${ops_per_sec / 1_000_000:5.2f} Mop/s'
} else if ops_per_sec >= 1_000 {
return '${ops_per_sec / 1_000:5.2f} Kop/s'
} else {
return '${ops_per_sec:5.2f} op/s'
}
}
fn print_results_table(results []TestResult, title string) {
println('|---------------------------|------------|----------|--------------|--------------|')
for res in results {
perf_str := format_perf(res.ops_per_sec)
println('| ${res.test_name:-25} | ${res.iterations:10} | ${res.time_ms:8} | ${perf_str:-12} | ${res.notes:-12} |')
}
}
fn main() {
println('# V Language Closure Performance Benchmark Report')
// Configurable test parameters
base_iter := 100_000_000 // 100 million iterations
creation_iter := 10_000_000 // 1 million iterations
mem_count := 100_000
threads := 8
thread_iter := 125_000
// Execute tests
baseline_result := baseline_call_performance(base_iter)
creation_results := benchmark_closure_creation(creation_iter)
call_results := benchmark_closure_call(base_iter)
thread_result := benchmark_threaded_creation(threads, thread_iter)
mem_result := benchmark_memory_usage(mem_count)
// Print result tables
println('\n## 1. Closure Performance Analysis')
println('| Test Name | Iterations | Time(ms) | Ops/sec | Notes |')
print_results_table([baseline_result], '1. Performance Baseline')
print_results_table(creation_results, '2. Closure Creation Performance')
print_results_table(call_results, '3. Closure Call Performance')
print_results_table([thread_result], '4. Multi-threaded Performance')
// Print memory results
println('\n## 2. Memory Overhead Analysis')
println('| Test Name | Closure Count | Start Mem(KB) | End Mem(KB) | Delta(KB) | Bytes/Closure |')
println('|-------------------------|---------------|---------------|------------|-----------|---------------|')
println('| ${mem_result.test_name:-20} | ${mem_result.count:13} | ${mem_result.start_mem_kb:13} | ${mem_result.end_mem_kb:10} | ${mem_result.delta_kb:9} | ${mem_result.bytes_per_closure:13} |')
println('\n**Verification Sum: ${mem_result.check_sum}** (Calculated from random sample of 100 closures)')
println('\n## Test Environment')
println('- V Language Version: ${version.full_v_version(false)}')
println('- CPU Cores: ${runtime.nr_cpus()}')
println('- System Memory: ${runtime.total_memory()! / 1024 / 1024} MB')
println('- Operating System: ${os.user_os()}')
println('\n> Test Time: ${time.now().format_ss_micro()}')
}

View file

@ -23,6 +23,9 @@ fn C.realloc(a &u8, b int) &u8
fn C.free(ptr voidptr) fn C.free(ptr voidptr)
fn C.mmap(addr_length int, length isize, prot int, flags int, fd int, offset u64) voidptr
fn C.mprotect(addr_length int, len isize, prot int) int
fn C.aligned_alloc(align isize, size isize) voidptr fn C.aligned_alloc(align isize, size isize) voidptr
// windows aligned memory functions // windows aligned memory functions
@ -34,6 +37,9 @@ fn C._aligned_offset_realloc(voidptr, size isize, align isize, offset isize) voi
fn C._aligned_msize(voidptr, align isize, offset isize) isize fn C._aligned_msize(voidptr, align isize, offset isize) isize
fn C._aligned_recalloc(voidptr, num isize, size isize, align isize) voidptr fn C._aligned_recalloc(voidptr, num isize, size isize, align isize) voidptr
fn C.VirtualAlloc(voidptr, isize, u32, u32) voidptr
fn C.VirtualProtect(voidptr, isize, u32, &u32) bool
@[noreturn; trusted] @[noreturn; trusted]
fn C.exit(code int) fn C.exit(code int)
@ -529,6 +535,10 @@ fn C.abs(number int) int
fn C.GetDiskFreeSpaceExA(const_path &char, free_bytes_available_to_caller &u64, total_number_of_bytes &u64, total_number_of_free_bytes &u64) bool fn C.GetDiskFreeSpaceExA(const_path &char, free_bytes_available_to_caller &u64, total_number_of_bytes &u64, total_number_of_free_bytes &u64) bool
fn C.GetNativeSystemInfo(voidptr)
fn C.sysconf(name int) int
// C.SYSTEM_INFO contains information about the current computer system. This includes the architecture and type of the processor, the number of processors in the system, the page size, and other such information. // C.SYSTEM_INFO contains information about the current computer system. This includes the architecture and type of the processor, the number of processors in the system, the page size, and other such information.
@[typedef] @[typedef]
pub struct C.SYSTEM_INFO { pub struct C.SYSTEM_INFO {

View file

@ -0,0 +1,2 @@
The files in this directory implement the `closure` feature of the V language, which is called
internally by the V compiler.

View file

@ -0,0 +1,241 @@
@[has_globals]
module closure
// Inspired from Chris Wellons's work
// https://nullprogram.com/blog/2017/01/08/
const assumed_page_size = int(0x4000)
@[heap]
struct Closure {
ClosureMutex
mut:
closure_ptr voidptr
closure_get_data fn () voidptr = unsafe { nil }
closure_cap int
v_page_size int = int(0x4000)
}
__global g_closure = Closure{}
enum MemoryProtectAtrr {
read_exec
read_write
}
// refer to https://godbolt.org/z/r7P3EYv6c for a complete assembly
// vfmt off
pub const closure_thunk = $if amd64 {
[
u8(0xF3), 0x44, 0x0F, 0x7E, 0x3D, 0xF7, 0xBF, 0xFF, 0xFF, // movq xmm15, QWORD PTR [rip - userdata]
0xFF, 0x25, 0xF9, 0xBF, 0xFF, 0xFF // jmp QWORD PTR [rip - fn]
]
} $else $if i386 {
[
u8(0xe8), 0x00, 0x00, 0x00, 0x00, // call here
// here:
0x59, // pop ecx
0x66, 0x0F, 0x6E, 0xF9, // movd xmm7, ecx
0xff, 0xA1, 0xff, 0xbf, 0xff, 0xff, // jmp DWORD PTR [ecx - 0x4001] # <fn>
]
} $else $if arm64 {
[
u8(0x11), 0x00, 0xFE, 0x5C, // ldr d17, userdata
0x30, 0x00, 0xFE, 0x58, // ldr x16, fn
0x00, 0x02, 0x1F, 0xD6 // br x16
]
} $else $if arm32 {
[
u8(0x04), 0xC0, 0x4F, 0xE2, // adr ip, here
// here:
0x01, 0xC9, 0x4C, 0xE2, // sub ip, ip, #0x4000
0x90, 0xCA, 0x07, 0xEE, // vmov s15, ip
0x00, 0xC0, 0x9C, 0xE5, // ldr ip, [ip, 0]
0x1C, 0xFF, 0x2F, 0xE1 // bx ip
]
} $else $if rv64 {
[
u8(0x97), 0xCF, 0xFF, 0xFF, // auipc t6, 0xffffc
0x03, 0xBF, 0x8F, 0x00, // ld t5, 8(t6)
0x07, 0xB3, 0x0F, 0x00, // fld ft6, 0(t6)
0x67, 0x00, 0x0F, 0x00, // jr t5
]
} $else $if rv32 {
[
u8(0x97), 0xCF, 0xFF, 0xFF, // auipc t6, 0xffffc
0x03, 0xAF, 0x4F, 0x00, // lw t5, 4(t6)
0x07, 0xAB, 0x0F, 0x00, // flw fs6, 0(t6)
0x67, 0x00, 0x0F, 0x00 // jr t5
]
} $else $if s390x {
[
u8(0xC0), 0x70, 0xFF, 0xFF, 0xE0, 0x00, // larl %r7, -16384
0x68, 0xF0, 0x70, 0x00, // ld %f15, 0(%r7)
0xE3, 0x70, 0x70, 0x08, 0x00, 0x04, // lg %r7, 8(%r7)
0x07, 0xF7, // br %r7
]
} $else $if ppc64le {
[
u8(0xa6), 0x02, 0x08, 0x7c, // mflr %r0
0x05, 0x00, 0x00, 0x48, // bl here
0xa6, 0x02, 0xc8, 0x7d, // here: mflr %r14
0xf8, 0xbf, 0xce, 0x39, // addi %r14, %r14, -16392
0x00, 0x00, 0xce, 0xc9, // lfd %f14, 0(%r14)
0x08, 0x00, 0xce, 0xe9, // ld %r14, 8(%r14)
0xa6, 0x03, 0x08, 0x7c, // mtlr %r0
0xa6, 0x03, 0xc9, 0x7d, // mtctr %r14
0x20, 0x04, 0x80, 0x4e, // bctr
]
} $else $if loongarch64 {
[
u8(0x92), 0xFF, 0xFF, 0x1D, // pcaddu12i t6, -4
0x48, 0x02, 0x80, 0x2B, // fld.d f8, t6, 0
0x51, 0x22, 0xC0, 0x28, // ld.d t5, t6, 8
0x20, 0x02, 0x00, 0x4C, // jr t5
]
} $else {
[]u8{}
}
const closure_get_data_bytes = $if amd64 {
[
u8(0x66), 0x4C, 0x0F, 0x7E, 0xF8, // movq rax, xmm15
0xC3 // ret
]
} $else $if i386 {
[
u8(0x66), 0x0F, 0x7E, 0xF8, // movd eax, xmm7
0x8B, 0x80, 0xFB, 0xBF, 0xFF, 0xFF, // mov eax, DWORD PTR [eax - 0x4005]
0xc3 // ret
]
} $else $if arm64 {
[
u8(0x20), 0x02, 0x66, 0x9E, // fmov x0, d17
0xC0, 0x03, 0x5F, 0xD6 // ret
]
} $else $if arm32 {
[
u8(0x90), 0x0A, 0x17, 0xEE, // vmov r0, s15
0x04, 0x00, 0x10, 0xE5, // ldr r0, [r0, #-4]
0x1E, 0xFF, 0x2F, 0xE1 // bx lr
]
} $else $if rv64 {
[
u8(0x53), 0x05, 0x03, 0xE2, // fmv.x.d a0, ft6
0x67, 0x80, 0x00, 0x00, // ret
]
} $else $if rv32 {
[
u8(0x53), 0x05, 0x0B, 0xE0, // fmv.x.w a0, fs6
0x67, 0x80, 0x00, 0x00 // ret
]
} $else $if s390x {
[
u8(0xB3), 0xCD, 0x00, 0x2F, // lgdr %r2, %f15
0x07, 0xFE, // br %r14
]
} $else $if ppc64le {
[
u8(0x66), 0x00, 0xc3, 0x7d, // mfvsrd %r3, %f14
0x20, 0x00, 0x80, 0x4e, // blr
]
} $else $if loongarch64 {
[
u8(0x04), 0xB9, 0x14, 0x01, // movfr2gr.d a0, f8
0x20, 0x00, 0x00, 0x4C, // ret
]
} $else {
[]u8{}
}
// vfmt on
// equal to `max(2*sizeof(void*), sizeof(__closure_thunk))`, rounded up to the next multiple of `sizeof(void*)`
// NOTE: This is a workaround for `-usecache` bug, as it can't include `fn get_closure_size()` needed by `const closure_size` in `build-module` mode.
const closure_size_1 = if 2 * u32(sizeof(voidptr)) > u32(closure_thunk.len) {
2 * u32(sizeof(voidptr))
} else {
u32(closure_thunk.len) + u32(sizeof(voidptr)) - 1
}
const closure_size = int(closure_size_1 & ~(u32(sizeof(voidptr)) - 1))
// closure_alloc allocates executable memory pages for closures(INTERNAL COMPILER USE ONLY).
fn closure_alloc() {
p := closure_alloc_platform()
if isnil(p) {
return
}
// Setup executable and guard pages
x := unsafe { p + g_closure.v_page_size } // End of guard page
mut remaining := g_closure.v_page_size / closure_size // Calculate slot count
g_closure.closure_ptr = x // Current allocation pointer
g_closure.closure_cap = remaining // Remaining slot count
// Fill page with closure templates
for remaining > 0 {
unsafe { vmemcpy(x, closure_thunk.data, closure_thunk.len) } // Copy template
remaining--
unsafe {
x += closure_size // Move to next slot
}
}
closure_memory_protect_platform(g_closure.closure_ptr, g_closure.v_page_size, .read_exec)
}
// closure_init initializes global closure subsystem(INTERNAL COMPILER USE ONLY).
fn closure_init() {
// Determine system page size
mut page_size := get_page_size_platform()
g_closure.v_page_size = page_size // Store calculated size
// Initialize thread-safety lock
closure_mtx_lock_init_platform()
// Initial memory allocation
closure_alloc()
// Install closure handler template
unsafe {
// Temporarily enable write access to executable memory
closure_memory_protect_platform(g_closure.closure_ptr, page_size, .read_write)
// Copy closure entry stub code
vmemcpy(g_closure.closure_ptr, closure_get_data_bytes.data, closure_get_data_bytes.len)
// Re-enormalize execution protection
closure_memory_protect_platform(g_closure.closure_ptr, page_size, .read_exec)
}
// Setup global closure handler pointer
g_closure.closure_get_data = g_closure.closure_ptr
// Advance allocation pointer past header
unsafe {
g_closure.closure_ptr = &u8(g_closure.closure_ptr) + closure_size
}
g_closure.closure_cap-- // Account for header slot
}
// closure_create creates closure objects at compile-time(INTERNAL COMPILER USE ONLY).
@[direct_array_access]
fn closure_create(func voidptr, data voidptr) voidptr {
closure_mtx_lock_platform()
// Handle memory exhaustion
if g_closure.closure_cap == 0 {
closure_alloc() // Allocate new memory page
}
g_closure.closure_cap-- // Decrement slot counter
// Claim current closure slot
mut curr_closure := g_closure.closure_ptr
unsafe {
// Move to next available slot
g_closure.closure_ptr = &u8(g_closure.closure_ptr) + closure_size
// Write closure metadata (data + function pointer)
mut p := &voidptr(&u8(curr_closure) - assumed_page_size)
p[0] = data // Stored closure context
p[1] = func // Target function to execute
}
closure_mtx_unlock_platform()
// Return executable closure object
return curr_closure
}

View file

@ -0,0 +1,4 @@
module closure
// placeholder
// js gen need at least one `.v` file under the module dir.

View file

@ -0,0 +1,84 @@
module closure
$if !freestanding && !vinix {
#include <sys/mman.h>
}
@[typedef]
pub struct C.pthread_mutex_t {}
struct ClosureMutex {
closure_mtx C.pthread_mutex_t
}
@[inline]
fn closure_alloc_platform() &u8 {
mut p := &u8(unsafe { nil })
$if freestanding {
// Freestanding environments (no OS) use simple malloc
p = unsafe { malloc(g_closure.v_page_size * 2) }
if isnil(p) {
return unsafe { nil }
}
} $else {
// Main OS environments use mmap to get aligned pages
p = unsafe {
C.mmap(0, g_closure.v_page_size * 2, C.PROT_READ | C.PROT_WRITE, C.MAP_ANONYMOUS | C.MAP_PRIVATE,
-1, 0)
}
if p == &u8(C.MAP_FAILED) {
return unsafe { nil }
}
}
return p
}
@[inline]
fn closure_memory_protect_platform(ptr voidptr, size isize, attr MemoryProtectAtrr) {
$if freestanding {
// No memory protection in freestanding mode
} $else {
match attr {
.read_exec {
unsafe { C.mprotect(ptr, size, C.PROT_READ | C.PROT_EXEC) }
}
.read_write {
unsafe { C.mprotect(ptr, size, C.PROT_READ | C.PROT_WRITE) }
}
}
}
}
@[inline]
fn get_page_size_platform() int {
// Determine system page size
mut page_size := 0x4000
$if !freestanding {
// Query actual page size in OS environments
page_size = unsafe { int(C.sysconf(C._SC_PAGESIZE)) }
}
// Calculate required allocation size
page_size = page_size * (((assumed_page_size - 1) / page_size) + 1)
return page_size
}
@[inline]
fn closure_mtx_lock_init_platform() {
$if !freestanding || vinix {
C.pthread_mutex_init(&g_closure.closure_mtx, 0)
}
}
@[inline]
fn closure_mtx_lock_platform() {
$if !freestanding || vinix {
C.pthread_mutex_lock(&g_closure.closure_mtx)
}
}
@[inline]
fn closure_mtx_unlock_platform() {
$if !freestanding || vinix {
C.pthread_mutex_unlock(&g_closure.closure_mtx)
}
}

View file

@ -0,0 +1,53 @@
module closure
#include <synchapi.h>
struct ClosureMutex {
closure_mtx C.SRWLOCK
}
@[inline]
fn closure_alloc_platform() &u8 {
p := &u8(C.VirtualAlloc(0, g_closure.v_page_size * 2, C.MEM_COMMIT | C.MEM_RESERVE,
C.PAGE_READWRITE))
return p
}
@[inline]
fn closure_memory_protect_platform(ptr voidptr, size isize, attr MemoryProtectAtrr) {
mut tmp := u32(0)
match attr {
.read_exec {
_ := C.VirtualProtect(ptr, size, C.PAGE_EXECUTE_READ, &tmp)
}
.read_write {
_ := C.VirtualProtect(ptr, size, C.PAGE_READWRITE, &tmp)
}
}
}
@[inline]
fn get_page_size_platform() int {
// Determine system page size
mut si := C.SYSTEM_INFO{}
C.GetNativeSystemInfo(&si)
// Calculate required allocation size
page_size := int(si.dwPageSize) * (((assumed_page_size - 1) / int(si.dwPageSize)) + 1)
return page_size
}
@[inline]
fn closure_mtx_lock_init_platform() {
C.InitializeSRWLock(&g_closure.closure_mtx)
}
@[inline]
fn closure_mtx_lock_platform() {
C.AcquireSRWLockExclusive(&g_closure.closure_mtx)
}
@[inline]
fn closure_mtx_unlock_platform() {
C.ReleaseSRWLockExclusive(&g_closure.closure_mtx)
}

View file

@ -661,17 +661,6 @@ pub fn gen(files []&ast.File, mut table ast.Table, pref_ &pref.Preferences) GenO
if g.channel_definitions.len > 0 { if g.channel_definitions.len > 0 {
b.write_string2('\n// V channel code:\n', g.channel_definitions.str()) b.write_string2('\n// V channel code:\n', g.channel_definitions.str())
} }
if g.anon_fn_definitions.len > 0 {
if g.nr_closures > 0 {
b.writeln2('\n// V closure helpers', c_closure_helpers(g.pref))
}
/*
b.writeln('\n// V anon functions:')
for fn_def in g.anon_fn_definitions {
b.writeln(fn_def)
}
*/
}
if g.pref.is_coverage { if g.pref.is_coverage {
b.write_string2('\n// V coverage:\n', g.cov_declarations.str()) b.write_string2('\n// V coverage:\n', g.cov_declarations.str())
} }
@ -687,21 +676,6 @@ pub fn gen(files []&ast.File, mut table ast.Table, pref_ &pref.Preferences) GenO
if g.embedded_data.len > 0 { if g.embedded_data.len > 0 {
helpers.write_string2('\n// V embedded data:\n', g.embedded_data.str()) helpers.write_string2('\n// V embedded data:\n', g.embedded_data.str())
} }
if g.anon_fn_definitions.len > 0 {
if g.nr_closures > 0 {
helpers.writeln2('\n// V closure helpers', c_closure_fn_helpers(g.pref))
}
/*
b.writeln('\n// V anon functions:')
for fn_def in g.anon_fn_definitions {
b.writeln(fn_def)
}
*/
if g.pref.parallel_cc {
g.extern_out.writeln('extern void* __closure_create(void* fn, void* data);')
g.extern_out.writeln('extern void __closure_init();')
}
}
if g.pref.parallel_cc { if g.pref.parallel_cc {
helpers.writeln('\n// V global/const non-precomputed definitions:') helpers.writeln('\n// V global/const non-precomputed definitions:')
for var_name in g.sorted_global_const_names { for var_name in g.sorted_global_const_names {
@ -4367,7 +4341,7 @@ fn (mut g Gen) selector_expr(node ast.SelectorExpr) {
g.gen_closure_fn(expr_styp, m, name) g.gen_closure_fn(expr_styp, m, name)
} }
} }
g.write('__closure_create(${name}, ') g.write('builtin__closure__closure_create(${name}, ')
if !receiver.typ.is_ptr() { if !receiver.typ.is_ptr() {
g.write('memdup_uncollectable(') g.write('memdup_uncollectable(')
} }
@ -4523,7 +4497,7 @@ fn (mut g Gen) gen_closure_fn(expr_styp string, m ast.Fn, name string) {
g.extern_out.writeln(';') g.extern_out.writeln(';')
} }
sb.writeln(' {') sb.writeln(' {')
sb.writeln('\t${data_styp}* a0 = __CLOSURE_GET_DATA();') sb.writeln('\t${data_styp}* a0 = g_closure.closure_get_data();')
if m.return_type != ast.void_type { if m.return_type != ast.void_type {
sb.write_string('\treturn ') sb.write_string('\treturn ')
} else { } else {
@ -6535,10 +6509,6 @@ fn (mut g Gen) write_init_function() {
g.writeln('\tbuiltin_init();') g.writeln('\tbuiltin_init();')
} }
if g.nr_closures > 0 {
g.writeln('\t_closure_mtx_init();')
}
// reflection bootstrapping // reflection bootstrapping
if g.has_reflection { if g.has_reflection {
if var := g.global_const_defs['g_reflection'] { if var := g.global_const_defs['g_reflection'] {
@ -6597,6 +6567,10 @@ fn (mut g Gen) write_init_function() {
} }
} }
if g.nr_closures > 0 {
g.writeln('\tbuiltin__closure__closure_init();')
}
g.writeln('}') g.writeln('}')
if g.pref.printfn_list.len > 0 && '_vinit' in g.pref.printfn_list { if g.pref.printfn_list.len > 0 && '_vinit' in g.pref.printfn_list {
println(g.out.after(fn_vinit_start_pos)) println(g.out.after(fn_vinit_start_pos))
@ -6644,7 +6618,7 @@ fn (mut g Gen) write_init_function() {
g.writeln('void _vinit_caller() {') g.writeln('void _vinit_caller() {')
g.writeln('\tstatic bool once = false; if (once) {return;} once = true;') g.writeln('\tstatic bool once = false; if (once) {return;} once = true;')
if g.nr_closures > 0 { if g.nr_closures > 0 {
g.writeln('\t__closure_init(); // vinit_caller()') g.writeln('\tbuiltin__closure__closure_init(); // vinit_caller()')
} }
g.writeln('\t_vinit(0,0);') g.writeln('\t_vinit(0,0);')
g.writeln('}') g.writeln('}')

View file

@ -1,8 +1,5 @@
module c module c
import strings
import v.pref
// Note: @@@ here serve as placeholders. // Note: @@@ here serve as placeholders.
// They will be replaced with correct strings // They will be replaced with correct strings
// for each constant, during C code generation. // for each constant, during C code generation.
@ -51,264 +48,6 @@ static inline void __sort_ptr(uintptr_t a[], bool b[], int l) {
} }
' '
// Inspired from Chris Wellons's work
// https://nullprogram.com/blog/2017/01/08/
fn c_closure_helpers(pref_ &pref.Preferences) string {
mut builder := strings.new_builder(2048)
if pref_.os != .windows && pref_.is_bare == false {
builder.writeln('#include <sys/mman.h>')
}
builder.write_string('
#ifdef _MSC_VER
#define __RETURN_ADDRESS() ((char*)_ReturnAddress())
#elif defined(__TINYC__) && defined(_WIN32)
#define __RETURN_ADDRESS() ((char*)__builtin_return_address(0))
#else
#define __RETURN_ADDRESS() ((char*)__builtin_extract_return_addr(__builtin_return_address(0)))
#endif
static int _V_page_size = 0x4000; // 16K
#define ASSUMED_PAGE_SIZE 0x4000
#define _CLOSURE_SIZE (((2*sizeof(void*) > sizeof(__closure_thunk) ? 2*sizeof(void*) : sizeof(__closure_thunk)) + sizeof(void*) - 1) & ~(sizeof(void*) - 1))
// equal to `max(2*sizeof(void*), sizeof(__closure_thunk))`, rounded up to the next multiple of `sizeof(void*)`
// refer to https://godbolt.org/z/r7P3EYv6c for a complete assembly
#ifdef __V_amd64
static const char __closure_thunk[] = {
0xF3, 0x44, 0x0F, 0x7E, 0x3D, 0xF7, 0xBF, 0xFF, 0xFF, // movq xmm15, QWORD PTR [rip - userdata]
0xFF, 0x25, 0xF9, 0xBF, 0xFF, 0xFF // jmp QWORD PTR [rip - fn]
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x66, 0x4C, 0x0F, 0x7E, 0xF8, // movq rax, xmm15
0xC3 // ret
};
#elif defined(__V_x86)
static char __closure_thunk[] = {
0xe8, 0x00, 0x00, 0x00, 0x00, // call here
// here:
0x59, // pop ecx
0x66, 0x0F, 0x6E, 0xF9, // movd xmm7, ecx
0xff, 0xA1, 0xff, 0xbf, 0xff, 0xff, // jmp DWORD PTR [ecx - 0x4001] # <fn>
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x66, 0x0F, 0x7E, 0xF8, // movd eax, xmm7
0x8B, 0x80, 0xFB, 0xBF, 0xFF, 0xFF, // mov eax, DWORD PTR [eax - 0x4005]
0xc3 // ret
};
#elif defined(__V_arm64)
static char __closure_thunk[] = {
0x11, 0x00, 0xFE, 0x5C, // ldr d17, userdata
0x30, 0x00, 0xFE, 0x58, // ldr x16, fn
0x00, 0x02, 0x1F, 0xD6 // br x16
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x20, 0x02, 0x66, 0x9E, // fmov x0, d17
0xC0, 0x03, 0x5F, 0xD6 // ret
};
#elif defined(__V_arm32)
static char __closure_thunk[] = {
0x04, 0xC0, 0x4F, 0xE2, // adr ip, here
// here:
0x01, 0xC9, 0x4C, 0xE2, // sub ip, ip, #0x4000
0x90, 0xCA, 0x07, 0xEE, // vmov s15, ip
0x00, 0xC0, 0x9C, 0xE5, // ldr ip, [ip, 0]
0x1C, 0xFF, 0x2F, 0xE1 // bx ip
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x90, 0x0A, 0x17, 0xEE, // vmov r0, s15
0x04, 0x00, 0x10, 0xE5, // ldr r0, [r0, #-4]
0x1E, 0xFF, 0x2F, 0xE1 // bx lr
};
#elif defined (__V_rv64)
static char __closure_thunk[] = {
0x97, 0xCF, 0xFF, 0xFF, // auipc t6, 0xffffc
0x03, 0xBF, 0x8F, 0x00, // ld t5, 8(t6)
0x07, 0xB3, 0x0F, 0x00, // fld ft6, 0(t6)
0x67, 0x00, 0x0F, 0x00, // jr t5
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x53, 0x05, 0x03, 0xE2, // fmv.x.d a0, ft6
0x67, 0x80, 0x00, 0x00, // ret
};
#elif defined (__V_rv32)
static char __closure_thunk[] = {
0x97, 0xCF, 0xFF, 0xFF, // auipc t6, 0xffffc
0x03, 0xAF, 0x4F, 0x00, // lw t5, 4(t6)
0x07, 0xAB, 0x0F, 0x00, // flw fs6, 0(t6)
0x67, 0x00, 0x0F, 0x00 // jr t5
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x53, 0x05, 0x0B, 0xE0, // fmv.x.w a0, fs6
0x67, 0x80, 0x00, 0x00 // ret
};
#elif defined (__V_s390x)
static char __closure_thunk[] = {
0xC0, 0x70, 0xFF, 0xFF, 0xE0, 0x00, // larl %r7, -16384
0x68, 0xF0, 0x70, 0x00, // ld %f15, 0(%r7)
0xE3, 0x70, 0x70, 0x08, 0x00, 0x04, // lg %r7, 8(%r7)
0x07, 0xF7, // br %r7
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0xB3, 0xCD, 0x00, 0x2F, // lgdr %r2, %f15
0x07, 0xFE, // br %r14
};
#elif defined (__V_ppc64le)
static char __closure_thunk[] = {
0xa6, 0x02, 0x08, 0x7c, // mflr %r0
0x05, 0x00, 0x00, 0x48, // bl here
0xa6, 0x02, 0xc8, 0x7d, // here: mflr %r14
0xf8, 0xbf, 0xce, 0x39, // addi %r14, %r14, -16392
0x00, 0x00, 0xce, 0xc9, // lfd %f14, 0(%r14)
0x08, 0x00, 0xce, 0xe9, // ld %r14, 8(%r14)
0xa6, 0x03, 0x08, 0x7c, // mtlr %r0
0xa6, 0x03, 0xc9, 0x7d, // mtctr %r14
0x20, 0x04, 0x80, 0x4e, // bctr
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x66, 0x00, 0xc3, 0x7d, // mfvsrd %r3, %f14
0x20, 0x00, 0x80, 0x4e, // blr
};
#elif defined (__V_loongarch64)
static char __closure_thunk[] = {
0x92, 0xFF, 0xFF, 0x1D, // pcaddu12i t6, -4
0x48, 0x02, 0x80, 0x2B, // fld.d f8, t6, 0
0x51, 0x22, 0xC0, 0x28, // ld.d t5, t6, 8
0x20, 0x02, 0x00, 0x4C, // jr t5
};
static char __CLOSURE_GET_DATA_BYTES[] = {
0x04, 0xB9, 0x14, 0x01, // movfr2gr.d a0, f8
0x20, 0x00, 0x00, 0x4C, // ret
};
#endif
static void*(*__CLOSURE_GET_DATA)(void) = 0;
static inline void __closure_set_data(char* closure, void* data) {
void** p = (void**)(closure - ASSUMED_PAGE_SIZE);
p[0] = data;
}
static inline void __closure_set_function(char* closure, void* f) {
void** p = (void**)(closure - ASSUMED_PAGE_SIZE);
p[1] = f;
}
#ifdef _WIN32
#include <synchapi.h>
static SRWLOCK _closure_mtx;
#define _closure_mtx_init() InitializeSRWLock(&_closure_mtx)
#define _closure_mtx_lock() AcquireSRWLockExclusive(&_closure_mtx)
#define _closure_mtx_unlock() ReleaseSRWLockExclusive(&_closure_mtx)
#elif defined(_VFREESTANDING)
#define _closure_mtx_init()
#define _closure_mtx_lock()
#define _closure_mtx_unlock()
#else
static pthread_mutex_t _closure_mtx;
#define _closure_mtx_init() pthread_mutex_init(&_closure_mtx, 0)
#define _closure_mtx_lock() pthread_mutex_lock(&_closure_mtx)
#define _closure_mtx_unlock() pthread_mutex_unlock(&_closure_mtx)
#endif
')
return builder.str()
}
fn c_closure_fn_helpers(pref_ &pref.Preferences) string {
static_non_parallel := if pref_.parallel_cc { '' } else { 'static ' }
mut builder := strings.new_builder(2048)
builder.write_string('
${static_non_parallel}char* _closure_ptr = 0;
${static_non_parallel}int _closure_cap = 0;
static void __closure_alloc(void) {
#ifdef _WIN32
char* p = VirtualAlloc(NULL, _V_page_size * 2, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (p == NULL) return;
#elif defined(_VFREESTANDING)
char *p = malloc(_V_page_size * 2);
if (p == NULL) return;
#else
char* p = mmap(0, _V_page_size * 2, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (p == MAP_FAILED) return;
#endif
char* x = p + _V_page_size;
int remaining = _V_page_size / _CLOSURE_SIZE;
_closure_ptr = x;
_closure_cap = remaining;
while (remaining > 0) {
memcpy(x, __closure_thunk, sizeof(__closure_thunk));
remaining--;
x += _CLOSURE_SIZE;
}
#ifdef _WIN32
DWORD _tmp;
VirtualProtect(_closure_ptr, _V_page_size, PAGE_EXECUTE_READ, &_tmp);
#elif defined(_VFREESTANDING)
#else
mprotect(_closure_ptr, _V_page_size, PROT_READ | PROT_EXEC);
#endif
}
#ifdef _WIN32
void __closure_init() {
SYSTEM_INFO si;
GetNativeSystemInfo(&si);
uint32_t page_size = si.dwPageSize * (((ASSUMED_PAGE_SIZE - 1) / si.dwPageSize) + 1);
_V_page_size = page_size;
__closure_alloc();
DWORD _tmp;
VirtualProtect(_closure_ptr, page_size, PAGE_READWRITE, &_tmp);
memcpy(_closure_ptr, __CLOSURE_GET_DATA_BYTES, sizeof(__CLOSURE_GET_DATA_BYTES));
VirtualProtect(_closure_ptr, page_size, PAGE_EXECUTE_READ, &_tmp);
__CLOSURE_GET_DATA = (void*)_closure_ptr;
_closure_ptr += _CLOSURE_SIZE;
_closure_cap--;
}
#else
${static_non_parallel}void __closure_init() {
#ifndef _VFREESTANDING
uint32_t page_size = sysconf(_SC_PAGESIZE);
#else
uint32_t page_size = 0x4000;
#endif
page_size = page_size * (((ASSUMED_PAGE_SIZE - 1) / page_size) + 1);
_V_page_size = page_size;
__closure_alloc();
#ifndef _VFREESTANDING
mprotect(_closure_ptr, page_size, PROT_READ | PROT_WRITE);
#endif
memcpy(_closure_ptr, __CLOSURE_GET_DATA_BYTES, sizeof(__CLOSURE_GET_DATA_BYTES));
#ifndef _VFREESTANDING
mprotect(_closure_ptr, page_size, PROT_READ | PROT_EXEC);
#endif
__CLOSURE_GET_DATA = (void*)_closure_ptr;
_closure_ptr += _CLOSURE_SIZE;
_closure_cap--;
}
#endif
${static_non_parallel}void* __closure_create(void* fn, void* data) {
_closure_mtx_lock();
if (_closure_cap == 0) {
__closure_alloc();
}
_closure_cap--;
void* closure = _closure_ptr;
_closure_ptr += _CLOSURE_SIZE;
__closure_set_data(closure, data);
__closure_set_function(closure, fn);
_closure_mtx_unlock();
return closure;
}
')
return builder.str()
}
const c_common_macros = ' const c_common_macros = '
#define EMPTY_VARG_INITIALIZATION 0 #define EMPTY_VARG_INITIALIZATION 0
#define EMPTY_STRUCT_DECLARATION #define EMPTY_STRUCT_DECLARATION

View file

@ -142,9 +142,6 @@ fn (mut g Gen) gen_c_main_function_header() {
g.writeln('\tg_main_argv = ___argv;') g.writeln('\tg_main_argv = ___argv;')
} }
} }
if g.nr_closures > 0 {
g.writeln('\t__closure_init(); // main()')
}
} }
fn (mut g Gen) gen_c_main_header() { fn (mut g Gen) gen_c_main_header() {
@ -208,10 +205,6 @@ sapp_desc sokol_main(int argc, char* argv[]) {
(void)argc; (void)argv;') (void)argc; (void)argv;')
g.gen_c_main_trace_calls_hook() g.gen_c_main_trace_calls_hook()
if g.nr_closures > 0 {
g.writeln('\t__closure_init(); // main()')
}
if g.pref.gc_mode in [.boehm_full, .boehm_incr, .boehm_full_opt, .boehm_incr_opt, .boehm_leak] { if g.pref.gc_mode in [.boehm_full, .boehm_incr, .boehm_full_opt, .boehm_incr_opt, .boehm_leak] {
g.writeln('#if defined(_VGCBOEHM)') g.writeln('#if defined(_VGCBOEHM)')
if g.pref.gc_mode == .boehm_leak { if g.pref.gc_mode == .boehm_leak {

View file

@ -419,7 +419,7 @@ fn (mut g Gen) gen_fn_decl(node &ast.FnDecl, skip bool) {
} }
g.writeln(') {') g.writeln(') {')
if is_closure { if is_closure {
g.writeln('${cur_closure_ctx}* ${closure_ctx} = __CLOSURE_GET_DATA();') g.writeln('${cur_closure_ctx}* ${closure_ctx} = g_closure.closure_get_data();')
} }
for i, is_promoted in heap_promoted { for i, is_promoted in heap_promoted {
if is_promoted { if is_promoted {
@ -620,8 +620,8 @@ fn (mut g Gen) gen_anon_fn(mut node ast.AnonFn) {
} }
ctx_struct := g.closure_ctx(node.decl) ctx_struct := g.closure_ctx(node.decl)
// it may be possible to optimize `memdup` out if the closure never leaves current scope // it may be possible to optimize `memdup` out if the closure never leaves current scope
// TODO: in case of an assignment, this should only call "__closure_set_data" and "__closure_set_function" (and free the former data) // TODO: in case of an assignment, this should only call "closure_set_data" and "closure_set_function" (and free the former data)
g.write('__closure_create(${fn_name}, (${ctx_struct}*) memdup_uncollectable(&(${ctx_struct}){') g.write('builtin__closure__closure_create(${fn_name}, (${ctx_struct}*) memdup_uncollectable(&(${ctx_struct}){')
g.indent++ g.indent++
for var in node.inherited_vars { for var in node.inherited_vars {
mut has_inherited := false mut has_inherited := false

View file

@ -184,6 +184,9 @@ pub fn mark_used(mut table ast.Table, mut pref_ pref.Preferences, ast_files []&a
} }
if table.used_features.anon_fn { if table.used_features.anon_fn {
core_fns << 'memdup_uncollectable' core_fns << 'memdup_uncollectable'
core_fns << 'builtin.closure.closure_alloc'
core_fns << 'builtin.closure.closure_init'
core_fns << 'builtin.closure.closure_create'
} }
if table.used_features.arr_map { if table.used_features.arr_map {
include_panic_deps = true include_panic_deps = true

View file

@ -476,6 +476,9 @@ fn (mut p Parser) check_expr(precedence int) !ast.Expr {
} else { } else {
// Anonymous function // Anonymous function
node = p.anon_fn() node = p.anon_fn()
if p.file_backend_mode == .v || p.file_backend_mode == .c {
p.register_auto_import('builtin.closure')
}
// its a call // its a call
// NOTE: this could be moved to just before the pratt loop // NOTE: this could be moved to just before the pratt loop
// then anything can be a call, eg. `index[2]()` or `struct.field()` // then anything can be a call, eg. `index[2]()` or `struct.field()`

View file

@ -2007,6 +2007,9 @@ fn (mut p Parser) dot_expr(left ast.Expr) ast.Expr {
} }
is_filter := field_name in ['filter', 'map', 'any', 'all', 'count'] is_filter := field_name in ['filter', 'map', 'any', 'all', 'count']
if is_filter || field_name == 'sort' || field_name == 'sorted' { if is_filter || field_name == 'sort' || field_name == 'sorted' {
if p.file_backend_mode == .v || p.file_backend_mode == .c {
p.register_auto_import('builtin.closure')
}
p.open_scope() p.open_scope()
defer { defer {
p.close_scope() p.close_scope()

View file

@ -284,6 +284,22 @@ fn (mut p Parser) struct_decl(is_anon bool) ast.StructDecl {
// error is set in parse_type // error is set in parse_type
return ast.StructDecl{} return ast.StructDecl{}
} }
// for field_name []fn, cgen will generate closure, so detect here
if p.file_backend_mode == .v || p.file_backend_mode == .c {
sym := p.table.sym(typ)
mut elem_kind := ast.Kind.placeholder
if sym.kind == .array && (sym.info is ast.Array || sym.info is ast.Alias) {
elem_kind = p.table.sym(sym.array_info().elem_type).kind
} else if sym.kind == .array_fixed
&& (sym.info is ast.ArrayFixed || sym.info is ast.Alias) {
elem_kind = p.table.sym(sym.array_fixed_info().elem_type).kind
}
if elem_kind == .function {
p.register_auto_import('builtin.closure')
}
}
field_pos = field_start_pos.extend(p.prev_tok.pos()) field_pos = field_start_pos.extend(p.prev_tok.pos())
if typ.has_option_or_result() { if typ.has_option_or_result() {
option_pos = p.peek_token(-2).pos() option_pos = p.peek_token(-2).pos()

View file

@ -1,6 +1,10 @@
struct C.builtin__closure__Closure {
closure_cap int
}
fn setup(fname string) (int, int, []int) { fn setup(fname string) (int, int, []int) {
println(fname) println(fname)
return C._closure_cap, 42, []int{len: 5, init: index * 5} return unsafe { &C.builtin__closure__Closure(voidptr(&C.g_closure)).closure_cap }, 42, []int{len: 5, init: index * 5}
} }
fn test_array_filter() { fn test_array_filter() {
@ -9,7 +13,7 @@ fn test_array_filter() {
println('x: ${x} | i: ${i}') println('x: ${x} | i: ${i}')
return i < 20 return i < 20
})) }))
assert start_closure_cap - C._closure_cap == 1 assert start_closure_cap - unsafe { &C.builtin__closure__Closure(voidptr(&C.g_closure)).closure_cap } == 1
} }
fn test_array_map() { fn test_array_map() {
@ -18,7 +22,7 @@ fn test_array_map() {
println('x: ${x} | i: ${i}') println('x: ${x} | i: ${i}')
return x + i return x + i
})) }))
assert start_closure_cap - C._closure_cap == 1 assert start_closure_cap - unsafe { &C.builtin__closure__Closure(voidptr(&C.g_closure)).closure_cap } == 1
} }
fn test_array_any() { fn test_array_any() {
@ -27,7 +31,7 @@ fn test_array_any() {
println('x: ${x} | i: ${i}') println('x: ${x} | i: ${i}')
return i < x return i < x
})) }))
assert start_closure_cap - C._closure_cap == 1 assert start_closure_cap - unsafe { &C.builtin__closure__Closure(voidptr(&C.g_closure)).closure_cap } == 1
} }
fn test_array_all() { fn test_array_all() {
@ -36,5 +40,5 @@ fn test_array_all() {
println('x: ${x} | i: ${i}') println('x: ${x} | i: ${i}')
return i < x return i < x
})) }))
assert start_closure_cap - C._closure_cap == 1 assert start_closure_cap - unsafe { &C.builtin__closure__Closure(voidptr(&C.g_closure)).closure_cap } == 1
} }

View file

@ -15,8 +15,9 @@ import runtime
// math.bits is needed by strconv.ftoa // math.bits is needed by strconv.ftoa
pub const builtin_module_parts = ['math.bits', 'strconv', 'dlmalloc', 'strconv.ftoa', 'strings', pub const builtin_module_parts = ['math.bits', 'strconv', 'dlmalloc', 'strconv.ftoa', 'strings',
'builtin'] 'builtin', 'builtin.closure']
pub const bundle_modules = ['clipboard', 'fontstash', 'gg', 'gx', 'sokol', 'szip', 'ui']! pub const bundle_modules = ['clipboard', 'fontstash', 'gg', 'gx', 'sokol', 'szip', 'ui',
'builtin.closure']!
pub const external_module_dependencies_for_tool = { pub const external_module_dependencies_for_tool = {
'vdoc': ['markdown'] 'vdoc': ['markdown']