Skip to content

Conformance-driven compiler + executor: 47/47 oracle equivalence with the Java reference engine - #23

Open
dhes wants to merge 11 commits into
litlfred:mainfrom
dhes:conformance-spike
Open

Conformance-driven compiler + executor: 47/47 oracle equivalence with the Java reference engine#23
dhes wants to merge 11 commits into
litlfred:mainfrom
dhes:conformance-spike

Conversation

@dhes

@dhes dhes commented Sep 5, 2026

Copy link
Copy Markdown

Following up on #22 and the "test cases + real integration" direction: this PR rebuilds the FML compiler and StructureMap executor and holds them to an external standard — full-output equivalence with the reference Java engine (validator_cli -transform) on WHO's published content.

Scores

Suite Result
WHO smart-immunizations: all 33 published QR examples through the C4/D1/D5/D13/D17 maps 33/33 byte-identical (incl. generated-UUID structure)
ddcc + smart-ot execution fixtures (14, held separately — CC BY-NC-SA content) 14/14
Parse-only sweep: every .fml in the WorldHealthOrganization org (54 files, 4 repos) 54/54 compile

What changed

  • Compiler (FmlCompiler.kt): full rule grammar as exercised by the WHO corpus — uses/imports headers, typed and comma-less group signatures, source type casts (entry.resource : Patient), listMode/where/check, quoted and keyword element names, parenthesized FHIRPath targets and arguments, anonymous expression targets, nested then {} blocks, dependent group calls (import-aware), line-numbered errors everywhere.
  • Executor (new executor/FmlEngine.kt, replacing StructureMapExecutor.kt): tree-building executor with semantics learned from oracle diffs rather than assumed — SD-driven choice-element naming and cardinality (replace-vs-append), type-cast filtering, terminology-lookup c() display, translate() modes, cross-SD type propagation for nested logical models, and hard errors instead of silent no-ops (the old executor returned success=true with {} output on unimplemented constructs; nothing fails silently now).
  • Tests (jvmTest): ImmzConformanceSpike (33 fixtures; skips with instructions when the corpus isn't present), NcsaConformanceSpike (skips unless its license-separated corpus is present), FmlParseSweep (org-wide parse tally, env-pointed).

Test corpus

The 33-fixture suite lives in a standalone CC0 repo — https://github.com/dhes/fmlrunner-conformance — containing the pinned WHO IG package, the 16 .fml sources, reference-engine oracle outputs, and the regeneration harness. It's engine-agnostic: any FML implementation can be scored against it. Clone it (or set FMLRUNNER_CONFORMANCE_DIR) and gradle jvmTest runs the full 33.

Happy to restructure — vendor the fixtures into this repo, split the PR, adjust layout per #22 — whatever fits the project's direction. CANDIDATE-ENGINES.md in the corpus repo documents every discrepancy found on the way to 47/47 and what each taught us about the reference engine's semantics; WALKTHROUGH.md there is a non-technical end-to-end example.

Honest scope

Corpus-complete, not spec-complete: conceptmap blocks, multi-source rules, extends, and several transforms (cc, qty, reference, …) are unimplemented and now fail loudly rather than silently. The FHIRPath subset covers what the corpus exercises. The natural next step — adopting kotlin-fhir as the model layer and kotlin-fhirpath for expressions — is exactly what this conformance suite makes safe to attempt.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj

dhes and others added 11 commits September 3, 2026 16:18
…stic)

Result on current main: 0/16 WHO smart-immunizations FML maps compile
("StructureMap must have at least one group").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
…line-numbered errors

All 16 WHO smart-immunizations maps now clear the header and reach rule
parsing (previously 0 — every map died before its first group). Adds
structure/import to the StructureMap type (FHIR R4 shape), '/* */' comment
lexing, 'uses ... alias X as mode' and 'imports ...' clauses, optional
':' types on group inputs, and token line/context on all parser errors.

