canonical_json
Stable function · Document I/O
Tier: stable · Kind: function · Topic: Document I/O
| Julia | TypeScript | Python | Rust | Go |
|---|---|---|---|---|
canonical_json | canonicalJson | not exported | canonical_json | CanonicalJSON |
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.
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":"+"}"""
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.
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))
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.
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}");
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.