package processing.mode.cpp;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/*
* Consolidated smaller AST analysis passes -- ClassHoister, ArrayHoister,
* NameUsageScanner, PSketchInjector, ForwardDeclGenerator,
* EnumScopeExtractor. Each is otherwise unchanged; merged into one file
* to reduce file count. DependencyHoister, LifecycleRewriter, and
* CodeGen remain standalone files since each is substantial enough on
* its own. See DECISION_two_parser_implementations.md (in
* tools/cpp-parser/) for each pass's individual design history.
*/
/**
* Replaces CppBuild.java's hoistClassesOnly() + removeHoistedClasses().
*
* Ported from a parallel implementation's CppClassHoister (package
* processing.mode.cpp), retargeted onto this codebase's AST shapes. See
* DECISION_two_parser_implementations.md for why this is a port of the
* logic rather than a merge of the two ASTs.
*
* The original CppBuild.java pass was two separate character-walking scans
* over raw text that had to stay in lockstep (one collects class blocks,
* the other blanks the same ranges from the original source) -- a
* structural duplication risk: if either scan's class/enum/comment-skipping
* logic ever drifted from the other's, hoisted classes and "the rest"
* could silently disagree about where a class started or ended (this is
* the same bug class as the comment-spanning-newline bug already found and
* fixed in CppBuild.java's classifyTopLevelDecls).
*
* With a real AST this is one operation instead of two: classify
* cu.items() into "is a TypeDef" vs "isn't," reorder the TypeDefs, and the
* non-TypeDef items already ARE the correct "rest" with zero risk of the
* two halves disagreeing, since they're derived from the exact same parsed
* list rather than two independent re-scans of raw text.
*
* Ordering: base classes before derived, matching the original's
* bubble-style pairwise swap. The original CppBuild.java regex only ever
* captured a single base-class name per class, so this preserves that same
* single-base assumption for ordering purposes -- a class with multiple
* bases (this AST's TypeDef.baseClasses() supports a list, confirmed real
* by the multiple-inheritance kitchen-sink fixture) is ordered by its
* FIRST listed base only. This pass doesn't invent new ordering behavior
* for a case the original CppBuild.java pass never handled either.
*/
final class ClassHoister {
private ClassHoister() {}
public static final class Result {
/** Hoisted TypeDef nodes, ordered so each class's first base class
* (if any, and if that base is itself one of the hoisted classes)
* appears before it. */
public final List hoistedClasses;
/** Every other top-level item, in original order, with the
* hoisted TypeDefs removed. */
public final List rest;
public Result(List hoistedClasses, List rest) {
this.hoistedClasses = hoistedClasses;
this.rest = rest;
}
}
public static Result hoist(List items) {
List classBlocks = new ArrayList<>();
List rest = new ArrayList<>();
for (TopLevelItem item : items) {
if (item instanceof TypeDef td) {
classBlocks.add(td);
} else {
rest.add(item);
}
}
// Deduplicate: drop empty forward declarations when a full definition exists.
// When a forward decl is replaced by a full definition, use the full
// definition's position in the list (not the forward decl's position)
// so that class ordering matches source order of definitions.
java.util.Map best = new java.util.LinkedHashMap<>();
java.util.List order = new java.util.ArrayList<>();
for (TypeDef td : classBlocks) {
String name = td.name();
TypeDef existing = best.get(name);
if (existing == null) {
best.put(name, td);
order.add(name);
} else if (!td.members().isEmpty() && existing.members().isEmpty()) {
// Full definition replaces forward decl -- move to end to preserve source order
best.put(name, td);
order.remove(name);
order.add(name);
}
}
classBlocks = new ArrayList<>();
for (String n : order) classBlocks.add(best.get(n));
boolean changed = true;
for (int pass = 0; pass < classBlocks.size() * 2 && changed; pass++) {
changed = false;
for (int a = 0; a < classBlocks.size(); a++) {
TypeDef blockA = classBlocks.get(a);
if (blockA.baseClasses().isEmpty()) continue;
String aBase = blockA.baseClasses().get(0);
for (int b = a + 1; b < classBlocks.size(); b++) {
TypeDef blockB = classBlocks.get(b);
if (blockB.name().equals(aBase)) {
classBlocks.set(a, blockB);
classBlocks.set(b, blockA);
changed = true;
break;
}
}
if (changed) break;
}
}
return new Result(classBlocks, rest);
}
}
/**
* Replaces the two-phase array-hoisting block inside CppBuild.java's
* writeSketch(), which used classifyTopLevelDecls() (a character-walking
* scan) called repeatedly inside two separate "while (changed)" fixed-point
* loops -- one pass per single removal, since the original mutated a raw
* String and had to re-scan from scratch after every hoisted declaration.
*
* Ported from a parallel implementation's CppArrayHoister (package
* processing.mode.cpp), retargeted onto this codebase's AST. See
* DECISION_two_parser_implementations.md for the porting rationale.
*
* SIMPLIFICATION vs. the original port source: that implementation's
* VariableDecl groups multiple comma-separated declarators under one
* shared type ("int rectX, rectY;" stays as ONE VariableDecl with two
* Declarators), which meant its hoister had to split a single
* statement's declarators between "hoisted" and "kept" buckets. This
* codebase's VariableDecl is already single-declarator -- multi-declarator
* statements are desugared into separate VariableDecl nodes at PARSE time
* (see VariableDecl's javadoc) -- so that splitting logic doesn't apply
* here at all: each VariableDecl already represents exactly one
* declarator, so hoisting is just "does this VariableDecl go in this
* bucket or not," with no per-declarator partitioning step needed.
*
* Preserves the original's exact two-phase contract:
* PHASE 1: collect every array's non-numeric size identifier, then hoist
* every const-qualified scalar declaration whose name matches
* one of those identifiers.
* PHASE 2: hoist every array declaration, unconditionally, regardless of
* whether it depends on anything hoisted in phase 1.
* This guarantees sizing constants precede the arrays that use them in the
* hoisted-arrays output, matching the original's stated intent.
*/
final class ArrayHoister {
private ArrayHoister() {}
public static final class Result {
/** Sizing-constant VariableDecls (phase 1), in the order found. */
public final List hoistedSizingConstants = new ArrayList<>();
/** Array VariableDecls (phase 2), in the order found. */
public final List hoistedArrays = new ArrayList<>();
/** Every remaining top-level item, with hoisted declarations removed. */
public final List rest = new ArrayList<>();
}
public static Result hoist(List items) {
Result result = new Result();
// --- Identify every array declaration's non-numeric size identifier ---
Set arraySizeIdentifiers = new LinkedHashSet<>();
for (TopLevelItem item : items) {
if (!(item instanceof VariableDecl vd)) continue;
if (vd.arrayDims().isEmpty()) continue;
for (Expr dim : vd.arrayDims()) {
String sizeName = nonNumericIdentifierName(dim);
if (sizeName != null) arraySizeIdentifiers.add(sizeName);
}
}
// BUG FIX, found via a real, official Processing example sketch
// (Blur.pde): "float kernel[3][3] = {{v,v,v},{v,v,v},{v,v,v}};"
// depends on a plain (non-const) "float v" referenced inside its
// INITIALIZER, not its dimensions. Scan every array declaration's
// INITIALIZER for identifier references too, adding them to the
// same hoist-candidate set used for sizing identifiers.
for (TopLevelItem item : items) {
if (!(item instanceof VariableDecl vd)) continue;
if (vd.arrayDims().isEmpty()) continue;
if (vd.initializer() == null) continue;
collectIdentifierNames(vd.initializer(), arraySizeIdentifiers);
}
// --- PHASE 1: hoist scalar decls matching those names ---
// NOTE: no longer requires vd.isConst() -- a non-const scalar
// referenced inside an array's initializer has the exact same
// "must precede the array, at the same scope" requirement as a
// const sizing constant does.
List afterPhase1 = new ArrayList<>();
for (TopLevelItem item : items) {
if (item instanceof VariableDecl vd
&& vd.arrayDims().isEmpty()
&& arraySizeIdentifiers.contains(vd.name())) {
result.hoistedSizingConstants.add(vd);
} else {
afterPhase1.add(item);
}
}
// --- PHASE 2: hoist every array declaration, unconditionally ---
for (TopLevelItem item : afterPhase1) {
if (item instanceof VariableDecl vd && !vd.arrayDims().isEmpty()) {
result.hoistedArrays.add(vd);
} else {
result.rest.add(item);
}
}
return result;
}
/**
* Recursively walks an expression tree collecting every bare
* Identifier's name into `out`. Deliberately broad (any identifier
* anywhere in the initializer, not just direct children) for
* correctness on arbitrarily nested initializers -- collecting a
* name that doesn't correspond to any real top-level scalar is
* harmless (it simply never matches anything in PHASE 1 above).
*/
private static void collectIdentifierNames(Expr e, Set out) {
if (e instanceof Identifier id) {
out.add(id.name());
} else if (e instanceof InitializerListExpr il) {
for (Expr el : il.elements()) collectIdentifierNames(el, out);
} else if (e instanceof BinaryExpr be) {
collectIdentifierNames(be.left(), out);
collectIdentifierNames(be.right(), out);
} else if (e instanceof UnaryExpr ue) {
collectIdentifierNames(ue.operand(), out);
} else if (e instanceof CallExpr ce) {
collectIdentifierNames(ce.callee(), out);
for (Expr arg : ce.args()) collectIdentifierNames(arg, out);
} else if (e instanceof CastExpr cae) {
collectIdentifierNames(cae.expr(), out);
}
// Other Expr kinds (Literal, etc.) contain no identifiers to collect.
}
/**
* If `e` is a bare Identifier (not a numeric literal), returns its name;
* otherwise null. Mirrors the original CppBuild.java pass's
* "!d.sizeExpr.matches(\"\\d+\")" check, which only ever needed to
* distinguish a literal digit run from a named constant -- arbitrary
* non-identifier size expressions (e.g. "N+1") weren't handled by the
* original either, so this preserves that same scope rather than trying
* to extract every identifier from an arbitrary expression.
*/
private static String nonNumericIdentifierName(Expr e) {
if (e instanceof Identifier id) {
if (!id.name().matches("\\d+")) return id.name();
}
return null;
}
}
/**
* Walks an arbitrary AST subtree looking for references to a given name.
* Ported from a parallel implementation's NameUsageScanner (package
* processing.mode.cpp), retargeted onto this codebase's AST shapes.
* Originally written using exhaustive pattern-matching switches over the
* sealed Expr/Statement/TopLevelItem hierarchies (which the Java compiler
* verifies exhaustively, unlike a plain if-instanceof chain), but
* rewritten as a plain if-instanceof chain after the real processing4
* Gradle build rejected switch pattern matching as a disabled preview
* feature -- see the rewritten visit() method's own comment for details.
* Every branch's logic is unchanged from the original switch version.
*
* Exists specifically to answer the dependency questions DependencyHoister
* needs: "is name X called anywhere in here" and "is name X referenced as
* a bare identifier anywhere in here," which the original CppBuild.java
* regex passes answered with "\bname\s*\(" and "\bname\b" respectively
* over raw text.
*
* Correctness note (carried over from the port source): a true textual
* "\bname\b" regex over raw source would also match `name` inside a
* comment or a string literal. This walker, operating on the AST instead
* of text, structurally CANNOT match inside a comment (comments aren't
* part of the expression/statement tree at all -- they're attached
* separately as leadingComments) or inside a string/char literal (a
* Literal's text field is never inspected for substring matches here,
* only real Identifier nodes are). This is strictly more correct than the
* original for this edge case, not a casual behavior change.
*/
final class NameUsageScanner {
private NameUsageScanner() {}
public static boolean containsIdentifier(Node root, String name) {
return new Finder(name, false).visit(root);
}
public static boolean containsCall(Node root, String name) {
return new Finder(name, true).visit(root);
}
public static boolean containsBareFunctionCall(Node root) {
return new BareFunctionCallFinder().visit(root);
}
private static final class BareFunctionCallFinder {
boolean visit(Node n) {
if (n == null) return false;
if (n instanceof CallExpr c) {
if (c.callee() instanceof Identifier) return true;
for (var arg : c.args()) if (visit(arg)) return true;
return visit(c.callee());
}
if (n instanceof Block b) { for (var s : b.statements()) if (visit(s)) return true; }
if (n instanceof ExprStatement es) return visit(es.expr());
if (n instanceof ReturnStatement rs) return visit(rs.value());
if (n instanceof IfStatement ifs) return visit(ifs.condition()) || visit(ifs.thenBranch()) || visit(ifs.elseBranch());
if (n instanceof WhileStatement ws) return visit(ws.condition()) || visit(ws.body());
if (n instanceof ForStatement fs) return visit(fs.init()) || visit(fs.condition()) || visit(fs.update()) || visit(fs.body());
if (n instanceof DeclStatement ds) return visit(ds.initializer());
if (n instanceof BinaryExpr b) return visit(b.left()) || visit(b.right());
if (n instanceof UnaryExpr u) return visit(u.operand());
if (n instanceof AssignExpr ae) return visit(ae.target()) || visit(ae.value());
if (n instanceof MemberAccessExpr m) return visit(m.target());
if (n instanceof TernaryExpr t) return visit(t.condition()) || visit(t.thenExpr()) || visit(t.elseExpr());
if (n instanceof FunctionDecl fd) return visit(fd.body());
return false;
}
}
private static final class Finder {
final String name;
final boolean callOnly;
Finder(String name, boolean callOnly) {
this.name = name;
this.callOnly = callOnly;
}
boolean visit(Node n) {
if (n == null) return false;
// NOTE: this was originally written as a pattern-matching
// switch expression (Java 21's "case Identifier id -> ...").
// Rewritten as a plain if-instanceof chain after the real
// processing4 Gradle build rejected it with "patterns in
// switch statements are a preview feature and are disabled
// by default" -- this codebase's actual target Java level
// doesn't have that preview feature enabled, and changing
// Gradle's own configuration to enable a language preview
// feature for this one file was judged more invasive than
// just not using the feature. Every branch's logic below is
// IDENTICAL to the switch version; only the dispatch
// mechanism changed.
if (n instanceof Identifier id) return !callOnly && id.name().equals(name);
if (n instanceof ScopedName) return false;
if (n instanceof CallExpr c) {
boolean calleeIsTarget = c.callee() instanceof Identifier id && id.name().equals(name);
if (calleeIsTarget) return true;
if (visit(c.callee())) return true;
for (Expr a : c.args()) if (visit(a)) return true;
return false;
}
if (n instanceof TypeDef td) {
for (TopLevelItem m : td.members()) if (visit(m)) return true;
return false;
}
if (n instanceof FunctionDecl fd) {
for (Param p : fd.params()) {
if (p.defaultValue() != null && visit(p.defaultValue())) return true;
}
for (FunctionDecl.ConstructorInit init : fd.initializerList()) {
for (Expr a : init.args()) if (visit(a)) return true;
}
return fd.body() != null && visit(fd.body());
}
if (n instanceof VariableDecl vd) {
for (Expr dim : vd.arrayDims()) if (visit(dim)) return true;
return vd.initializer() != null && visit(vd.initializer());
}
if (n instanceof EnumDecl) return false;
if (n instanceof PreprocessorLine) return false;
if (n instanceof NamespaceDecl nd) {
for (TopLevelItem item : nd.items()) if (visit(item)) return true;
return false;
}
if (n instanceof UsingNamespaceDecl) return false;
if (n instanceof TopLevelStatement ts) return visit(ts.statement());
if (n instanceof Block b) {
for (Statement s : b.statements()) if (visit(s)) return true;
return false;
}
if (n instanceof DeclStatement ds) {
for (Expr dim : ds.arrayDims()) if (visit(dim)) return true;
return ds.initializer() != null && visit(ds.initializer());
}
if (n instanceof ExprStatement es) return visit(es.expr());
if (n instanceof IfStatement s) {
return visit(s.condition()) || visit(s.thenBranch())
|| (s.elseBranch() != null && visit(s.elseBranch()));
}
if (n instanceof ForStatement s) {
return (s.init() != null && visit(s.init()))
|| (s.condition() != null && visit(s.condition()))
|| (s.update() != null && visit(s.update()))
|| visit(s.body());
}
if (n instanceof RangeForStatement s) return visit(s.iterableExpr()) || visit(s.body());
if (n instanceof WhileStatement s) return visit(s.condition()) || visit(s.body());
if (n instanceof DoWhileStatement s) return visit(s.body()) || visit(s.condition());
if (n instanceof SwitchStatement s) {
if (visit(s.subject())) return true;
for (SwitchCase c : s.cases()) {
if (c.matchValue() != null && visit(c.matchValue())) return true;
for (Statement st : c.body()) if (visit(st)) return true;
}
return false;
}
if (n instanceof ReturnStatement s) return s.value() != null && visit(s.value());
if (n instanceof TryStatement s) {
if (visit(s.tryBlock())) return true;
for (CatchClause c : s.catchClauses()) {
if (c.body() != null && visit(c.body())) return true;
}
return false;
}
if (n instanceof DeleteStatement s) return visit(s.target());
if (n instanceof BreakStatement) return false;
if (n instanceof ContinueStatement) return false;
if (n instanceof BinaryExpr e) return visit(e.left()) || visit(e.right());
if (n instanceof UnaryExpr e) return visit(e.operand());
if (n instanceof PostfixExpr e) return visit(e.operand());
if (n instanceof AssignExpr e) return visit(e.target()) || visit(e.value());
if (n instanceof TernaryExpr e) return visit(e.condition()) || visit(e.thenExpr()) || visit(e.elseExpr());
if (n instanceof MemberAccessExpr e) return visit(e.target());
if (n instanceof IndexExpr e) return visit(e.target()) || visit(e.index());
if (n instanceof CastExpr e) return visit(e.expr());
if (n instanceof NewExpr e) {
for (Expr a : e.args()) if (visit(a)) return true;
return false;
}
if (n instanceof ArrayNewExpr e) return visit(e.sizeExpr());
if (n instanceof LambdaExpr e) return visit(e.body());
if (n instanceof InitializerListExpr e) {
for (Expr el : e.elements()) if (visit(el)) return true;
return false;
}
if (n instanceof Literal) return false;
return false;
}
}
}
/**
* Ports the "_PSketch base injection" step from CppBuild.java's
* writeSketch(), which currently does this with four chained regexes
* over hoisted-class text plus a separate removePSketchFromDataStructs()
* text scan.
*
* Every hoisted class/struct gets `_PSketch` injected as an additional
* public virtual base (giving it access to Processing API free functions
* via ADL/unqualified lookup), UNLESS it's a pure data type with no
* methods at all -- detected in the original by a regex testing whether
* the class body text contains a "ReturnTypeKeyword name(" shape. On the
* AST this is simply "does this TypeDef have any FunctionDecl member,"
* which is exact (no risk of a field initializer or comment that merely
* LOOKS call-shaped being mistaken for a real method, unlike the
* original's text-pattern match).
*
* Also folds in the original's separate "class defaults to public:"
* injection (Java/Processing has no notion of C++'s class-defaults-to-
* private rule, so every injected class needs an explicit "public:"
* right after the brace) -- expressed here as a flag on the result
* rather than literal text, since codegen decides how to render it.
*/
final class PSketchInjector {
private PSketchInjector() {}
/** One class/struct's injection decision, paired with its (possibly unchanged) TypeDef. */
public record Result(TypeDef typeDef, boolean injected) {
}
/**
* @param hoistedClasses the TypeDef nodes already hoisted to namespace
* scope (output of ClassHoister), in their final order
* @return the same TypeDefs, each with `_PSketch` added to baseClasses
* (as the LAST entry, matching the original's "$1, public
* virtual _PSketch" -- appended after the user's own bases,
* never first) unless the class has no methods at all
*/
public static List injectAll(List hoistedClasses) {
// Build map of class name -> TypeDef for transitive base lookup
java.util.Map byName = new java.util.LinkedHashMap<>();
for (TypeDef td : hoistedClasses) byName.put(td.name(), td);
// Determine which classes will get _PSketch injected
// A class should NOT get _PSketch if any of its bases already
// (transitively) gets _PSketch -- it will inherit it from them.
// This prevents ambiguous base errors in diamond hierarchies.
java.util.Set willBeInjected = new java.util.LinkedHashSet<>();
for (TypeDef td : hoistedClasses) {
Result r = inject(td);
if (r.injected()) willBeInjected.add(td.name());
}
// For each class that would get _PSketch, check if any of its
// transitive bases also gets _PSketch. If so, skip injection
// since it inherits _PSketch already.
java.util.Set skipInjection = new java.util.HashSet<>();
for (TypeDef td : hoistedClasses) {
if (!willBeInjected.contains(td.name())) continue;
// Check if any base (transitively) is also getting _PSketch
if (transitiveBasesGetPSketch(td, willBeInjected, byName)) {
skipInjection.add(td.name());
}
}
List results = new ArrayList<>(hoistedClasses.size());
for (TypeDef td : hoistedClasses) {
if (skipInjection.contains(td.name())) {
results.add(new Result(td, false)); // skip -- inherits via base
} else {
results.add(inject(td));
}
}
return results;
}
/** True if any transitive base of td is in the willBeInjected set. */
private static boolean transitiveBasesGetPSketch(
TypeDef td,
java.util.Set willBeInjected,
java.util.Map byName) {
for (String base : td.baseClasses()) {
// Strip "virtual ", "public ", "protected ", "private " qualifiers
String baseName = base.replaceAll("\b(virtual|public|protected|private)\b\s*", "").trim();
// Strip template args if any
int lt = baseName.indexOf('<');
if (lt >= 0) baseName = baseName.substring(0, lt).trim();
if (willBeInjected.contains(baseName)) return true;
TypeDef baseTd = byName.get(baseName);
if (baseTd != null && transitiveBasesGetPSketch(baseTd, willBeInjected, byName)) return true;
}
return false;
}
private static Result inject(TypeDef td) {
// Never inject _PSketch into template classes -- they cannot inherit
// from a non-template base like _PSketch without full specialization.
if (!td.templateParams().isEmpty() || td.name().contains("<")) {
return new Result(td, false);
}
if (!hasAnyMethod(td)) {
return new Result(td, false);
}
// Only inject _PSketch if the class actually uses Processing API.
// Classes that only use operators and member access (like Vec3) are
// pure data types -- injecting _PSketch breaks aggregate initialization.
if (!usesBareFunctionCalls(td)) {
return new Result(td, false);
}
List newBases = new ArrayList<>(td.baseClasses());
newBases.add("virtual _PSketch");
TypeDef injected = new TypeDef(td.kind(), td.name(), td.templateParams(), newBases, td.members(),
td.line(), td.col(), td.leadingComments());
return new Result(injected, true);
}
private static boolean usesBareFunctionCalls(TypeDef td) {
for (TopLevelItem member : td.members()) {
if (member instanceof FunctionDecl fd && fd.body() != null) {
if (NameUsageScanner.containsBareFunctionCall(fd)) {
return true;
}
}
}
return false;
}
/**
* True if this TypeDef has at least one FunctionDecl member. The
* original's "&& !body.contains(\"constexpr\")" exclusion is not
* separately modeled here since this AST's FunctionDecl has no
* constexpr flag at all (not confirmed needed by any corpus fixture
* so far) -- this check is currently a strict superset of the
* original's intent and has not yet been exercised against a
* constexpr-method corpus case to confirm the distinction matters in
* practice. Flagged here rather than silently assumed identical.
*/
private static boolean hasAnyMethod(TypeDef td) {
for (TopLevelItem member : td.members()) {
if (member instanceof FunctionDecl) {
return true;
}
}
return false;
}
}
/**
* Replaces the inline forward-declaration generation in CppBuild.java's
* writeSketch(), which re-derives a function's signature by regex-
* matching its own already-generated text back apart.
*
* On the AST this needs no pattern at all: a forward declaration of a
* FunctionDecl is simply the same FunctionDecl with body set to null --
* CodeGen's emitFunctionDecl already renders a null-body FunctionDecl as
* "ReturnType name(params);" (no body, trailing semicolon), since that's
* also the correct rendering for a genuine declaration-without-definition
* in ordinary C++. No new codegen logic was needed for this -- it already
* existed for a different reason (the AST always supported a bodyless
* FunctionDecl, even though nothing produced one until this pass).
*
* This lets a class method (e.g. "Agent::update" calling "dirToVec")
* compile even though the class itself appears earlier in the emitted
* file than the hoisted function's full definition -- matching the
* original's exact purpose.
*/
final class ForwardDeclGenerator {
private ForwardDeclGenerator() {}
/**
* @param hoistedFunctions the FunctionDecl nodes hoisted to namespace
* scope (e.g. via a free-function dependency
* pass analogous to DependencyHoister's
* function-hoisting half)
* @return one bodyless FunctionDecl per input, in the same order,
* suitable for emitting before the hoisted classes that call them
*/
public static List generate(List hoistedFunctions) {
List decls = new ArrayList<>(hoistedFunctions.size());
for (FunctionDecl fd : hoistedFunctions) {
decls.add(new FunctionDecl(
fd.returnType(), fd.name(), fd.templateParams(), fd.params(),
fd.initializerList(), null /* body -- this is what makes it a forward decl */,
fd.isConstructor(), fd.isDestructor(), fd.isVirtual(), fd.isOverride(),
fd.isConst(), fd.isConstexpr(), fd.isStatic(), false /* isPureVirtual */, false /* isDefault */, false /* isDelete */,
fd.line(), fd.col(), List.of() /* no comments on the forward decl */
));
}
return decls;
}
}
/**
* Replaces the enum-extraction half of the combined enumScope/
* constexprScope block in CppBuild.java's writeSketch(). That block does
* two unrelated things in one character-walking pass: pull out enum
* declarations (so they can be emitted before classes that use them) and
* pull out constexpr/static_assert/using-alias declarations. This pass
* covers only the enum half -- see "constexpr/static_assert/using-alias
* NOT ported" below for why the other half is intentionally left out for
* now rather than faked.
*
* The original's enum-extraction logic exists almost entirely to solve a
* problem this AST doesn't have: a naive per-line scan only matches an
* enum's OPENING line ("enum Direction {"), silently leaving the
* enumerator list itself behind in the "rest" bucket -- so the original
* needs a careful character-walking, brace-depth-tracking scan
* specifically to capture the whole multi-line block as one unit. On
* this AST, an EnumDecl produced by the parser is ALREADY one complete
* node (name, isScoped flag, and the full list of enumerator values) --
* there is no way for "half an enum" to exist in the tree at all, so
* this pass is just a filter: separate TopLevelItems into "is an
* EnumDecl" and "isn't," same pattern as ClassHoister/ArrayHoister.
*
* NOT PORTED: constexpr declarations, static_assert, and type-alias
* "using Name = Type;" declarations. Checked directly against the real
* 131-file example corpus (see tools/cpp-parser/real-corpus-snapshot/):
* zero files use "constexpr", "static_assert", or a type-alias "using"
* declaration anywhere. This AST also has no constexpr concept at all
* (VariableDecl/FunctionDecl only track isConst/isStatic) -- adding
* speculative support for a construct with zero corpus evidence would
* mean shipping untested guesswork dressed up as a port. If a future
* sketch (real or fixture) is found using any of these, that's the
* trigger to design and test the corresponding AST support properly,
* not to retrofit it blind now.
*/
final class EnumScopeExtractor {
private EnumScopeExtractor() {}
public static final class Result {
public final List enums = new ArrayList<>();
public final List rest = new ArrayList<>();
}
public static Result extract(List items) {
Result result = new Result();
for (TopLevelItem item : items) {
if (item instanceof EnumDecl ed) {
result.enums.add(ed);
} else {
result.rest.add(item);
}
}
return result;
}
}