Corpus status: 0/16 compile (failures now inside rule bodies — next rung).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Rewrites rule parsing to the spec shape the WHO corpus exercises:
comma-separated sources/targets; source list modes, 'as' bindings, and
where/check FHIRPath capture (tokens re-joined; strings re-quoted);
target transform invocations (create/append/translate/c/uuid),
'=' copy/evaluate forms, and literal params in FHIR value[x] shape;
nested 'then { }' rule blocks; dependent group calls; trailing rule
names. Adds rule/dependent/listMode/condition/check to the types and
switches transform parameters from List<String> to TransformParameter.

Corpus: 16/16 compile (was 0/16). Execution is NOT there yet: the
executor returns success=true with an empty {} for C4 — silent
no-op output; that wall is next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
…mpty output)

Replaces the no-op executor with FmlEngine: execution over generic JSON
trees (mutable target nodes; sources read from input JSON or from
target-built logical-model instances), scope chains, import-aware
dependent-group resolution, choice-type (value[x]) reads and writes,
and the corpus's transform set — copy/create/append/uuid/translate
(code|coding|display modes, real ConceptMaps)/c/evaluate. Unknown
transforms and unresolved groups/variables are hard errors, never
silent no-ops.

Type-aware where the reference engine is: registered
StructureDefinitions drive choice-element naming (declared LM element
types beat lexical guessing) and singular-vs-repeating semantics
(declared max 1 -> last write wins; computed re-writes of singular
elements replace, creations append).

Score vs github.com/dhes/fmlrunner-conformance oracles: 31/33 match,
0 errors. The 2 residual diffs are a reference-engine terminology
quirk (drops the display it cannot verify for a LOINC coding on
tx.fhir.org; keeps the same-shaped IMMZ coding's display) — our
output renders the map faithfully.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
…es match

The 2 residual diffs were not an oracle quirk to waive — they were a
c() semantics gap: the reference engine resolves Coding.display from
terminology, ignoring the transform's third argument, and omits the
display when the code system is unavailable (e.g. unlicensed LOINC).
Implemented: registerCodeSystem() feeds a display store; c() looks up
(system, code) and emits no display on a miss. LOINC's absence now
falls out naturally instead of being explained away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Grammar gaps found by an org-wide parse sweep (ddcc, smart-ot,
smart-example-immz) and closed:
- source type cast: entry.resource : Patient
- comma-less multiline group signatures
- element names that are quoted strings (src."-260") or FML keywords
  (Name.prefix), and keywords in value/parameter positions
- parenthesized FHIRPath as target transform and as transform argument
- anonymous expression targets: (expr) as var

FmlParseSweep jvmTest runs the sweep over FML_SWEEP_DIR and tallies
failures by error signature. Oracle regression unchanged: 33/33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Two mechanisms: logical-model roots need resourceType emission;
bare target contexts (-> tgt then ...) unsupported by the executor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Executor semantics learned from ddcc/smart-ot oracle diffs:
- entry roots emit resourceType from the SD's type (resource + logical kinds)
- bare target contexts (-> tgt then ...) bind without mutating
- source type casts filter by resourceType
- declared max governs replace-vs-append even for computed writes
- declared child types resolve from their own SD (nested LM URL, named
  datatype core SD, backbone falls back to parent's leaf map)
- primitive writes into complex-declared elements drop (reference behavior)
- created nodes remember their type for choice-element naming and
  core-SD cardinality lookups
- FHIRPath conditions: in(), and, exists(), exists().not(), .value on prims
- leaf-flattened SD maps: shallower element wins collisions
- comparison contract: uuids number by fullUrl (entry identity) first

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Immz maps now load from the conformance repo's maps/ dir, making
github.com/dhes/fmlrunner-conformance the single dependency for the
33-fixture run. The ncsa spike skips without its (unpublished) corpus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Pure-common code; bumps kotlinx-serialization/datetime to
wasm/native-capable versions. Publishes org.litlfred.fmlrunner:fmlrunner:0.1.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Singleton values of repeating elements (name, given, entry, ...) were
emitted bare, which normalized-diff scoring tolerated but strict FHIR
parsers reject. Arrays now come from declared cardinality when an SD is
registered, with the REPEATING_ELEMENTS set as fallback. 47/47 intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGgxE9TK56BNPJJX2PnYhj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant