Skip to content

Conformance fixes and performance optimizations#13

Merged
NirBarak-RecoLabs merged 6 commits into
mainfrom
conformance-fixes-and-performance-optimizations
Apr 25, 2026
Merged

Conformance fixes and performance optimizations#13
NirBarak-RecoLabs merged 6 commits into
mainfrom
conformance-fixes-and-performance-optimizations

Conversation

@NirBarak-RecoLabs
Copy link
Copy Markdown
Collaborator

Summary

Batch of JSONata conformance fixes plus evaluator / WASM performance work, landed as 6 focused commits.

Conformance

  • Signature validation (<n+n:o>, <n+s-:a<n>>, <u>, optional-skip):
    • Cap variadic consumption so trailing mandatory params still have args → no more spurious T0410 too few arguments.
    • Skip optional specs on type mismatch and retry the same arg against the next spec.
    • Implement the - (context) modifier: inject the current focus when the argument is absent. Adds ParamSpec.Context.
    • Add the u type specifier (union of bool / number / string / null) to parser and evaluator.
    • New cases 035–040 under testdata/groups/function-signatures/.
  • $split non-string input: align with the JSONata spec by returning T0410 instead of nil. Updates function-split/case016/case017 to match. (Reverts the jsonata-js-compat behaviour previously in functions/string_funcs.go.)
  • Deterministic map iteration: sort raw-map keys via slices.Sorted(maps.Keys(m)) in DecodeRawMap / MapKeys / MapRange so Go-native map[string]any inputs yield stable iteration order.

Performance

  • Pre-parse signatures at registration: add ParsedSig []parser.ParamSpec to SignedBuiltin and Lambda, introduce a newSignedBuiltin registration helper, and drop the per-call parser.ParseSig from evalFunction / callFunction.
  • Eval hot paths:
    • float64 + float64 fast-path in eval_binary.go (skips coercion).
    • DeepEqual primitive short-circuits (same-type float64 / string / bool) before normalizeNumber.
    • slices.Clip instead of slices.Clone in CollapseSequence / CollapseToSlice (ownership transfer).
    • Reusable HOF argument buffers (hofArity / hofArgsBuf / fillHofArgs) across $map / $filter / $single / $reduce / $sift / $each, plus a pre-allocated comparator args slice for $sort.

Public API + WASM

  • New Expression methods in gnata.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 (wasm/main.go, playground.html) now exports six functions; the two new ones are gnataEvalMap and gnataEvalWithVars. gnataEval / gnataEvalHandle now go through EvalBytes so they also benefit from the gjson fast path.
  • README.md / AGENTS.md: fix the StreamEvaluator description (gjson.GetBytes per fast-path expression, not gjson.GetManyBytes for the whole event), list the new public API, and expand the WASM section with a table of all six JS exports.

Commits

  1. Fix signature validation for variadic, optional-skip, context, and u-type
  2. Return T0410 from $split on non-string input
  3. Sort raw map keys for deterministic iteration
  4. Pre-parse function signatures at registration
  5. Optimize eval hot paths: float arith, DeepEqual, HOF buffers
  6. Add EvalMap, EvalBytesWithVars; expose via WASM; fix gjson docs

Validation

  • go build ./...
  • GOOS=js GOARCH=wasm go build ./wasm/
  • go test ./... ✓ — including all 6 new signature cases + 2 modified $split cases
  • golangci-lint run ./... ✓ (0 issues)

…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.
@NirBarak-RecoLabs NirBarak-RecoLabs merged commit 1a36a16 into main Apr 25, 2026
2 checks passed
@NirBarak-RecoLabs NirBarak-RecoLabs deleted the conformance-fixes-and-performance-optimizations branch April 25, 2026 08:47
@NirBarak-RecoLabs NirBarak-RecoLabs mentioned this pull request Apr 25, 2026
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.
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.

2 participants