Version: dev

canonical_json

Stable function · Document I/O

Tier: stable · Kind: function · Topic: Document I/O

JuliaTypeScriptPythonRustGo
canonical_jsoncanonicalJsonnot exportedcanonical_jsonCanonicalJSON

Julia

canonical_json(expr::ASTExpr)
canonical_json(expr::ASTExpr) -> String

Emit the canonical on-wire JSON form of an expression per RFC §5.4.6: keys sorted, no extraneous whitespace, shortest round-trip floats with trailing-.0 disambiguation for integer-valued magnitudes, exponent-form (1e25, 5e-324) outside [1e-6, 1e21), signed zero preserved.

Emissible node fields: every binding (TS/Python/Rust/Julia) serializes exactly op, args, wrt, dim, fn, name, value — the field set the cross-language canonical fixtures (tests/conformance/canonical/) pin. A node carrying any OTHER set field (aggregate expr_body/ranges/ output_idx/semiring, table/table_axes/output, join/filter, geometry id/manifold, …) has NO faithful canonical JSON in this format: emitting only the pinned fields would make structurally-different aggregates byte-identical (the defect class behind the fn bc-node bug). Rather than emit ambiguous bytes — or diverge unilaterally from the other bindings by emitting more fields — such nodes throw CanonicalizeError("E_CANONICAL_UNSUPPORTED_FIELD", …). The one in-package caller (tree-walk CSE _cse_key) treats any CanonicalizeError as “decline sharing”, which is exactly the safe behavior.

Source

Example (canonicalize)

# The canonical form orders commutative arguments and drops identities, so two
# spellings of the same expression compare equal.
a = canonicalize(parse_expression("b + a + 0"))
b = canonicalize(parse_expression("a + b"))
@assert to_ascii(a) == to_ascii(b) == "a + b"
@assert canonical_json(a) == canonical_json(b) == """{"args":["a","b"],"op":"+"}"""

From docs/api/examples/julia/expressions.jl

TypeScript

function canonicalJson(expr: Expr): string

Emit canonical on-wire JSON per §5.4.6 (sorted keys, no whitespace).

FAIL-CLOSED (matching the Julia reference): canonicalize PRESERVES every node field, then this validates the canonicalized tree and throws E_CANONICAL_UNSUPPORTED_FIELD if any node carries an own key (with a defined value) outside the emissible ∪ tolerated set. A node carrying a non-emissible aggregate/geometry field has NO faithful canonical JSON — emitting only the pinned fields would collapse structurally-different nodes to identical bytes.

Source

Example (canonicalize)

const a = parseExpression('b + a*2 + 0')
const b = parseExpression('0 + 2*a + b')

// Operands are ordered and identities dropped.
assert.equal(toAscii(canonicalize(a)), 'b + 2 * a')
assert.deepEqual(canonicalize(a), canonicalize(b))

// The canonical JSON text is a stable key for hashing or comparison.
assert.equal(canonicalJson(a), canonicalJson(b))

From docs/api/examples/typescript/canonicalize.ts

Python

Python does not export this function.

Rust

pub fn canonical_json(expr: &Expr) -> Result<String, CanonicalizeError>

Emit the canonical on-wire JSON form of an expression (§5.4.6).

Calls canonicalize first, then serializes with sorted keys, no extraneous whitespace, and the strict number formatting of §5.4.6.

Source · Rust docs

Example (canonical_json)

// Two spellings of the same sum serialize identically once canonicalized.
let a = canonical_json(&parse_expression("x + y + 1")?)?;
let b = canonical_json(&parse_expression("1 + y + x")?)?;
assert_eq!(a, b);
println!("{a}");

From docs/api/examples/rust/expressions.rs

Go

func CanonicalJSON(expr Expression) ([]byte, error)

CanonicalJSON emits the canonical on-wire JSON form of an expression per §5.4.6: keys sorted, no extraneous whitespace, shortest-round-trip float literals with trailing-.0 disambiguation for integer-valued floats, and strict lowercase-e exponent notation without a leading +.

The input is canonicalized first; pass an already-canonical tree for a no-op canonicalization pass.

Source · Go docs