Porter's Guide
This guide is for engineers writing a second-language implementation of XTL. It distinguishes what is normatively part of XTL (and your port must match) from what is incidental to the TypeScript reference implementation (and your port should not copy).
If you have not already read spec/STABILITY.md and
conformance/runner-protocol.md, start
there. This document complements them; it does not replace them.
Where the contract lives
The spec prose is the contract. The conformance corpus is the executable check of the contract. Both must agree; when they diverge, file a bug.
In strict precedence order:
- The spec prose (
spec/language.md,spec/evaluation.md) defines the language. When prose is silent or ambiguous, this is a spec bug — file an issue. The corpus should not be used to disambiguate ambiguous prose; the prose itself should be amended. - The ADRs (
spec/decisions/) record why the prose is what it is. Read them when you hit a "but why is it like this?" — the rationale is usually there. ADRs are normatively part of the spec when theirStatus:isacceptedorprocess-normative. - The conformance corpus (
conformance/fixtures/) is the executable check. A fixture failing on a port that otherwise matches the spec prose is a bug — either in the port or in the fixture. A fixture passing on a port that violates spec prose is also a bug — the fixture is under-asserting. - The reference implementation is a fourth-place tiebreaker and NOT a specification. Mimicking its internals will copy bugs and miss simplifications.
Note on this precedence vs the older "fixtures-first" reading. Earlier versions of this guide put the corpus first, on the theory that "what runs is what's true." The 2026-05-18 reviewer pass surfaced that this conflicts with
spec/README.md's "Conformance Precedence" section (spec prose wins). The version above is the harmonized form: spec is the contract; corpus is the test that the contract is being honored. Disagreement between them is a bug, not a resolved ordering.
What you MUST match
These are non-negotiable for any port that wants to claim XTL conformance.
Function table is bounded (ADR-0043)
The XTL function set is intentionally smaller than Excel's catalog, gated by the Excel-native preference principle (ADR-0043): a function is in XTL only when its evaluation must happen before the workbook is written. Anything Excel can compute at workbook-open time stays in output-cell formulas, which xl3 preserves verbatim (ADR-0046).
For your port this means: implement only the functions in
spec/language.md "Functions" — don't add
locally-popular Excel functions like SQRT, ISNUMBER, SUMIF,
NETWORKDAYS, etc. as XTL functions, even if they are easy. Those
are intentionally Excel-formula territory (see ADR-0045). Adding them
would diverge your port from xl3 and produce templates that don't
round-trip between implementations.
If you have a real render-time use case that XTL doesn't currently support, open an issue using the Function re-proposal template so the maintainer can ADR-track the gap.
Error codes (ADR-0015)
Stable error.code strings of the form xl3/<category>/<id>. Hosts
dispatch on these codes for localization and programmatic handling.
The full catalog is in src/error-codes.ts;
the snapshot test in
src/__tests__/error-codes.test.ts
pins it.
Your port emits the same code at the same logical site. The
English Error.message is also part of the conformance contract —
fixtures use expected_error substring matching against it. Localize
in a layer above the engine, not by changing the engine's English text.
Message style guide
Stick to a consistent voice so substring matching across ports stays predictable. The reference impl follows these rules:
- Capitalize the first word of the message.
- Subject + verb form. Subject is the offending entity:
Source "X",Column "X",Input "X",Cell A5,Output filename "...",XLOOKUP,@join key columns, etc. - Quote identifiers with
"..."so they survive substring matches:Source "Renewals"notSource Renewals. - Reference reserved sheets by name (
__sources__,__inputs__) with no quotes — they are syntactic, not user-supplied. - Detail follows a
:for parse failures:Input "month" cannot be parsed as a date: empty value. - No "you must". Use
"X must be a Y", not"you must provide a Y". - No log-style prefixes like
XLOOKUP: ...or[error] .... - No trailing period unless the message is a full sentence with multiple clauses; one-clause messages omit it for terseness.
When you discover a message in the reference impl that violates these rules, that is a defect — file an issue against xl3 with the proposed rewording.
Date semantics (ADR-0017)
- Date components MUST be read in UTC.
getUTCFullYear/getUTCMonth/getUTCDate(or your language's equivalent), never the host-local variants. YYYY-MM-DDfor midnight,YYYY-MM-DDTHH:mm:ssotherwise.TODAY()returns "today in UTC". Hosts that need locale-specific dates compute them outside the engine and pass via__inputs__.- String-to-Date coercion (numFmt path) builds at UTC midnight.
CI runs the conformance suite under three timezones (UTC, America/New_York, Asia/Seoul). Your port should do the same.