Skip to content

Epic: Better XTalk compatibility — canonical source and linter #425

Description

@hoebat

Objective

Make XTalk source code canonical, readable, and portable again.

The first phase is deliberately narrow:

  1. define and enforce the canonical XTalk source language;
  2. build a linter that explains which layer a form belongs to;
  3. clean the xt.lang.* source files first;
  4. use JavaScript as the reference implementation;
  5. only then address Python, Dart, and other target-language differences.

The goal is to stop target-language constraints leaking into canonical XTalk source.

Why this is needed

XTalk currently has several overlapping layers:

XTalk source
  -> grammar/staging and macro expansion
  -> typed lowering/inference
  -> target-specific rewrites
  -> target emitter

The implementation already contains enough evidence that the language works across targets, but source code has accumulated compatibility forms such as:

  • large amounts of explicit x:get-key;
  • explicit x:not-nil? added for target truthiness constraints;
  • block forms appearing in value positions;
  • target-specific lambda and conditional workarounds;
  • inconsistent handling of object fields, array destructuring, and snake_case.

The immediate problem is not that XTalk lacks features. It is that the canonical source layer is no longer clearly separated from the lower-level semantic IR and target adapters.

Current findings

1. Dot access already has the intended canonical shape

The typed lowerer already maps:

(. obj ["key"])
=> (x:get-key obj "key")

(. obj ["a"] ["b"])
=> (x:get-path obj ["a" "b"] nil)

See src/hara/typed/xtalk_lower.clj.

The grammar also defines . as the index/access operator, while x:get-key, x:get-path, and x:set-key are macro-level access operations in src/hara/common/grammar_xtalk.clj.

The canonical authoring rule should therefore be:

(. obj ["key"])
(. obj [key])
(. obj ["a"] ["b"])

x:get-key and x:get-path should remain valid semantic/internal forms, but should not be required for ordinary source code.

A redundant simple form such as:

(x:get-key obj "key")

should be lintable as:

(. obj ["key"])

provided it has no non-nil default and no special dynamic semantics.

2. Python currently exposes the access-layer leak

Live emission currently differs:

(. a ["key"])
=> a["key"]

(x:get-key a "key")
=> a.get("key")

The second form preserves XTalk nil-safe access semantics; the first does not necessarily do so.

This is exactly the kind of difference that must be resolved in the target rewrite layer rather than in canonical source. Dot access and x:get-key should first normalize to one semantic access representation, then Python should decide how to emit that representation.

3. Destructuring has two distinct canonical meanings

The typed environment already distinguishes:

