| Crates.io | tlq-fhirpath |
| lib.rs | tlq-fhirpath |
| version | 0.1.10 |
| created_at | 2026-01-06 15:15:33.19074+00 |
| updated_at | 2026-01-10 13:00:37.355337+00 |
| description | FHIRPath engine. |
| homepage | https://thalamiq.io |
| repository | https://github.com/thalamiq/tlq-fhir-core |
| max_upload_size | |
| id | 2026059 |
| size | 1,119,305 |
fhir-fhirpathFHIRPath compiler + VM with support for the HL7 R5 FHIRPath test suite. The crate is designed to:
PlanPlan against JSON-backed FHIR resourcesThis repository also contains a CLI wrapper in crates/cli that exercises the engine.
use tlq_fhirpath::{Context, Engine, Value};
use serde_json::json;
let engine = Engine::with_fhir_version("R5")?;
let resource = json!({"resourceType": "Patient", "gender": "male"});
let root = Value::from_json(resource);
let ctx = Context::new(root);
// Rooted expression
let result = engine.evaluate_expr("Patient.gender", &ctx, None)?;
// Unrooted expression with type-aware compilation
let result = engine.evaluate_expr("gender", &ctx, Some("Patient"))?;
# Ok::<(), tlq_fhirpath::Error>(())
From the workspace root:
# evaluate (resource file)
cargo run -p cli -- fp "Patient.gender" fhir-test-cases/r5/examples/patient-example.json --output fhirpath
# evaluate (stdin)
cat fhir-test-cases/r5/examples/observation-example.json \
| cargo run -p cli -- fp "valueQuantity.value" - --output fhirpath
# strict semantics (invalid paths error instead of returning empty)
cargo run -p cli -- fp "valueQuantity.value" fhir-test-cases/r5/examples/observation-example.json --strict
The engine implements a classic compiler pipeline:
src/lexer.rs) → token streamsrc/parser.rs) → AST (src/ast.rs)src/analyzer.rs) → HIR (src/hir.rs)src/typecheck.rs) → typed HIRsrc/codegen.rs) → bytecode Plan (src/vm.rs)src/vm.rs, src/vm/operations.rs, src/vm/functions/*) → CollectionThe top-level orchestration lives in src/engine.rs.
Engine (src/engine.rs) owns the “global” registries and caches:
TypeRegistry (src/types.rs): System type IDs + helpers used throughout compilation.FunctionRegistry (src/functions.rs): compile-time PHF map from function name → FunctionId + signature metadata.VariableRegistry (src/variables.rs): assigns numeric IDs to external variables (e.g. %resource).FhirContext (fhir-context crate): StructureDefinition access for type inference and validation.LruCache<String, Arc<Plan>>: caches compiled bytecode, keyed by (base_type, expr) when a base type is provided.Key property: compilation can be expensive; evaluation is intended to be cheap, so Engine::compile() + Engine::evaluate() is the “hot path” for repeated evaluation.
The runtime value model is in src/value.rs:
Value is an Arc<ValueData> for cheap cloning.Collection is an optimized container for the “mostly singleton” nature of FHIRPath:
SmallVecArc-backed representation for O(1) clonesValueData variants cover:
Boolean, Integer, Decimal, StringDate, DateTime, Time (each with explicit precision)Quantity (value + unit)Object (JSON object as HashMap<field, Collection>)Empty (FHIRPath empty collection marker; the VM treats {} as an empty collection, not a singleton Empty value)Temporals model two distinct aspects:
DatePrecision, DateTimePrecision, TimePrecision)DateTime:
timezone_offset: None → no timezone was specified (unknown/local)Some(0) → explicit ZSome(n) → fixed offset seconds east of UTC (e.g. +08:00 is 28800)This matters for both equality/ordering (comparability rules) and boundary functions.
Context (src/context.rs) contains:
$this (current focus), $index (iteration index)resource / root (the original evaluation input)variables for environment values (e.g. %resource, %context, %rootResource (and legacy %root), %profile, %sct, %loinc, …)strict flag for strict semantic validationThe VM also tracks $total for aggregate() and threads it through nested evaluation contexts.
The lexer (src/lexer.rs) produces tokens for:
=, !=, <, <=, |, in, contains, …)@2014-01, @2014-01-01T08, @T10:30:00.000)$this, $index, $total, %resource, …)The parser (src/parser.rs) builds an AST (src/ast.rs) using precedence rules that match the FHIRPath spec.
Notable parsing details:
is / as bind at the correct precedence relative to equality and union.T08 → T08:00) to match the FHIR constraints used by the HL7 suite.The analyzer (src/analyzer.rs) is the semantic lowering step:
ExprType) and cardinality ranges where possible.FunctionId using FunctionRegistry.{ a, b, c } become chained unions (a | b | c)src/hir.rs) better suited for codegen and execution.HIR nodes include:
base + [segments]) where a segment is a field, choice segment, or indexwhere, select, repeat, aggregate, exists(predicate), and all(predicate)After analysis, a second pass (src/typecheck.rs) uses FhirContext to:
This pass is where “compile-time strictness” comes from: passing a base type (e.g. Some("Patient")) enables StructureDefinition-backed validation.
CodeGenerator (src/codegen.rs) emits a Plan (src/vm.rs):
opcodes: the bytecode instruction streamValuesArc<str>is / as operationssubplans for closuresImportant design choices:
Where, Select, Repeat, Aggregate, Exists, All, Iif) so the VM can evaluate subplans lazily and with correct scoping for $this/$index/$total.$this (rather than “empty”) to match FHIRPath behavior.The VM (src/vm.rs) is a stack machine:
Collections.src/vm/operations.rs.src/vm/functions.rs (split into src/vm/functions/* modules).The VM stack holds Collections (not single Values). Most opcodes follow “push result collection”.
Core opcodes:
PushConst: push a literal from the constant pool. ValueData::Empty is treated specially and becomes an empty collection.LoadThis, LoadIndex, LoadTotal: load $this, $index, $total.Navigate(segment_id): field navigation (dot access).Index(i): positional indexing ([i]).CallBinary(impl_id): run a binary operator implementation (e.g. Eq, Union, DivInt, …).CallFunction(func_id, argc): call a normal function with an explicit base (for method calls the base is the receiver collection).Higher-order opcodes execute nested Plans (“subplans”) with a derived Context:
Where(subplan_id): for each item in input, run predicate subplan with $this=item, $index=index, and retain matches.Select(subplan_id): map each item via projection subplan and flatten.Repeat(subplan_id): iterative projection with cycle detection.Aggregate(subplan_id, init_subplan_id?): fold left with $total set and correct $index propagation.Exists(subplan_id?): exists() optionally with predicate.All(subplan_id): all(predicate) with spec behavior (all({}) == true).Iif(true_plan, false_plan, else_plan?): lazy branch evaluation. Important: branch VMs preserve $index and $total.If you’re debugging a behavioral difference, it’s often easiest to visualize the VM plan and then reason about stack effects.
Navigate)Navigate is responsible for “dot access” across collections. Key behaviors:
Context.strict == true), unknown fields error when the base collection was non-empty.FHIR choice elements are represented as [x] in StructureDefinitions but appear as expanded properties in JSON (e.g. valueQuantity, valueString, …).
This implementation supports two modes:
valueQuantity is rejected (encourages spec expressions like value.ofType(Quantity)).This is why CLI behavior depends on whether it compiles with a base type and/or strict semantics.
src/vm/operations.rs implements the core operator semantics:
+, -, *, /, div, mod)=, !=, ~, !~)<, <=, >, >=)in, contains)| union deduplicates)Some semantics are subtle:
~) includes special handling for strings (case/whitespace normalization), quantities (unit conversion + least-precise rounding), and complex types.is, as, ofTypeType operations are implemented across:
src/parser.rs, src/hir.rs)src/vm.rs for TypeIs/TypeAs, and src/vm/functions/type_helpers.rs)Important behavior:
Boolean, Integer) or FHIR types depending on context.is(T) supports inheritance checks for FHIR types (e.g. Age is Quantity).as(T) and ofType(T) are implemented as exact type filters/casts (no inheritance), which matches HL7 suite expectations for subtype-vs-supertype edge cases.string1 produce execution errorsSystem.* types do not necessarily error (some HL7 cases expect non-matching rather than failure)FHIR JSON represents dates/dateTimes/times as strings. This engine keeps JSON strings as ValueData::String by default (no eager parsing).
To still support correct temporal comparisons in common cases (e.g. Period.start <= Period.end), src/temporal_parse.rs provides:
parse_temporal_pair() which tries to interpret two strings as date/dateTime/time valuesThe comparison/equality operators consult this helper when comparing String vs String.
src/functions.rs provides a compile-time PHF map from function name → ID and metadata:
min_args, max_argsreturn_type (TypeId::Unknown for polymorphic functions)At runtime, src/vm/functions.rs dispatches by numeric FunctionId into modules:
vm/functions/existence.rs: empty, exists, all, allTrue, subsetOf, …vm/functions/filtering.rs: ofType, extension, …vm/functions/conversion.rs: toInteger, toDecimal, toString, …vm/functions/string.rs, math.rs, utility.rs, …Higher-order functions that require closures often compile to opcodes rather than “normal” functions, to preserve correct scope and laziness.
resolve() and Reference HandlingEngine can be constructed with a custom ResourceResolver (src/resolver.rs), which is used by the resolve() function. This allows integration with:
If no resolver is configured, resolve() returns empty (or errors, depending on strictness and function behavior).
From crates/fhir-fhirpath/Cargo.toml:
regex, base64, hex, html-escape (enables parts of the string/function surface).xml-support adds XML evaluation helpers via the optional fhir-format dependency.The crate supports visualizing compiler IR stages via src/visualize.rs:
The CLI subcommand visualize wraps this capability.
This repo includes the HL7 R5 FHIRPath suite runner:
crates/fhir-fhirpath/tests/hl7/test_hl7_suite.rsfhir-test-cases/r5/fhirpath/tests-fhir-r5.xmlRun the full suite:
cargo test -p fhir-fhirpath --test test_hl7_suite -- --ignored --nocapture
Run a specific group:
HL7_TEST_GROUP=testPlus cargo test -p fhir-fhirpath --test test_hl7_suite -- --ignored --nocapture
Plan caching and a compact opcode set.$this/$index/$total scoping and laziness.src/temporal_parse.rs), which is pragmatic but not a full “typed JSON model”.FhirContext availability; with incomplete contexts, typing falls back to unknowns.src/engine.rs to follow the pipeline.src/functions.rs (registry metadata)src/vm/functions.rs and appropriate module under src/vm/functions/visualize output early when debugging parser precedence or HIR lowering.