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,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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user