Trieste Development Workflow
Plan and implement Trieste-based compiler passes, AST transformations, and language features in rego-cpp.
When to Use
- Adding or modifying a compiler pass in the file-to-rego or rego-to-bundle pipeline
- Implementing new Rego language syntax (new tokens, grammar rules)
- Changing well-formedness definitions
- Debugging pass failures or well-formedness violations
- Implementing complex multi-step features that touch the AST pipeline
- Any task requiring coordination across parser, passes, built-ins, and VM
Core Concepts
Trieste is a multi-pass term-rewriting system. Understanding these concepts is mandatory before proceeding:
- Pass: A
PassDef that takes an AST conforming to an input well-formedness (WF) definition and rewrites it to conform to an output WF definition. Passes run repeatedly until no more rules match (fixpoint), unless dir::once is specified.
- Well-formedness (WF): A structural specification of valid AST shapes. Each pass declares its output WF. WF definitions are incremental — each extends the previous with
| (choice).
- Pattern → Effect rules: Each pass contains rules of the form
Pattern >> Effect. Patterns match AST subtrees; effects produce replacement subtrees.
- Driver/Reader/Rewriter: Trieste helpers that chain passes into pipelines. rego-cpp uses
Reader for parsing and Rewriter for transformation.
- Generative testing: Trieste can generate random ASTs from WF definitions to fuzz each pass. This discovers edge cases in rewrite rules.
Procedure
Step 0: Understand the Current AST
Before any implementation, you must understand the AST structure at the point you're modifying.
Read the well-formedness definitions for the passes surrounding your change:
- File-to-rego passes: defined in
src/file_to_rego.cc (WF definitions inline with passes)
- Rego-to-bundle passes: defined in
src/rego_to_bundle.cc
- Base WF:
include/rego/rego.hh → wf
- Bundle WF:
include/rego/rego.hh → wf_bundle
- Internal WF:
src/internal.hh → wf_bundle_input
Dump the AST at the relevant pass to see the actual tree shape:
./build/tools/rego eval --dump_passes .copilot/pass-debug/ -p <pass_name> '<query>'
Or write a minimal .rego file and use --wf to check well-formedness.
Never assume node structure — always verify by reading the WF definition. Nodes are typically wrapped (e.g., Array elements inside Term nodes). Use unwrap() helpers.
Step 1: Multi-Planner Analysis
For any non-trivial feature, use the multi-planner approach — analyze the problem from multiple perspectives before writing code. This prevents costly rework.
Perspective 1: Reference Implementation (OPA)
How does OPA implement this feature?
- Check OPA's documentation for the feature's specification
- Inspect OPA's IR output to see how OPA compiles the feature:
mkdir -p .copilot/opa-ir-test
# Create minimal policy exercising the feature
cat > .copilot/opa-ir-test/policy.rego << 'EOF'
package test
# ... minimal example using the feature
EOF
/tmp/opa build --bundle .copilot/opa-ir-test --target plan -e test/<entry> -o .copilot/opa-ir-test/bundle.tar.gz
cd .copilot/opa-ir-test && tar xzf bundle.tar.gz && python3 -m json.tool plan.json
- Test both constant and variable expressions — OPA's optimizer may fold constants, hiding the general compilation path
- Record: internal built-in names, calling conventions, undefined-handling patterns
Perspective 2: AST Pipeline Impact
Where in the rego-cpp pipeline does this feature need to be handled?
- Parser changes? — Does this require new tokens in
include/rego/rego.hh and rules in src/parse.cc?
- Which file-to-rego passes are affected? — Map the feature to specific passes in the 18-pass file-to-rego pipeline (see pass-pipeline.md)
- Which rego-to-bundle passes are affected? — Map to the 11-pass rego-to-bundle pipeline
- VM changes? — Does
src/virtual_machine.cc need new opcodes or evaluation logic?
- Built-in additions? — Any new built-in functions required?
- New Term alternative? — If adding a new node type to
Term, audit all type-dispatch sites:
src/dependency_graph.cc — add_lhs_var / add_rhs must handle the new type
src/resolver.cc — variable resolution may need a case
src/virtual_machine.cc — evaluation dispatch
src/encoding.cc — serialization in to_key()
src/opblock.cc — lowering to opcodes in term_to_opblock()
Perspective 3: Well-formedness Chain
How do WF definitions need to change?
- Trace the WF chain from the first affected pass to the last
- Identify which node types need to be added, modified, or removed at each stage
- Verify that WF changes are incremental — each definition extends the previous
- Check that no downstream pass is broken by the WF changes
Perspective 4: Test Strategy
How will you verify correctness at each stage?
- YAML test cases — Write expected input/output pairs in
tests/regocpp.yaml or tests/bugs.yaml
- OPA conformance tests — Identify which OPA test subdirectories exercise the feature
- Generative testing — Plan to run the Trieste
test command to check WF validity
- Incremental verification — After each pass modification, run targeted tests before proceeding
Step 2: Implementation Plan
Based on the multi-planner analysis, create a sequenced implementation plan:
- Order changes by pipeline stage — parser first, then file-to-rego passes in order, then rego-to-bundle passes, then VM
- Implement one pass at a time — never modify multiple passes simultaneously without testing between changes
- Write test cases first — add YAML test cases for the feature before implementing, so you can verify each step
- Use smallest possible passes — prefer adding a new small pass over making an existing pass more complex (Trieste philosophy: "there is no downside to having many passes")
Step 3: Incremental Implementation
For each pass change:
- Read the current pass code and its surrounding WF definitions
- Modify the WF definition for the pass output if needed (define new node shapes)
- Add rewrite rules using the pattern → effect DSL:
// Standard pattern: match context, capture nodes, produce replacement
In(ParentType) * T(NodeType)[Capture] >> [](Match& _) {
return NewNode << _(Capture);
},
- Add error rules for invalid inputs the WF would allow:
// Catch-all for malformed nodes (order matters — put after positive rules)
T(BadNode)[Node] >> [](Match& _) {
return err(_(Node), "descriptive error message");
},
- Run targeted tests immediately:
# Run specific test case
./build/tests/rego_test -wf tests/regocpp.yaml
# Or specific OPA subdirectory
./build/tests/rego_test -wf opa/v1/test/cases/testdata/v1/<subdir>
- Dump the AST to verify the transformation:
./build/tools/rego eval --dump_passes .copilot/pass-debug/ '<query>'
Step 4: Validation
After all passes are implemented:
- Run the full rego-cpp test suite:
ctest --test-dir build -R "rego_test_regocpp|rego_test_bugs|rego_test_cts|rego_test_cpp_api"
- Run OPA conformance tests (if applicable):
ctest --test-dir build -R rego_test_opa --output-on-failure
- Run generative testing to check WF validity:
./build/tools/rego test -f -c 1000
- Run with AddressSanitizer for memory safety:
cmake --preset asan-clang && ninja -C build-asan && ctest --test-dir build-asan
Key Patterns Reference
PassDef Structure
PassDef my_pass()
{
return {
"my_pass", // Name (for debugging/logging)
wf_my_pass, // Output well-formedness definition
dir::bottomup | dir::once, // Traversal: topdown/bottomup, once/fixpoint
{
// Rules (matched in order, first match wins)
In(Parent) * T(Child)[C] >> [](Match& _) { return _(C); },
}
};
}
Traversal Directions
| Direction |
Meaning |
dir::bottomup |
Process children before parents |
dir::topdown |
Process parents before children |
dir::once |
Single traversal (combine with above) |
| (no once) |
Repeat until fixpoint (no rules match) |
Pattern DSL Quick Reference
| Pattern |
Meaning |
T(Foo) |
Match a node of type Foo |
T(Foo)[X] |
Match Foo, bind to variable X |
T(Foo) / T(Bar) |
Match Foo or Bar |
A * B |
Match A followed by B (siblings) |
P << C |
Match children C inside parent P |
In(P) |
Parent context is P (not part of match) |
Any |
Match any single node |
Any++[X] |
Match one or more remaining nodes, bind to X |
End |
Assert no more siblings |
_(X) |
In effect: get single node bound to X |
_[X] |
In effect: get all nodes bound to X (NodeRange) |
*_[X] |
In effect: get children of nodes bound to X |
Well-formedness DSL
inline const auto wf_my_pass =
wf_previous_pass // Inherit from previous pass
| (NewNode <<= ChildA * ChildB) // NewNode has exactly ChildA then ChildB
| (Container <<= Element++) // Container has 0+ Elements
| (Container <<= Element++[1]) // Container has 1+ Elements
| (Wrapper <<= (ChoiceA | ChoiceB)) // Wrapper has one of ChoiceA or ChoiceB
| (Parent <<= Name * Body)[Name] // [Name] = Name is stored in symbol table
;
Creating AST Nodes
// Node with children
NewNode << child1 << child2
// Node with string content (location)
TokenType ^ "string content"
// Splice children from a matched range
Container << *_[MatchVar] // all children of matched nodes
Container << _[MatchVar] // all matched nodes themselves
// Empty node (remove from tree)
return {};
Common Mistakes
- Not reading the WF definition first — The #1 source of bugs. Nodes are wrapped in unexpected ways.
- Modifying multiple passes without testing between — Errors compound and become impossible to diagnose.
- Comparing
child->type() directly — Use unwrap() helpers; nodes are wrapped in Term/Scalar layers.
- Forgetting error rules — Generative testing will generate inputs that your positive rules don't handle. You must add error rules for these cases.
- Wrong traversal direction —
bottomup processes children first (useful when collapsing); topdown processes parents first (useful when pushing structure down).
- Rule ordering — Rules are matched in order. If a general rule comes before a specific one, the specific rule will never fire.
- Missing
dir::once — Without it, the pass runs to fixpoint. This is correct for most passes but causes infinite loops if rules don't converge.
- Creating parallel paths instead of reusing the standard pipeline — When adding a new compound node type (e.g.,
TemplateString), prefer routing its sub-expressions through the existing Group → Literal → Expr pipeline rather than creating a custom parallel path (e.g., TemplateString <<= (TemplateLiteral | Expr)++). The standard pipeline already handles with/as, some, comprehensions, and other features. Creating a parallel path means manually replicating all of that machinery. In the parser, use m.term() to separate groups naturally and m.in(NodeType) to detect context on closing delimiters, rather than m.push(Brace) which creates a separate nesting scope. Convert specialized tokens (e.g., TemplateLiteral) to standard types (e.g., Scalar << String << JSONString) as early as possible (in the prep pass) to minimize WF cascading.
- Not auditing
dependency_graph.cc when adding new Term alternatives — The dependency graph in src/dependency_graph.cc has explicit if (lhs == Type) cases for every node type that can appear as a Term child. When adding a new Term alternative, you must add a corresponding case there. Missing cases cause "Unable to unify due to cycle" errors. Also audit resolver.cc and virtual_machine.cc for similar type-dispatch patterns.
1---2name: trieste-dev3description: Plan and implement Trieste-based compiler passes and AST transformations for rego-cpp. Use when: adding new compiler passes, modifying AST structure, implementing new Rego language features, debugging pass failures, working with well-formedness definitions, or performing any multi-step implementation that touches the Trieste pass pipeline. Includes the multi-planner approach for complex features.4---56# Trieste Development Workflow78Plan and implement Trieste-based compiler passes, AST transformations, and language features in rego-cpp.910## When to Use1112- Adding or modifying a compiler pass in the file-to-rego or rego-to-bundle pipeline13- Implementing new Rego language syntax (new tokens, grammar rules)14- Changing well-formedness definitions15- Debugging pass failures or well-formedness violations16- Implementing complex multi-step features that touch the AST pipeline17- Any task requiring coordination across parser, passes, built-ins, and VM1819## Core Concepts2021Trieste is a multi-pass term-rewriting system. Understanding these concepts is mandatory before proceeding:2223- **Pass**: A `PassDef` that takes an AST conforming to an input well-formedness (WF) definition and rewrites it to conform to an output WF definition. Passes run repeatedly until no more rules match (fixpoint), unless `dir::once` is specified.24- **Well-formedness (WF)**: A structural specification of valid AST shapes. Each pass declares its output WF. WF definitions are **incremental** — each extends the previous with `|` (choice).25- **Pattern → Effect rules**: Each pass contains rules of the form `Pattern >> Effect`. Patterns match AST subtrees; effects produce replacement subtrees.26- **Driver/Reader/Rewriter**: Trieste helpers that chain passes into pipelines. rego-cpp uses `Reader` for parsing and `Rewriter` for transformation.27- **Generative testing**: Trieste can generate random ASTs from WF definitions to fuzz each pass. This discovers edge cases in rewrite rules.2829## Procedure3031### Step 0: Understand the Current AST3233Before any implementation, you must understand the AST structure at the point you're modifying.34351. **Read the well-formedness definitions** for the passes surrounding your change:36 - File-to-rego passes: defined in `src/file_to_rego.cc` (WF definitions inline with passes)37 - Rego-to-bundle passes: defined in `src/rego_to_bundle.cc`38 - Base WF: `include/rego/rego.hh` → `wf`39 - Bundle WF: `include/rego/rego.hh` → `wf_bundle`40 - Internal WF: `src/internal.hh` → `wf_bundle_input`41422. **Dump the AST** at the relevant pass to see the actual tree shape:43 ```bash44 ./build/tools/rego eval --dump_passes .copilot/pass-debug/ -p <pass_name> '<query>'45 ```46 Or write a minimal `.rego` file and use `--wf` to check well-formedness.47483. **Never assume node structure** — always verify by reading the WF definition. Nodes are typically wrapped (e.g., Array elements inside Term nodes). Use `unwrap()` helpers.4950### Step 1: Multi-Planner Analysis5152For any non-trivial feature, use the **multi-planner approach** — analyze the problem from multiple perspectives before writing code. This prevents costly rework.5354#### Perspective 1: Reference Implementation (OPA)5556How does OPA implement this feature?57581. **Check OPA's documentation** for the feature's specification592. **Inspect OPA's IR output** to see how OPA compiles the feature:60 ```bash61 mkdir -p .copilot/opa-ir-test62 # Create minimal policy exercising the feature63 cat > .copilot/opa-ir-test/policy.rego << 'EOF'64 package test65 # ... minimal example using the feature66 EOF67 /tmp/opa build --bundle .copilot/opa-ir-test --target plan -e test/<entry> -o .copilot/opa-ir-test/bundle.tar.gz68 cd .copilot/opa-ir-test && tar xzf bundle.tar.gz && python3 -m json.tool plan.json69 ```703. **Test both constant and variable expressions** — OPA's optimizer may fold constants, hiding the general compilation path714. **Record**: internal built-in names, calling conventions, undefined-handling patterns7273#### Perspective 2: AST Pipeline Impact7475Where in the rego-cpp pipeline does this feature need to be handled?76771. **Parser changes?** — Does this require new tokens in `include/rego/rego.hh` and rules in `src/parse.cc`?782. **Which file-to-rego passes are affected?** — Map the feature to specific passes in the 18-pass file-to-rego pipeline (see [pass-pipeline.md](./references/pass-pipeline.md))793. **Which rego-to-bundle passes are affected?** — Map to the 11-pass rego-to-bundle pipeline804. **VM changes?** — Does `src/virtual_machine.cc` need new opcodes or evaluation logic?815. **Built-in additions?** — Any new built-in functions required?826. **New Term alternative?** — If adding a new node type to `Term`, audit all type-dispatch sites:83 - `src/dependency_graph.cc` — `add_lhs_var` / `add_rhs` must handle the new type84 - `src/resolver.cc` — variable resolution may need a case85 - `src/virtual_machine.cc` — evaluation dispatch86 - `src/encoding.cc` — serialization in `to_key()`87 - `src/opblock.cc` — lowering to opcodes in `term_to_opblock()`8889#### Perspective 3: Well-formedness Chain9091How do WF definitions need to change?92931. Trace the WF chain from the first affected pass to the last942. Identify which node types need to be added, modified, or removed at each stage953. Verify that WF changes are **incremental** — each definition extends the previous964. Check that no downstream pass is broken by the WF changes9798#### Perspective 4: Test Strategy99100How will you verify correctness at each stage?1011021. **YAML test cases** — Write expected input/output pairs in `tests/regocpp.yaml` or `tests/bugs.yaml`1032. **OPA conformance tests** — Identify which OPA test subdirectories exercise the feature1043. **Generative testing** — Plan to run the Trieste `test` command to check WF validity1054. **Incremental verification** — After each pass modification, run targeted tests before proceeding106107### Step 2: Implementation Plan108109Based on the multi-planner analysis, create a sequenced implementation plan:1101111. **Order changes by pipeline stage** — parser first, then file-to-rego passes in order, then rego-to-bundle passes, then VM1122. **Implement one pass at a time** — never modify multiple passes simultaneously without testing between changes1133. **Write test cases first** — add YAML test cases for the feature before implementing, so you can verify each step1144. **Use smallest possible passes** — prefer adding a new small pass over making an existing pass more complex (Trieste philosophy: "there is no downside to having many passes")115116### Step 3: Incremental Implementation117118For each pass change:1191201. **Read the current pass code** and its surrounding WF definitions1212. **Modify the WF definition** for the pass output if needed (define new node shapes)1223. **Add rewrite rules** using the pattern → effect DSL:123 ```cpp124 // Standard pattern: match context, capture nodes, produce replacement125 In(ParentType) * T(NodeType)[Capture] >> [](Match& _) {126 return NewNode << _(Capture);127 },128 ```1294. **Add error rules** for invalid inputs the WF would allow:130 ```cpp131 // Catch-all for malformed nodes (order matters — put after positive rules)132 T(BadNode)[Node] >> [](Match& _) {133 return err(_(Node), "descriptive error message");134 },135 ```1365. **Run targeted tests** immediately:137 ```bash138 # Run specific test case139 ./build/tests/rego_test -wf tests/regocpp.yaml140 # Or specific OPA subdirectory141 ./build/tests/rego_test -wf opa/v1/test/cases/testdata/v1/<subdir>142 ```1436. **Dump the AST** to verify the transformation:144 ```bash145 ./build/tools/rego eval --dump_passes .copilot/pass-debug/ '<query>'146 ```147148### Step 4: Validation149150After all passes are implemented:1511521. **Run the full rego-cpp test suite**:153 ```bash154 ctest --test-dir build -R "rego_test_regocpp|rego_test_bugs|rego_test_cts|rego_test_cpp_api"155 ```1562. **Run OPA conformance tests** (if applicable):157 ```bash158 ctest --test-dir build -R rego_test_opa --output-on-failure159 ```1603. **Run generative testing** to check WF validity:161 ```bash162 ./build/tools/rego test -f -c 1000163 ```1644. **Run with AddressSanitizer** for memory safety:165 ```bash166 cmake --preset asan-clang && ninja -C build-asan && ctest --test-dir build-asan167 ```168169## Key Patterns Reference170171### PassDef Structure172173```cpp174PassDef my_pass()175{176 return {177 "my_pass", // Name (for debugging/logging)178 wf_my_pass, // Output well-formedness definition179 dir::bottomup | dir::once, // Traversal: topdown/bottomup, once/fixpoint180 {181 // Rules (matched in order, first match wins)182 In(Parent) * T(Child)[C] >> [](Match& _) { return _(C); },183 }184 };185}186```187188### Traversal Directions189190| Direction | Meaning |191|-----------|---------|192| `dir::bottomup` | Process children before parents |193| `dir::topdown` | Process parents before children |194| `dir::once` | Single traversal (combine with above) |195| *(no once)* | Repeat until fixpoint (no rules match) |196197### Pattern DSL Quick Reference198199| Pattern | Meaning |200|---------|---------|201| `T(Foo)` | Match a node of type `Foo` |202| `T(Foo)[X]` | Match `Foo`, bind to variable `X` |203| `T(Foo) / T(Bar)` | Match `Foo` or `Bar` |204| `A * B` | Match `A` followed by `B` (siblings) |205| `P << C` | Match children `C` inside parent `P` |206| `In(P)` | Parent context is `P` (not part of match) |207| `Any` | Match any single node |208| `Any++[X]` | Match one or more remaining nodes, bind to `X` |209| `End` | Assert no more siblings |210| `_(X)` | In effect: get single node bound to `X` |211| `_[X]` | In effect: get all nodes bound to `X` (NodeRange) |212| `*_[X]` | In effect: get children of nodes bound to `X` |213214### Well-formedness DSL215216```cpp217inline const auto wf_my_pass =218 wf_previous_pass // Inherit from previous pass219 | (NewNode <<= ChildA * ChildB) // NewNode has exactly ChildA then ChildB220 | (Container <<= Element++) // Container has 0+ Elements221 | (Container <<= Element++[1]) // Container has 1+ Elements222 | (Wrapper <<= (ChoiceA | ChoiceB)) // Wrapper has one of ChoiceA or ChoiceB223 | (Parent <<= Name * Body)[Name] // [Name] = Name is stored in symbol table224 ;225```226227### Creating AST Nodes228229```cpp230// Node with children231NewNode << child1 << child2232233// Node with string content (location)234TokenType ^ "string content"235236// Splice children from a matched range237Container << *_[MatchVar] // all children of matched nodes238Container << _[MatchVar] // all matched nodes themselves239240// Empty node (remove from tree)241return {};242```243244## Common Mistakes2452461. **Not reading the WF definition first** — The #1 source of bugs. Nodes are wrapped in unexpected ways.2472. **Modifying multiple passes without testing between** — Errors compound and become impossible to diagnose.2483. **Comparing `child->type()` directly** — Use `unwrap()` helpers; nodes are wrapped in Term/Scalar layers.2494. **Forgetting error rules** — Generative testing will generate inputs that your positive rules don't handle. You must add error rules for these cases.2505. **Wrong traversal direction** — `bottomup` processes children first (useful when collapsing); `topdown` processes parents first (useful when pushing structure down).2516. **Rule ordering** — Rules are matched in order. If a general rule comes before a specific one, the specific rule will never fire.2527. **Missing `dir::once`** — Without it, the pass runs to fixpoint. This is correct for most passes but causes infinite loops if rules don't converge.2538. **Creating parallel paths instead of reusing the standard pipeline** — When adding a new compound node type (e.g., `TemplateString`), prefer routing its sub-expressions through the existing `Group → Literal → Expr` pipeline rather than creating a custom parallel path (e.g., `TemplateString <<= (TemplateLiteral | Expr)++`). The standard pipeline already handles `with`/`as`, `some`, comprehensions, and other features. Creating a parallel path means manually replicating all of that machinery. In the parser, use `m.term()` to separate groups naturally and `m.in(NodeType)` to detect context on closing delimiters, rather than `m.push(Brace)` which creates a separate nesting scope. Convert specialized tokens (e.g., `TemplateLiteral`) to standard types (e.g., `Scalar << String << JSONString`) as early as possible (in the `prep` pass) to minimize WF cascading.2549. **Not auditing `dependency_graph.cc` when adding new Term alternatives** — The dependency graph in `src/dependency_graph.cc` has explicit `if (lhs == Type)` cases for every node type that can appear as a Term child. When adding a new Term alternative, you must add a corresponding case there. Missing cases cause "Unable to unify due to cycle" errors. Also audit `resolver.cc` and `virtual_machine.cc` for similar type-dispatch patterns.