Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+1
View File
@@ -0,0 +1 @@
*.c
+59
View File
@@ -0,0 +1,59 @@
# Run all ARM64 tests
```
./v run vlib/v2_toberemoved/gen/arm64/tests/run_tests.v
```
# Run specific test with test_ssa_backends
```
cd cmd/v2
./test_ssa_backends arm64 ../../vlib/v2_toberemoved/gen/arm64/tests/string_concat.v
```
# Or directly with v2
```
./v2 -backend arm64 ../../vlib/v2_toberemoved/gen/arm64/tests/string_concat.v
```
# Useful cleanc flags
```
# Disable builtin+strconv object cache for this build:
./v2 -backend cleanc -nocache file.v
# Print C compiler and link command(s):
./v2 -backend cleanc -showcc file.v
# Print extended parse/build stats:
./v2 -stats -backend cleanc file.v
# Includes per-pass C Gen timing lines to help locate slow codegen phases.
# Print every parsed file (full and .vh groups):
./v2 -print-parsed-files -backend cleanc file.v
# Disable markused pruning (emit all functions):
./v2 -nomarkused -backend cleanc file.v
```
# Eval backend
```
# Execute a small V program directly from the v2 AST:
./v2 -eval file.v
# Pass runtime args to the interpreted program:
./v2 -eval file.v -- arg1 arg2
# Self-host under eval without recursively reusing the outer compiler flags:
./v2 -eval cmd/v2/v2.v -- -o /tmp/v2self cmd/v2/v2.v
# Current scope: literals, locals/consts, plain fn calls, if/for/range,
# arrays, indexing, len, println/print, and basic string interpolation.
```
# SSA C backend (restored)
```
# Run the SSA -> C backend directly:
./v2 -backend c cmd/v2/test.v
# Compare SSA C output against reference V output:
cd cmd/v2
../../v run test_ssa_backends.v c
```
+346
View File
@@ -0,0 +1,346 @@
// 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 main
import gg
import v2.profiler
// draw_header renders the top stats bar
fn draw_header(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Background
ctx.draw_rect_filled(0, 0, w, f32(app.header_height), header_bg)
// Get stats
stats := app.cached_stats
// Format and draw stats
heap_str := 'HEAP: ${profiler.format_bytes(stats.live_bytes)}'
peak_str := 'Peak: ${profiler.format_bytes(stats.peak_bytes)}'
allocs_str := '${stats.total_allocs} allocs'
frees_str := '${stats.total_frees} frees'
leaks_str := '${stats.leak_count} leaks'
// Draw stats with spacing
mut x := f32(20)
y := f32(22)
ctx.draw_text(int(x), int(y), heap_str, color: text_color, size: 20)
x += 180
ctx.draw_text(int(x), int(y), peak_str, color: text_color, size: 20)
x += 160
ctx.draw_text(int(x), int(y), allocs_str, color: new_alloc_color, size: 20)
x += 140
ctx.draw_text(int(x), int(y), frees_str, color: freed_color, size: 20)
x += 120
// Show leaks in red if there are any
leak_clr := if stats.leak_count > 0 { leak_color } else { text_color }
ctx.draw_text(int(x), int(y), leaks_str, color: leak_clr, size: 20)
// Mode and frame counter on right
mode_str := if app.demo_mode {
'DEMO'
} else if app.live_mode {
'LIVE'
} else {
'LOCAL'
}
ctx.draw_text(int(w) - 250, int(y), mode_str, color: gg.Color{100, 255, 100, 255}, size: 20)
frame_str := 'Frame: ${stats.frame_count}'
ctx.draw_text(int(w) - 150, int(y), frame_str, color: text_color, size: 20)
// Separator line
ctx.draw_line(0, f32(app.header_height), w, f32(app.header_height), grid_color)
}
// draw_histogram renders the allocation histogram
fn draw_histogram(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Histogram area
hist_y := f32(app.header_height)
hist_h := f32(app.histogram_height)
center_y := hist_y + hist_h / 2
// Background
ctx.draw_rect_filled(0, hist_y, w, hist_h, bg_color)
// Center line
ctx.draw_line(0, center_y, w, center_y, grid_color)
// Get frame data (always use cached data - it's populated by update_cache from snapshot or demo)
frames := app.cached_frames
if frames.len == 0 {
ctx.draw_text(int(w / 2) - 80, int(center_y) - 10, 'No frame data',
color: text_color
size: 18
)
return
}
// Calculate max values for scaling
mut max_new := u64(1)
mut max_freed := u64(1)
for frame in frames {
if frame.new_bytes > max_new {
max_new = frame.new_bytes
}
if frame.freed_bytes > max_freed {
max_freed = frame.freed_bytes
}
}
// Bar dimensions
margin := f32(40)
bar_area_width := w - margin * 2
bar_width := bar_area_width / f32(frames.len)
if bar_width < 1 {
return
}
bar_gap := bar_width * 0.1
actual_bar_w := bar_width - bar_gap
max_bar_height := (hist_h / 2) - 10
// Draw bars for each frame
for i, frame in frames {
x := margin + f32(i) * bar_width
// New allocations (cyan, above center)
if frame.new_bytes > 0 {
new_height := f32(frame.new_bytes) / f32(max_new) * max_bar_height
bar_color := if i == app.selected_frame { selected_color } else { new_alloc_color }
ctx.draw_rect_filled(x, center_y - new_height, actual_bar_w, new_height, bar_color)
}
// Freed allocations (purple, below center)
if frame.freed_bytes > 0 {
freed_height := f32(frame.freed_bytes) / f32(max_freed) * max_bar_height
bar_color := if i == app.selected_frame {
gg.Color{150, 130, 255, 200}
} else {
freed_color
}
ctx.draw_rect_filled(x, center_y, actual_bar_w, freed_height, bar_color)
}
// Hover highlight
if i == app.hover_frame && i != app.selected_frame {
ctx.draw_rect_empty(x - 1, hist_y + 5, actual_bar_w + 2, hist_h - 10, timeline_cursor)
}
}
// Legend
ctx.draw_rect_filled(10, hist_y + 10, 12, 12, new_alloc_color)
ctx.draw_text(26, int(hist_y) + 8, 'New', color: text_color, size: 14)
ctx.draw_rect_filled(70, hist_y + 10, 12, 12, freed_color)
ctx.draw_text(86, int(hist_y) + 8, 'Freed', color: text_color, size: 14)
// Separator line
ctx.draw_line(0, hist_y + hist_h, w, hist_y + hist_h, grid_color)
}
// draw_timeline renders the frame timeline with scrubber
fn draw_timeline(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Timeline area
timeline_y := f32(app.header_height + app.histogram_height)
timeline_h := f32(app.timeline_height)
// Background
ctx.draw_rect_filled(0, timeline_y, w, timeline_h, header_bg)
frames := app.cached_frames
if frames.len == 0 {
return
}
// Draw frame ticks
margin := f32(40)
tick_area_width := w - margin * 2
tick_spacing := tick_area_width / f32(frames.len)
// Draw tick marks
for i := 0; i < frames.len; i++ {
x := margin + f32(i) * tick_spacing
tick_height := if i % 10 == 0 { f32(15) } else { f32(8) }
ctx.draw_line(x, timeline_y + 20, x, timeline_y + 20 + tick_height, grid_color)
// Frame number labels every 10 frames
if i % 10 == 0 && tick_spacing > 3 {
ctx.draw_text(int(x) - 10, int(timeline_y) + 40, '${i}', color: text_color, size: 12)
}
}
// Draw selected frame cursor
if app.selected_frame >= 0 && app.selected_frame < frames.len {
cursor_x := margin + f32(app.selected_frame) * tick_spacing
// Triangle cursor
draw_triangle_filled(ctx, cursor_x, timeline_y + 15, cursor_x - 8, timeline_y + 5,
cursor_x + 8, timeline_y + 5, timeline_cursor)
// Vertical line
ctx.draw_line(cursor_x, timeline_y + 15, cursor_x, timeline_y + timeline_h - 5,
timeline_cursor)
// Frame info
frame := frames[app.selected_frame]
info := 'Frame ${app.selected_frame}: +${profiler.format_bytes(frame.new_bytes)} / -${profiler.format_bytes(frame.freed_bytes)}'
ctx.draw_text(int(w / 2) - 100, int(timeline_y) + 55, info, color: timeline_cursor, size: 16)
}
// Separator line
ctx.draw_line(0, timeline_y + timeline_h, w, timeline_y + timeline_h, grid_color)
}
// draw_controls renders the filter controls
fn draw_controls(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Controls area
ctrl_y := f32(app.header_height + app.histogram_height + app.timeline_height)
ctrl_h := f32(app.controls_height)
// Background
ctx.draw_rect_filled(0, ctrl_y, w, ctrl_h, bg_color)
// Filter buttons
mut x := f32(20)
y := ctrl_y + 10
// Filter label
ctx.draw_text(int(x), int(y), 'Filter:', color: text_color, size: 16)
x += 60
// Filter mode buttons
filters := ['All', 'Leaks', 'Large', 'Frame']
modes := [FilterMode.all, FilterMode.new_not_freed, FilterMode.large, FilterMode.current_frame]
for i, label in filters {
btn_w := f32(60)
btn_h := f32(24)
// Highlight active filter
btn_color := if app.filter_mode == modes[i] { selected_color } else { header_bg }
ctx.draw_rect_filled(x, y - 2, btn_w, btn_h, btn_color)
ctx.draw_rect_empty(x, y - 2, btn_w, btn_h, grid_color)
ctx.draw_text(int(x) + 8, int(y), label, color: text_color, size: 14)
x += btn_w + 10
}
// Separator line
ctx.draw_line(0, ctrl_y + ctrl_h, w, ctrl_y + ctrl_h, grid_color)
}
// draw_details renders the selection details panel
fn draw_details(mut app App) {
ctx := app.gg
w := f32(ctx.width)
h := f32(ctx.height)
// Details area
details_y := f32(app.header_height + app.histogram_height + app.timeline_height +
app.controls_height)
details_h := h - details_y
// Background
ctx.draw_rect_filled(0, details_y, w, details_h, header_bg)
// Show selected allocation info or instructions
if app.selected_alloc >= 0 {
allocs := app.cached_allocs
if app.selected_alloc < allocs.len {
alloc := allocs[app.selected_alloc]
// Format allocation info
ptr_str := 'Address: 0x${voidptr(alloc.ptr):p}'
size_str := 'Size: ${profiler.format_bytes(u64(alloc.size))} (${alloc.size} bytes)'
frame_str := 'Allocated: frame ${alloc.frame}'
status_str := if alloc.freed {
'Status: FREED (frame ${alloc.free_frame})'
} else {
'Status: LIVE (potential leak)'
}
status_color := if alloc.freed { freed_color } else { leak_color }
mut y := details_y + 15
ctx.draw_text(20, int(y), ptr_str, color: text_color, size: 16)
ctx.draw_text(250, int(y), size_str, color: text_color, size: 16)
ctx.draw_text(500, int(y), frame_str, color: text_color, size: 16)
ctx.draw_text(700, int(y), status_str, color: status_color, size: 16)
// Source location if available
if alloc.file.len > 0 {
y += 25
loc_str := 'Source: ${alloc.file}:${alloc.line}'
ctx.draw_text(20, int(y), loc_str, color: new_alloc_color, size: 16)
}
}
} else {
// Instructions
ctx.draw_text(20, int(details_y) + 20,
'Click on a histogram bar to select a frame, or use keyboard arrows to navigate',
color: text_color
size: 16
)
}
}
// draw_frame is the main frame callback
pub fn draw_frame(mut app App) {
if app.gg == unsafe { nil } {
return
}
app.gg.begin()
// Update data
if app.demo_mode {
update_demo_data(mut app)
} else {
app.update_cache()
}
// Draw all sections
draw_header(mut app)
draw_histogram(mut app)
draw_timeline(mut app)
draw_controls(mut app)
draw_details(mut app)
// Draw status message if in live mode with no data
if app.live_mode && app.cached_frames.len == 0 {
ctx := app.gg
w := f32(ctx.width)
h := f32(ctx.height)
ctx.draw_text(int(w / 2) - 200, int(h / 2), app.status_msg,
color: text_color
size: 20
)
}
app.gg.end()
}
// Helper: draw a filled triangle
fn draw_triangle_filled(ctx &gg.Context, x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32, c gg.Color) {
ctx.draw_convex_poly([x1, y1, x2, y2, x3, y3], c)
}
+233
View File
@@ -0,0 +1,233 @@
// 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 main
import gg
import os
import v2.profiler
// handle_event processes user input events
pub fn handle_event(e &gg.Event, mut app App) {
match e.typ {
.mouse_down {
handle_mouse_click(e, mut app)
}
.mouse_move {
handle_mouse_move(e, mut app)
}
.key_down {
handle_key_down(e, mut app)
}
else {}
}
}
// handle_mouse_click processes mouse click events
fn handle_mouse_click(e &gg.Event, mut app App) {
x := e.mouse_x
y := e.mouse_y
// Check which region was clicked
hist_y := f32(app.header_height)
hist_h := f32(app.histogram_height)
timeline_y := hist_y + hist_h
timeline_h := f32(app.timeline_height)
ctrl_y := timeline_y + timeline_h
ctrl_h := f32(app.controls_height)
// Click in histogram area
if y >= hist_y && y < hist_y + hist_h {
handle_histogram_click(x, y, mut app)
return
}
// Click in timeline area
if y >= timeline_y && y < timeline_y + timeline_h {
handle_timeline_click(x, y, mut app)
return
}
// Click in controls area
if y >= ctrl_y && y < ctrl_y + ctrl_h {
handle_controls_click(x, y, mut app)
return
}
}
// handle_histogram_click selects a frame from the histogram
fn handle_histogram_click(x f32, y f32, mut app App) {
frames := app.cached_frames
if frames.len == 0 {
return
}
w := f32(app.gg.width)
margin := f32(40)
bar_area_width := w - margin * 2
// Calculate frame index
relative_x := x - margin
if relative_x < 0 || relative_x > bar_area_width {
return
}
bar_width := bar_area_width / f32(frames.len)
frame_idx := int(relative_x / bar_width)
if frame_idx >= 0 && frame_idx < frames.len {
app.selected_frame = frame_idx
// Select first allocation in this frame
allocs := app.cached_allocs
for i, alloc in allocs {
if alloc.frame == u64(frame_idx) {
app.selected_alloc = i
break
}
}
}
}
// handle_timeline_click scrubs the timeline
fn handle_timeline_click(x f32, y f32, mut app App) {
// Same logic as histogram click for frame selection
handle_histogram_click(x, y, mut app)
}
// handle_controls_click handles filter button clicks
fn handle_controls_click(x f32, y f32, mut app App) {
// Filter buttons start at x=80
btn_start := f32(80)
btn_w := f32(60)
btn_gap := f32(10)
relative_x := x - btn_start
if relative_x < 0 {
return
}
btn_idx := int(relative_x / (btn_w + btn_gap))
modes := [FilterMode.all, FilterMode.new_not_freed, FilterMode.large, FilterMode.current_frame]
if btn_idx >= 0 && btn_idx < modes.len {
app.filter_mode = modes[btn_idx]
}
}
// handle_mouse_move updates hover state
fn handle_mouse_move(e &gg.Event, mut app App) {
x := e.mouse_x
y := e.mouse_y
hist_y := f32(app.header_height)
hist_h := f32(app.histogram_height)
// Check if hovering over histogram
if y >= hist_y && y < hist_y + hist_h {
frames := app.cached_frames
if frames.len > 0 {
w := f32(app.gg.width)
margin := f32(40)
bar_area_width := w - margin * 2
relative_x := x - margin
if relative_x >= 0 && relative_x <= bar_area_width {
bar_width := bar_area_width / f32(frames.len)
app.hover_frame = int(relative_x / bar_width)
if app.hover_frame >= frames.len {
app.hover_frame = -1
}
} else {
app.hover_frame = -1
}
}
} else {
app.hover_frame = -1
}
}
// handle_key_down processes keyboard input
fn handle_key_down(e &gg.Event, mut app App) {
match e.key_code {
.left {
// Previous frame
if app.selected_frame > 0 {
app.selected_frame--
}
}
.right {
// Next frame
frames := app.cached_frames
if app.selected_frame < frames.len - 1 {
app.selected_frame++
}
}
.up {
// Previous allocation
if app.selected_alloc > 0 {
app.selected_alloc--
}
}
.down {
// Next allocation
allocs := app.cached_allocs
if app.selected_alloc < allocs.len - 1 {
app.selected_alloc++
}
}
.enter {
// Open source file in editor
open_in_editor(app)
}
.f {
// Cycle filter modes
app.filter_mode = match app.filter_mode {
.all { .new_not_freed }
.new_not_freed { .large }
.large { .current_frame }
.current_frame { .all }
}
}
.r {
// Reset profiler data
if !app.demo_mode {
profiler.reset()
}
}
.escape {
// Clear selection
app.selected_frame = -1
app.selected_alloc = -1
}
.space {
// Toggle demo mode
app.demo_mode = !app.demo_mode
if app.demo_mode {
// Initialize demo data
init_demo_data(mut app)
}
}
else {}
}
}
// open_in_editor opens the selected allocation's source file in VS Code
fn open_in_editor(app &App) {
if app.selected_alloc < 0 {
return
}
allocs := app.cached_allocs
if app.selected_alloc >= allocs.len {
return
}
alloc := allocs[app.selected_alloc]
if alloc.file.len == 0 {
return
}
// Try to open in VS Code with line number
cmd := 'code -g "${alloc.file}:${alloc.line}"'
os.execute(cmd)
}
+154
View File
@@ -0,0 +1,154 @@
// 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 main
import gg
import os
import v2.profiler
fn main() {
// Initialize profiler system
profiler.profiler_init()
// Create application state (on stack, like the working example)
mut app := App{}
// Check command line args for mode
args := os.args
if '--demo' in args {
app.demo_mode = true
app.live_mode = false
init_demo_data(mut app)
} else {
// Default to live mode - read from snapshot file
app.demo_mode = false
app.live_mode = true
}
// Create gg context
app.gg = gg.new_context(
width: window_width
height: window_height
create_window: true
window_title: 'V2 Allocation Profiler'
bg_color: bg_color
frame_fn: frame
event_fn: event_handler
user_data: &app
)
// Run the application
app.gg.run()
}
// frame is the frame callback for gg
fn frame(mut app App) {
draw_frame(mut app)
}
// event_handler handles gg events
fn event_handler(e &gg.Event, mut app App) {
handle_event(e, mut app)
}
// init_demo_data populates the profiler with simulated data for demonstration
fn init_demo_data(mut app App) {
app.demo_mode = true
app.demo_frame = 0
// Create simulated frame and allocation data
app.cached_frames = []profiler.FrameData{cap: 100}
app.cached_allocs = []profiler.AllocRecord{cap: 500}
// Simulate 100 frames of allocation activity
mut total_live := u64(0)
mut peak := u64(0)
mut alloc_idx := 0
mut total_allocs := u64(0)
mut total_frees := u64(0)
for frame_num := 0; frame_num < 100; frame_num++ {
mut frame_data := profiler.FrameData{
frame_num: u64(frame_num)
}
// Simulate some allocations each frame
// More allocations early, fewer later (typical startup pattern)
num_allocs := if frame_num < 20 { 10 - frame_num / 4 } else { 2 + (frame_num % 3) }
for _ in 0 .. num_allocs {
// Random-ish allocation sizes
size := 64 + ((frame_num * 17 + alloc_idx * 31) % 4096)
app.cached_allocs << profiler.AllocRecord{
ptr: unsafe { voidptr(u64(0x1000) + u64(alloc_idx) * 0x100) }
size: size
frame: u64(frame_num)
file: ['main.v', 'parser.v', 'scanner.v', 'types.v', 'gen.v'][alloc_idx % 5]
line: 100 + (alloc_idx % 500)
timestamp: i64(frame_num) * 16_666_667 + i64(alloc_idx) * 1000
freed: false
}
frame_data.new_allocs << alloc_idx
frame_data.new_bytes += u64(size)
total_live += u64(size)
total_allocs++
alloc_idx++
}
// Simulate some frees (but not all - create some "leaks")
// Free some allocations from previous frames
if frame_num > 5 {
num_frees := num_allocs - 1 // Always free slightly fewer than we allocate
mut freed_count := 0
for i := 0; i < app.cached_allocs.len && freed_count < num_frees; i++ {
if !app.cached_allocs[i].freed && app.cached_allocs[i].frame < u64(frame_num - 3) {
// 80% chance to free
if (i * 7 + frame_num * 13) % 10 < 8 {
app.cached_allocs[i].freed = true
app.cached_allocs[i].free_frame = u64(frame_num)
frame_data.freed_idxs << i
frame_data.freed_bytes += u64(app.cached_allocs[i].size)
total_live -= u64(app.cached_allocs[i].size)
total_frees++
freed_count++
}
}
}
}
frame_data.live_bytes = total_live
if total_live > peak {
peak = total_live
}
app.cached_frames << frame_data
}
// Count leaks
mut leak_count := 0
for alloc in app.cached_allocs {
if !alloc.freed {
leak_count++
}
}
// Update stats
app.cached_stats = profiler.Statistics{
live_bytes: total_live
peak_bytes: peak
total_allocs: total_allocs
total_frees: total_frees
leak_count: leak_count
frame_count: 100
}
}
// update_demo_data simulates ongoing activity in demo mode
fn update_demo_data(mut app App) {
// In demo mode, we just use the static simulated data
// For a more dynamic demo, we could add new frames here
app.demo_frame++
}
+176
View File
@@ -0,0 +1,176 @@
// 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 main
import gg
import v2.profiler
// Filter modes for allocation view
pub enum FilterMode {
all // Show all allocations
new_not_freed // Show allocations that haven't been freed (potential leaks)
large // Show allocations larger than threshold
current_frame // Show allocations from selected frame only
}
// App holds the GUI application state
pub struct App {
pub mut:
gg &gg.Context = unsafe { nil }
// View state
selected_frame int = -1 // -1 means no selection
selected_alloc int = -1 // Index of selected allocation
filter_mode FilterMode = .all
large_threshold int = 1024 // Threshold for "large" filter (bytes)
search_text string
// Scroll/pan state
timeline_scroll int // Horizontal scroll for timeline
histogram_scroll int // Scroll for histogram if needed
// Cached data (updated each frame)
cached_frames []profiler.FrameData
cached_allocs []profiler.AllocRecord
cached_stats profiler.Statistics
// Layout dimensions
header_height int = 60
histogram_height int = 200
timeline_height int = 80
controls_height int = 40
details_height int = 60
// Hover state
hover_frame int = -1
hover_alloc int = -1
// Mode
demo_mode bool // Use simulated data for demo (false = live mode)
demo_frame int // Current demo frame counter
live_mode bool = true // Read from profiler snapshot file
// Status
status_msg string = 'Waiting for profiler data...'
}
// Colors for the profiler visualization
pub const bg_color = gg.Color{30, 30, 30, 255} // Dark gray background
pub const new_alloc_color = gg.Color{0, 200, 255, 200} // Cyan for new allocations
pub const freed_color = gg.Color{180, 100, 255, 200} // Purple for freed allocations
pub const leak_color = gg.Color{255, 100, 100, 255} // Red for leaks/live
pub const timeline_cursor = gg.Color{255, 255, 0, 255} // Yellow for timeline cursor
pub const text_color = gg.Color{220, 220, 220, 255} // Light gray text
pub const header_bg = gg.Color{40, 40, 45, 255} // Slightly lighter header
pub const grid_color = gg.Color{60, 60, 60, 255} // Grid lines
pub const selected_color = gg.Color{100, 150, 255, 255} // Selection highlight
// Window dimensions
pub const window_width = 1200
pub const window_height = 700
// init_app creates a new App instance (unused, kept for reference)
fn init_app_heap() &App {
return &App{}
}
// update_cache refreshes cached profiler data
pub fn (mut app App) update_cache() {
if app.demo_mode {
return
}
if app.live_mode {
// Read from snapshot file
if snap := profiler.read_snapshot() {
app.cached_frames = snap.frames
app.cached_allocs = snap.allocs
app.cached_stats = profiler.Statistics{
live_bytes: snap.live_bytes
peak_bytes: snap.peak_bytes
total_allocs: snap.total_allocs
total_frees: snap.total_frees
leak_count: count_leaks(snap.allocs)
frame_count: snap.frame_count
}
app.status_msg = 'Live: Frame ${snap.frame_count}'
} else {
app.status_msg = 'Waiting for profiler data at ${profiler.profiler_data_path}...'
}
return
}
// In-process mode (same process)
app.cached_frames = profiler.get_frames()
app.cached_allocs = profiler.get_allocs()
app.cached_stats = profiler.get_statistics()
}
fn count_leaks(allocs []profiler.AllocRecord) int {
mut count := 0
for alloc in allocs {
if !alloc.freed {
count++
}
}
return count
}
// get_filtered_allocs returns allocations matching current filter
pub fn (app &App) get_filtered_allocs() []profiler.AllocRecord {
mut result := []profiler.AllocRecord{}
allocs := app.cached_allocs
for alloc in allocs {
match app.filter_mode {
.all {
result << alloc
}
.new_not_freed {
if !alloc.freed {
result << alloc
}
}
.large {
if alloc.size >= app.large_threshold {
result << alloc
}
}
.current_frame {
if app.selected_frame >= 0 && alloc.frame == u64(app.selected_frame) {
result << alloc
}
}
}
}
// Apply search filter if text is present
if app.search_text.len > 0 {
mut filtered := []profiler.AllocRecord{}
for alloc in result {
if alloc.file.contains(app.search_text) {
filtered << alloc
}
}
return filtered
}
return result
}
// get_frame_at_x returns the frame index at the given x coordinate in timeline
pub fn (app &App) get_frame_at_x(x f32, timeline_x f32, timeline_width f32) int {
frames := app.cached_frames
if frames.len == 0 {
return -1
}
// Calculate which frame this x position corresponds to
relative_x := x - timeline_x
if relative_x < 0 || relative_x > timeline_width {
return -1
}
frame_width := timeline_width / f32(frames.len)
frame_idx := int(relative_x / frame_width)
if frame_idx >= 0 && frame_idx < frames.len {
return frame_idx
}
return -1
}
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println('hello world')
}
File diff suppressed because it is too large. Load diff
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
repo_root="$(CDPATH= cd -- "${script_dir}/../.." && pwd)"
v1_compiler="${V:-${repo_root}/v}"
if [ ! -x "${v1_compiler}" ]; then
echo "FAIL: v1 compiler not found: ${v1_compiler}"
exit 1
fi
exec "${v1_compiler}" run "${script_dir}/test_all.vsh" "$@"
+299
View File
@@ -0,0 +1,299 @@
#!/usr/bin/env -S v
import os
const total_steps = 15
const v2_bak = '/tmp/v2_src_bak_test_all'
const cleanc_cache = '/tmp/v2_cleanc_obj_cache'
const gitly_clone_dir = '/tmp/v2_test_gitly_clone'
const gitly_repo_url = 'https://github.com/vlang/gitly'
struct Config {
v1_compiler string
script_dir string
repo_root string
v2_src string
backend_arch string
backend_flag string
}
struct Args {
backend_arch string
opt_flag string
}
fn main() {
cfg := parse_config()
os.chdir(cfg.script_dir) or { fail('failed to enter ${cfg.script_dir}: ${err}') }
rm_rf(cleanc_cache)
section(1, '${cfg.backend_arch} self-host hello world')
backup_v2_src(cfg)
run('${q(cfg.v1_compiler)} -gc none -cc cc -o v2 v2.v')
restore_v2_src(cfg)
run('./v2 --no-parallel -backend ${cfg.backend_arch} -gc none -nocache ${cfg.backend_flag} -o v3 v2.v')
run('./v3 --no-parallel -backend ${cfg.backend_arch} ${cfg.backend_flag} -o hello_backend hello.v')
run('./hello_backend')
cleanup_files(['v3', 'hello_backend'])
section(2, '${cfg.backend_arch} self-host chain (v2->v3->v4->v5->v6)')
println(' Building v3 from v2...')
run('./v2 --no-parallel -nocache -gc none -backend ${cfg.backend_arch} ${cfg.backend_flag} -o v3_chain v2.v')
println(' Building v4 from v3...')
run('./v3_chain --no-parallel -nocache -gc none -backend ${cfg.backend_arch} ${cfg.backend_flag} -o v4_chain v2.v')
println(' Building v5 from v4...')
run('./v4_chain --no-parallel -nocache -gc none -backend ${cfg.backend_arch} ${cfg.backend_flag} -o v5_chain v2.v')
println(' Building v6 from v5...')
run('./v5_chain --no-parallel -nocache -gc none -backend ${cfg.backend_arch} ${cfg.backend_flag} -o v6_chain v2.v')
v5_size := os.file_size('v5_chain')
v6_size := os.file_size('v6_chain')
if v5_size == v6_size {
println(' v5=v6 (${v5_size} bytes) - chain converged')
} else {
fail(' FAIL: v5 (${v5_size}) != v6 (${v6_size})')
}
cleanup_files(['v3_chain', 'v4_chain', 'v5_chain', 'v6_chain'])
section(3, 'Self-host test')
run('bash test_v2_self.sh')
section(4, 'Builtin test files (cleanc)')
rm_rf(cleanc_cache)
run('./v2 ../../vlib/builtin/array_test.v')
run('./v2 ../../vlib/builtin/string_test.v')
run('./v2 ../../vlib/builtin/map_test.v')
section(5, 'Builtin test files (${cfg.backend_arch})')
for backend_test in ['array_test.v', 'string_test.v', 'map_test.v'] {
run('./v2 -backend ${cfg.backend_arch} ${cfg.backend_flag} "../../vlib/builtin/${backend_test}"')
}
section(6, 'Math test')
run('./v2 ../../vlib/math/math_test.v')
section(7, 'Math test (${cfg.backend_arch})')
run('./v2 -backend ${cfg.backend_arch} ${cfg.backend_flag} ../../vlib/math/math_test.v')
section(8, 'Sumtype tests')
for st in sumtype_tests() {
run('./v2 ${st}')
}
section(9, 'Sumtype tests (${cfg.backend_arch})')
for st in sumtype_tests() {
run('./v2 -backend ${cfg.backend_arch} ${cfg.backend_flag} ${st}')
}
section(10, 'SSA backends test (${cfg.backend_arch})')
run('${q(cfg.v1_compiler)} -gc none run test_ssa_backends.v ${cfg.backend_arch} ${cfg.backend_flag}')
section(11, 'SSA backends test (cleanc)')
run('${q(cfg.v1_compiler)} -gc none run test_ssa_backends.v cleanc')
section(12, 'Transformer unit tests')
run('${q(cfg.v1_compiler)} -gc none ../../vlib/v2_toberemoved/transformer/transformer_test.v')
section(13, 'Transformer integration test')
run('${q(cfg.v1_compiler)} -gc none ../../vlib/v2_toberemoved/transformer/transformer_v2_darwin_test.v')
section(14, 'Cleanc runtime tests')
run('${q(cfg.v1_compiler)} -gc none run ../../vlib/v2_toberemoved/gen/cleanc/tests/run_tests.v')
section(15, 'Gitly build (cleanc, -d use_openssl -d sqlite)')
v2_bin := os.join_path(cfg.script_dir, 'v2')
gitly_dir := resolve_gitly_dir()
os.chdir(gitly_dir) or { fail('failed to enter ${gitly_dir}: ${err}') }
run('${q(v2_bin)} -v -showcc -o x -stats -d use_openssl -d sqlite .')
cleanup_files(['x'])
os.chdir(cfg.script_dir) or { fail('failed to return to ${cfg.script_dir}: ${err}') }
println('')
println('=== ALL TESTS PASSED ===')
}
fn resolve_gitly_dir() string {
local := os.join_path(os.home_dir(), 'code', 'gitly')
if os.is_dir(local) {
println(' Using local gitly at ${local}')
return local
}
if os.is_dir(gitly_clone_dir) {
println(' Reusing cloned gitly at ${gitly_clone_dir}')
return gitly_clone_dir
}
println(' Cloning ${gitly_repo_url} into ${gitly_clone_dir}...')
run('git clone --depth 1 ${gitly_repo_url} ${q(gitly_clone_dir)}')
return gitly_clone_dir
}
fn parse_config() Config {
args := parse_args()
script_dir := os.real_path(@DIR)
repo_root := os.real_path(os.join_path(script_dir, '..', '..'))
v1_compiler := absolute_path(os.getenv_opt('V') or { os.join_path(repo_root, 'v') })
if !os.is_executable(v1_compiler) {
fail('FAIL: v1 compiler not found: ${v1_compiler}')
}
backend_flag := args.opt_flag
return Config{
v1_compiler: v1_compiler
script_dir: script_dir
repo_root: repo_root
v2_src: os.join_path(repo_root, 'vlib', 'v2')
backend_arch: args.backend_arch
backend_flag: backend_flag
}
}
fn parse_args() Args {
mut backend_arch := ''
mut opt_flag := '-O0'
mut i := 1
for i < os.args.len {
arg := os.args[i]
match arg {
'-arch', '--arch' {
i++
if i >= os.args.len {
fail('missing value after -arch/--arch')
}
backend_arch = parse_backend_arch(os.args[i])
}
'-prod' {
opt_flag = '-prod'
}
'-O0' {
opt_flag = '-O0'
}
'-h', '--help' {
println('Usage: v run cmd/v2/test_all.vsh [-arch|--arch auto|x64|arm64] [-prod|-O0]')
println('')
println('By default, backend tests use the native host architecture.')
println('With -arch/--arch auto, backend tests use the native host architecture.')
println('With -arch/--arch x64, backend tests use the x64 backend.')
println('With -arch/--arch arm64, backend tests use the ARM64 backend.')
println('With -prod, x64 and ARM64 backend builds use -prod.')
println('With -O0, x64 and ARM64 backend builds use -O0.')
println('With x64 backend builds, -O0 is passed through, but SSA optimize is still forced by the backend.')
exit(0)
}
else {
fail('unknown argument: ${arg}')
}
}
i++
}
if backend_arch == '' {
backend_arch = native_backend_arch()
}
return Args{
backend_arch: backend_arch
opt_flag: opt_flag
}
}
fn parse_backend_arch(arch string) string {
match arch {
'auto' {
return native_backend_arch()
}
'x64', 'arm64' {
return arch
}
else {
fail('unsupported architecture: ${arch}; expected auto, x64 or arm64')
}
}
return ''
}
fn native_backend_arch() string {
machine := os.uname().machine.to_lower()
match machine {
'x86_64', 'amd64' {
return 'x64'
}
'aarch64', 'arm64' {
return 'arm64'
}
else {
fail('unsupported native architecture: ${machine}; pass -arch/--arch x64 or -arch/--arch arm64')
}
}
return ''
}
fn absolute_path(path string) string {
if os.is_abs_path(path) {
return path
}
return os.join_path(os.getwd(), path)
}
fn section(step int, title string) {
if step > 1 {
println('')
}
println('=== ${step}/${total_steps}: ${title} ===')
}
fn backup_v2_src(cfg Config) {
rm_rf(v2_bak)
run('cp -R ${q(cfg.v2_src)} ${q(v2_bak)}')
}
fn restore_v2_src(cfg Config) {
run('rsync -a --delete ${q(v2_bak + '/')} ${q(cfg.v2_src + '/')}')
}
fn rm_rf(path string) {
if !os.exists(path) {
return
}
if os.is_dir(path) {
os.rmdir_all(path) or { fail('failed to remove ${path}: ${err}') }
return
}
os.rm(path) or { fail('failed to remove ${path}: ${err}') }
}
fn cleanup_files(paths []string) {
for path in paths {
os.rm(path) or {}
}
}
fn run(cmd string) {
code := os.system(cmd)
if code != 0 {
exit(code)
}
}
fn q(path string) string {
return os.quoted_path(path)
}
fn fail(message string) {
eprintln(message)
exit(1)
}
fn sumtype_tests() []string {
return [
'test_sumtype.v',
'test_sumtype2.v',
'test_sumtype3.v',
'test_sumtype4.v',
'test_sumtype_pos.v',
'test_sumtype_data.v',
'test_sumtype_ifexpr.v',
'test_sumtype_nested.v',
'test_sumtype_many.v',
'test_sumtype_global.v',
]
}
+229
View File
@@ -0,0 +1,229 @@
// 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 main
import os
import time
fn main() {
t0 := time.now()
// Build v2 compiler
println('[*] Building v2...')
vroot := os.dir(@VEXE)
v2_source := os.join_path(vroot, 'cmd', 'v2', 'v2.v')
v2_binary := os.join_path(vroot, 'cmd', 'v2', 'v2')
build_res := os.execute('${@VEXE} -gc none -cc cc ${v2_source} -o ${v2_binary}')
if build_res.exit_code != 0 {
eprintln('Error: Failed to build v2')
eprintln(build_res.output)
exit(1)
}
// Determine backends from command line args.
// If none are provided, run the default backend set.
mut backends := []string{}
if os.args.contains('cleanc') {
backends << 'cleanc'
}
if os.args.contains('c') {
backends << 'c'
}
if os.args.contains('arm64') {
backends << 'arm64'
}
if os.args.contains('x64') {
backends << 'x64'
}
if backends.len == 0 {
backends = ['cleanc', 'c', 'arm64']
}
// Parse test file from args or default to test.v
// Support: ./test_ssa_backends arm64 path/to/file.v
mut input_file := 'test.v'
for arg in os.args {
if arg.ends_with('.v') && arg != @FILE {
input_file = arg
break
}
}
if !os.exists(input_file) {
eprintln('Error: ${input_file} not found')
exit(1)
}
// Derive output binary name from input file
base_name := os.file_name(input_file).replace('.v', '')
ref_output_path := './.${base_name}_ref.out.tmp'
gen_output_path := './.${base_name}_gen.out.tmp'
// Get expected output: use .out file if --skip-builtin, otherwise run reference compiler
mut expected_out := ''
out_file := input_file.replace('.v', '.out')
if os.args.contains('--skip-builtin') && os.exists(out_file) {
println('[*] Using expected output from ${out_file}')
expected_out = os.read_file(out_file) or { '' }.trim_space().replace('\r\n', '\n')
} else {
// Run Reference (v run test.v)
println('[*] Running reference: ${@VEXE} -enable-globals run ${input_file}...')
os.rm(ref_output_path) or {}
ref_cc := if os.user_os() == 'macos' { '-cc cc ' } else { '' }
ref_cmd := '${@VEXE} -gc none ${ref_cc}-n -w -enable-globals run ${input_file} > ${ref_output_path} 2>&1'
ref_res := os.execute(ref_cmd)
ref_out := os.read_file(ref_output_path) or { '' }
os.rm(ref_output_path) or {}
if ref_res.exit_code != 0 {
eprintln('Error: Reference run failed')
eprintln(ref_out)
exit(1)
}
// Normalize newlines
expected_out = ref_out.trim_space().replace('\r\n', '\n')
}
mut had_failures := false
for backend in backends {
backend_t0 := time.now()
// Run v2 with selected backend
println('[*] Running v2 -backend ${backend} ${input_file}...')
mut backend_flags := '-gc none -backend ${backend}'
if backend in ['arm64', 'x64'] {
if os.args.contains('-prod') {
backend_flags += ' -prod'
} else if os.args.contains('-O0') {
backend_flags += ' -O0'
}
}
if backend == 'cleanc' {
// cleanc needs full per-run codegen for this suite right now.
backend_flags += ' -nomarkused -nocache'
}
if os.args.contains('--skip-builtin') && !backend_flags.contains('--skip-builtin') {
backend_flags += ' --skip-builtin'
}
v2_cmd := '${v2_binary} ${backend_flags} ${input_file} -o ${base_name}'
v2_res := os.execute(v2_cmd)
if v2_res.exit_code != 0 {
eprintln('Error: v2 compilation failed for backend ${backend}')
eprintln(v2_res.output)
had_failures = true
continue
}
println(v2_res.output)
println('compilation took ${time.since(backend_t0)}')
// Save the v2-produced binary before running another backend (which would overwrite it)
saved_binary := './${base_name}_${backend}_v2'
os.rm(saved_binary) or {}
if os.user_os() == 'windows' {
os.rm('${saved_binary}.exe') or {}
}
os.cp('./${base_name}', saved_binary) or {
eprintln('Error: Failed to save v2 binary for backend ${backend}')
had_failures = true
continue
}
// Run generated binary
println('[*] Running generated binary (${backend})...')
mut cmd := saved_binary
if os.user_os() == 'windows' {
cmd = '${saved_binary}.exe'
}
os.rm(gen_output_path) or {}
// 60s timeout to catch infinite loops in ARM64-generated code
has_timeout := os.exists('/opt/homebrew/bin/timeout') || os.exists('/usr/bin/timeout')
has_gtimeout := os.exists('/opt/homebrew/bin/gtimeout') || os.exists('/usr/bin/gtimeout')
timeout_cmd := if has_timeout {
'timeout'
} else if has_gtimeout {
'gtimeout'
} else {
''
}
gen_cmd := if timeout_cmd != '' {
'${timeout_cmd} 60 ${cmd} > ${gen_output_path} 2>&1'
} else {
'${cmd} > ${gen_output_path} 2>&1'
}
gen_res := os.execute(gen_cmd)
gen_out := os.read_file(gen_output_path) or { '' }
os.rm(gen_output_path) or {}
if gen_res.exit_code != 0 {
if gen_res.exit_code == 124 || gen_res.exit_code == 142 || gen_res.exit_code == 14 {
eprintln('Error: Execution timed out (infinite loop detected) for backend ${backend}')
had_failures = true
continue
}
println('Warning: Binary exited with code ${gen_res.exit_code}')
}
// Strip terminal control characters that script command may prepend
mut cleaned := gen_out.replace('\r\n', '\n').replace('\x04', '').replace('\x08', '')
// Remove "^D" literal string that macOS script may add
if cleaned.starts_with('^D') {
cleaned = cleaned[2..]
}
actual_out := cleaned.trim_space()
// Compare
if expected_out == actual_out {
println('\n[SUCCESS] Backend ${backend}: outputs match!')
continue
}
had_failures = true
println('\n[FAILURE] Backend ${backend}: outputs differ')
expected_lines := expected_out.split('\n')
actual_lines := actual_out.split('\n')
// Find first differing line
mut first_diff := -1
max_lines := if expected_lines.len > actual_lines.len {
expected_lines.len
} else {
actual_lines.len
}
for i in 0 .. max_lines {
exp := if i < expected_lines.len { expected_lines[i] } else { '<missing>' }
act := if i < actual_lines.len { actual_lines[i] } else { '<missing>' }
if exp != act {
first_diff = i
break
}
}
if first_diff >= 0 {
context := 2
start := if first_diff > context { first_diff - context } else { 0 }
end := if first_diff + context + 1 < max_lines {
first_diff + context + 1
} else {
max_lines
}
println('\nExpected (reference compiler):')
for i in start .. end {
line := if i < expected_lines.len { expected_lines[i] } else { '<missing>' }
println('${i + 1}: ${line}')
}
println('\nGot (v2 ${backend}):')
for i in start .. end {
line := if i < actual_lines.len { actual_lines[i] } else { '<missing>' }
println('${i + 1}: ${line}')
}
}
}
if had_failures {
println('\n[FAILURE] One or more backends failed')
exit(1)
} else {
println('\n[SUCCESS] All requested backends passed')
}
println('total time ${time.since(t0)}')
}
+28
View File
@@ -0,0 +1,28 @@
type Animal = Cat | Dog
struct Cat {
name string
}
struct Dog {
name string
age int
}
fn make_animal() Animal {
return Animal(Cat{
name: 'whiskers'
})
}
fn main() {
a := make_animal()
match a {
Cat {
println(a.name)
}
Dog {
println(a.name)
}
}
}
+50
View File
@@ -0,0 +1,50 @@
struct Cat {
name string
}
struct Dog {
name string
age int
}
struct Bird {
name string
can_fly bool
}
type Animal = Bird | Cat | Dog
fn make_cat() Animal {
return Animal(Cat{
name: 'whiskers'
})
}
fn make_bird() Animal {
return Animal(Bird{
name: 'tweety'
can_fly: true
})
}
fn indirect() Animal {
a := make_cat()
return a
}
fn get_name(a Animal) string {
match a {
Cat { return a.name }
Dog { return a.name }
Bird { return a.name }
}
}
fn main() {
a := make_cat()
b := make_bird()
c := indirect()
println(get_name(a))
println(get_name(b))
println(get_name(c))
}
+44
View File
@@ -0,0 +1,44 @@
struct Cat {
name string
}
struct Dog {
name string
age int
}
struct Bird {
name string
can_fly bool
}
type Animal = Bird | Cat | Dog
fn make_conditional(which int) Animal {
result := if which == 0 {
Animal(Cat{
name: 'whiskers'
})
} else {
Animal(Bird{
name: 'tweety'
can_fly: true
})
}
return result
}
fn get_name(a Animal) string {
match a {
Cat { return a.name }
Dog { return a.name }
Bird { return a.name }
}
}
fn main() {
a := make_conditional(0)
b := make_conditional(1)
println(get_name(a))
println(get_name(b))
}
+38
View File
@@ -0,0 +1,38 @@
struct Ident {
name string
line int
col int
}
struct Literal {
value string
}
type Expr = Ident | Literal
fn make_ident() Ident {
return Ident{
name: 'foo'
line: 42
col: 10
}
}
fn wrap_expr() Expr {
i := make_ident()
return i
}
fn main() {
e := wrap_expr()
match e {
Ident {
println(e.name)
println(e.line)
println(e.col)
}
Literal {
println(e.value)
}
}
}
+40
View File
@@ -0,0 +1,40 @@
struct Pos {
x int
y int
}
struct Ident {
pos Pos
name string
}
struct Lit {
pos Pos
val int
}
type Expr = Ident | Lit
fn (e Expr) get_pos() Pos {
return match e {
Ident { e.pos }
Lit { e.pos }
}
}
fn make_ident() Expr {
return Expr(Ident{
pos: Pos{
x: 10
y: 20
}
name: 'hello'
})
}
fn main() {
e := make_ident()
p := e.get_pos()
println(p.x.str())
println(p.y.str())
}
+71
View File
@@ -0,0 +1,71 @@
struct Pos {
x int
y int
}
struct Ident {
pos Pos
name string
}
struct EmptyExpr {
dummy u8
}
type Expr = EmptyExpr | Ident
const empty_expr = Expr(EmptyExpr{})
fn (e Expr) get_pos() Pos {
return match e {
Ident { e.pos }
EmptyExpr { Pos{} }
}
}
fn parse_expr() Expr {
return Expr(Ident{
pos: Pos{
x: 42
y: 99
}
name: 'macos'
})
}
struct Parser {
mut:
tok int
comptime bool
}
fn (mut p Parser) expr_or_type() Expr {
return Expr(Ident{
pos: Pos{
x: 77
y: 88
}
name: 'linux'
})
}
fn (mut p Parser) if_expr(is_comptime bool) {
// Exact pattern from the real parser
mut cond := if p.tok == 42 {
empty_expr // global constant
} else {
if is_comptime { p.expr_or_type() } else { p.expr_or_type() }
}
// This line is what crashes in v3
guard_pos := cond.get_pos()
println(guard_pos.x.str())
println(guard_pos.y.str())
}
fn main() {
mut parser := Parser{
tok: 99
comptime: true
}
parser.if_expr(true) // should print 77, 88
}
+48
View File
@@ -0,0 +1,48 @@
struct Pos {
x int
y int
}
struct Ident {
pos Pos
name string
}
struct EmptyExpr {
dummy u8
}
type Expr = EmptyExpr | Ident
fn (e Expr) get_pos() Pos {
return match e {
Ident { e.pos }
EmptyExpr { Pos{} }
}
}
fn parse_expr() Expr {
return Expr(Ident{
pos: Pos{
x: 42
y: 99
}
name: 'macos'
})
}
fn test_if_expr(is_lcbr bool) {
mut cond := if is_lcbr {
Expr(EmptyExpr{})
} else {
parse_expr()
}
p := cond.get_pos()
println(p.x.str())
println(p.y.str())
}
fn main() {
test_if_expr(false) // should print 42, 99
test_if_expr(true) // should print 0, 0
}
+126
View File
@@ -0,0 +1,126 @@
struct Pos {
x int
y int
}
struct V0 {
pos Pos
a int
}
struct V1 {
pos Pos
b int
}
struct V2 {
pos Pos
c int
}
struct V3 {
pos Pos
d int
}
struct V4 {
pos Pos
e int
}
struct V5 {
pos Pos
f int
}
struct V6 {
pos Pos
g int
}
struct V7 {
pos Pos
h int
}
struct V8 {
dummy u8
} // EmptyExpr equivalent (tag 8)
struct V9 {
pos Pos
j int
}
struct V10 {
pos Pos
k int
}
struct V11 {
pos Pos
l int
}
struct V12 {
pos Pos
m int
}
struct V13 {
pos Pos
name string
} // Ident equivalent (tag 13)
struct V14 {
pos Pos
n int
}
type Expr = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14
fn (e Expr) get_pos() Pos {
return match e {
V0 { e.pos }
V1 { e.pos }
V2 { e.pos }
V3 { e.pos }
V4 { e.pos }
V5 { e.pos }
V6 { e.pos }
V7 { e.pos }
V8 { Pos{} }
V9 { e.pos }
V10 { e.pos }
V11 { e.pos }
V12 { e.pos }
V13 { e.pos }
V14 { e.pos }
}
}
fn make_v13() Expr {
return Expr(V13{
pos: Pos{
x: 42
y: 99
}
name: 'hello'
})
}
fn test_it(is_lcbr bool, is_comptime bool) {
mut cond := if is_lcbr {
Expr(V8{})
} else {
if is_comptime { make_v13() } else { make_v13() }
}
p := cond.get_pos()
println(p.x.str())
println(p.y.str())
}
fn main() {
println('test with 15 variants, accessing V13 (tag 13):')
test_it(false, true)
}
+107
View File
@@ -0,0 +1,107 @@
// Test sum type with struct variants (not just primitives)
// This mirrors the AST's Expr sum type pattern
struct BasicLit {
kind int
value string
}
struct Ident {
name string
}
struct CallOrCast {
lhs Expr
expr Expr
}
type Expr = BasicLit | CallOrCast | Ident
fn make_basic_lit(val string) Expr {
return BasicLit{
kind: 1
value: val
}
}
fn make_ident(name string) Expr {
return Ident{
name: name
}
}
fn make_call_or_cast(lhs Expr, expr Expr) Expr {
return CallOrCast{
lhs: lhs
expr: expr
}
}
fn test_basic_lit() {
e := make_basic_lit('42')
if e is BasicLit {
assert e.value == '42'
assert e.kind == 1
} else {
assert false
}
}
fn test_ident() {
e := make_ident('hello')
if e is Ident {
assert e.name == 'hello'
} else {
assert false
}
}
fn test_call_or_cast() {
lhs := make_ident('int')
arg := make_basic_lit('0')
e := make_call_or_cast(lhs, arg)
if e is CallOrCast {
if e.lhs is Ident {
assert e.lhs.name == 'int'
} else {
assert false
}
if e.expr is BasicLit {
assert e.expr.value == '0'
} else {
assert false
}
} else {
assert false
}
}
fn test_nested_sum_field_access() {
// Create CallOrCast{lhs: Ident("int"), expr: BasicLit("0")}
// This mirrors: __global g_main_argc = int(0)
lhs := Expr(Ident{
name: 'int'
})
arg := Expr(BasicLit{
kind: 1
value: '0'
})
coce := CallOrCast{
lhs: lhs
expr: arg
}
e := Expr(coce)
if e is CallOrCast {
// Access the nested Expr fields
inner_expr := e.expr
if inner_expr is BasicLit {
assert inner_expr.value == '0'
} else {
eprintln('ERROR: inner_expr is not BasicLit')
assert false
}
} else {
eprintln('ERROR: e is not CallOrCast')
assert false
}
}
+27
View File
@@ -0,0 +1,27 @@
type MyUnion = int | string
fn (u MyUnion) tag() int {
return match u {
int { 0 }
string { 1 }
// else { -1 }
}
}
fn make_int_union() MyUnion {
return MyUnion(42)
}
fn make_string_union() MyUnion {
return MyUnion('hello')
}
fn main() {
a := make_int_union()
t := a.tag()
println(t.str())
b := make_string_union()
t2 := b.tag()
println(t2.str())
}
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
v2_source="${script_dir}/v2.v"
v2_bin="${script_dir}/v2"
v3_bin="${script_dir}/v3"
v4_bin="${script_dir}/v4"
v5_bin="${script_dir}/v5"
# Variable for the backend (originally cleanc)
backend="cleanc"
v1_compiler="${V:-${repo_root}/v}"
if [[ ! -x "${v1_compiler}" ]]; then
fallback_v1_compiler="${HOME}/code/v/v"
if [[ -x "${fallback_v1_compiler}" ]]; then
v1_compiler="${fallback_v1_compiler}"
else
echo "FAILURE: v1 compiler not found or not executable: ${v1_compiler}"
exit 1
fi
fi
# V1's formatter may clobber v2 source files — backup and restore.
v2_src="${repo_root}/vlib/v2_toberemoved"
v2_bak="/tmp/v2_src_bak_self_test"
rm -rf "${v2_bak}"
cp -R "${v2_src}" "${v2_bak}"
# Build v2 with v1.
rm -f "${v2_bin}" "${v3_bin}" "${v3_bin}.c" "${v4_bin}" "${v4_bin}.c" "${v5_bin}" "${v5_bin}.c"
"${v1_compiler}" -gc none -cc cc -o "${v2_bin}" "${v2_source}"
# Restore v2 sources after V1 build.
rsync -a --delete "${v2_bak}/" "${v2_src}/"
# Use clang instead of TCC for v2-compiled C code.
export V2CC="${V2CC:-cc}"
# Clear stale caches to avoid reusing incompatible generated C across runs.
rm -rf /tmp/v2_cleanc_obj_cache
# Use v2 to compile itself to v3 (using defined backend).
"${v2_bin}" -gc none -o "${v3_bin}" -backend "${backend}" "${v2_source}"
echo "SUCCESS: v2 successfully compiled itself to v3"
echo "v3 binary size: $(ls -lh "${v3_bin}" | awk '{print $5}')"
if [[ "${backend}" != "cleanc" ]]; then
"${v3_bin}" -gc none -o "${v4_bin}" -backend "${backend}" "${v2_source}"
printf '\nV4 compiled\n\n'
"${v4_bin}" -gc none -o "${v5_bin}" -backend "${backend}" "${v2_source}"
printf '\nV5 compiled\n\n'
fi
# Test that v3 runs and produces expected output.
output="$("${v3_bin}" 2>&1 || true)"
expected='At least 1 .v file expected'
if echo "${output}" | grep -q "${expected}"; then
echo "SUCCESS"
else
echo "FAILURE: Expected '${expected}' but got:"
echo "${output}"
exit 1
fi
echo ''
echo '=== SELF-COMPILATION TEST PASSED ==='
+310
View File
@@ -0,0 +1,310 @@
// 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 main
import os
import time
import v2.pref
import v2.builder
fn main() {
compile_args, runtime_args := split_eval_runtime_args(os.args[1..])
// Check for subcommands
if compile_args.len > 0 && compile_args[0] == 'ast' {
run_ast_command(compile_args[1..])
return
}
if compile_args.len > 0 && compile_args[0] == 'test-self' {
run_test_self()
return
}
mut prefs := pref.new_preferences_from_args(compile_args)
files := get_files(compile_args)
if files.len == 0 {
eprintln('At least 1 .v file expected')
exit(1)
}
mut eval_runtime_args := [files[0]]
eval_runtime_args << runtime_args
prefs.eval_runtime_args = eval_runtime_args
mut b := builder.new_builder(&prefs)
b.build(files)
// Auto-run test binaries after compilation (matching v1 behavior)
if prefs.output_file == '' && is_test_file(files) && should_auto_run_test_binary(&prefs) {
last := os.file_name(files.last())
output_name := if last.ends_with('.vv2') {
last.all_before_last('.vv2')
} else {
last.all_before_last('.v')
}
if os.exists(output_name) {
ret := os.system('./' + output_name)
os.rm(output_name) or {}
c_file := output_name + '.c'
if !prefs.keep_c && os.exists(c_file) {
os.rm(c_file) or {}
}
exit(ret)
}
}
}
fn split_eval_runtime_args(args []string) ([]string, []string) {
for i, arg in args {
if arg == '--' {
return args[..i], args[i + 1..]
}
}
return args, []string{}
}
fn run_ast_command(args []string) {
if args.len == 0 {
eprintln('Usage: v2 ast <file.v>')
eprintln('Dumps AST to <file>_ast.json and <file>_ast_transformed.json')
exit(1)
}
// Find the vast2 tool relative to vexe
vroot := os.dir(@VEXE)
vast2_path := os.join_path(vroot, 'cmd', 'tools', 'vast2', 'vast2')
// Build vast2 if it doesn't exist
if !os.exists(vast2_path) {
eprintln('Building vast2 tool...')
vast2_source := os.join_path(vroot, 'cmd', 'tools', 'vast2', 'vast2.v')
build_result := os.execute('${@VEXE} ${vast2_source}')
if build_result.exit_code != 0 {
eprintln('Failed to build vast2 tool:')
eprintln(build_result.output)
exit(1)
}
}
// Run vast2 with the provided arguments
cmd := '${vast2_path} ${args.join(' ')}'
result := os.execute(cmd)
print(result.output)
if result.exit_code != 0 {
exit(result.exit_code)
}
}
fn is_test_file(files []string) bool {
for file in files {
if file.ends_with('_test.v') || file.ends_with('_test.vv2') {
return true
}
}
return false
}
fn should_auto_run_test_binary(prefs &pref.Preferences) bool {
if prefs == unsafe { nil } {
return true
}
if !prefs.can_run_target_binary_locally() {
return false
}
return true
}
// get_files extracts source files from args, excluding options and their values
fn get_files(args []string) []string {
return pref.source_files_from_args(args)
}
fn resolve_own_path() string {
arg0 := os.args[0]
if os.is_abs_path(arg0) {
return arg0
}
return os.join_path(os.getwd(), arg0)
}
// detect_vroot walks up from `start` looking for a directory containing vlib/builtin.
fn detect_vroot(start string) string {
mut dir := start
if !os.is_abs_path(dir) {
dir = os.join_path(os.getwd(), dir)
}
for _ in 0 .. 10 {
if os.is_dir(os.join_path(dir, 'vlib', 'builtin')) {
return dir
}
parent := os.dir(dir)
if parent == dir {
break
}
dir = parent
}
return dir
}
fn run_test_self() {
t0 := time.now()
// Resolve the v2 binary's own path. Cannot use @VEXE because when v1
// compiles v2, @VEXE bakes in v1's path instead of v2's.
vexe := resolve_own_path()
// Walk up from the binary to find the repo root (directory containing vlib/builtin).
// This allows running from subdirectories like cmd/v2/.
vroot := detect_vroot(vexe)
v2_dir := os.join_path(vroot, 'cmd', 'v2')
mut all_test_files := []string{}
// Builtin test files
for name in ['array_test.v', 'string_test.v', 'map_test.v'] {
all_test_files << os.join_path(vroot, 'vlib', 'builtin', name)
}
// Math test
all_test_files << os.join_path(vroot, 'vlib', 'math', 'math_test.v')
// Sumtype tests in cmd/v2/
v2_files := os.ls(v2_dir) or { []string{} }
for file in v2_files {
if file.starts_with('test_sumtype') && file.ends_with('.v') {
all_test_files << os.join_path(v2_dir, file)
}
}
// Cleanc regression tests
cleanc_tests_dir := os.join_path(vroot, 'vlib', 'v2', 'gen', 'cleanc', 'tests')
cleanc_files := os.ls(cleanc_tests_dir) or { []string{} }
for file in cleanc_files {
if file.ends_with('.v') && file != 'run_tests.v' {
all_test_files << os.join_path(cleanc_tests_dir, file)
}
}
total := all_test_files.len
mut passed := 0
mut failed := 0
mut failed_files := []string{}
eprintln('---- v2 test-self: ${total} test files ----')
for i, test_file in all_test_files {
short_name := test_file.replace(vroot + '/', '')
t1 := time.now()
// Determine output binary path
base := os.file_name(test_file).all_before_last('.v')
out_bin := os.join_path(os.temp_dir(), 'v2_test_self_${base}')
// Compile (use os.system to avoid pipe deadlocks with popen/GC)
compile_cmd := '${vexe} -o ${out_bin} "${test_file}" > /dev/null 2>&1'
compile_ret := os.system(compile_cmd)
compile_ms := f64(time.since(t1)) / f64(time.millisecond)
if compile_ret != 0 || !os.exists(out_bin) {
failed++
failed_files << short_name
eprintln(' FAIL [${i + 1:4}/${total}] C: ${compile_ms:7.1} ms ${short_name}')
os.rm(out_bin) or {}
os.rm('${out_bin}.c') or {}
continue
}
// Run the compiled binary
t2 := time.now()
run_ret := os.system(out_bin + ' > /dev/null 2>&1')
run_ms := f64(time.since(t2)) / f64(time.millisecond)
if run_ret != 0 {
failed++
failed_files << short_name
eprintln(' FAIL [${i + 1:4}/${total}] C: ${compile_ms:7.1} ms, R: ${run_ms:7.1} ms ${short_name}')
} else {
passed++
eprintln('OK [${i + 1:4}/${total}] C: ${compile_ms:7.1} ms, R: ${run_ms:7.1} ms ${short_name}')
}
os.rm(out_bin) or {}
os.rm('${out_bin}.c') or {}
}
elapsed := time.since(t0)
eprintln('------------------------------------------------------------------------')
eprintln('${passed} passed, ${failed} failed, ${total} total (${elapsed})')
if failed_files.len > 0 {
eprintln('Failed:')
for f in failed_files {
eprintln(' ${f}')
}
}
// Always run the self-compilation chain, even when some tests fail.
// Self-compilation chain: v2 -> v3 -> v4 -> v5
eprintln('')
eprintln('---- v2 self-compilation chain ----')
v2_source := os.join_path(v2_dir, 'v2.v')
backend := 'cleanc'
tmpdir := os.temp_dir()
v3_bin := os.join_path(tmpdir, 'v2_self_v3')
v4_bin := os.join_path(tmpdir, 'v2_self_v4')
v5_bin := os.join_path(tmpdir, 'v2_self_v5')
steps := [
[vexe, v3_bin, 'v2 -> v3'],
[v3_bin, v4_bin, 'v3 -> v4'],
[v4_bin, v5_bin, 'v4 -> v5'],
]
for step in steps {
compiler := step[0]
out := step[1]
label := step[2]
ts := time.now()
cmd := '${compiler} -gc none -o ${out} -backend ${backend} "${v2_source}" > /dev/null 2>&1'
ret := os.system(cmd)
ms := f64(time.since(ts)) / f64(time.millisecond)
if ret != 0 || !os.exists(out) {
eprintln(' FAIL ${label} (${ms:.1} ms)')
// Clean up
for bin in [v3_bin, v4_bin, v5_bin] {
os.rm(bin) or {}
os.rm('${bin}.c') or {}
}
exit(1)
}
eprintln('OK ${label} (${ms:.1} ms)')
}
// Verify that v3 runs and produces expected output
result := os.execute('${v3_bin} 2>&1')
expected := 'At least 1 .v file expected'
if !result.output.contains(expected) {
eprintln("FAIL v3 output check: expected '${expected}', got:")
eprintln(result.output)
for bin in [v3_bin, v4_bin, v5_bin] {
os.rm(bin) or {}
os.rm('${bin}.c') or {}
}
exit(1)
}
eprintln('OK v3 output verified')
// Clean up
for bin in [v3_bin, v4_bin, v5_bin] {
os.rm(bin) or {}
os.rm('${bin}.c') or {}
}
total_elapsed := time.since(t0)
eprintln('')
if failed > 0 {
eprintln('=== SELF-COMPILATION OK, but ${failed} test(s) failed ===')
eprintln('Total time: ${total_elapsed}')
exit(1)
}
eprintln('=== SELF-COMPILATION TEST PASSED ===')
eprintln('Total time: ${total_elapsed}')
}