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

File diff suppressed because it is too large. Load diff
+778
View File
@@ -0,0 +1,778 @@
// 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 ast
import v2.token
// Cursor is a lightweight, by-value handle pointing at one FlatNode inside a
// FlatAst. Consumers walk the AST via `match c.kind() {}` and descend via
// `c.edge(i)` / `c.list_at(i)` without rehydrating to ast.Stmt / ast.Expr.
//
// A Cursor is 16 bytes: an &FlatAst pointer plus a FlatNodeId. It is safe to
// copy, store in arrays, and pass by value. An "invalid" Cursor (id < 0) is
// used as a sentinel where the legacy AST would use empty_stmt / empty_expr.
pub struct Cursor {
pub:
flat &FlatAst = unsafe { nil }
id FlatNodeId
}
@[inline]
pub fn (c Cursor) is_valid() bool {
return c.flat != unsafe { nil } && c.id >= 0 && c.id < c.flat.nodes.len
}
@[inline]
pub fn (c Cursor) kind() FlatNodeKind {
return c.flat.nodes[c.id].kind
}
@[inline]
pub fn (c Cursor) pos() token.Pos {
return c.flat.nodes[c.id].pos
}
@[inline]
pub fn (c Cursor) flags() u8 {
return c.flat.nodes[c.id].flags
}
@[inline]
pub fn (c Cursor) flag(bit u8) bool {
return (c.flat.nodes[c.id].flags & bit) != 0
}
@[inline]
pub fn (c Cursor) aux() u16 {
return c.flat.nodes[c.id].aux
}
@[inline]
pub fn (c Cursor) extra_int() int {
return c.flat.nodes[c.id].extra
}
// extra_str interprets the node's `extra` field as an interned string id.
// Use only on kinds where the schema documents `extra` as a string slot
// (e.g. stmt_directive's value, stmt_import's alias).
@[inline]
pub fn (c Cursor) extra_str() string {
return c.flat.string_at(c.flat.nodes[c.id].extra)
}
// name returns the interned primary string (FlatNode.name_id) for this node.
// Most kinds use this for identifiers, type names, fn names, etc.
@[inline]
pub fn (c Cursor) name() string {
return c.flat.string_at(c.flat.nodes[c.id].name_id)
}
@[inline]
pub fn (c Cursor) name_id() int {
return c.flat.nodes[c.id].name_id
}
// ident reads an expr_ident cursor directly into an Ident.
pub fn (c Cursor) ident() Ident {
if !c.is_valid() || c.kind() != .expr_ident {
return Ident{}
}
return Ident{
pos: c.pos()
name: c.name()
}
}
// import_stmt reads a stmt_import cursor directly into an ImportStmt.
pub fn (c Cursor) import_stmt() ImportStmt {
if !c.is_valid() || c.kind() != .stmt_import {
return ImportStmt{}
}
mut symbols := []Expr{cap: c.edge_count()}
for i in 0 .. c.edge_count() {
sym := c.edge(i)
if sym.kind() == .expr_ident {
symbols << Expr(sym.ident())
}
}
return ImportStmt{
name: c.name()
alias: c.extra_str()
is_aliased: c.flag(flag_is_aliased)
symbols: symbols
}
}
// fn_decl_signature reads a stmt_fn_decl cursor into a body-less FnDecl. This
// mirrors FlatAst.decode_fn_decl_signature without going through FlatReader.
pub fn (c Cursor) fn_decl_signature() FnDecl {
if !c.is_valid() || c.kind() != .stmt_fn_decl {
return FnDecl{}
}
return FnDecl{
attributes: attrs_from_cursor(c.list_at(2))
is_public: c.flag(flag_is_public)
is_method: c.flag(flag_is_method)
is_static: c.flag(flag_is_static)
receiver: parameter_from_cursor(c.edge(0))
language: unsafe { Language(int(c.aux())) }
name: c.name()
typ: fn_type_from_cursor(c.edge(1))
pos: c.pos()
}
}
// fn_decl reads a stmt_fn_decl cursor into a legacy FnDecl. Unlike
// fn_decl_signature, this materializes the body statements for consumers that
// still use legacy statement walkers internally.
pub fn (c Cursor) fn_decl() FnDecl {
if !c.is_valid() || c.kind() != .stmt_fn_decl {
return FnDecl{}
}
signature := c.fn_decl_signature()
return FnDecl{
attributes: signature.attributes
is_public: signature.is_public
is_method: signature.is_method
is_static: signature.is_static
receiver: signature.receiver
language: signature.language
name: signature.name
typ: signature.typ
stmts: c.list_at(3).stmts()
pos: signature.pos
}
}
// stmt reads a cursor through FlatAst.decode_stmt. This is the escape hatch
// for legacy statement walkers; prefer cursor-specific readers when possible.
pub fn (c Cursor) stmt() Stmt {
if !c.is_valid() {
return empty_stmt
}
return c.flat.decode_stmt(c.id)
}
// expr reads a cursor through FlatAst.decode_expr. This is the escape hatch
// for legacy expression walkers; prefer cursor-specific readers when possible.
pub fn (c Cursor) expr() Expr {
if !c.is_valid() {
return empty_expr
}
return c.flat.decode_expr(c.id)
}
// type_expr reads a type-expression cursor into the legacy Expr shape used by
// signature consumers. It is intentionally narrower than FlatReader.read_expr:
// non-type payloads such as field defaults or statement bodies stay omitted.
pub fn (c Cursor) type_expr() Expr {
if !c.is_valid() {
return empty_expr
}
match c.kind() {
.expr_empty {
return empty_expr
}
.expr_basic_literal {
return Expr(BasicLiteral{
kind: unsafe { token.Token(int(c.aux())) }
value: c.name()
pos: c.pos()
})
}
.expr_ident {
return Expr(c.ident())
}
.expr_lifetime {
return Expr(LifetimeExpr{
name: c.name()
pos: c.pos()
})
}
.expr_modifier {
return Expr(ModifierExpr{
kind: unsafe { token.Token(int(c.aux())) }
expr: c.edge(0).type_expr()
pos: c.pos()
})
}
.expr_prefix {
return Expr(PrefixExpr{
op: unsafe { token.Token(int(c.aux())) }
expr: c.edge(0).type_expr()
pos: c.pos()
})
}
.expr_selector {
rhs := c.edge(1)
return Expr(SelectorExpr{
lhs: c.edge(0).type_expr()
rhs: Ident{
name: rhs.name()
pos: rhs.pos()
}
pos: c.pos()
})
}
.expr_generic_args {
return Expr(GenericArgs{
lhs: c.edge(0).type_expr()
args: type_exprs_from_edges(c, 1)
pos: c.pos()
})
}
.expr_generic_arg_or_index {
return Expr(GenericArgOrIndexExpr{
lhs: c.edge(0).type_expr()
expr: c.edge(1).type_expr()
pos: c.pos()
})
}
.typ_anon_struct {
return Expr(Type(AnonStructType{
generic_params: c.list_at(0).type_exprs()
embedded: c.list_at(1).type_exprs()
fields: field_decl_type_list(c.list_at(2))
}))
}
.typ_array_fixed {
return Expr(Type(ArrayFixedType{
len: c.edge(0).type_expr()
elem_type: c.edge(1).type_expr()
}))
}
.typ_array {
return Expr(Type(ArrayType{
elem_type: c.edge(0).type_expr()
}))
}
.typ_channel {
return Expr(Type(ChannelType{
cap: c.edge(0).type_expr()
elem_type: c.edge(1).type_expr()
}))
}
.typ_fn {
return Expr(Type(FnType{
generic_params: c.list_at(0).type_exprs()
params: parameter_list_from_cursor(c.list_at(1))
return_type: c.edge(2).type_expr()
}))
}
.typ_generic {
return Expr(Type(GenericType{
name: c.edge(0).type_expr()
params: type_exprs_from_edges(c, 1)
}))
}
.typ_map {
return Expr(Type(MapType{
key_type: c.edge(0).type_expr()
value_type: c.edge(1).type_expr()
}))
}
.typ_nil {
return Expr(Type(NilType{}))
}
.typ_none {
return Expr(Type(NoneType{}))
}
.typ_option {
return Expr(Type(OptionType{
base_type: c.edge(0).type_expr()
}))
}
.typ_pointer {
return Expr(Type(PointerType{
base_type: c.edge(0).type_expr()
lifetime: c.name()
}))
}
.typ_result {
return Expr(Type(ResultType{
base_type: c.edge(0).type_expr()
}))
}
.typ_thread {
return Expr(Type(ThreadType{
elem_type: c.edge(0).type_expr()
}))
}
.typ_tuple {
return Expr(Type(TupleType{
types: type_exprs_from_edges(c, 0)
}))
}
else {
return empty_expr
}
}
}
@[inline]
pub fn (c Cursor) edge_count() int {
return c.flat.nodes[c.id].edge_count
}
// edge returns a Cursor over the i-th direct child edge of this node. Out-of-
// range indices return an invalid Cursor (id == invalid_flat_node_id), which
// callers can detect via `c.is_valid()`. This matches FlatReader's behaviour
// where `r.edge(n, i)` may return invalid_flat_node_id for missing slots.
@[inline]
pub fn (c Cursor) edge(i int) Cursor {
return Cursor{
flat: c.flat
id: c.flat.child_at(c.id, i)
}
}
// list_at treats edge `i` as a reference to an `aux_list` node and returns a
// CursorList over its children. The flat schema represents list-typed fields
// (e.g. StructDecl.fields, FnDecl.stmts, EnumDecl.attributes) as a single
// edge to an aux_list whose children are the actual list items. Use `edge()`
// for fields stored as direct child edges of the parent (e.g. AssignStmt's
// LHS/RHS, AssertStmt's expr/extra).
@[inline]
pub fn (c Cursor) list_at(edge_i int) CursorList {
return CursorList{
flat: c.flat
parent_id: c.flat.child_at(c.id, edge_i)
}
}
// for_body_list views a stmt_for node's body as a CursorList. The stmt_for flat
// layout is edges [init, cond, post, body_stmt_0, body_stmt_1, ...] — the body
// is trailing edges of the for-node itself, NOT a separate aux_list — so the
// body list is this node's children from edge index 3 onward.
@[inline]
pub fn (c Cursor) for_body_list() CursorList {
return CursorList{
flat: c.flat
parent_id: c.id
offset: 3
}
}
// CursorList is a view over the children of an `aux_list` node. It is the
// flat equivalent of `[]ast.Stmt` / `[]ast.Expr` / `[]FieldDecl` etc., except
// no slice is materialised — `at(i)` decodes one child at a time.
pub struct CursorList {
pub:
flat &FlatAst = unsafe { nil }
parent_id FlatNodeId
// offset skips the first `offset` child edges of `parent_id`. Default 0 (the
// whole child list). Lets a trailing edge range be viewed as a list when the
// items are direct child edges of a node rather than an aux_list — e.g. a
// ForStmt body lives in edges [3..] of the stmt_for node itself (see
// `Cursor.for_body_list`).
offset int
}
@[inline]
pub fn (l CursorList) len() int {
if l.flat == unsafe { nil } || l.parent_id < 0 || l.parent_id >= l.flat.nodes.len {
return 0
}
return l.flat.nodes[l.parent_id].edge_count - l.offset
}
@[inline]
pub fn (l CursorList) at(i int) Cursor {
return Cursor{
flat: l.flat
id: l.flat.child_at(l.parent_id, l.offset + i)
}
}
// type_exprs reads every item in a cursor list through Cursor.type_expr.
pub fn (l CursorList) type_exprs() []Expr {
mut out := []Expr{cap: l.len()}
for i in 0 .. l.len() {
out << l.at(i).type_expr()
}
return out
}
// stmts reads every item in a cursor list through FlatAst.decode_stmt. This is
// the escape hatch for legacy statement walkers; prefer cursor-specific
// readers when the caller only needs a declaration signature or metadata.
pub fn (l CursorList) stmts() []Stmt {
mut out := []Stmt{cap: l.len()}
for i in 0 .. l.len() {
c := l.at(i)
if !c.is_valid() {
continue
}
out << c.stmt()
}
return out
}
// attribute_expr reads the small expression subset used inside attributes.
// Attribute payloads are normally identifiers or strings; type_expr handles
// the type-like fallback cases without opening the full FlatReader.
pub fn (c Cursor) attribute_expr() Expr {
if !c.is_valid() {
return empty_expr
}
match c.kind() {
.expr_empty {
return empty_expr
}
.expr_basic_literal {
return Expr(BasicLiteral{
kind: unsafe { token.Token(int(c.aux())) }
value: c.name()
pos: c.pos()
})
}
.expr_ident {
return Expr(c.ident())
}
.expr_string {
return Expr(StringLiteral{
kind: unsafe { StringLiteralKind(int(c.aux())) }
value: c.name()
pos: c.pos()
})
}
.expr_selector {
rhs := c.edge(1)
return Expr(SelectorExpr{
lhs: c.edge(0).attribute_expr()
rhs: Ident{
name: rhs.name()
pos: rhs.pos()
}
pos: c.pos()
})
}
else {
return c.type_expr()
}
}
}
// attribute reads an aux_attribute cursor into the legacy Attribute shape.
pub fn (c Cursor) attribute() Attribute {
if !c.is_valid() || c.kind() != .aux_attribute {
return Attribute{}
}
return Attribute{
name: c.name()
value: c.edge(0).attribute_expr()
// comptime_cond (`@[if cond ?]`) can be an arbitrary expression, not the
// ident/string subset attribute_expr handles — decode it fully (mirrors
// FlatReader.read_attribute), else complex conditions silently become
// empty_expr and `@[if ...]` functions are never elided.
comptime_cond: c.edge(1).expr()
pos: c.pos()
}
}
// attributes reads every aux_attribute in a cursor list.
pub fn (l CursorList) attributes() []Attribute {
mut out := []Attribute{cap: l.len()}
for i in 0 .. l.len() {
attr := l.at(i)
if !attr.is_valid() {
continue
}
out << attr.attribute()
}
return out
}
// import_stmts reads every stmt_import in a cursor list.
pub fn (l CursorList) import_stmts() []ImportStmt {
mut out := []ImportStmt{cap: l.len()}
for i in 0 .. l.len() {
imp := l.at(i)
if !imp.is_valid() || imp.kind() != .stmt_import {
continue
}
out << imp.import_stmt()
}
return out
}
// field_init reads an aux_field_init cursor. Field values are still legacy
// expression consumers today, so this materializes only the value expression,
// not the parent declaration.
pub fn (c Cursor) field_init() FieldInit {
if !c.is_valid() || c.kind() != .aux_field_init {
return FieldInit{}
}
value_c := c.edge(0)
return FieldInit{
name: c.name()
value: value_c.expr()
}
}
// field_inits reads every aux_field_init in a cursor list.
pub fn (l CursorList) field_inits() []FieldInit {
mut out := []FieldInit{cap: l.len()}
for i in 0 .. l.len() {
out << l.at(i).field_init()
}
return out
}
// field_decl reads an aux_field_decl cursor. Set decode_value to false when a
// caller only needs declaration metadata and type expressions.
pub fn (c Cursor) field_decl(decode_value bool) FieldDecl {
if !c.is_valid() || c.kind() != .aux_field_decl {
return FieldDecl{}
}
value_c := c.edge(1)
return FieldDecl{
name: c.name()
typ: c.edge(0).type_expr()
value: if decode_value { value_c.expr() } else { empty_expr }
attributes: c.list_at(2).attributes()
is_public: c.flag(flag_is_public)
is_mut: c.flag(flag_is_mut)
is_module_mut: c.flag(flag_field_is_module_mut)
is_interface_method: c.flag(flag_field_is_interface_method)
}
}
// field_decls reads every aux_field_decl in a cursor list.
pub fn (l CursorList) field_decls(decode_values bool) []FieldDecl {
mut out := []FieldDecl{cap: l.len()}
for i in 0 .. l.len() {
out << l.at(i).field_decl(decode_values)
}
return out
}
// const_decl reads a stmt_const_decl cursor.
pub fn (c Cursor) const_decl() ConstDecl {
if !c.is_valid() || c.kind() != .stmt_const_decl {
return ConstDecl{}
}
return ConstDecl{
is_public: c.flag(flag_is_public)
fields: c.list_at(0).field_inits()
}
}
// enum_decl reads a stmt_enum_decl cursor. Set decode_values to false when
// only field names/attributes are needed.
pub fn (c Cursor) enum_decl(decode_values bool) EnumDecl {
if !c.is_valid() || c.kind() != .stmt_enum_decl {
return EnumDecl{}
}
return EnumDecl{
attributes: c.list_at(1).attributes()
is_public: c.flag(flag_is_public)
name: c.name()
as_type: c.edge(0).type_expr()
fields: c.list_at(2).field_decls(decode_values)
}
}
// global_decl reads a stmt_global_decl cursor. Set decode_values to false when
// only field metadata and declared types are needed.
pub fn (c Cursor) global_decl(decode_values bool) GlobalDecl {
if !c.is_valid() || c.kind() != .stmt_global_decl {
return GlobalDecl{}
}
return GlobalDecl{
attributes: c.list_at(0).attributes()
fields: c.list_at(1).field_decls(decode_values)
is_public: c.flag(flag_is_public)
}
}
// interface_decl reads a stmt_interface_decl cursor.
pub fn (c Cursor) interface_decl() InterfaceDecl {
if !c.is_valid() || c.kind() != .stmt_interface_decl {
return InterfaceDecl{}
}
return InterfaceDecl{
is_public: c.flag(flag_is_public)
attributes: c.list_at(0).attributes()
name: c.name()
generic_params: c.list_at(1).type_exprs()
embedded: c.list_at(2).type_exprs()
fields: c.list_at(3).field_decls(true)
}
}
// struct_decl reads a stmt_struct_decl cursor.
pub fn (c Cursor) struct_decl() StructDecl {
if !c.is_valid() || c.kind() != .stmt_struct_decl {
return StructDecl{}
}
return StructDecl{
attributes: c.list_at(0).attributes()
is_public: c.flag(flag_is_public)
is_union: c.flag(flag_is_union)
implements: c.list_at(1).type_exprs()
embedded: c.list_at(2).type_exprs()
language: unsafe { Language(int(c.aux())) }
name: c.name()
generic_params: c.list_at(3).type_exprs()
fields: c.list_at(4).field_decls(true)
pos: c.pos()
}
}
// type_decl reads a stmt_type_decl cursor.
pub fn (c Cursor) type_decl() TypeDecl {
if !c.is_valid() || c.kind() != .stmt_type_decl {
return TypeDecl{}
}
return TypeDecl{
is_public: c.flag(flag_is_public)
language: unsafe { Language(int(c.aux())) }
name: c.name()
generic_params: c.list_at(2).type_exprs()
base_type: c.edge(0).type_expr()
variants: c.list_at(3).type_exprs()
}
}
fn attrs_from_cursor(list CursorList) []Attribute {
return list.attributes()
}
fn fn_type_from_cursor(c Cursor) FnType {
if !c.is_valid() || c.kind() != .typ_fn {
return FnType{}
}
return FnType{
generic_params: c.list_at(0).type_exprs()
params: parameter_list_from_cursor(c.list_at(1))
return_type: c.edge(2).type_expr()
}
}
fn type_exprs_from_edges(c Cursor, start int) []Expr {
if !c.is_valid() || start >= c.edge_count() {
return []Expr{}
}
mut out := []Expr{cap: c.edge_count() - start}
for i in start .. c.edge_count() {
out << c.edge(i).type_expr()
}
return out
}
fn parameter_from_cursor(c Cursor) Parameter {
if !c.is_valid() {
return Parameter{}
}
return Parameter{
name: c.name()
typ: c.edge(0).type_expr()
is_mut: c.flag(flag_is_mut)
pos: c.pos()
}
}
fn parameter_list_from_cursor(list CursorList) []Parameter {
mut out := []Parameter{cap: list.len()}
for i in 0 .. list.len() {
out << parameter_from_cursor(list.at(i))
}
return out
}
fn field_decl_type_list(list CursorList) []FieldDecl {
mut out := []FieldDecl{cap: list.len()}
for i in 0 .. list.len() {
field := list.at(i)
if !field.is_valid() {
continue
}
out << FieldDecl{
name: field.name()
typ: field.edge(0).type_expr()
is_public: field.flag(flag_is_public)
is_mut: field.flag(flag_is_mut)
is_module_mut: field.flag(flag_field_is_module_mut)
is_interface_method: field.flag(flag_field_is_interface_method)
}
}
return out
}
// FileCursor is a typed wrapper over a FlatFile entry. It exposes the
// file-level metadata (name, mod, selector_names) and the three top-level
// child lists (attributes, imports, stmts) without rehydrating a full
// ast.File.
pub struct FileCursor {
pub:
flat &FlatAst = unsafe { nil }
idx int // index into flat.files
}
// file_cursor returns a FileCursor over the i-th FlatFile in this FlatAst.
@[inline]
pub fn (flat &FlatAst) file_cursor(idx int) FileCursor {
return FileCursor{
flat: unsafe { flat }
idx: idx
}
}
// file_cursors returns one FileCursor per FlatFile. Allocates an int-sized
// array; for hot loops prefer `for i in 0 .. flat.files.len { flat.file_cursor(i) }`.
pub fn (flat &FlatAst) file_cursors() []FileCursor {
mut out := []FileCursor{cap: flat.files.len}
for i in 0 .. flat.files.len {
out << flat.file_cursor(i)
}
return out
}
@[inline]
pub fn (fc FileCursor) flat_file() FlatFile {
return fc.flat.files[fc.idx]
}
// root returns a Cursor positioned at this file's root FlatNode (kind == .file).
@[inline]
pub fn (fc FileCursor) root() Cursor {
return Cursor{
flat: fc.flat
id: fc.flat.files[fc.idx].file_id
}
}
@[inline]
pub fn (fc FileCursor) name() string {
return fc.flat.string_at(fc.flat.files[fc.idx].name_idx)
}
@[inline]
pub fn (fc FileCursor) mod() string {
return fc.flat.string_at(fc.flat.files[fc.idx].mod_idx)
}
@[inline]
pub fn (fc FileCursor) selector_names() map[int]string {
return fc.flat.files[fc.idx].selector_names
}
// attrs returns the file's top-level attribute list (edge 0 of the file node).
@[inline]
pub fn (fc FileCursor) attrs() CursorList {
return fc.root().list_at(0)
}
// imports returns the file's top-level import list (edge 1 of the file node).
@[inline]
pub fn (fc FileCursor) imports() CursorList {
return fc.root().list_at(1)
}
// stmts returns the file's top-level statement list (edge 2 of the file node).
@[inline]
pub fn (fc FileCursor) stmts() CursorList {
return fc.root().list_at(2)
}
@@ -0,0 +1,362 @@
// 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 ast
// Tests for the Cursor API live alongside the implementation since they
// operate on hand-built FlatAst fixtures — building them via the parser
// would introduce a circular dep (parser depends on ast). The fixtures
// mirror the shape that parse_file emits for a few representative inputs.
// build_minimal_file constructs a FlatAst with one file containing:
// module foo
// fn add(a int, b int) int { return a + b }
// using the FlatBuilder API directly. We then exercise Cursor over it.
fn build_minimal_file() FlatAst {
mut b := new_flat_builder()
src := File{
name: 'inline_minimal.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(FnDecl{
name: 'add'
typ: FnType{
params: [
Parameter{
name: 'a'
typ: Expr(Ident{
name: 'int'
})
},
Parameter{
name: 'b'
typ: Expr(Ident{
name: 'int'
})
},
]
return_type: Expr(Ident{
name: 'int'
})
}
stmts: [
Stmt(ReturnStmt{
exprs: [
Expr(InfixExpr{
op: .plus
lhs: Expr(Ident{
name: 'a'
})
rhs: Expr(Ident{
name: 'b'
})
}),
]
}),
]
}),
]
}
b.append_file(src)
return b.flat
}
fn test_file_cursor_basics() {
flat := build_minimal_file()
assert flat.files.len == 1
fc := flat.file_cursor(0)
assert fc.name() == 'inline_minimal.v'
assert fc.mod() == 'foo'
root := fc.root()
assert root.is_valid()
assert root.kind() == .file
}
fn test_file_cursor_stmts_iteration() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
stmts := fc.stmts()
// module decl + fn decl
assert stmts.len() == 2
assert stmts.at(0).kind() == .stmt_module
assert stmts.at(0).name() == 'foo'
assert stmts.at(1).kind() == .stmt_fn_decl
assert stmts.at(1).name() == 'add'
}
fn test_fn_decl_flags_and_body() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
fn_cur := fc.stmts().at(1)
assert fn_cur.kind() == .stmt_fn_decl
// is_method/is_public/is_static all false for plain `fn add(...)`
assert !fn_cur.flag(flag_is_method)
assert !fn_cur.flag(flag_is_public)
assert !fn_cur.flag(flag_is_static)
// edge layout per read_stmt: 0=receiver, 1=typ, 2=attrs, 3=stmts(aux_list)
stmts := fn_cur.list_at(3)
assert stmts.len() == 1
ret := stmts.at(0)
assert ret.kind() == .stmt_return
}
fn test_fn_decl_signature_from_cursor() {
flat := build_minimal_file()
fn_cur := flat.file_cursor(0).stmts().at(1)
decl := fn_cur.fn_decl_signature()
assert decl.name == 'add'
assert !decl.is_method
assert decl.stmts.len == 0
assert decl.typ.params.len == 2
assert decl.typ.params[0].name == 'a'
assert decl.typ.params[0].typ is Ident
assert (decl.typ.params[0].typ as Ident).name == 'int'
assert decl.typ.params[1].name == 'b'
assert decl.typ.return_type is Ident
assert (decl.typ.return_type as Ident).name == 'int'
}
fn test_fn_decl_from_cursor_reads_body() {
flat := build_minimal_file()
fn_cur := flat.file_cursor(0).stmts().at(1)
decl := fn_cur.fn_decl()
assert decl.name == 'add'
assert decl.stmts.len == 1
assert decl.stmts[0] is ReturnStmt
}
fn test_stmt_and_expr_escape_hatches_from_cursor() {
flat := build_minimal_file()
ret_cur := flat.file_cursor(0).stmts().at(1).list_at(3).at(0)
ret_stmt := ret_cur.stmt()
assert ret_stmt is ReturnStmt
infix_expr := ret_cur.edge(0).expr()
assert infix_expr is InfixExpr
assert (infix_expr as InfixExpr).op == .plus
}
fn test_return_stmt_exprs_via_edges() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
ret := fc.stmts().at(1).list_at(3).at(0)
assert ret.kind() == .stmt_return
// ReturnStmt stores its exprs as direct edges of the parent (not aux_list)
assert ret.edge_count() == 1
infix := ret.edge(0)
assert infix.kind() == .expr_infix
// InfixExpr edges: 0=lhs, 1=rhs (per read_expr)
assert infix.edge_count() == 2
lhs := infix.edge(0)
rhs := infix.edge(1)
assert lhs.kind() == .expr_ident
assert lhs.name() == 'a'
assert rhs.kind() == .expr_ident
assert rhs.name() == 'b'
}
fn test_invalid_cursor_sentinels() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
bogus := fc.stmts().at(99) // out of range
assert !bogus.is_valid()
bogus_edge := fc.root().edge(99) // out of range
assert !bogus_edge.is_valid()
empty_list := fc.root().list_at(99)
assert empty_list.len() == 0
}
fn test_type_expr_from_cursor_reads_signature_types() {
mut b := new_flat_builder()
src := File{
name: 'inline_type_expr.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(FnDecl{
name: 'use_box'
typ: FnType{
params: [
Parameter{
name: 'items'
typ: Expr(Type(ArrayType{
elem_type: Expr(GenericArgs{
lhs: Expr(Ident{
name: 'Box'
})
args: [
Expr(Ident{
name: 'int'
}),
]
})
}))
},
]
return_type: Expr(Type(OptionType{
base_type: Expr(Ident{
name: 'string'
})
}))
}
}),
]
}
b.append_file(src)
fn_cur := b.flat.file_cursor(0).stmts().at(1)
fn_type := fn_cur.edge(1)
params := fn_type.list_at(1)
param_typ := params.at(0).edge(0).type_expr()
assert param_typ is Type
array_typ := param_typ as Type
assert array_typ is ArrayType
array_expr := array_typ as ArrayType
assert array_expr.elem_type is GenericArgs
generic := array_expr.elem_type as GenericArgs
assert generic.lhs is Ident
assert (generic.lhs as Ident).name == 'Box'
assert generic.args.len == 1
assert generic.args[0] is Ident
assert (generic.args[0] as Ident).name == 'int'
return_typ := fn_type.edge(2).type_expr()
assert return_typ is Type
option_typ := return_typ as Type
assert option_typ is OptionType
option_expr := option_typ as OptionType
assert option_expr.base_type is Ident
assert (option_expr.base_type as Ident).name == 'string'
}
fn test_decl_readers_from_cursor() {
mut b := new_flat_builder()
src := File{
name: 'inline_decl_readers.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'answer'
value: Expr(BasicLiteral{
kind: .number
value: '42'
})
},
]
}),
Stmt(EnumDecl{
name: 'Mode'
attributes: [
Attribute{
name: 'flag'
},
]
fields: [
FieldDecl{
name: 'read'
value: Expr(BasicLiteral{
kind: .number
value: '1'
})
},
]
}),
Stmt(StructDecl{
name: 'Person'
attributes: [
Attribute{
name: 'heap'
},
]
fields: [
FieldDecl{
name: 'name'
typ: Expr(Ident{
name: 'string'
})
value: Expr(StringLiteral{
value: 'unknown'
})
attributes: [
Attribute{
name: 'json'
value: Expr(StringLiteral{
value: 'full_name'
})
},
]
is_public: true
},
]
}),
]
}
b.append_file(src)
stmts := b.flat.file_cursor(0).stmts()
const_decl := stmts.at(1).const_decl()
assert const_decl.fields.len == 1
assert const_decl.fields[0].name == 'answer'
assert const_decl.fields[0].value is BasicLiteral
assert (const_decl.fields[0].value as BasicLiteral).value == '42'
enum_decl := stmts.at(2).enum_decl(true)
assert enum_decl.name == 'Mode'
assert enum_decl.attributes.has('flag')
assert enum_decl.fields.len == 1
assert enum_decl.fields[0].value is BasicLiteral
assert (enum_decl.fields[0].value as BasicLiteral).value == '1'
struct_decl := stmts.at(3).struct_decl()
assert struct_decl.name == 'Person'
assert struct_decl.attributes.has('heap')
assert struct_decl.fields.len == 1
assert struct_decl.fields[0].name == 'name'
assert struct_decl.fields[0].is_public
assert struct_decl.fields[0].typ is Ident
assert (struct_decl.fields[0].typ as Ident).name == 'string'
assert struct_decl.fields[0].value is StringLiteral
assert (struct_decl.fields[0].value as StringLiteral).value == 'unknown'
assert struct_decl.fields[0].attributes.len == 1
assert struct_decl.fields[0].attributes[0].name == 'json'
assert struct_decl.fields[0].attributes[0].value is StringLiteral
assert (struct_decl.fields[0].attributes[0].value as StringLiteral).value == 'full_name'
}
// count_unique_kinds_via_cursor walks the whole graph rooted at fc through
// edges with dedup (the FlatBuilder shares aux_list nodes for empty lists,
// so a naive DFS revisits them). Used to cross-check that Cursor traversal
// reaches every reachable node in the FlatAst.
fn count_unique_kinds_via_cursor(fc FileCursor) (map[string]int, int) {
flat := fc.flat
mut seen := []bool{len: flat.nodes.len}
mut out := map[string]int{}
mut stack := []Cursor{cap: 64}
mut unique := 0
stack << fc.root()
for stack.len > 0 {
c := stack.pop()
if !c.is_valid() || seen[c.id] {
continue
}
seen[c.id] = true
unique++
out[c.kind().str()]++
for i in 0 .. c.edge_count() {
stack << c.edge(i)
}
}
return out, unique
}
fn test_cursor_walk_matches_reachable_count() {
flat := build_minimal_file()
_, unique := count_unique_kinds_via_cursor(flat.file_cursor(0))
// Walking from the single file root via Cursor must reach the same
// set of unique nodes as the reference reachability walker.
assert unique == flat.count_reachable_nodes()
}
+17
View File
@@ -0,0 +1,17 @@
// 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 ast
// NOTE: this is just a very naive example of how it could possibly work.
// actual implementation may work during AST -> IR (or not). it may also
// need type information which we don't have here. as I said, just an example.
pub fn (match_expr &MatchExpr) desugar() Expr {
// Keep this helper conservative for now; match lowering happens in transformer.
return match_expr.expr
}
pub fn (or_expr &OrExpr) desugar() Expr {
// Keep this helper conservative for now; or-lowering happens in transformer.
return or_expr.expr
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,92 @@
// 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 ast
// Tests for `FlatBuilder.append_file_stmts`. The helper is the foundational
// primitive behind the transformer's upcoming `post_pass_to_flat` port —
// it appends pre-emitted stmt nodes to a registered file's stmt list and
// re-emits the file root, mirroring the post-pass mutation pattern used by
// `inject_embed_file_helper`, `inject_test_main`, and `generated_fns_parts`.
fn make_minimal_file_with_stmts(extra_stmts []Stmt) File {
mut stmts := []Stmt{cap: 1 + extra_stmts.len}
stmts << Stmt(ModuleStmt{
name: 'foo'
})
for s in extra_stmts {
stmts << s
}
return File{
name: 'inline_minimal.v'
mod: 'foo'
stmts: stmts
}
}
fn make_extra_stmt_a() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'A'
value: Expr(BasicLiteral{
kind: .number
value: '1'
})
},
]
})
}
fn make_extra_stmt_b() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'B'
value: Expr(BasicLiteral{
kind: .number
value: '2'
})
},
]
})
}
// Reference: build a file directly with both extras at construction time.
fn build_reference_with_extras() FlatAst {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts([make_extra_stmt_a(),
make_extra_stmt_b()]))
return b.flat
}
// Subject: build the bare file, emit extras separately into the same builder,
// then splice them in via append_file_stmts.
fn build_subject_with_appended_extras() FlatAst {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts([]))
a_id := b.emit_stmt(make_extra_stmt_a())
b_id := b.emit_stmt(make_extra_stmt_b())
b.append_file_stmts(0, [a_id, b_id])
return b.flat
}
fn test_append_file_stmts_signature_matches_reference() {
ref_sig := build_reference_with_extras().signature()
sub_sig := build_subject_with_appended_extras().signature()
assert ref_sig == sub_sig
}
fn test_append_file_stmts_zero_extras_returns_existing_root() {
mut b := new_flat_builder()
original_id := b.append_file(make_minimal_file_with_stmts([]))
returned_id := b.append_file_stmts(0, [])
assert returned_id == original_id
}
fn test_append_file_stmts_invalid_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts([]))
returned_id := b.append_file_stmts(-1, [FlatNodeId(0)])
assert returned_id == invalid_flat_node_id
}
@@ -0,0 +1,210 @@
// 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 ast
// Tests for `FlatBuilder.append_flat` — the flat-to-flat merge primitive behind
// the flat parallel transform. It concatenates a whole `src` FlatAst into a
// builder, relocating node ids / edge targets and re-interning strings so the
// merged result decodes identically to `src` standalone. The key correctness
// risks: (1) node-id / edge relocation, (2) name_id string remap across a
// DIFFERENT intern order in the destination, (3) re-interning the three kinds
// whose `extra` slot holds a string id (file mod, stmt_directive value,
// stmt_import alias) while leaving packed-int extras (counts/flags) untouched.
fn make_ident_stmt(name string) Stmt {
return Stmt(ExprStmt{
expr: Expr(Ident{
name: name
})
})
}
fn make_for_with_idents(names []string) Stmt {
mut body := []Stmt{}
for n in names {
body << make_ident_stmt(n)
}
return Stmt(ForStmt{
init: Stmt(EmptyStmt{})
cond: Expr(BasicLiteral{
kind: .number
value: '1'
})
stmts: body
})
}
// build a small flat with a file root whose stmt list is `stmts`.
fn build_named_flat(file_name string, mod string, stmts []Stmt) (FlatBuilder, FlatNodeId) {
mut b := new_flat_builder()
mut ids := []FlatNodeId{}
for s in stmts {
ids << b.emit_stmt(s)
}
file_id := b.append_file_with_stmt_ids(File{
name: file_name
mod: mod
}, ids)
return b, file_id
}
// Seed a destination builder with DIFFERENT strings so the src->dst string
// remap is non-trivial (src ids must not coincide with dst ids).
fn seeded_dst() FlatBuilder {
mut dst := new_flat_builder()
dst.intern('zzz_pre_a')
dst.intern('zzz_pre_b')
dst.intern('zzz_pre_c')
dst.emit_stmt(make_ident_stmt('zzz_pre_d'))
return dst
}
fn test_append_flat_preserves_subtree_structure_and_name_strings() {
mut a, a_file := build_named_flat('a.v', 'main', [
make_for_with_idents(['alpha', 'beta']),
make_ident_stmt('gamma'),
])
// edge 2 of a .file node is the stmts list; rooting the signature there
// avoids the intern-order-dependent .file `extra` slot.
a_stmts := a.flat.child_at(a_file, 2)
a_sig := a.flat.subtree_signature(a_stmts)
mut dst := seeded_dst()
pre_nodes := dst.flat.nodes.len
pre_edges := dst.flat.edges.len
off := dst.append_flat(a.flat)
assert off == pre_nodes
assert dst.flat.nodes.len == pre_nodes + a.flat.nodes.len
assert dst.flat.edges.len == pre_edges + a.flat.edges.len
// Structure + every interned ident/value survives the id+string relocation.
assert dst.flat.subtree_signature(a_stmts + off) == a_sig
}
fn test_append_flat_two_sources_stay_independent() {
mut a, a_file := build_named_flat('a.v', 'main', [
make_for_with_idents(['alpha', 'beta']),
])
mut b, b_file := build_named_flat('b.v', 'other', [make_ident_stmt('delta'),
make_ident_stmt('epsilon')])
a_sig := a.flat.subtree_signature(a.flat.child_at(a_file, 2))
b_sig := b.flat.subtree_signature(b.flat.child_at(b_file, 2))
mut dst := seeded_dst()
off_a := dst.append_flat(a.flat)
off_b := dst.append_flat(b.flat)
assert dst.flat.files.len == 2
assert dst.flat.subtree_signature(a.flat.child_at(a_file, 2) + off_a) == a_sig
assert dst.flat.subtree_signature(b.flat.child_at(b_file, 2) + off_b) == b_sig
// b's offset accounts for everything already present (seed + a).
assert off_b == off_a + a.flat.nodes.len
}
fn test_append_flat_reinterns_string_extra_kinds() {
mut a := new_flat_builder()
imp_id := a.emit_stmt(Stmt(ImportStmt{
name: 'os'
alias: 'myos'
is_aliased: true
}))
dir_id := a.emit_stmt(Stmt(Directive{
name: 'flag'
value: '-lpthread'
}))
file_id := a.append_file_with_stmt_ids(File{
name: 'a.v'
mod: 'mymod'
}, [imp_id, dir_id])
mut dst := new_flat_builder()
// Shift the intern table so src string ids differ from dst string ids.
dst.intern('os')
dst.intern('zzz')
dst.intern('flag_other')
dst.intern('unrelated_mod')
off := dst.append_flat(a.flat)
// stmt_import.alias lives in `extra` — must resolve to the merged table.
merged_imp := Cursor{
flat: &dst.flat
id: imp_id + off
}.stmt() as ImportStmt
assert merged_imp.name == 'os'
assert merged_imp.alias == 'myos'
assert merged_imp.is_aliased
// stmt_directive.value lives in `extra`.
merged_dir := Cursor{
flat: &dst.flat
id: dir_id + off
}.stmt() as Directive
assert merged_dir.name == 'flag'
assert merged_dir.value == '-lpthread'
// file mod is carried both in FlatFile.mod_idx (decoder reads this) and in
// the .file node's `extra` (must also be re-interned for consistency).
mf := dst.flat.files[dst.flat.files.len - 1]
assert dst.flat.string_at(mf.name_idx) == 'a.v'
assert dst.flat.string_at(mf.mod_idx) == 'mymod'
assert dst.flat.string_at(dst.flat.nodes[file_id + off].extra) == 'mymod'
}
fn test_append_flat_empty_source_is_noop() {
mut dst := seeded_dst()
pre_nodes := dst.flat.nodes.len
pre_edges := dst.flat.edges.len
pre_files := dst.flat.files.len
empty := FlatAst{}
off := dst.append_flat(&empty)
assert off == pre_nodes
assert dst.flat.nodes.len == pre_nodes
assert dst.flat.edges.len == pre_edges
assert dst.flat.files.len == pre_files
}
fn test_copy_subtree_from_preserves_stmt_shape_and_strings() {
mut src := new_flat_builder()
imp_id := src.emit_stmt(Stmt(ImportStmt{
name: 'os'
alias: 'myos'
is_aliased: true
}))
dir_id := src.emit_stmt(Stmt(Directive{
name: 'flag'
value: '-lm'
}))
for_id := src.emit_stmt(make_for_with_idents(['alpha', 'beta']))
mut dst := seeded_dst()
copied_imp := dst.copy_subtree_from(&src.flat, imp_id)
copied_dir := dst.copy_subtree_from(&src.flat, dir_id)
copied_for := dst.copy_subtree_from(&src.flat, for_id)
assert dst.flat.subtree_signature(copied_for) == src.flat.subtree_signature(for_id)
merged_imp := Cursor{
flat: &dst.flat
id: copied_imp
}.stmt() as ImportStmt
assert merged_imp.name == 'os'
assert merged_imp.alias == 'myos'
assert merged_imp.is_aliased
merged_dir := Cursor{
flat: &dst.flat
id: copied_dir
}.stmt() as Directive
assert merged_dir.name == 'flag'
assert merged_dir.value == '-lm'
}
fn test_copy_subtree_from_invalid_root_returns_invalid() {
mut src := new_flat_builder()
mut dst := new_flat_builder()
assert dst.copy_subtree_from(&src.flat, -1) == invalid_flat_node_id
assert dst.copy_subtree_from(&src.flat, 100) == invalid_flat_node_id
}
@@ -0,0 +1,222 @@
// 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 ast
import v2.token
fn field_flags_ident_expr(name string) Expr {
return Expr(Ident{
name: name
})
}
fn field_flags_int_expr() Expr {
return field_flags_ident_expr('int')
}
fn make_field_flags_file() File {
return File{
name: 'field_flags.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
Stmt(StructDecl{
name: 'Config'
fields: [
FieldDecl{
name: 'public_value'
typ: field_flags_int_expr()
is_public: true
},
FieldDecl{
name: 'mutable_value'
typ: field_flags_int_expr()
is_mut: true
},
FieldDecl{
name: 'module_mut_value'
typ: field_flags_int_expr()
is_module_mut: true
},
]
}),
Stmt(InterfaceDecl{
name: 'Runner'
fields: [
FieldDecl{
name: 'run'
typ: field_flags_int_expr()
is_interface_method: true
},
]
}),
]
}
}
fn test_field_decl_flags_survive_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(make_field_flags_file())
files := b.flat.to_files()
assert files.len == 1
assert files[0].stmts.len == 3
assert files[0].stmts[1] is StructDecl
struct_decl := files[0].stmts[1] as StructDecl
assert struct_decl.fields.len == 3
assert struct_decl.fields[0].is_public
assert !struct_decl.fields[0].is_mut
assert !struct_decl.fields[0].is_module_mut
assert !struct_decl.fields[0].is_interface_method
assert !struct_decl.fields[1].is_public
assert struct_decl.fields[1].is_mut
assert !struct_decl.fields[1].is_module_mut
assert !struct_decl.fields[1].is_interface_method
assert !struct_decl.fields[2].is_public
assert !struct_decl.fields[2].is_mut
assert struct_decl.fields[2].is_module_mut
assert !struct_decl.fields[2].is_interface_method
assert files[0].stmts[2] is InterfaceDecl
interface_decl := files[0].stmts[2] as InterfaceDecl
assert interface_decl.fields.len == 1
assert !interface_decl.fields[0].is_public
assert !interface_decl.fields[0].is_mut
assert !interface_decl.fields[0].is_module_mut
assert interface_decl.fields[0].is_interface_method
}
fn test_global_decl_public_flag_survives_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(File{
name: 'global_decl_public.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
Stmt(GlobalDecl{
is_public: true
fields: [
FieldDecl{
name: 'counter'
typ: field_flags_int_expr()
},
]
}),
]
})
files := b.flat.to_files()
assert files[0].stmts.len == 2
assert files[0].stmts[1] is GlobalDecl
global_decl := files[0].stmts[1] as GlobalDecl
assert global_decl.is_public
assert global_decl.fields.len == 1
}
fn test_emit_field_decl_by_ids_preserves_field_flags() {
field := FieldDecl{
name: 'global_counter'
typ: field_flags_int_expr()
is_public: true
is_mut: true
}
mut b := new_flat_builder()
b.append_file(File{
name: 'direct_field_flags.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
]
})
typ_id := b.emit_expr(field.typ)
value_id := b.emit_expr(field.value)
attrs_id := b.emit_attribute_list(field.attributes)
field_id := b.emit_field_decl_by_ids(field, typ_id, value_id, attrs_id)
fields_id := b.emit_aux_list_from_ids([field_id])
decl_attrs_id := b.emit_attribute_list([])
global_id := b.emit_global_decl_by_ids(true, decl_attrs_id, fields_id)
b.append_file_stmts(0, [global_id])
files := b.flat.to_files()
assert files[0].stmts.len == 2
assert files[0].stmts[1] is GlobalDecl
global_decl := files[0].stmts[1] as GlobalDecl
assert global_decl.is_public
assert global_decl.fields.len == 1
assert global_decl.fields[0].is_public
assert global_decl.fields[0].is_mut
assert !global_decl.fields[0].is_module_mut
assert !global_decl.fields[0].is_interface_method
}
fn field_flags_array_update_from_stmt(stmt Stmt) ArrayInitExpr {
assert stmt is ExprStmt
expr_stmt := stmt as ExprStmt
assert expr_stmt.expr is ArrayInitExpr
return expr_stmt.expr as ArrayInitExpr
}
fn test_array_init_update_expr_survives_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(File{
name: 'array_update_expr.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
Stmt(ExprStmt{
expr: Expr(ArrayInitExpr{
typ: field_flags_int_expr()
update_expr: field_flags_ident_expr('base')
exprs: [
field_flags_ident_expr('item'),
]
})
}),
]
})
files := b.flat.to_files()
assert files[0].stmts.len == 2
array_expr := field_flags_array_update_from_stmt(files[0].stmts[1])
assert array_expr.update_expr is Ident
assert (array_expr.update_expr as Ident).name == 'base'
assert array_expr.exprs.len == 1
assert array_expr.exprs[0] is Ident
assert (array_expr.exprs[0] as Ident).name == 'item'
}
fn test_emit_array_init_expr_by_ids_preserves_update_expr() {
mut b := new_flat_builder()
b.append_file(File{
name: 'direct_array_update_expr.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
]
})
typ_id := b.emit_expr(field_flags_int_expr())
init_id := b.emit_expr(empty_expr)
cap_id := b.emit_expr(empty_expr)
len_id := b.emit_expr(empty_expr)
update_expr_id := b.emit_expr(field_flags_ident_expr('base'))
elem_id := b.emit_expr(field_flags_ident_expr('item'))
array_id := b.emit_array_init_expr_by_ids(typ_id, init_id, cap_id, len_id, update_expr_id, [
elem_id,
], token.Pos{})
stmt_id := b.emit_expr_stmt_by_id(array_id)
b.append_file_stmts(0, [stmt_id])
files := b.flat.to_files()
assert files[0].stmts.len == 2
array_expr := field_flags_array_update_from_stmt(files[0].stmts[1])
assert array_expr.update_expr is Ident
assert (array_expr.update_expr as Ident).name == 'base'
assert array_expr.exprs.len == 1
assert array_expr.exprs[0] is Ident
assert (array_expr.exprs[0] as Ident).name == 'item'
}
@@ -0,0 +1,49 @@
// 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 ast
fn lock_expr_ident(name string) Expr {
return Expr(Ident{
name: name
})
}
fn lock_expr_stmt(value string) Stmt {
return Stmt(ExprStmt{
expr: Expr(BasicLiteral{
kind: .number
value: value
})
})
}
fn test_lock_expr_counts_survive_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(File{
name: 'lock_expr.v'
mod: 'lock_expr'
stmts: [
Stmt(ModuleStmt{
name: 'lock_expr'
}),
Stmt(ExprStmt{
expr: Expr(LockExpr{
lock_exprs: [lock_expr_ident('a'), lock_expr_ident('b')]
rlock_exprs: [lock_expr_ident('c')]
stmts: [lock_expr_stmt('1'), lock_expr_stmt('2')]
})
}),
]
})
files := b.flat.to_files()
assert files.len == 1
assert files[0].stmts.len == 2
assert files[0].stmts[1] is ExprStmt
expr_stmt := files[0].stmts[1] as ExprStmt
assert expr_stmt.expr is LockExpr
lock_expr := expr_stmt.expr as LockExpr
assert lock_expr.lock_exprs.len == 2
assert lock_expr.rlock_exprs.len == 1
assert lock_expr.stmts.len == 2
}
@@ -0,0 +1,128 @@
// 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 ast
// Tests for `FlatBuilder.prepend_file_stmts`. Sibling of `append_file_stmts`
// (s150) — same shape, opposite order. Needed by `inject_live_reload`'s
// file-level prepend where c_decls + global_decls go BEFORE the file's
// existing stmts.
fn make_minimal_file_with_stmts_prepend(extra_stmts []Stmt) File {
mut stmts := []Stmt{cap: 1 + extra_stmts.len}
for s in extra_stmts {
stmts << s
}
stmts << Stmt(ModuleStmt{
name: 'foo'
})
stmts << Stmt(ConstDecl{
fields: [
FieldInit{
name: 'TAIL'
value: Expr(BasicLiteral{
kind: .number
value: '0'
})
},
]
})
return File{
name: 'inline_prepend.v'
mod: 'foo'
stmts: stmts
}
}
fn make_prepend_const_a() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'PA'
value: Expr(BasicLiteral{
kind: .number
value: '11'
})
},
]
})
}
fn make_prepend_const_b() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'PB'
value: Expr(BasicLiteral{
kind: .number
value: '22'
})
},
]
})
}
// Reference: construct the file with both prepended consts BEFORE the module
// stmt at append time.
//
// Why this layout: prepend_file_stmts puts new stmts at the head of the
// stmts list — even before the ModuleStmt — so the reference must mirror
// that ordering at construction time to produce a bit-equal signature.
//
// Why subtree-of-stmts-list instead of full file signature: the file root's
// `extra` slot stores `intern(mod)` as a raw index. When the reference path
// emits the file's stmts in order, the prepended consts intern their strings
// BEFORE 'foo' the module name → 'foo' lands at a later intern slot. The
// subject path interns 'foo' first (via the bare file's ModuleStmt) → 'foo'
// at slot 0. Comparing the stmts-list subtree sidesteps this leak: it covers
// the actual prepended stmts ordering + content (what the primitive controls)
// without including the file root's leaky `extra` field. The s155 multi-file
// test uses the same subtree_signature workaround for the same reason.
fn build_reference_stmts_list_signature() string {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts_prepend([make_prepend_const_a(),
make_prepend_const_b()]))
stmts_list_id := b.flat.child_at(b.flat.files[0].file_id, 2)
return b.flat.subtree_signature(stmts_list_id)
}
// Subject: build the bare file, emit prepended stmts separately, then splice
// them via prepend_file_stmts.
fn build_subject_stmts_list_signature() string {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts_prepend([]))
a_id := b.emit_stmt(make_prepend_const_a())
c_id := b.emit_stmt(make_prepend_const_b())
b.prepend_file_stmts(0, [a_id, c_id])
stmts_list_id := b.flat.child_at(b.flat.files[0].file_id, 2)
return b.flat.subtree_signature(stmts_list_id)
}
fn test_prepend_file_stmts_signature_matches_reference() {
ref_sig := build_reference_stmts_list_signature()
sub_sig := build_subject_stmts_list_signature()
assert ref_sig == sub_sig
}
fn test_prepend_file_stmts_zero_prepended_returns_existing_root() {
mut b := new_flat_builder()
original_id := b.append_file(make_minimal_file_with_stmts_prepend([]))
returned_id := b.prepend_file_stmts(0, [])
assert returned_id == original_id
}
fn test_prepend_file_stmts_invalid_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts_prepend([]))
returned_id := b.prepend_file_stmts(-1, [FlatNodeId(0)])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_file_stmts_updates_files_table() {
mut b := new_flat_builder()
original_file_id := b.append_file(make_minimal_file_with_stmts_prepend([]))
x_id := b.emit_stmt(make_prepend_const_a())
new_file_id := b.prepend_file_stmts(0, [x_id])
assert new_file_id != original_file_id
assert b.flat.files[0].file_id == new_file_id
}
@@ -0,0 +1,110 @@
// 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 ast
// Tests for `FlatBuilder.prepend_to_fn_body`. The helper is the seed primitive
// for the remaining prepend-style post_pass mutations
// (`inject_main_runtime_const_init_calls`, `inject_live_reload`): it re-emits
// a stmt_fn_decl with extras prepended to its body, returning the new
// FlatNodeId so callers can rewire the surrounding file/stmt list.
//
// We compare via `subtree_signature(fn_id)` rather than the whole-file
// `signature()` because the file root's `extra=intern(mod)` slot would leak
// intern order between the reference (which builds extras before file mod
// gets re-interned) and subject (which interns extras AFTER file mod) paths.
fn make_fn_decl_with_body(name string, body []Stmt) Stmt {
return Stmt(FnDecl{
name: name
stmts: body
})
}
fn make_const_stmt(name string, value string) Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: name
value: Expr(BasicLiteral{
kind: .number
value: value
})
},
]
})
}
// Reference: emit a FnDecl whose body is already `[extras + old]`.
fn build_reference_fn_with_prepended() (FlatAst, FlatNodeId) {
mut b := new_flat_builder()
fn_id := b.emit_stmt(make_fn_decl_with_body('foo', [
make_const_stmt('A', '1'),
make_const_stmt('B', '2'),
make_const_stmt('C', '3'),
]))
return b.flat, fn_id
}
// Subject: emit a FnDecl whose body is `[old]`, emit extras separately,
// then prepend via the primitive under test.
fn build_subject_fn_with_prepended() (FlatAst, FlatNodeId) {
mut b := new_flat_builder()
base_fn_id := b.emit_stmt(make_fn_decl_with_body('foo', [
make_const_stmt('C', '3'),
]))
a_id := b.emit_stmt(make_const_stmt('A', '1'))
b_id := b.emit_stmt(make_const_stmt('B', '2'))
new_fn_id := b.prepend_to_fn_body(base_fn_id, [a_id, b_id])
return b.flat, new_fn_id
}
fn test_prepend_to_fn_body_signature_matches_reference() {
ref_flat, ref_fn_id := build_reference_fn_with_prepended()
sub_flat, sub_fn_id := build_subject_fn_with_prepended()
ref_sig := ref_flat.subtree_signature(ref_fn_id)
sub_sig := sub_flat.subtree_signature(sub_fn_id)
assert ref_sig == sub_sig
}
fn test_prepend_to_fn_body_zero_extras_returns_existing_id() {
mut b := new_flat_builder()
original_id := b.emit_stmt(make_fn_decl_with_body('foo', [
make_const_stmt('C', '3'),
]))
returned_id := b.prepend_to_fn_body(original_id, [])
assert returned_id == original_id
}
fn test_prepend_to_fn_body_invalid_id_returns_invalid() {
mut b := new_flat_builder()
a_id := b.emit_stmt(make_const_stmt('A', '1'))
returned_id := b.prepend_to_fn_body(FlatNodeId(-1), [a_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_fn_body_non_fn_decl_returns_invalid() {
// Pass a non-FnDecl node id (a ConstDecl) — primitive must refuse.
mut b := new_flat_builder()
const_id := b.emit_stmt(make_const_stmt('C', '3'))
extra_id := b.emit_stmt(make_const_stmt('A', '1'))
returned_id := b.prepend_to_fn_body(const_id, [extra_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_fn_body_empty_original_body() {
// FnDecl with no original body stmts; prepending should still produce a
// new FnDecl whose body is exactly the extras.
ref_flat, ref_fn_id := fn () (FlatAst, FlatNodeId) {
mut b := new_flat_builder()
fn_id := b.emit_stmt(make_fn_decl_with_body('foo', [make_const_stmt('A', '1')]))
return b.flat, fn_id
}()
mut sb := new_flat_builder()
base_fn_id := sb.emit_stmt(make_fn_decl_with_body('foo', []))
a_id := sb.emit_stmt(make_const_stmt('A', '1'))
new_fn_id := sb.prepend_to_fn_body(base_fn_id, [a_id])
assert ref_flat.subtree_signature(ref_fn_id) == sb.flat.subtree_signature(new_fn_id)
}
@@ -0,0 +1,95 @@
// 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 ast
// Tests for `FlatBuilder.prepend_to_for_body`. The ForStmt analog of s154's
// `prepend_to_fn_body`. Body-stmt encoding differs: ForStmt encodes body
// stmts as INLINE edges starting at index 3 (no list child), so this
// primitive rebuilds the for's edge list directly rather than rebuilding a
// stmts-list child node.
fn make_for_body_assert_stmt(value string) Stmt {
return Stmt(AssertStmt{
expr: Expr(BasicLiteral{
kind: .number
value: value
})
})
}
// Build a stmt_for via the legacy add_stmt path so the test exercises the
// same encoding that real callsites produce.
fn build_for_stmt(body_stmts []Stmt) (FlatBuilder, FlatNodeId) {
mut b := new_flat_builder()
for_stmt := Stmt(ForStmt{
init: Stmt(EmptyStmt{})
cond: Expr(BasicLiteral{
kind: .number
value: '1'
})
stmts: body_stmts
})
for_id := b.emit_stmt(for_stmt)
return b, for_id
}
fn ref_sig_for_body(body_stmts []Stmt) string {
mut b, for_id := build_for_stmt(body_stmts)
return b.flat.subtree_signature(for_id)
}
fn sub_sig_for_prepend(initial_body []Stmt, extra_values []string) string {
mut b, for_id := build_for_stmt(initial_body)
mut extra_ids := []FlatNodeId{}
for v in extra_values {
extra_ids << b.emit_stmt(make_for_body_assert_stmt(v))
}
new_for_id := b.prepend_to_for_body(for_id, extra_ids)
return b.flat.subtree_signature(new_for_id)
}
fn test_prepend_to_for_body_signature_matches_reference() {
ref_sig := ref_sig_for_body([make_for_body_assert_stmt('99'),
make_for_body_assert_stmt('1')])
sub_sig := sub_sig_for_prepend([make_for_body_assert_stmt('1')], ['99'])
assert ref_sig == sub_sig
}
fn test_prepend_to_for_body_zero_extras_returns_existing_id() {
mut b, for_id := build_for_stmt([make_for_body_assert_stmt('1')])
returned_id := b.prepend_to_for_body(for_id, [])
assert returned_id == for_id
}
fn test_prepend_to_for_body_invalid_id_returns_invalid() {
mut b := new_flat_builder()
extra_id := b.emit_stmt(make_for_body_assert_stmt('99'))
returned_id := b.prepend_to_for_body(FlatNodeId(-1), [extra_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_for_body_non_for_kind_returns_invalid() {
mut b := new_flat_builder()
// stmt_assert is not stmt_for — kind check must reject it.
assert_id := b.emit_stmt(make_for_body_assert_stmt('1'))
extra_id := b.emit_stmt(make_for_body_assert_stmt('99'))
returned_id := b.prepend_to_for_body(assert_id, [extra_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_for_body_empty_original_body() {
// ForStmt with no body stmts at all — extras become the entire new body.
ref_sig := ref_sig_for_body([make_for_body_assert_stmt('99')])
sub_sig := sub_sig_for_prepend([]Stmt{}, ['99'])
assert ref_sig == sub_sig
}
fn test_prepend_to_for_body_preserves_existing_body_order() {
// Two existing body stmts + one prepended; order must be [extra, b1, b2].
ref_sig := ref_sig_for_body([make_for_body_assert_stmt('99'),
make_for_body_assert_stmt('1'), make_for_body_assert_stmt('2')])
sub_sig := sub_sig_for_prepend([make_for_body_assert_stmt('1'),
make_for_body_assert_stmt('2')], ['99'])
assert ref_sig == sub_sig
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,153 @@
// 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 ast
// Tests for `FlatBuilder.replace_file_stmt`. The companion primitive to
// `prepend_to_fn_body`: when a caller swaps a stmt for a re-emitted variant
// (e.g. the new FnDecl returned by `prepend_to_fn_body`), `replace_file_stmt`
// rebuilds the enclosing file root so the file's stmts list references the
// new id. Together they support the prepend-style post_pass mutations
// (`inject_main_runtime_const_init_calls`, s155).
fn make_minimal_file_with_three_consts() File {
return File{
name: 'inline.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'A'
value: Expr(BasicLiteral{
kind: .number
value: '1'
})
},
]
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'B'
value: Expr(BasicLiteral{
kind: .number
value: '2'
})
},
]
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'C'
value: Expr(BasicLiteral{
kind: .number
value: '3'
})
},
]
}),
]
}
}
fn make_replacement_const_stmt() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'Z'
value: Expr(BasicLiteral{
kind: .number
value: '99'
})
},
]
})
}
fn make_const_stmt_named(name string, value string) Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: name
value: Expr(BasicLiteral{
kind: .number
value: value
})
},
]
})
}
// Reference: build the file with stmt index 2 (the `B` const) already
// replaced by the `Z` const at construction time.
fn build_reference_with_replacement() FlatAst {
mut b := new_flat_builder()
b.append_file(File{
name: 'inline.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
make_const_stmt_named('A', '1'),
make_replacement_const_stmt(),
make_const_stmt_named('C', '3'),
]
})
return b.flat
}
// Subject: build the bare three-const file, emit the replacement stmt
// separately, then splice it in via `replace_file_stmt(0, 2, z_id)`.
fn build_subject_with_replacement() FlatAst {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
b.replace_file_stmt(0, 2, z_id)
return b.flat
}
fn test_replace_file_stmt_signature_matches_reference() {
ref_sig := build_reference_with_replacement().signature()
sub_sig := build_subject_with_replacement().signature()
assert ref_sig == sub_sig
}
fn test_replace_file_stmt_invalid_file_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
returned_id := b.replace_file_stmt(-1, 0, z_id)
assert returned_id == invalid_flat_node_id
}
fn test_replace_file_stmt_invalid_stmt_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
// File has 4 stmts (module + 3 consts); idx 99 is out of range.
returned_id := b.replace_file_stmt(0, 99, z_id)
assert returned_id == invalid_flat_node_id
}
fn test_replace_file_stmt_invalid_new_id_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
returned_id := b.replace_file_stmt(0, 0, FlatNodeId(-1))
assert returned_id == invalid_flat_node_id
}
fn test_replace_file_stmt_updates_files_table() {
mut b := new_flat_builder()
original_file_id := b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
new_file_id := b.replace_file_stmt(0, 1, z_id)
// The flat.files[0].file_id must point at the new file root, not the old.
assert new_file_id != original_file_id
assert b.flat.files[0].file_id == new_file_id
}
@@ -0,0 +1,83 @@
// 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 ast
// Tests for `FlatBuilder.replace_fn_body_stmts`. The companion primitive to
// `prepend_to_fn_body`: replaces the FnDecl's body entirely (vs prepending
// to it). Needed by `inject_live_reload` which builds a fully-rewritten
// FnDecl body in one shot (preamble + rewritten for-stmts + rewritten calls).
fn make_assert_stmt_value(value string) Stmt {
return Stmt(AssertStmt{
expr: Expr(BasicLiteral{
kind: .number
value: value
})
})
}
fn build_fn_decl(body_stmts []Stmt) (FlatBuilder, FlatNodeId) {
mut b := new_flat_builder()
fn_stmt := Stmt(FnDecl{
name: 'f'
stmts: body_stmts
})
fn_id := b.emit_stmt(fn_stmt)
return b, fn_id
}
fn ref_sig_for_fn_body(body_stmts []Stmt) string {
mut b, fn_id := build_fn_decl(body_stmts)
return b.flat.subtree_signature(fn_id)
}
fn sub_sig_for_replace(initial_body []Stmt, new_body_values []string) string {
mut b, fn_id := build_fn_decl(initial_body)
mut new_ids := []FlatNodeId{}
for v in new_body_values {
new_ids << b.emit_stmt(make_assert_stmt_value(v))
}
new_fn_id := b.replace_fn_body_stmts(fn_id, new_ids)
return b.flat.subtree_signature(new_fn_id)
}
fn test_replace_fn_body_stmts_signature_matches_reference() {
ref_sig := ref_sig_for_fn_body([make_assert_stmt_value('7'),
make_assert_stmt_value('8'), make_assert_stmt_value('9')])
sub_sig := sub_sig_for_replace([make_assert_stmt_value('1'),
make_assert_stmt_value('2')], ['7', '8', '9'])
assert ref_sig == sub_sig
}
fn test_replace_fn_body_stmts_empty_new_body() {
// Replacing with an empty body — the fn becomes bodyless.
ref_sig := ref_sig_for_fn_body([]Stmt{})
sub_sig := sub_sig_for_replace([make_assert_stmt_value('1')], []string{})
assert ref_sig == sub_sig
}
fn test_replace_fn_body_stmts_invalid_id_returns_invalid() {
mut b := new_flat_builder()
x_id := b.emit_stmt(make_assert_stmt_value('1'))
returned_id := b.replace_fn_body_stmts(FlatNodeId(-1), [x_id])
assert returned_id == invalid_flat_node_id
}
fn test_replace_fn_body_stmts_non_fn_kind_returns_invalid() {
mut b := new_flat_builder()
// stmt_assert, not stmt_fn_decl — kind check must reject it.
assert_id := b.emit_stmt(make_assert_stmt_value('1'))
x_id := b.emit_stmt(make_assert_stmt_value('2'))
returned_id := b.replace_fn_body_stmts(assert_id, [x_id])
assert returned_id == invalid_flat_node_id
}
fn test_replace_fn_body_stmts_returns_new_id() {
mut b, fn_id := build_fn_decl([make_assert_stmt_value('1')])
x_id := b.emit_stmt(make_assert_stmt_value('2'))
new_fn_id := b.replace_fn_body_stmts(fn_id, [x_id])
// New id must differ from the original — the old fn is unreachable garbage.
assert new_fn_id != fn_id
assert new_fn_id != invalid_flat_node_id
}
@@ -0,0 +1,56 @@
// 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 ast
import strings
// signature returns a canonical, content-addressed string representation of
// the flat AST. Two flat ASTs produced from the same legacy AST must produce
// identical signatures — this is the round-trip lossless invariant.
//
// The signature includes per-node fields (kind, flags, aux, extra, pos) and
// resolves all interned strings to their text content, so signatures are
// independent of the order in which the intern table was populated.
pub fn (flat &FlatAst) signature() string {
mut sb := strings.new_builder(flat.nodes.len * 16)
for ff in flat.files {
sb.write_string('FILE name=${flat.string_at(ff.name_idx)} mod=${flat.string_at(ff.mod_idx)} root=')
flat.write_node_sig(mut sb, ff.file_id, 0)
sb.write_string('\n')
}
return sb.str()
}
// subtree_signature returns the same canonical, content-addressed string as
// `signature()` but rooted at a single node id rather than the file roots.
// Useful for tests that want to pin the shape of a specific subtree (e.g.
// the `FlatBuilder.prepend_to_fn_body` bit-equality test) without managing
// a file root or worrying about intern-order leakage in the `.file` node's
// `extra` slot.
pub fn (flat &FlatAst) subtree_signature(node_id FlatNodeId) string {
mut sb := strings.new_builder(128)
flat.write_node_sig(mut sb, node_id, 0)
return sb.str()
}
fn (flat &FlatAst) write_node_sig(mut sb strings.Builder, id FlatNodeId, depth int) {
if id < 0 || id >= flat.nodes.len {
sb.write_string('<nil>')
return
}
n := flat.nodes[id]
sb.write_string('(')
sb.write_string(n.kind.str())
sb.write_string(' flags=${n.flags:x} aux=${n.aux:x} extra=${n.extra} pos=${n.pos.offset}:${n.pos.id}')
if n.name_id >= 0 {
sb.write_string(' name="')
sb.write_string(flat.string_at(n.name_id))
sb.write_string('"')
}
for i in 0 .. n.edge_count {
sb.write_string(' ')
flat.write_node_sig(mut sb, flat.edges[n.first_edge + i].child_id, depth + 1)
}
sb.write_string(')')
}
@@ -0,0 +1,83 @@
// 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 ast
import v2.token
fn string_inter_test_expr() Expr {
return Expr(Ident{
name: 'value'
})
}
fn string_inter_test_literal(width int, precision int) Expr {
return Expr(StringInterLiteral{
kind: .v
values: ['value=', '']
inters: [
StringInter{
format: .decimal
width: width
precision: precision
expr: string_inter_test_expr()
format_expr: empty_expr
resolved_fmt: '%d'
},
]
})
}
fn assert_string_inter_roundtrip(files []File, width int, precision int) {
assert files.len == 1
assert files[0].stmts.len == 2
assert files[0].stmts[1] is ExprStmt
expr_stmt := files[0].stmts[1] as ExprStmt
assert expr_stmt.expr is StringInterLiteral
lit := expr_stmt.expr as StringInterLiteral
assert lit.inters.len == 1
assert lit.inters[0].width == width
assert lit.inters[0].precision == precision
}
fn test_string_inter_width_precision_survive_flat_roundtrip_losslessly() {
width := 1_000_000
precision := -1_000_000
mut b := new_flat_builder()
b.append_file(File{
name: 'string_inter.v'
mod: 'string_inter'
stmts: [
Stmt(ModuleStmt{
name: 'string_inter'
}),
Stmt(ExprStmt{
expr: string_inter_test_literal(width, precision)
}),
]
})
assert_string_inter_roundtrip(b.flat.to_files(), width, precision)
}
fn test_emit_string_inter_by_ids_preserves_large_width_precision() {
width := 900_000
precision := -900_000
mut b := new_flat_builder()
b.append_file(File{
name: 'string_inter_direct.v'
mod: 'string_inter'
stmts: [
Stmt(ModuleStmt{
name: 'string_inter'
}),
]
})
expr_id := b.emit_expr(string_inter_test_expr())
format_expr_id := b.emit_expr(empty_expr)
inter_id :=
b.emit_string_inter_by_ids(.decimal, width, precision, expr_id, format_expr_id, '%d')
lit_id := b.emit_string_inter_literal_by_ids(.v, ['value=', ''], [inter_id], token.Pos{})
stmt_id := b.emit_expr_stmt_by_id(lit_id)
b.append_file_stmts(0, [stmt_id])
assert_string_inter_roundtrip(b.flat.to_files(), width, precision)
}