Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,510 @@
|
||||
// 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.
|
||||
// vtest build: macos
|
||||
//
|
||||
// Differential test harness for the flat-native markused walker.
|
||||
//
|
||||
// Markused decides which functions/methods the backend must emit. A wrong
|
||||
// answer is silent — too few keys → linker errors, too many keys → bloated
|
||||
// binary and slow build. The cursor-based walker that will replace the
|
||||
// recursive walker must produce a bit-identical key set on every program
|
||||
// that exercises the dispatch surface.
|
||||
//
|
||||
// This file provides the comparison machinery and a growing fixture catalog.
|
||||
// The fixtures are intentionally small, one per language construct, so that
|
||||
// when the diff fails the failing fixture immediately localises the bug.
|
||||
// Until mark_used_flat lands, the "differential" tests assert determinism
|
||||
// (legacy == legacy across two runs) — they prove the harness itself works
|
||||
// without depending on the new walker.
|
||||
module markused
|
||||
|
||||
import os
|
||||
import time
|
||||
import v2.ast
|
||||
import v2.parser
|
||||
import v2.pref
|
||||
import v2.token
|
||||
import v2.types
|
||||
|
||||
// ParsedFixture bundles every artefact a markused walker (legacy or flat)
|
||||
// needs to run on a source string. It is returned by parse_fixture so each
|
||||
// test fixture writes the source once and gets everything else for free.
|
||||
struct ParsedFixture {
|
||||
files []ast.File
|
||||
flat ast.FlatAst
|
||||
env &types.Environment
|
||||
}
|
||||
|
||||
fn parse_fixture(src string) ParsedFixture {
|
||||
tmp := '/tmp/v2_markused_diff_${os.getpid()}_${rand_suffix()}.v'
|
||||
os.write_file(tmp, src) or { panic('write_file: ${err}') }
|
||||
defer {
|
||||
os.rm(tmp) or {}
|
||||
}
|
||||
prefs := &pref.Preferences{
|
||||
skip_builtin: true
|
||||
}
|
||||
mut fs := token.FileSet.new()
|
||||
mut par := parser.Parser.new(prefs)
|
||||
files := par.parse_files([tmp], mut fs)
|
||||
flat := ast.flatten_files(files)
|
||||
// Skip the type checker entirely — fixtures use println / etc. which
|
||||
// would require pulling in builtin. The walker treats a near-empty env
|
||||
// as "no type info available" and falls back to syntactic receiver
|
||||
// candidates. Determinism is preserved (env in == env out for both runs)
|
||||
// which is all the differential harness needs.
|
||||
env := types.Environment.new()
|
||||
return ParsedFixture{
|
||||
files: files
|
||||
flat: flat
|
||||
env: env
|
||||
}
|
||||
}
|
||||
|
||||
// rand_suffix produces a short pseudo-unique tag so concurrent test runs
|
||||
// don't trample each other's tempfiles. We avoid pulling in `rand` so the
|
||||
// harness has no dependency on RNG seeding inside the test binary.
|
||||
fn rand_suffix() string {
|
||||
t := u64(time.now().unix_milli()) & 0xFFFFFFFF
|
||||
return '${t:x}'
|
||||
}
|
||||
|
||||
// DiffReport captures the symmetric difference between two used_keys maps.
|
||||
// It is a struct (not a bool) so failing tests can print exactly which keys
|
||||
// drifted, which is essential for debugging walker bugs in 100+-key fixtures.
|
||||
struct DiffReport {
|
||||
only_in_a []string
|
||||
only_in_b []string
|
||||
equal bool
|
||||
}
|
||||
|
||||
fn diff_used_keys(a map[string]bool, b map[string]bool) DiffReport {
|
||||
mut only_a := []string{}
|
||||
mut only_b := []string{}
|
||||
for k, _ in a {
|
||||
if k !in b {
|
||||
only_a << k
|
||||
}
|
||||
}
|
||||
for k, _ in b {
|
||||
if k !in a {
|
||||
only_b << k
|
||||
}
|
||||
}
|
||||
only_a.sort()
|
||||
only_b.sort()
|
||||
return DiffReport{
|
||||
only_in_a: only_a
|
||||
only_in_b: only_b
|
||||
equal: only_a.len == 0 && only_b.len == 0
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_used_keys_equal(label string, a map[string]bool, b map[string]bool) {
|
||||
d := diff_used_keys(a, b)
|
||||
if d.equal {
|
||||
return
|
||||
}
|
||||
eprintln('[${label}] used_keys diverged:')
|
||||
if d.only_in_a.len > 0 {
|
||||
eprintln(' only in A (${d.only_in_a.len}):')
|
||||
for k in d.only_in_a {
|
||||
eprintln(' ${k}')
|
||||
}
|
||||
}
|
||||
if d.only_in_b.len > 0 {
|
||||
eprintln(' only in B (${d.only_in_b.len}):')
|
||||
for k in d.only_in_b {
|
||||
eprintln(' ${k}')
|
||||
}
|
||||
}
|
||||
assert false, '${label}: used_keys differ (see above)'
|
||||
}
|
||||
|
||||
// run_legacy is a thin wrapper around mark_used so individual fixtures read
|
||||
// as "legacy = run_legacy(p); flat = run_flat(p); assert_used_keys_equal".
|
||||
fn run_legacy(p ParsedFixture) map[string]bool {
|
||||
return mark_used(p.files, p.env)
|
||||
}
|
||||
|
||||
fn run_flat(p ParsedFixture) map[string]bool {
|
||||
return mark_used_flat(&p.flat, p.env)
|
||||
}
|
||||
|
||||
// fixtures — each test exercises one slice of the walker dispatch surface.
|
||||
// Adding a new fixture means: (1) write the smallest V program that hits the
|
||||
// dispatch arm, (2) assert legacy determinism here, (3) when flat lands,
|
||||
// flip to assert_used_keys_equal(name, run_legacy(p), run_flat(p)).
|
||||
|
||||
const fixture_plain_fn = 'module main
|
||||
|
||||
fn add(a int, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println(add(1, 2))
|
||||
}
|
||||
'
|
||||
|
||||
const fixture_method_call = 'module main
|
||||
|
||||
struct Counter {
|
||||
mut:
|
||||
n int
|
||||
}
|
||||
|
||||
fn (mut c Counter) inc() {
|
||||
c.n++
|
||||
}
|
||||
|
||||
fn (c Counter) value() int {
|
||||
return c.n
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut c := Counter{}
|
||||
c.inc()
|
||||
println(c.value())
|
||||
}
|
||||
'
|
||||
|
||||
const fixture_infix_operator = 'module main
|
||||
|
||||
struct Vec {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
fn (a Vec) + (b Vec) Vec {
|
||||
return Vec{a.x + b.x, a.y + b.y}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
v := Vec{1, 2} + Vec{3, 4}
|
||||
println(v.x)
|
||||
}
|
||||
'
|
||||
|
||||
const fixture_array_and_loop = 'module main
|
||||
|
||||
fn sum(xs []int) int {
|
||||
mut s := 0
|
||||
for x in xs {
|
||||
s += x
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
fn main() {
|
||||
xs := [1, 2, 3]
|
||||
println(sum(xs))
|
||||
}
|
||||
'
|
||||
|
||||
const fixture_match_expr = 'module main
|
||||
|
||||
fn describe(n int) string {
|
||||
return match n {
|
||||
0 { "zero" }
|
||||
1 { "one" }
|
||||
else { "many" }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println(describe(1))
|
||||
}
|
||||
'
|
||||
|
||||
const fixture_if_guard = 'module main
|
||||
|
||||
fn maybe(n int) ?int {
|
||||
if n > 0 {
|
||||
return n
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if v := maybe(3) {
|
||||
println(v)
|
||||
}
|
||||
}
|
||||
'
|
||||
|
||||
const fixture_global_init = 'module main
|
||||
|
||||
__global g_counter = 0
|
||||
|
||||
fn bump() {
|
||||
g_counter++
|
||||
}
|
||||
|
||||
fn main() {
|
||||
bump()
|
||||
println(g_counter)
|
||||
}
|
||||
'
|
||||
|
||||
const fixture_global_init_calls_fn = 'module main
|
||||
|
||||
struct Value {
|
||||
n int
|
||||
}
|
||||
|
||||
fn make_value() Value {
|
||||
return Value{n: 1}
|
||||
}
|
||||
|
||||
__global state = make_value()
|
||||
|
||||
fn main() {
|
||||
_ = state.n
|
||||
}
|
||||
'
|
||||
|
||||
// fixture_generic_fn exercises walker dispatch on generic function calls.
|
||||
// The walker must mark both the generic base and any concrete bindings
|
||||
// reachable through env.generic_types — the closest analogue of a real
|
||||
// codebase\'s "[]T → []int" specialization path.
|
||||
const fixture_generic_fn = 'module main
|
||||
|
||||
fn identity[T](x T) T {
|
||||
return x
|
||||
}
|
||||
|
||||
fn main() {
|
||||
a := identity[int](42)
|
||||
b := identity[string]("hi")
|
||||
println(a)
|
||||
println(b)
|
||||
}
|
||||
'
|
||||
|
||||
// fixture_closure exercises closure capture: the walker must not lose
|
||||
// reachability when the called fn is the value of a local variable that
|
||||
// itself was assigned a fn literal closing over outer state.
|
||||
const fixture_closure = 'module main
|
||||
|
||||
fn make_counter() fn () int {
|
||||
mut n := 0
|
||||
return fn [mut n] () int {
|
||||
n++
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
c := make_counter()
|
||||
println(c())
|
||||
println(c())
|
||||
}
|
||||
'
|
||||
|
||||
// fixture_sumtype_is_as covers the .is / as / smart-cast dispatch arms
|
||||
// in walk_expr that resolve variant-specific method calls.
|
||||
const fixture_sumtype_is_as = 'module main
|
||||
|
||||
type Shape = Circle | Square
|
||||
|
||||
struct Circle {
|
||||
r f64
|
||||
}
|
||||
|
||||
struct Square {
|
||||
side f64
|
||||
}
|
||||
|
||||
fn area(s Shape) f64 {
|
||||
return match s {
|
||||
Circle { 3.14 * s.r * s.r }
|
||||
Square { s.side * s.side }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
c := Shape(Circle{r: 2.0})
|
||||
if c is Circle {
|
||||
println(c.r)
|
||||
}
|
||||
println(area(c))
|
||||
}
|
||||
'
|
||||
|
||||
// fixture_defer_and_map exercises defer statement walking plus map literal
|
||||
// init / index / "in" lookup — three distinct walk_stmt / walk_expr arms
|
||||
// that share no helpers with the simpler fixtures.
|
||||
const fixture_defer_and_map = 'module main
|
||||
|
||||
fn lookup() int {
|
||||
mut m := {"a": 1, "b": 2}
|
||||
defer {
|
||||
m["a"] = 9
|
||||
}
|
||||
if "b" in m {
|
||||
return m["b"]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println(lookup())
|
||||
}
|
||||
'
|
||||
|
||||
// fixture_string_interp_and_array_slice covers string-interpolation
|
||||
// expressions and array slicing — both have their own walk_expr arms
|
||||
// with their own sub-expression edges. Uses a raw string (r'...') so
|
||||
// the outer V parser does not try to interpolate the inner ${...}.
|
||||
const fixture_string_interp_and_array_slice = r'module main
|
||||
|
||||
fn slice_sum(xs []int) string {
|
||||
tail := xs[1..]
|
||||
mut s := 0
|
||||
for x in tail {
|
||||
s += x
|
||||
}
|
||||
return "sum=${s} count=${tail.len}"
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println(slice_sum([10, 20, 30, 40]))
|
||||
}
|
||||
'
|
||||
|
||||
fn all_fixtures() []string {
|
||||
return [
|
||||
fixture_plain_fn,
|
||||
fixture_method_call,
|
||||
fixture_infix_operator,
|
||||
fixture_array_and_loop,
|
||||
fixture_match_expr,
|
||||
fixture_if_guard,
|
||||
fixture_global_init,
|
||||
fixture_global_init_calls_fn,
|
||||
fixture_generic_fn,
|
||||
fixture_closure,
|
||||
fixture_sumtype_is_as,
|
||||
fixture_defer_and_map,
|
||||
fixture_string_interp_and_array_slice,
|
||||
]
|
||||
}
|
||||
|
||||
// --- flat-vs-legacy diff tests ---
|
||||
// These are the real safety net for the markused migration. Each PR that
|
||||
// rewrites part of mark_used_flat must keep every fixture green. While the
|
||||
// shim still calls flat.to_files() + the legacy walker, both runs must
|
||||
// trivially produce the same map — once the body diverges, these fixtures
|
||||
// catch regressions immediately and the DiffReport pinpoints which key set
|
||||
// drifted.
|
||||
|
||||
fn test_flat_matches_legacy_plain_fn() {
|
||||
p := parse_fixture(fixture_plain_fn)
|
||||
assert_used_keys_equal('plain_fn', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_method_call() {
|
||||
p := parse_fixture(fixture_method_call)
|
||||
assert_used_keys_equal('method_call', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_infix_operator() {
|
||||
p := parse_fixture(fixture_infix_operator)
|
||||
assert_used_keys_equal('infix_operator', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_array_and_loop() {
|
||||
p := parse_fixture(fixture_array_and_loop)
|
||||
assert_used_keys_equal('array_and_loop', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_match_expr() {
|
||||
p := parse_fixture(fixture_match_expr)
|
||||
assert_used_keys_equal('match_expr', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_if_guard() {
|
||||
p := parse_fixture(fixture_if_guard)
|
||||
assert_used_keys_equal('if_guard', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_global_init() {
|
||||
p := parse_fixture(fixture_global_init)
|
||||
assert_used_keys_equal('global_init', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_global_init_calls_fn() {
|
||||
p := parse_fixture(fixture_global_init_calls_fn)
|
||||
assert_used_keys_equal('global_init_calls_fn', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_generic_fn() {
|
||||
p := parse_fixture(fixture_generic_fn)
|
||||
assert_used_keys_equal('generic_fn', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_closure() {
|
||||
p := parse_fixture(fixture_closure)
|
||||
assert_used_keys_equal('closure', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_sumtype_is_as() {
|
||||
p := parse_fixture(fixture_sumtype_is_as)
|
||||
assert_used_keys_equal('sumtype_is_as', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_defer_and_map() {
|
||||
p := parse_fixture(fixture_defer_and_map)
|
||||
assert_used_keys_equal('defer_and_map', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
fn test_flat_matches_legacy_string_interp_and_array_slice() {
|
||||
p := parse_fixture(fixture_string_interp_and_array_slice)
|
||||
assert_used_keys_equal('string_interp_and_array_slice', run_legacy(p), run_flat(p))
|
||||
}
|
||||
|
||||
// test_flat_is_deterministic — two runs of mark_used_flat on the same fixture
|
||||
// must produce the same map. Guards against accidental nondeterminism
|
||||
// (e.g. iteration order over a map of types) creeping in as the body
|
||||
// of the shim is progressively replaced.
|
||||
fn test_flat_is_deterministic_all_fixtures() {
|
||||
for i, src in all_fixtures() {
|
||||
p := parse_fixture(src)
|
||||
assert_used_keys_equal('fixture-${i}-determinism', run_flat(p), run_flat(p))
|
||||
}
|
||||
}
|
||||
|
||||
// test_fixtures_produce_nonempty_results guards against silently broken
|
||||
// harness setup: every fixture has at least main() reachable, so a zero-key
|
||||
// result means our parse/typecheck/walk pipeline broke before we even got
|
||||
// to compare anything.
|
||||
fn test_fixtures_produce_nonempty_results() {
|
||||
for i, src in all_fixtures() {
|
||||
p := parse_fixture(src)
|
||||
keys := run_legacy(p)
|
||||
assert keys.len > 0, 'fixture #${i} produced empty used_keys — pipeline broken before walker'
|
||||
}
|
||||
}
|
||||
|
||||
// test_flat_ast_round_trips_every_fixture verifies the cursor-side machinery
|
||||
// works end-to-end on the fixture surface: every fixture parses, flattens,
|
||||
// and is reachable via the cursor API from each file root. This is what
|
||||
// the future flat walker will consume, so it must not blow up here.
|
||||
fn test_flat_ast_round_trips_every_fixture() {
|
||||
for i, src in all_fixtures() {
|
||||
p := parse_fixture(src)
|
||||
assert p.flat.files.len > 0, 'fixture #${i}: flat has no files'
|
||||
for fi in 0 .. p.flat.files.len {
|
||||
fc := p.flat.file_cursor(fi)
|
||||
assert fc.root().is_valid(), 'fixture #${i} file ${fi}: invalid root'
|
||||
assert fc.root().kind() == .file, 'fixture #${i} file ${fi}: root not .file'
|
||||
// Touch each top-level list so an aux_list miswiring trips here
|
||||
// rather than deep inside the future walker port.
|
||||
_ = fc.attrs().len()
|
||||
_ = fc.imports().len()
|
||||
_ = fc.stmts().len()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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 markused
|
||||
|
||||
import v2.ast
|
||||
import v2.types
|
||||
|
||||
// mark_used_flat is the flat-input entry point for the markused pass. It
|
||||
// accepts a FlatAst — the canonical post-parse representation — instead of
|
||||
// a rehydrated []ast.File.
|
||||
//
|
||||
// The collect_defs phase reads each file's imports and top-level stmts via
|
||||
// FlatAst.read_file_imports / FileCursor.stmts, so the per-file File struct
|
||||
// (with its attribute/import/stmt slices and selector_names map) is never
|
||||
// materialized. Function bodies are still decoded as legacy ast.Stmt and
|
||||
// stored in FnInfo for walk_collected — that phase is the next slice to
|
||||
// port. The differential harness in markused_diff_test.v guards
|
||||
// bit-for-bit equality with the legacy walker at every step.
|
||||
pub fn mark_used_flat(flat &ast.FlatAst, env &types.Environment) map[string]bool {
|
||||
return mark_used_flat_with_options(flat, env, MarkUsedOptions{})
|
||||
}
|
||||
|
||||
pub fn mark_used_flat_with_options(flat &ast.FlatAst, env &types.Environment, opts MarkUsedOptions) map[string]bool {
|
||||
mut w := new_walker([]ast.File{}, env, opts)
|
||||
w.collect_defs_from_flat(flat)
|
||||
return w.walk_collected_from_flat(flat)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user