Author: Yash Kumar, a contributor to Google Summer of Code 2026.
Table of Content
- About the Project
- Problem Statement
- Solution Overview
- Description
- Key Features
- Core Working Example
- How it works
- Measuring against the specification
- What lies ahead
- Learnings and Experience
- What was the one most important thing I took out of this experience?
- PRs and Issues
About the Project
Concerto is the modelling language behind the Accord Project. My project builds a runtime for the Concerto language in Rust, following the TypeScript implementation of that runtime. It begins with the part that costs the most time in practice: deciding whether a model is valid.
The output of the project is two Rust crates, in accordproject/concerto-rust. The first is concerto-metamodel, which holds the Rust types generated from Concerto’s own metamodel. The second is concerto-core, which is written by hand and does the real work: it reads a model into memory and validates it.
Problem Statement
Validation is the hot path. Every tool that touches a Concerto model has to answer two questions before it can do anything useful: is this JSON a valid Concerto model, and is this instance a valid instance of a type? The TypeScript runtime answers both, but it re-traverses the type tree on every question and keeps no index between them, so the cost compounds wherever models are loaded in bulk.
An earlier experiment by my mentors, concerto-validate-rs, did the same structural validation in Rust approximately ten times faster without any optimisations. That result set the goal for this project: a single validation core in Rust, correct enough to be trusted, and structured so it can later be called from JavaScript through WASM or NAPI, and from C#.
The deliverable this summer was the model side of that. Concerto describes itself: a model arrives as a JSON abstract syntax tree whose own shape is defined by Concerto’s metamodel. Giving every node of that tree a real Rust type is the first half of the work. The second is the semantic rules that decide whether a model holds together once every declaration is in view, such as whether a super type exists, whether a relationship points at something that can be identified, and whether two declarations quietly claim the same name.
Solution Overview
Description
Concerto’s TypeScript runtime treats declarations as an inheritance hierarchy. Concepts, assets, participants, transactions and events all extend a common class declaration, and every field extends a common property. Rust has no subtyping. Mirroring that hierarchy with traits was possible, but it would have fought the language the whole way, so each family of AST node became a sum type instead, selected by matching on the node’s $class:
The five class-like declarations collapse into a single ClassDeclaration carrying a ClassKind tag. They are structurally identical and differ only in what they mean, so there was nothing to gain from keeping them apart. A sum type also means the compiler notifies you when a new kind appears and some match has not been updated, which caught real gaps during review.
The only object with state is ModelManager. It owns the loaded model files, preloads the built-in concerto@1.0.0 system model, and answers the questions that need more than one namespace in view: resolving a type, collecting every property along an inheritance chain, and deciding whether one type is assignable to another.
Key Features
- All 30 semantic rules from the specification. Identifier rules, super type resolution, identity rules, property and relationship type rules, import rules, map key and value rules, decorator rules, and the numeric, length and regex validators.
- Two-phase validation. Rules that a declaration can break on its own are raised while the model loads. Rules that need to look at other declarations run in a separate pass over the loaded ModelManager. The split is not a preference; it is where the reference runtime raises each rule, which I confirmed by parsing models with validation disabled and seeing which errors still appeared.
- Generated types, never hand-edited. The concerto-metamodel crate is generated from the upstream metamodel by a build script. It hashes the pinned Concerto CLI version against a recorded one and regenerates only when they differ, so ordinary builds and CI stay offline and need no Node.js.
- A test for every rule. 84 unit tests and 7 documentation tests, with each rule covered by a model that breaks it and one that satisfies it, so a regression shows up as a named failure rather than a changed count.
- Strict Concerto v4 semantics. Wildcard imports are rejected, namespace versions are mandatory, and type resolution is exact rather than version-insensitive.
- Errors that separate the two kinds of failure. A model that cannot be read at all is an IllegalModel. A model that reads cleanly but breaks a rule is a ValidationFailed. Validation reports the first problem in a stable order, so the same set of models always reports the same error.
Core Working Example
A model with a relationship pointing at a type that has no identifier:
Address has no identity, so nothing can point at a particular one. The model loads without complaint, because on its own each declaration is well formed, and the problem only appears once both declarations are in view.
How it works
- Loading. The JSON AST is read into the sum types above. Each node is re-read from its raw JSON rather than deserialised straight into the generated types, because the generated base structs drop the parts the runtime needs, such as validators and referenced types.
- Validation. validate_models walks the loaded model files in namespace order, applying the rules that need more than one declaration in view.
- Reporting. The first rule a model breaks stops the pass and is returned. Because the walk is ordered, the same models always produce the same error.
Measuring against the specification
The Accord Project publishes a conformance test suite, built during last year’s Summer of Code, which names 30 semantic rules and ships model fixtures for them. It is the natural yardstick for this work, and the runtime now implements all 30.
Measured against the suite’s own fixtures, the runtime gives the expected accept or reject outcome on 62 of the 65 scenarios whose AST fixtures exist in the repository. Why the denominator is 65 rather than 75, and where the three misses fall, is worth naming honestly rather than rounding away:
- Ten scenarios in the suite point at ASTs that are not in the repository and cannot be generated, because their .cto sources do not parse. Since both the TypeScript and Rust harnesses load ASTs rather than .cto, no runtime can reach them. My mentor is tracking that upstream.
- Two scenarios expect an error that the reference runtime does not raise, and the messages they expect do not appear anywhere in concerto-core.
- One fixture, model_file_002_unique_namespace_imports.json, is a pre-v4 AST: its namespace carries no version and its imports use the abstract Import class with fields that v4 does not define. It is rejected now that versions are mandatory, which my mentor confirmed is the right behaviour for the moment.
The conformance workflow in concerto-rust now runs on every push and pull request. The suite ships 75 scenarios, of which 65 have usable AST fixtures, and the runtime produces the expected outcome on 62 of those. The job itself is still red because the upstream Rust harness does not build against this runtime, and fixing that is priority work.
What lies ahead
- Instance validation. The runtime validates models. The other half of the original goal, checking that a JSON instance is a valid instance of a type, is the natural next phase and the one that delivers the speed story.
- Updating the conformance harness. The Rust harness in concerto-conformance was written before this runtime existed and expects an API that was never built. Bringing it onto the current API is what turns the numbers above into a green CI job.
- Benchmarks. The ten times figure comes from my mentors’ earlier experiment, not from this codebase. A criterion benchmark against the TypeScript runtime on the same models would turn it into a number this project can stand behind.
- Bindings. WASM and NAPI for JavaScript, and C#, so the same validation core serves every implementation instead of each one carrying its own.
Learnings and Experience
This summer, as a participant in Google Summer of Code 2026, I had the opportunity to work with the Accord Project under the guidance of my mentors, Ertugrul Karademir and Jamie Shorten. I came in expecting the hard part to be Rust. It was not. The hard part was working out what correct meant.
What was the one most important thing I took out of this experience?
A test suite is evidence, not truth. Early on I measured the runtime against the conformance fixtures and reported a number I was pleased with. Looking closer, ten of those scenarios were passing because their fixture files were missing: a missing file produces an error, and the test only checked that an error happened. The number was inflated and I had published it. Reading the specification instead of the fixtures gave a claim that survived scrutiny: rules implemented, not scenarios passed.
The same habit caught a bug of my own. I added a rule that every declaration name had to be a valid identifier, watched the tests pass, and moved on. They passed because the check never ran on concepts, assets, participants, transactions or events: those take an early return a few lines further up, so the most common declarations in any model skipped it entirely. What surfaced was a matrix of fifteen models, each written to break one specific rule and checked for the error it should produce. Tests that only cover what you were already thinking about will happily agree with you.
I also learned to check the reference implementation before deciding where a rule belongs. Twice I put a check in the wrong phase and the conformance number did not move, which was the clue that the TypeScript runtime raised it somewhere else. Parsing with validation disabled and observing which errors still appeared settled it every time.
Beyond the code, I learned how a professional open source project actually runs: small and frequent pull requests, review comments worth thinking hard about, and the habit of filing an issue before fixing something so the reasoning is recorded. My mentors published two npm releases specifically to unblock me, which is a kind of support I did not expect and will not forget.
PRs and Issues
Table of Pull Requests
Table of GitHub Issues
| Issue | Status |
|---|---|
| Rust codegen: serialize_datetime_option panics on a None value | Closed |
| Version-insensitive type lookup is inconsistent: inheritance traversal requires the namespace version | Closed |
Roadmap issues addressed
| Issue | Status |
|---|---|
| Build Internal Representation of Concerto Types | Complete |
| Create a build script to sync concerto-metamodel crate | Complete |
| Implement Model Manager or equivalent | Complete |
| Build validation paths of the internal representation | All 30 rules implemented |
| Make concerto-validate tests pass | Suite enabled in CI, harness update pending |