Conformance fixes and performance optimizations#13
Merged
NirBarak-RecoLabs merged 6 commits intoApr 25, 2026
Conversation
…type Addresses four conformance gaps in function signature validation against the JSONata spec: - Variadic overflow: cap consumption to leave room for mandatory params that follow, preventing spurious T0410 "too few arguments" errors when a trailing fixed param is present after a variadic. - Optional-skip: when an optional param's type doesn't match, skip the spec and retry the same arg against the next spec instead of raising T0410. - '-' (context) modifier: inject the current focus value when the argument is absent. Adds Context field to ParamSpec and threads focus through processCallArgs / validateCallArgs. - 'u' type specifier (union of primitives: bool, number, string, null). Recognised by both the parser and evaluator. Adds test cases 035-040 covering each scenario end-to-end.
$split previously returned nil (undefined) for non-string arguments, which matched the jsonata-js reference implementation but diverged from the JSONata spec, which prescribes a T0410 type error. Align with the spec by returning T0410 for non-string args, and update case016/case017 to expect the error code instead of an undefined result.
When a Go native map[string]any enters the evaluator (via DecodeRawMap or MapKeys/MapRange in paths that walk raw maps), iteration order was Go-randomised, which leaked into ordering-sensitive operations like $keys, $lookup fallbacks, and aggregate results. Sort keys alphabetically using slices.Sorted(maps.Keys(m)) before iteration so identical inputs produce identical outputs across runs.
Signature strings were re-parsed on every SignedBuiltin / Lambda call via parser.ParseSig, which is pure overhead since the string never changes after the function is defined. Parse once and cache: - Add ParsedSig []parser.ParamSpec to SignedBuiltin and Lambda. - Introduce a newSignedBuiltin helper in functions/register.go that parses at construction time; use it for signature-carrying registrations. - Populate Lambda.ParsedSig once in evalLambda when the signature is compiled from the AST. - Replace the per-call parser.ParseSig() lookups in evalFunction and callFunction with direct reads of the cached ParsedSig slice. Hot-path-only change; no behavioural difference.
Five targeted hot-path optimisations in the evaluator and function dispatch layer: - Arithmetic fast-path (eval_binary.go): when both operands are already float64, bypass the generic numeric-coercion path and go straight to evalArithFloat64. This is the common case for numeric expressions after AST evaluation. - DeepEqual primitive fast-path (value.go): for same-type float64 / string / bool comparisons, skip normalizeNumber and compare directly. Drops a per-call allocation that showed up in profiles on equality-heavy expressions. - Sequence collapse (value.go): use slices.Clip in CollapseSequence and CollapseToSlice instead of slices.Clone. Since the sequence is being discarded, we can transfer ownership of the backing array rather than allocating a copy. - HOF argument buffers (hof_funcs.go, object_funcs.go): replace the per-iteration hofArgs slice with reusable hofArity / hofArgsBuf / fillHofArgs helpers applied to $map, $filter, $single, $reduce, $sift, and $each. Same arity detection, zero per-iteration allocation. - Sort comparator args (array_funcs.go): pre-allocate the 2-element sortArgs slice once per $sort call instead of rebuilding it on each comparator invocation. No behavioural changes; all existing tests pass.
Extends the public Expression API and the WASM bridge with two evaluation paths that were previously only reachable from the StreamEvaluator or the byte-level API, and cleans up the docs to match how gjson is actually used. Expression API (gnata.go): - EvalMap(ctx, data map[string]json.RawMessage): O(1) top-level key lookup via DecodeRawMap, with gjson fast paths for nested access inside each RawMessage. Useful when the caller already has the JSON decoded into a map and wants to avoid re-serialising. - EvalBytesWithVars(ctx, data json.RawMessage, vars map[string]any): evaluate raw JSON bytes with external $-variable bindings, while keeping the gjson fast-path eligible. WASM bridge (wasm/main.go, playground.html): - Export two new JS functions, gnataEvalMap and gnataEvalWithVars, mirroring the new Expression methods. gnataEvalMap takes a JS object and feeds its top-level keys as json.RawMessage values; gnataEvalWithVars takes a vars JSON blob for $-bindings. - Route gnataEval and gnataEvalHandle through EvalBytes so they also benefit from the gjson fast path. - Add JS wrappers in playground.html so the new exports are usable from the browser playground. Documentation (README.md, AGENTS.md): - Fix the StreamEvaluator description: the hot path uses gjson.GetBytes per fast-path expression, not a single gjson.GetManyBytes call for the whole event. - Update the public-API list in README to include EvalMap and EvalBytesWithVars, and expand the WASM section with a table of all six exported JS functions.
MickeyShnaiderman-RecoLabs
approved these changes
Apr 24, 2026
Merged
NirBarak-RecoLabs
added a commit
that referenced
this pull request
Apr 26, 2026
Bump npm package gnata-js from 0.2.1 to 0.2.2 to ship the conformance fixes and performance optimizations from #13: - Signature validation for variadic, optional-skip, context, and union (u-type) parameters - $split returns T0410 on non-string input (spec alignment) - Deterministic key iteration for raw map[string]any inputs - Pre-parsed function signatures at registration (no per-call re-parse) - Eval hot-path optimizations: float64 arithmetic fast path, primitive DeepEqual fast path, reusable HOF argument buffers - New Expression.EvalMap and Expression.EvalBytesWithVars APIs - WASM exports gnataEvalMap and gnataEvalWithVars for the browser playground Also realigns npm/package-lock.json with the package name (gnata-js) and version after prior drift. Tagging v0.2.2 on the merge commit triggers publish-npm.yml.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Batch of JSONata conformance fixes plus evaluator / WASM performance work, landed as 6 focused commits.
Conformance
<n+n:o>,<n+s-:a<n>>,<u>, optional-skip):T0410 too few arguments.-(context) modifier: inject the current focus when the argument is absent. AddsParamSpec.Context.utype specifier (union of bool / number / string / null) to parser and evaluator.testdata/groups/function-signatures/.$splitnon-string input: align with the JSONata spec by returningT0410instead ofnil. Updatesfunction-split/case016/case017to match. (Reverts the jsonata-js-compat behaviour previously infunctions/string_funcs.go.)slices.Sorted(maps.Keys(m))inDecodeRawMap/MapKeys/MapRangeso Go-nativemap[string]anyinputs yield stable iteration order.Performance
ParsedSig []parser.ParamSpectoSignedBuiltinandLambda, introduce anewSignedBuiltinregistration helper, and drop the per-callparser.ParseSigfromevalFunction/callFunction.float64 + float64fast-path ineval_binary.go(skips coercion).DeepEqualprimitive short-circuits (same-typefloat64/string/bool) beforenormalizeNumber.slices.Clipinstead ofslices.CloneinCollapseSequence/CollapseToSlice(ownership transfer).hofArity/hofArgsBuf/fillHofArgs) across$map/$filter/$single/$reduce/$sift/$each, plus a pre-allocated comparator args slice for$sort.Public API + WASM
Expressionmethods ingnata.go:EvalMap(ctx, map[string]json.RawMessage) (any, error)— O(1) top-level key lookup with gjson fast paths on nested fields.EvalBytesWithVars(ctx, json.RawMessage, map[string]any) (any, error)— raw-bytes evaluation with external$-variable bindings, still gjson-fast-path eligible.wasm/main.go,playground.html) now exports six functions; the two new ones aregnataEvalMapandgnataEvalWithVars.gnataEval/gnataEvalHandlenow go throughEvalBytesso they also benefit from the gjson fast path.README.md/AGENTS.md: fix the StreamEvaluator description (gjson.GetBytesper fast-path expression, notgjson.GetManyBytesfor the whole event), list the new public API, and expand the WASM section with a table of all six JS exports.Commits
Fix signature validation for variadic, optional-skip, context, and u-typeReturn T0410 from $split on non-string inputSort raw map keys for deterministic iterationPre-parse function signatures at registrationOptimize eval hot paths: float arith, DeepEqual, HOF buffersAdd EvalMap, EvalBytesWithVars; expose via WASM; fix gjson docsValidation
go build ./...✓GOOS=js GOARCH=wasm go build ./wasm/✓go test ./...✓ — including all 6 new signature cases + 2 modified$splitcasesgolangci-lint run ./...✓ (0 issues)