From 3ed5bd418b8c06142f07b684bdc8fe6a53f84129 Mon Sep 17 00:00:00 2001 From: Josh Mandel Date: Wed, 28 Jan 2026 15:24:31 -0600 Subject: [PATCH 1/8] Add fhirpath.zig WASM engine support (R4 + R5) Integrates the fhirpath.zig engine as a local browser-based engine, loading WASM and model binaries from joshuamandel.com/fhirpath.zig. This is a Zig-based FHIRPath implementation compiled to WebAssembly. Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 143 +++++++++++++++++++++++++++++++++ types/fhirpath_test_engine.ts | 25 ++++++ 2 files changed, 168 insertions(+) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index 1ed510e..e12b2a9 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -964,6 +964,141 @@ export function convertOptionsToParametersJavaR5R6( return parameters; } +// --- fhirpath.zig WASM engine support --- + +const FHIRPATH_ZIG_BASE = "https://joshuamandel.com/fhirpath.zig"; + +let _zigEnginePromise: Promise | null = null; +let _zigSchemasRegistered: Set = new Set(); + +async function getZigEngine(): Promise { + if (!_zigEnginePromise) { + _zigEnginePromise = (async () => { + // Dynamic import of the JS wrapper from the deployed site + const mod = await import(/* webpackIgnore: true */ `${FHIRPATH_ZIG_BASE}/fhirpath.js`); + const FhirPathEngine = mod.FhirPathEngine; + const engine = await FhirPathEngine.instantiate(`${FHIRPATH_ZIG_BASE}/fhirpath.wasm`); + return engine; + })(); + } + return _zigEnginePromise; +} + +async function ensureZigSchema(engine: any, fhirVersion: string): Promise { + const version = fhirVersion.toLowerCase(); + const schemaName = version; // "r4" or "r5" + if (!_zigSchemasRegistered.has(schemaName)) { + await engine.registerSchemaFromUrl({ + name: schemaName, + prefix: "FHIR", + url: `${FHIRPATH_ZIG_BASE}/model-${schemaName}.bin`, + isDefault: _zigSchemasRegistered.size === 0, + }); + _zigSchemasRegistered.add(schemaName); + } + return schemaName; +} + +/** + * Evaluate FHIRPath expression using fhirpath.zig WASM engine + */ +export async function evaluateExpressionUsingFhirpathZig( + options: FhirPathEvaluationOptions, + fhirVersion: 'R4' | 'R5' = 'R4' +): Promise { + const result: FhirPathEvaluationResult = { + results: [], + debugTraceData: [], + processedByEngine: `fhirpath.zig (${fhirVersion} WASM)` + }; + + try { + const engine = await getZigEngine(); + const schemaName = await ensureZigSchema(engine, fhirVersion); + + // Set current time + engine.setNowDate(new Date()); + + const resourceJson = options.resourceJson || '{}'; + + // Evaluate context expression to get context nodes, or use root + let contexts: { path?: string; json: string }[] = []; + if (options.contextExpression) { + try { + const contextResult = engine.eval({ + expr: options.contextExpression, + json: resourceJson, + schema: schemaName, + }); + for (const node of contextResult) { + contexts.push({ + path: node.meta?.typeName || undefined, + json: JSON.stringify(node.data), + }); + } + } catch (err: any) { + result.saveOutcome = CreateOperationOutcome('fatal', 'exception', + `Context expression error: ${err.message || err}`); + result.showOutcome = true; + return result; + } + } else { + contexts.push({ json: resourceJson }); + } + + // Evaluate main expression for each context + for (const ctx of contexts) { + const resData: ResultData = { + context: ctx.path, + result: [], + trace: [] + }; + + try { + const evalResult = engine.eval({ + expr: options.expression, + json: ctx.json, + schema: schemaName, + }); + + for (const node of evalResult) { + const typeName = node.meta?.typeName || ''; + const data = node.data; + + let value: any; + if (data !== null && data !== undefined) { + if (typeof data === 'object' && data.value !== undefined && data.unit !== undefined) { + // Quantity + value = `${data.value} '${data.unit}'`; + } else if (typeof data === 'object') { + value = JSON.stringify(data, null, 2); + } else { + value = data; + } + } + + resData.result.push({ + type: typeName, + value: typeof value === 'string' ? value : JSON.stringify(value), + }); + } + } catch (err: any) { + result.saveOutcome = CreateOperationOutcome('fatal', 'exception', err.message || String(err)); + result.showOutcome = true; + return result; + } + + result.results.push(resData); + } + } catch (err: any) { + result.saveOutcome = CreateOperationOutcome('fatal', 'exception', + `Failed to load fhirpath.zig engine: ${err.message || err}`); + result.showOutcome = true; + } + + return result; +} + /** * Get engine URL and determine if it's a local (non-RESTful) engine */ @@ -981,6 +1116,9 @@ export async function getEngineInfo(selectedEngine: IFhirPathEngineDetails): Pro if (engineName === "fhirpath.js") { return { isLocal: true, requiresSpecialParameterHandling: false, astSupported: true }; } + if (engineName === "fhirpath.zig") { + return { isLocal: true, requiresSpecialParameterHandling: false, astSupported: false }; + } } // RESTful engines - use the URL from the engine details if available @@ -1014,6 +1152,11 @@ export async function evaluateFhirPathExpression( const engineInfo = await getEngineInfo(selectedEngine); if (engineInfo.isLocal) { + const engineName = selectedEngine.name.toLowerCase(); + if (engineName === "fhirpath.zig") { + const fhirVersion = selectedEngine.fhirVersion.toLowerCase() === 'r5' ? 'R5' : 'R4'; + return await evaluateExpressionUsingFhirpathZig(options, fhirVersion); + } // Use local fhirpath.js engine const fhirVersion = selectedEngine.fhirVersion.toLowerCase() === 'r5' ? 'R5' : 'R4'; return await evaluateExpressionUsingFhirpathJs(options, fhirVersion as 'R4' | 'R5' | 'R6'); diff --git a/types/fhirpath_test_engine.ts b/types/fhirpath_test_engine.ts index 940cdd0..155ae94 100644 --- a/types/fhirpath_test_engine.ts +++ b/types/fhirpath_test_engine.ts @@ -391,6 +391,31 @@ export let registeredEngines: { [key: string]: IFhirPathEngineDetails } = { supportsAST: true, supportsXML: false }, + "fhirpath.zig (R4)": { + name: "fhirpath.zig", + legacyName: "fhirpath.zig (R4)", + fhirVersion: "R4", + appInsightsEngineName: "fhirpath.zig", + publisher: "Joshua Mandel", + githubRepo: "https://github.com/jmandel/fhirpath.zig", + description: "A Zig/WASM FHIRPath engine running locally in the browser.", + external: false, + supportsAST: false, + supportsXML: false + }, + "fhirpath.zig (R5)": { + name: "fhirpath.zig", + legacyName: "fhirpath.zig (R5)", + fhirVersion: "R5", + appInsightsEngineName: "fhirpath.zig", + publisher: "Joshua Mandel", + githubRepo: "https://github.com/jmandel/fhirpath.zig", + description: "A Zig/WASM FHIRPath engine running locally in the browser for FHIR R5.", + external: false, + supportsAST: false, + supportsXML: false + }, + "CQL (R4)": { name: "CQL-Facade", legacyName: "CQL (R4)", From 97f5b856a9fae1f548f3e23c30f27b494fc10bf6 Mon Sep 17 00:00:00 2001 From: Josh Mandel Date: Thu, 29 Jan 2026 12:19:13 -0600 Subject: [PATCH 2/8] Enable XML support for fhirpath.zig engine - Set supportsXML: true for both R4 and R5 entries - Detect XML input (starts with '<') and route to evalXml() - Context expression on XML uses evalXml for initial eval, then JSON for per-context re-evaluation Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 27 +++++++++++++++------------ types/fhirpath_test_engine.ts | 4 ++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index e12b2a9..ce927cc 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -1019,17 +1019,22 @@ export async function evaluateExpressionUsingFhirpathZig( // Set current time engine.setNowDate(new Date()); - const resourceJson = options.resourceJson || '{}'; + const resourceText = options.resourceJson || '{}'; + const isXml = resourceText.trimStart().startsWith('<'); + + // Helper: call eval or evalXml depending on input format + function zigEval(expr: string, input: string, inputIsXml: boolean) { + if (inputIsXml) { + return engine.evalXml({ expr, xml: input, schema: schemaName }); + } + return engine.eval({ expr, json: input, schema: schemaName }); + } // Evaluate context expression to get context nodes, or use root let contexts: { path?: string; json: string }[] = []; if (options.contextExpression) { try { - const contextResult = engine.eval({ - expr: options.contextExpression, - json: resourceJson, - schema: schemaName, - }); + const contextResult = zigEval(options.contextExpression, resourceText, isXml); for (const node of contextResult) { contexts.push({ path: node.meta?.typeName || undefined, @@ -1043,7 +1048,7 @@ export async function evaluateExpressionUsingFhirpathZig( return result; } } else { - contexts.push({ json: resourceJson }); + contexts.push({ json: resourceText }); } // Evaluate main expression for each context @@ -1055,11 +1060,9 @@ export async function evaluateExpressionUsingFhirpathZig( }; try { - const evalResult = engine.eval({ - expr: options.expression, - json: ctx.json, - schema: schemaName, - }); + // Context results are always JSON (serialized from first eval) + const ctxIsXml = isXml && !options.contextExpression; + const evalResult = zigEval(options.expression, ctx.json, ctxIsXml); for (const node of evalResult) { const typeName = node.meta?.typeName || ''; diff --git a/types/fhirpath_test_engine.ts b/types/fhirpath_test_engine.ts index 155ae94..e17efc4 100644 --- a/types/fhirpath_test_engine.ts +++ b/types/fhirpath_test_engine.ts @@ -401,7 +401,7 @@ export let registeredEngines: { [key: string]: IFhirPathEngineDetails } = { description: "A Zig/WASM FHIRPath engine running locally in the browser.", external: false, supportsAST: false, - supportsXML: false + supportsXML: true }, "fhirpath.zig (R5)": { name: "fhirpath.zig", @@ -413,7 +413,7 @@ export let registeredEngines: { [key: string]: IFhirPathEngineDetails } = { description: "A Zig/WASM FHIRPath engine running locally in the browser for FHIR R5.", external: false, supportsAST: false, - supportsXML: false + supportsXML: true }, "CQL (R4)": { From 7d01d7d5a3618bfb7dfe085532c1595a53748750 Mon Sep 17 00:00:00 2001 From: Josh Mandel Date: Thu, 29 Jan 2026 12:20:17 -0600 Subject: [PATCH 3/8] Fix XML context handling: track format per context entry Each context entry now carries its own isXml flag. When there's no context expression, the original resource is passed as-is (XML or JSON). Context sub-results from expression evaluation are always JSON since node.data is a JS object. Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index ce927cc..99a1859 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -1031,14 +1031,16 @@ export async function evaluateExpressionUsingFhirpathZig( } // Evaluate context expression to get context nodes, or use root - let contexts: { path?: string; json: string }[] = []; + let contexts: { path?: string; text: string; isXml: boolean }[] = []; if (options.contextExpression) { try { const contextResult = zigEval(options.contextExpression, resourceText, isXml); for (const node of contextResult) { + // Context results are always JSON (node.data is a JS object) contexts.push({ path: node.meta?.typeName || undefined, - json: JSON.stringify(node.data), + text: JSON.stringify(node.data), + isXml: false, }); } } catch (err: any) { @@ -1048,7 +1050,8 @@ export async function evaluateExpressionUsingFhirpathZig( return result; } } else { - contexts.push({ json: resourceText }); + // No context expression — use the original resource as-is + contexts.push({ text: resourceText, isXml }); } // Evaluate main expression for each context @@ -1060,9 +1063,7 @@ export async function evaluateExpressionUsingFhirpathZig( }; try { - // Context results are always JSON (serialized from first eval) - const ctxIsXml = isXml && !options.contextExpression; - const evalResult = zigEval(options.expression, ctx.json, ctxIsXml); + const evalResult = zigEval(options.expression, ctx.text, ctx.isXml); for (const node of evalResult) { const typeName = node.meta?.typeName || ''; From a77e7ae6fbec207a915c6d9133841b6313543bf1 Mon Sep 17 00:00:00 2001 From: Josh Mandel Date: Thu, 29 Jan 2026 12:21:17 -0600 Subject: [PATCH 4/8] Prefer isXmlResource flag over content sniffing for XML detection Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index 99a1859..0fe11eb 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -1020,7 +1020,7 @@ export async function evaluateExpressionUsingFhirpathZig( engine.setNowDate(new Date()); const resourceText = options.resourceJson || '{}'; - const isXml = resourceText.trimStart().startsWith('<'); + const isXml = options.isXmlResource ?? resourceText.trimStart().startsWith('<'); // Helper: call eval or evalXml depending on input format function zigEval(expr: string, input: string, inputIsXml: boolean) { From 6b20f441a829444285ea7d17073a9766ffccc684 Mon Sep 17 00:00:00 2001 From: Josh Mandel Date: Thu, 29 Jan 2026 12:22:12 -0600 Subject: [PATCH 5/8] Use isXmlResource flag directly, drop content sniffing Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index 0fe11eb..6c54fe5 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -1020,7 +1020,7 @@ export async function evaluateExpressionUsingFhirpathZig( engine.setNowDate(new Date()); const resourceText = options.resourceJson || '{}'; - const isXml = options.isXmlResource ?? resourceText.trimStart().startsWith('<'); + const isXml = !!options.isXmlResource; // Helper: call eval or evalXml depending on input format function zigEval(expr: string, input: string, inputIsXml: boolean) { From d2f71ce4cd260ad815ecadbbea1cba36a9637c86 Mon Sep 17 00:00:00 2001 From: Josh Mandel Date: Thu, 29 Jan 2026 15:25:58 -0600 Subject: [PATCH 6/8] Update to new fhirpath.zig API (options object pattern) instantiate() now takes an options object instead of positional args, and registerSchemaFromUrl is merged into registerSchema with a url option. Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index 6c54fe5..117d968 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -977,7 +977,9 @@ async function getZigEngine(): Promise { // Dynamic import of the JS wrapper from the deployed site const mod = await import(/* webpackIgnore: true */ `${FHIRPATH_ZIG_BASE}/fhirpath.js`); const FhirPathEngine = mod.FhirPathEngine; - const engine = await FhirPathEngine.instantiate(`${FHIRPATH_ZIG_BASE}/fhirpath.wasm`); + const engine = await FhirPathEngine.instantiate({ + wasmUrl: `${FHIRPATH_ZIG_BASE}/fhirpath.wasm`, + }); return engine; })(); } @@ -988,7 +990,7 @@ async function ensureZigSchema(engine: any, fhirVersion: string): Promise Date: Thu, 29 Jan 2026 15:28:02 -0600 Subject: [PATCH 7/8] Drop redundant wasmUrl (defaults via import.meta.url) Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index 117d968..d06be24 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -977,9 +977,7 @@ async function getZigEngine(): Promise { // Dynamic import of the JS wrapper from the deployed site const mod = await import(/* webpackIgnore: true */ `${FHIRPATH_ZIG_BASE}/fhirpath.js`); const FhirPathEngine = mod.FhirPathEngine; - const engine = await FhirPathEngine.instantiate({ - wasmUrl: `${FHIRPATH_ZIG_BASE}/fhirpath.wasm`, - }); + const engine = await FhirPathEngine.instantiate(); return engine; })(); } From 6f9e9041489bc2cb816946ac472625123626a6e6 Mon Sep 17 00:00:00 2001 From: Josh Mandel Date: Thu, 29 Jan 2026 15:29:47 -0600 Subject: [PATCH 8/8] Drop explicit schema URLs (defaults via import.meta.url) Co-Authored-By: Claude Opus 4.5 --- helpers/fhirpath_api_engine.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/helpers/fhirpath_api_engine.ts b/helpers/fhirpath_api_engine.ts index d06be24..e42277f 100644 --- a/helpers/fhirpath_api_engine.ts +++ b/helpers/fhirpath_api_engine.ts @@ -990,8 +990,6 @@ async function ensureZigSchema(engine: any, fhirVersion: string): Promise