ADR-0014: Flat Compilation Intermediate Representation (IR)
Status
Accepted
Date
2026-05-21 (updated 2026-05-28)
Based On
- ADR-0010 — YAML Syntax v2
- ADR-0011 — Environment and Services
- ADR-0012 — Compilation and Packaging
- ADR-0013 — Preconditions Specification (superseded, not published)
Context
The TestLab compilation pipeline (ADR-0012) transforms YAML v2 source files into a portable package. However, the internal representation between the compiler output and the Player input has not been formally specified. Without a well-defined Intermediate Representation (IR), the compiler and Player are tightly coupled — changes to one break the other, and tooling (debuggers, trace viewers, decompilers) cannot interoperate.
Forces
- The compiler must perform ALL intelligence: variable resolution, type checking, reference validation, phase flattening, and assertion wiring. It emits a fully-resolved, self-describing output.
- The Player must be dumb: iterate instructions sequentially, resolve
$refvalues from a flat dictionary, dispatch step executors, store outputs, and run inline assertions. Zero parsing, zero graph traversal, zero ambiguity resolution at runtime. - The IR must be decompilable back to source YAML without data loss (supporting round-trip editing).
- The IR must support O(1) variable lookup at runtime — no tree walking or scope chain resolution.
- The IR must be self-describing — every instruction carries enough metadata to be understood in isolation (phase, position, type, expected inputs/outputs).
- The IR must support structured tracing — every instruction has a stable
indexthat correlates compiled IR to execution trace logs. - The format must be JSON-serializable for embedding in
.tckpkgarchives, streaming over SSE, and storage in databases.
Design Constraints
| Constraint | Rationale |
|---|---|
| Flat instruction array | No recursion, no nesting, no conditional branches at IR level |
| All references pre-resolved | ${{ }} syntax does not exist in the IR — only $ref objects |
| Phase metadata preserved | Each instruction knows its origin phase for decompilation |
| Inline assertions | Validation blocks are properties of their parent instruction, not separate instructions |
| Deterministic ordering | Same source YAML always produces the same IR (byte-identical with same compiler version) |
| Forward-reference prohibition | produced_by index must always be less than the consumer's index |
Decision
1. Design Philosophy
The Flat Compilation IR follows three core principles:
1.1 Compiler Does All Thinking
The compiler is the intelligent component. It resolves all variable references, validates type compatibility, checks temporal ordering (no forward references), inlines testdata, resolves service configurations, and flattens the three-phase structure (setup → steps → teardown) into a single ordered instruction array. The compiled output is a fully-resolved execution plan — the Player performs zero interpretation.
1.2 Flat Structures Only
The IR uses two primary data structures:
- Symbol table: A flat dictionary mapping canonical keys to symbol metadata. O(1) lookup by key.
- Instructions: A flat ordered array. The Player iterates index 0 to N-1, no branching, no recursion.
There are no nested scopes, no scope chains, no tree structures that require traversal at runtime.
1.3 Self-Describing Instructions
Every instruction carries all metadata needed to: - Execute it (type, parameters, timeout) - Trace it (index, id, phase) - Decompile it (phase, phase_index) - Validate its results (inline assertions)
An instruction can be extracted from the array and understood in isolation — no external context is required beyond the symbol table values.
2. Two-File Compilation Output
The compiler produces two files — not a standalone JSON per test:
| File | Purpose | Contains |
|---|---|---|
manifest.yaml |
Package metadata, integrity, test registry | TCK identity, compilation fingerprint, test list |
tck-execution.json |
Execution payload | env, tests, preconditions, all compiled tests |
2.1 manifest.yaml Schema
kind: manifest
package:
format: tckpkg
format_version: "1.0.0"
testlab: v1-alpha
tck:
id: "<tck-id>"
namespace: "<namespace>"
metadata:
name: "<string>"
version: "<string>"
description: "<string>"
authors: [...]
copyright_holders: [...]
license: Apache-2.0
standards: [...]
tags: [...]
dataspace_version: "<string>"
dataspace: # omitted when the TCK names no release
ecosystem: "<string>"
version: "<string>"
infrastructure: # omitted when the TCK requires nothing
engine:
<capability>:
required: <bool>
standard: {id: "<string>", version: "<string>"} # omitted when unstated
sut:
<capability>:
required: <bool>
compilation:
compiled_at: "<ISO 8601 UTC>"
compiler_version: "<semver>"
fingerprint:
nonce: "blake2b:<hex>" # unique per compilation (blake2b of random bytes)
public_key: "ed25519:<base64>" # persistent compiler installation key
checksum: "blake2b:<hex>" # blake2b hash of tck-execution.json
tests: # NOT present in encrypted mode
- id: "<test-id>"
file: "<source path>"
source_hash: "blake2b:<hex>"
Key fields:
| Field | Description |
|---|---|
kind |
Always manifest — identifies the file type |
package.format |
Always tckpkg |
compilation.fingerprint.nonce |
blake2b hash of random bytes — unique per compilation |
compilation.fingerprint.public_key |
Ed25519 public key of the compiler installation |
compilation.fingerprint.checksum |
blake2b hash of the entire tck-execution.json content |
tests |
Registry of compiled tests with source hashes (omitted in encrypted mode) |
tck.dataspace |
The ecosystem release the TCK certifies against (ADR-0019). Written only when the TCK names one — a release nobody declared is the engine's default and is never claimed on the TCK's behalf |
tck.infrastructure |
What must be bound before the package can run (ADR-0019), resolved: the manifest-level block when the TCK states one, otherwise the per-test blocks merged the way the player merges them. This is what makes a compiled package answer "what must I bind" without decompiling it |
2.2 tck-execution.json Schema
{
"env": {
"variables": [...],
"services": [...],
"schemas": {...},
"testdata": {...}
},
"tests": [
{ "id": "<test-id>", "file": "<source path>", "source_hash": "blake2b:<hex>" }
],
"preconditions": [
{ "id": "...", "uses": "...", "name": "...", "with": {...}, "returns": {...}, "seed": {...} }
],
"compiled_tests": [
{
"id": "<test-id>",
"metadata": { "name": "...", "version": "...", "description": "..." },
"symbol_table": { "<canonical_key>": {...} },
"instructions": [ {...}, {...} ]
}
]
}
Root-level sections:
| Section | Description |
|---|---|
env |
Shared environment: variables, services, schemas (asset refs), testdata (asset refs) |
tests |
Test registry with source file paths and integrity hashes |
preconditions |
Pre-execution requirements (at root level, NOT inside env). May include seed mapping (see ADR-0013 §10) |
compiled_tests |
Array of compiled test units — the core IR |
2.3 compiled_tests[] Entry (CompiledTest)
Each entry in compiled_tests represents one compiled test:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Test identifier (lowercase kebab-case) |
metadata |
object | Yes | Human-readable metadata (name, version, description) |
symbol_table |
object | Yes | Flat dictionary of all symbols for this test (see §5) |
instructions |
array | Yes | Ordered array of instruction objects (see §4) |
No duplication of package-level fields
compiled_tests entries do NOT contain format_version, compiled_at, compiler_version, namespace, or testlab. Those belong exclusively in the manifest.
3. Key Architectural Decisions
| Decision | Rationale |
|---|---|
All tests in one tck-execution.json |
Single file load, atomic execution, simpler Player |
id field name (not test_id) |
Consistent naming across all objects |
| Preconditions at root level | They apply to ALL tests, not to a specific env |
| blake2b hashing (not SHA-256) | Faster, equally secure, native in Python |
| Fingerprint block (nonce + public_key + checksum) | Tamper detection without full PKI |
| Manifest uses grouped sections | Clear separation: package / tck / compilation |
kind: manifest discriminator |
Enables tooling to identify file type without parsing |
4. Instruction Schema
Each instruction in the instructions array follows this schema:
{
"index": 0,
"id": "<step_id>",
"uses": "<step_type>",
"name": "<human label>",
"with": { "<param>": "<value or $ref>" },
"returns": { "<output_name>": { "type": "<type>", "class": "<class>" } },
"validate": [ { "uses": "validate/...", "with": {...} } ],
"phase": "setup|steps|teardown",
"phase_index": 0
}
| Field | Type | Required | Description |
|---|---|---|---|
index |
integer | Yes | Global position (0-based). Monotonically increasing. |
id |
string | Yes | Step identifier (unique within the test). |
uses |
string | Yes | Step executor type (e.g., connector/create_asset). |
name |
string | Yes | Human-readable label for tracing/debugging. |
with |
object | Yes | Input parameters. Values are literals or $ref objects. |
returns |
object | Yes | Declared outputs with type metadata. |
validate |
array | No | Inline assertions executed after the step. |
phase |
string | Yes | Origin phase: setup, steps, or teardown. |
phase_index |
integer | Yes | Position within the phase (for decompilation). |
5. Symbol Table
The symbol table is a flat dictionary providing O(1) lookup for all resolvable values:
{
"env.provider_url": { "source": "env.variables", "type": "string", "default": "" },
"env.testlab_dsp_url": { "source": "env.variables", "type": "string", "default": "", "seeded_by": "testlab_connector.dsp_url" },
"setup.gen_id.generated_id": { "source": "step_output", "produced_by": 0, "type": "string" },
"env.services.testlab_connector": { "source": "env.services", "produced_by": -1, "type": "class", "class": "ConnectorService" }
}
| Field | Description |
|---|---|
source |
Origin: env.variables, env.services, env.schemas, env.testdata, env.preconditions, step_output |
produced_by |
Instruction index that produces this value. -1 for environment values. Omitted for env.variables entries. |
type |
Runtime type: string, integer, object, class |
class |
(Optional) Semantic class: ConnectorService, MockInstance, Uuid, etc. |
default |
(Optional) Default value for env.variables entries. Empty string if not explicitly set. |
seeded_by |
(Optional) <precondition_id>.<return_field> — indicates this variable is seeded by a precondition output (see ADR-0013 §10). |
Invariant: For any symbol with produced_by >= 0, any instruction referencing it via $ref must have a higher index. No forward references.
6. $ref Resolution
All variable references in the IR use { "$ref": "<symbol_key>" }. The Player resolves by looking up the key in the symbol table, then fetching the runtime value. O(1) — no parsing, no interpolation.
Consequences
Positive
- Decoupled compiler/Player: The IR is a stable contract between the two components.
- Tooling ecosystem: Debuggers, trace viewers, and decompilers can operate on the IR independently.
- Single-file execution:
tck-execution.jsoncontains everything the Player needs — one read, one parse. - Predictable execution: Flat instruction array with no branching means deterministic behavior.
- Fast symbol resolution: O(1) lookup at runtime — no scope chain walking.
Negative
- Larger file size: All tests in one JSON file can be large for big TCKs.
- No selective execution from IR alone: The Player must load all compiled tests even to run one (filtering is done after load).
Supersedes
This ADR supersedes the original single-file-per-test design. All compilation now produces the two-file output described in §2.