This document tracks the parity of expr-js (TypeScript) against its source of truth, expr-lang/expr v1.17.8 (Go).
Goal: expr-js is a maintainable TypeScript port of expr-lang/expr v1.17.8. Future upgrades are applied by diffing upstream Go and porting equivalent changes with minimal translation effort.
| Project | Version |
|---|---|
| expr-lang/expr (Go, upstream) | v1.17.8 |
| expr-js (this port) | 1.17.8 |
Every upstream package is mirrored 1:1 under src/ (file-per-file where the
TypeScript type system allows):
| Upstream Go | expr-js TS | Status |
|---|---|---|
file/ |
src/file/ |
Ported (location, source, error) |
ast/ |
src/ast/ |
Ported (node, print, visitor, find, dump) |
parser/lexer/ |
src/parser/lexer/ |
Ported (lexer, state, token, utils) |
parser/operator/ |
src/parser/operator/ |
Ported |
parser/utils/ |
src/parser/utils/ |
Ported |
parser/ |
src/parser/ |
Ported (parser.go -> parser.ts) |
conf/ |
src/conf/ |
Ported (config) |
builtin/ |
src/builtin/ |
Ported (builtin, lib, utils, validation, function) |
checker/nature/ |
src/checker/nature/ |
Ported (nature, type, kind) |
checker/ |
src/checker/ |
Ported (checker, info) |
compiler/ |
src/compiler/ |
Ported (compiler.go -> compiler.ts) |
vm/ |
src/vm/ |
Ported (vm, program, opcodes, utils) |
vm/runtime/ |
src/vm/runtime/ |
Ported (runtime, helpers, sort, gotime) |
optimizer/ |
src/optimizer/ |
Ported (all 15 files) |
patcher/ |
src/patcher/ |
Ported (operator_override, with_context, with_timezone) |
types/ |
src/types/ |
Ported |
internal/ring/ |
src/internal/ring/ |
Ported |
expr.go |
src/expr.ts |
Ported (public API) |
docgen/ |
src/docgen/ |
Ported (docgen, markdown, index) |
repl/ |
src/repl/ |
Ported (Node readline REPL) |
debug/ |
src/debug/ |
Ported (headless; TUI = FORCED_DIVERGENCE) |
Go-style names are preserved (source parity, Rule 3). camelCase aliases are added for JS ergonomics. Both styles work.
| Go | expr-js (Go-style) | expr-js (alias) |
|---|---|---|
Compile |
Compile |
compile |
Run |
Run |
run |
Eval |
Eval |
evaluate |
parser.Parse |
Parse |
parse |
Options ported: Env, AllowUndefinedVariables, Operator, ConstExpr,
AsAny, AsKind, AsBool, AsInt, AsInt64, AsFloat64, DisableIfOperator,
WarnOnAny, Optimize, DisableShortCircuit, Patch, Function,
DisableAllBuiltins, DisableBuiltin, EnableBuiltin, WithContext,
Timezone, MaxNodes.
| Gate | Command | Result |
|---|---|---|
| Typecheck | tsc -p tsconfig.json --noEmit |
PASS (0 errors) |
| Dual build | npm run build (ESM + CJS + .d.ts) |
PASS |
| Unit tests | tsx --test tests/unit/core.test.ts |
24/24 PASS |
| Smoke tests | tsx tests/smoke.ts |
21/21 PASS |
| Parity tests | tsx --test tests/parity.test.ts |
118/118 PASS |
| Expr corpus | tsx --test tests/go-parity/expr/expr.parity.test.ts |
139/160 (21 N/A) |
| Checker corpus | tsx --test tests/go-parity/checker/checker.parity.test.ts |
15/45 (30 N/A) |
| Builtin corpus | tsx --test tests/go-parity/builtin/builtin.parity.test.ts |
137/157 (20 N/A) |
| Upstream literal | tsx --test tests/upstream/**/*_test.ts |
112/140 (28 N/A, 35 files) |
| Total | 665 tests, 566 pass, 99 skip, 0 fail |
Go is the source of truth. parity/gen (Go) evaluates expressions with the
upstream engine and emits tagged JSON fixtures into parity/fixtures. The TS
runner tests/parity.test.ts replays each fixture against expr-js and asserts
equivalent output with int/float fidelity.
- Regenerate fixtures:
cd parity/gen && go run . - Run parity:
tsx --test tests/parity.test.ts
| Category | Bucket | Count | Notes |
|---|---|---|---|
| basics | PASS | 30 | arithmetic, comparison, logic, ternary, range, in |
| numeric | PASS | 20 | int/float semantics, modulo, exponent, casts |
| builtins | PASS | 20 | len/max/min/sum/type/keys/values/sort/etc. |
| strings | PASS | 14 | concat, contains/startsWith/endsWith, slicing, split |
| collections | PASS | 10 | arrays, maps, indexing, concat |
| predicates | PASS | 14 | filter/map/all/any/reduce/groupBy/sortBy |
| advanced | PASS | 10 | let, pipes, #index, nested ternary |
| Total | 118 | All replayed against Go truth |
Upstream Go-only tests classified NOT_APPLICABLE:
internal/testify,internal/spew,internal/difflib,internal/deref— vendored Go test infrastructure, not part of the expr language surface.*_bench_test.go— performance benchmarks (out of scope; goal is semantic, not performance, parity).test/deref, reflect/Stringer-specific tests — depend on Go pointer/interface deref and reflection with no JS analog.test/fuzz(Go native fuzzing) — replaced conceptually by the fixture harness.
PASS_WITH_ADAPTER (host-env binding): tests that bind Go struct methods / interfaces require a JS object/method shim before the expression is portable. The engine supports this (methods on env objects are bound and callable); such cases are exercised via the env-based unit tests.
All divergences are forced by language differences; behavior is preserved.
Go has int/int64/float64 as distinct types. TypeScript has one number
(IEEE-754) plus bigint. expr-js maps Go int/int64 -> bigint, Go float64
-> number. This preserves Go semantics exactly:
- integer ops stay integer:
2 + 3 == 5n /always yields float64:1 / 2 == 0.5%is integer-only and truncates toward zero**yields float64- int64 precision is preserved (no float rounding for large ints)
- int64 overflow wraps (emulated in
runtime/helpers.ts)
Go's checker uses reflect.Type/reflect.Value. TypeScript has no
reflection. expr-js implements a TypeDescriptor system
(src/checker/nature/type.ts) reproducing the subset of reflect.Type the
checker needs: Kind, Elem, Key, struct fields, func in/out, identity. The VM
operates on native JS values (Array, Map, object, string, function) instead of
reflect.Value.
Go specializes calls via vm.FuncTypes (generated). expr-js has no typed
dispatch tables: checker.TypedFuncIndex always returns [0,false] and
IsFastFunc returns false, so all calls route through generic
OpCall/OpCallN. Results are identical; only the internal opcode differs.
Go returns (value, error). expr-js throws (FileError) and returns the value
directly. Error messages mirror Go strings where feasible.
Modeled by GoTime/GoDuration (src/vm/runtime/gotime.ts) wrapping epoch-ms
and bigint nanoseconds. time.Location is an opaque marker.
Go generates ~3700 lines of per-type-pair arithmetic
(vm/runtime/helpers[generated].go). expr-js collapses this to compact
bigint/number dispatch in src/vm/runtime/helpers.ts — same behavior, far less
code, because the JS numeric domain has two members instead of 13.
TypeScript reserved/global shadowing is tolerated where the Go name must be
preserved for diff-based maintenance (e.g. unescape, Function, Map,
Array, String in types). These are lint advisories only; tsc accepts
them. No semantic rename was made.
| Package | TS files | Status |
|---|---|---|
docgen/ |
src/docgen/docgen.ts, markdown.ts, index.ts |
Ported. Full functional parity. See NON_CORE_PARITY.md §1. |
repl/ |
src/repl/repl.ts + src/test/fuzz/fuzz_env.ts |
Ported. Node readline REPL. History persistence = FORCED_DIVERGENCE. See NON_CORE_PARITY.md §2. |
debug/ |
src/debug/debugger.ts |
Ported (headless). Data side (disassembly + execution + stack) fully ported. Interactive TUI (tview/tcell) = FORCED_DIVERGENCE. See NON_CORE_PARITY.md §3. |
This project is high functional parity + ~complete source-level file parity, with all divergences classified. It is NOT claimed as byte-for-byte bytecode parity (see DESIGN_DECISION B1/B2).
- Core language + runtime: every non-test, non-generated Go file has a TS
counterpart. See
tests/go-parity/FILE_MAPPING.md. - Closed this audit: conf/env.go, checker/nature/utils.go, patcher/value/value.go, internal/deref/deref.go (previously gaps).
- Split (3) / collapsed (1) / not-ported(func_types): all
classified FORCED_DIVERGENCE or roadmapped — see
tests/go-parity/DIVERGENCES.md.
| Corpus | Source | Total | Pass | Pass w/ adapter | N/A | Result |
|---|---|---|---|---|---|---|
| expr | expr_test.go TestExpr | 160 | 88 | 51 | 21 | 139/160 evaluated pass |
| checker | checker_test.go TestCheck_error | 45 | 0 | 15 | 30 | 15/45 evaluated pass |
| builtin | builtin_test.go TestBuiltin | 157 | 99 | 38 | 20 | 137/157 evaluated pass |
| eval (curated) | parity/gen | 118 | 118 | 0 | 0 | 118/118 |
| unit | hand-written | 24 | 24 | — | — | 24/24 |
Machine-readable: tests/go-parity/CLASSIFICATION.json.
inover constant array (Set membership missing in runtime.In)- env function rejected "doesn't return value" (typeOfValue 0-output func)
duration * floatreturned duration instead of float64
See tests/go-parity/ENGINE_BUGS.md.
High functional parity, near-complete source-level file parity, every divergence classified (FORCED_DIVERGENCE / DESIGN_DECISION / NOT_IMPLEMENTED_YET). Not byte-for-byte bytecode parity (typed dispatch + generated helpers are documented design divergences).