(var #{name age} m)

as object/field destructuring, from:

(var [name age] arr)

as positional/array destructuring.

The canonical checker must preserve this distinction:

  • set target -> object fields;
  • vector target -> array positions;
  • map target -> explicitly supported map destructuring;
  • anything else -> diagnostic.

Object destructuring should normalize field names through the same field-key function used by record typing and map literals.

For example, these should canonicalize to the same external field:

:arr-name
:arr_name
arr-name
arr_name

The checker must also detect collisions after normalization, such as:

#{arr-name arr_name}

Local binding names and external field keys must remain separate concepts. A local binding may be emitted as arr_name, while its canonical source symbol remains arr-name.

4. Field-key normalization is partly implemented but not consistently enforced

hara.typed.xtalk-common/field-key already normalizes field keys to snake_case, and typed map inference already uses it.

However, normalization is not consistently applied to:

  • dot access;
  • explicit x:get-key;
  • object destructuring;
  • emitted map keys;
  • collision detection;
  • target-specific access rewrites.

The canonical linter must make field-key normalization visible and deterministic.

There is also an important boundary to define: declared XTalk record fields should be normalized, but arbitrary external protocol keys such as clientInfo, protocolVersion, or other wire-format identifiers must not be silently rewritten. The implementation needs a clear distinction between canonical field keys and opaque external object keys.

5. Block/value context is not currently checked strongly enough

The grammar classifies if, cond, when, let, do, while, and for as :block.

:? is the value-oriented conditional.

The intended rule is:

(def a (if b 1 2))       ; illegal
(var a (if b 1 2))      ; illegal
(return (if b 1 2))      ; illegal

Use:

(def a (:? b 1 2))
(var a (:? b 1 2))
(return (:? b 1 2))

The current inference path can infer the branch union for if, but it does not carry enough value-position context to report this as a canonical error. Direct emission can therefore produce invalid target code.

The linter must add an explicit value/block context pass before target emission.

6. Dart truthiness belongs in the Dart adapter

The Dart rewrite already detects boolean-compatible expressions and wraps general XTalk truthiness using:

(and (x:not-nil? value)
     (not= false value))

The canonical source should remain:

(:? value then else)

Dart-specific bool adaptation should happen in hara.model.spec-dart.rewrite.

Explicit x:not-nil? remains valid when the programmer means “present even if false”; it should not be required merely because Dart has stricter conditional typing.

7. Python lambda lifting already belongs in the target rewrite

Python already has a lambda compatibility predicate and a hoisting rewriter. Complex canonical lambdas can be lifted into named helper functions during Python rewriting.

That machinery should be retained and improved later. It should not force Python-shaped forms into canonical XTalk.

8. Python conditional emission needs parity coverage

Live Python emission currently turns:

(:? b 1 2)

into an and/or expression. That is unsafe when the true branch is falsey.

This should become a target conformance test and be fixed in the Python adapter after the canonical JavaScript contract is locked.

Canonical source contract for phase one

The first implementation should establish these forms as canonical:

;; object access
(. obj ["name"])
(. obj [key])
(. obj ["meta"] ["role"])

;; value conditionals
(:? test then else)

;; object destructuring
(var #{name age} m)

;; positional destructuring
(var [name age] arr)

;; ordinary lambdas
(fn [x] ...)
(fn:> [x] expression)

The following remain valid but are lower-level or exceptional:

(x:get-key obj key default)
(x:get-path obj path default)
(x:get-idx arr index default)
(x:not-nil? value)
(x:nil? value)

Linter design

The linter should operate on canonical XTalk source before target-specific rewriting.

Initial diagnostics:

  • XT001 block-in-value
  • XT002 redundant-get-key
  • XT003 access-kind-mismatch
  • XT004 field-key-collision
  • XT005 invalid-destructuring-target
  • XT006 field-key-normalization
  • XT007 unsupported-canonical-form
  • XT008 target rewrite required (informational only)

The linter should report:

  • source location;
  • original form;
  • canonical interpretation;
  • suggested rewrite where safe;
  • layer responsible for the issue:
    • source/canonical;
    • typed semantic;
    • target rewrite;
    • emitter/runtime.

It should expose a programmatic API suitable for a Kimi guard, rather than putting the semantic logic entirely in clj-kondo. clj-kondo can provide syntax and lightweight style integration, while the Foundation XTalk analyzer should own semantic checks.

Staged implementation

Stage 0 — Canonical fixtures

Create a small golden corpus covering:

  • dot access;
  • nested paths;
  • dynamic keys;
  • defaults;
  • object and array destructuring;
  • snake_case normalization;
  • field collisions;
  • block-in-value errors;
  • :? truthiness;
  • nil checks;
  • lambdas.

Each fixture should record:

  • source form;
  • canonical normalized form;
  • inferred type;
  • JavaScript output.

Stage 1 — Build the linter

Add a source-level XTalk linter with:

  • context-aware value/block checking;
  • access normalization;
  • destructuring classification;
  • field-key normalization;
  • redundant x:get-key detection;
  • structured diagnostics;
  • JSON output for Kimi/editor integration.

Initially run it in warning mode over src-lang/xt.

Stage 2 — Clean xt.lang.*

Use the linter to clean src-lang/xt/lang/* first.

The target is not broad mechanical replacement. Every rewrite must preserve:

  • nil/default behavior;
  • dynamic versus static keys;
  • object versus array access;
  • truthy versus non-nil semantics.

JavaScript is the reference target for this stage.

Stage 3 — Lock JavaScript as the canonical reference

For every canonical fixture:

  • source must pass the linter;
  • typed analysis must succeed;
  • JavaScript emission must be stable;
  • runtime tests must pass.

This stage establishes what XTalk means before target languages are allowed to influence the source contract.

Stage 4 — Clean the remaining src-lang/xt source

Apply the same canonical rules to the rest of the XTalk source tree.

Generated test-lang/xtbench files should not be edited manually; regenerate them from canonical seeds where applicable.

Stage 5 — Cross-language conformance

Only after the canonical JavaScript layer is concrete. Lua is included alongside Python and Dart in this conformance stage:

  • compare Python, Dart, Lua, and other targets against the canonical fixtures;
  • classify failures as target rewrite, emitter, runtime, or actual language limitation;
  • add target-specific rewrites without changing canonical source.

Stage 6 — Kimi guard and CI enforcement

Promote diagnostics gradually:

  • canonical errors -> blocking;
  • redundant syntax -> warning/autofix;
  • target adaptation notices -> informational;
  • unsupported target capability -> target-specific failure.

Definition of done

This epic is complete when:

  • canonical XTalk source forms are documented and mechanically checked;
  • xt.lang.* no longer requires target-specific compatibility syntax;
  • dot access and x:get-key have one semantic meaning;
  • object and array destructuring are unambiguous;
  • field-key normalization and collisions are deterministic;
  • block forms cannot silently appear in value positions;
  • JavaScript is a stable reference implementation;
  • Python/Dart differences are isolated in target adapters;
  • the linter can explain whether a failure belongs to source, typing, rewriting, or emission;
  • Kimi can run the linter as a guard before edits are accepted.

Initial implementation issue candidates

  1. Define canonical XTalk fixture format and JavaScript golden tests.
  2. Add context-aware canonical source walker.
  3. Add access normalization and redundant x:get-key diagnostics.
  4. Add destructuring and field-key checks.
  5. Add block-in-value diagnostics.
  6. Add JSON linter API for Kimi.
  7. Clean src-lang/xt/lang/*.
  8. Fix JavaScript canonical emission/runtime parity.
  9. Add Python/Dart/Lua conformance matrix after the canonical phase.

Update — 2026-07-22

Implemented and pushed in commit fd927be1.

  • Added typed canonical lowering context for free XTalk forms.
  • Local bindings are inferred before structural access lowering.
  • Python now receives canonical x:get-key and emits nil-safe .get(...) access.
  • Added lazy dependent-namespace type registration.
  • Removed the obsolete +intrinsic-targets+ remapping.
  • Added regressions for method-call preservation, runtime helper resolution, and canonical-stage behavior.
  • Verified xt.event.base-route-test with 54 passing checks.

The remaining epic work is the staged cleanup of canonical xt.lang.* sources and later target conformance work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions