-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAstPasses.java
More file actions
724 lines (655 loc) · 33.1 KB
/
Copy pathAstPasses.java
File metadata and controls
724 lines (655 loc) · 33.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
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<TypeDef> hoistedClasses;
/** Every other top-level item, in original order, with the
* hoisted TypeDefs removed. */
public final List<TopLevelItem> rest;
public Result(List<TypeDef> hoistedClasses, List<TopLevelItem> rest) {
this.hoistedClasses = hoistedClasses;
this.rest = rest;
}
}
public static Result hoist(List<TopLevelItem> items) {
List<TypeDef> classBlocks = new ArrayList<>();
List<TopLevelItem> 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<String, TypeDef> best = new java.util.LinkedHashMap<>();
java.util.List<String> 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<VariableDecl> hoistedSizingConstants = new ArrayList<>();
/** Array VariableDecls (phase 2), in the order found. */
public final List<VariableDecl> hoistedArrays = new ArrayList<>();
/** Every remaining top-level item, with hoisted declarations removed. */
public final List<TopLevelItem> rest = new ArrayList<>();
}
public static Result hoist(List<TopLevelItem> items) {
Result result = new Result();
// --- Identify every array declaration's non-numeric size identifier ---
Set<String> 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<TopLevelItem> 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<String> 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<Result> injectAll(List<TypeDef> hoistedClasses) {
// Build map of class name -> TypeDef for transitive base lookup
java.util.Map<String, TypeDef> 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<String> 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<String> 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<Result> 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<String> willBeInjected,
java.util.Map<String, TypeDef> 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) {
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<String> 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<FunctionDecl> generate(List<FunctionDecl> hoistedFunctions) {
List<FunctionDecl> 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<EnumDecl> enums = new ArrayList<>();
public final List<TopLevelItem> rest = new ArrayList<>();
}
public static Result extract(List<TopLevelItem> 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;
}
}