packages feed

tramaj-hs-0.4.0.0: CHANGELOG.md

# Changelog

## 0.4.0.0

**Breaking: integers and floats are two number types** (`../specs/decisions.md`
\S18, `../specs/reference.md` \S3). A literal or a JSON number with neither a
fraction nor an exponent is an integer; one with either is a float. Nothing
converts between them: `eq(1, 1.0)` is `false`, `lt`/`lte`/`gt`/`gte` over an
integer and a float are a `TypeMismatch`, and `has`/`lookup` take an integer
index. The integer range is the signed 64-bit one; an integer literal outside
it is a parse error and a context integer outside it a `TypeMismatch`. A
float is written with a fraction or an exponent everywhere, so `str(1.0)` is
`1.0` where it was `1`. The arithmetic builtins are not part of this.

New `Tramaj.Json`: a JSON value with `JInt` and `JFloat`, `jsonParser` (which
types a number by its text) and `stringify`. `evalProgram`, `runProgram`,
`Output`, `Node`, `NodeAttribute`, `Annotations`, `nodeToJson`,
`nodeFromJson`, `nodeAttributeToJson` and `mapActions` use it where they used
`Data.Aeson.Value`, which holds one number type; `fromAeson` and `toAeson`
bridge the two. `NumberLit` is split into `IntLit`/`FloatLit`, `TCScalarNum`
into `TCScalarInt`/`TCScalarFloat` and `RCScalarNum` into
`RCScalarInt`/`RCScalarFloat`. The type primitive `number` is gone; `int` and
`float` replace it. Needs `aeson >= 2.2.1`, for its token decoder.

**The arithmetic profile** (`../specs/reference.md` \S9, \S11, \S12,
`../specs/v3-symbols.md` \S1.9, \S5.2 to \S5.5), as an option of one
evaluation that is off by default. Nine builtins: `sum`, `product`, `negate`,
`inverse`, `quotient`, `floor-quotient`, `modulo`, `floor` and `real`, over
the two number types with no conversion or promotion. Integer results are
held to the signed 64-bit range at every step of a fold and nothing wraps;
float results are one correctly rounded double operation at a time, in a left
fold. Over a symbol they build a term, written
`{"$term": <op>, "arguments": [...]}`, which crosses wherever a symbol does
and is refused wherever a symbol is.

New in `Tramaj.Eval`: `Options (..)` (`optMode`, `optArithmetic`),
`defaultOptions` (concrete mode, arithmetic off), `evalProgramWith` and
`runProgramWith`, which take an `Options` where `evalProgram` and
`runProgram` take a `Mode`. Those two keep their signatures and run without
the profile, so the nine names stay unbound there and `sum(1, 2)` is an
`UnboundName`. The option applies to the root and to every library the
evaluation runs. New in `Tramaj.Analysis`: `arithmeticNames`, `arithmeticOps`
and `deepArithmeticOps`, which report the arithmetic names a program
references free, so a host that leaves the profile off can refuse a program
before running it.

**Breaking**, with or without the profile:

- `EvalError` gains the constructor `NotRepresentable`: integer overflow, a
  zero divisor, a float result that is not finite. A host that matches every
  constructor needs a case for it.
- `"$term"` joins `"$sym"` and `"$type"` as a reserved key. `{"$term": 1}` in
  a program is a parse error, and a context carrying that key is a
  `TypeMismatch`, in concrete mode always and in symbolic mode unless it is a
  well-formed term and the profile is on.

**Sorting, number formatting and `round`** (`../specs/decisions.md` \S20,
`../specs/reference.md` \S11).

- `sort-by(list, fn)` and `sort-by-descending(list, fn)` order a list by the
  key `fn` gives each element. Keys are all integers, all floats or all
  strings (compared by code point); anything else is a `TypeMismatch`, and a
  symbol or a term a `NotConcrete`. Both are stable, each on its own terms,
  and `fn` is applied once per element, in index order. They are core
  forms, in every profile.
- `format-number(x, decimals, group)` writes an integer or a float in
  positional decimal notation with `decimals` digits (0 to 20) after the
  point and `group` between the groups of three digits of the integer part.
  It rounds the exact value of the number, ties away from zero, never writes
  an exponent or a negative zero, and has no locale. An ordinary builtin, in
  every profile.
- `round(x)` is the tenth builtin of the arithmetic profile: the integer
  nearest to `x`, ties away from zero, a `NotRepresentable` outside the
  integer range, and a term over a symbol.

**Breaking:**

- `Expr` gains the constructor `SortBy Bool Expr Expr` (descending,
  collection, key function). A host that matches every constructor needs a
  case for it; `subExprs` and every analysis already traverse it.
- `sort-by` and `sort-by-descending` are special-form names, as `map` is. A
  program that bound one of them and called it (`$sort-by(...)`) now gets
  the form, or a parse error if the call does not have two arguments.
- `format-number` joins `builtinNames`, and `round` joins `arithmeticNames`,
  so `arithmeticOps` reports it. A program that binds either name still
  shadows it.

New in `Tramaj.Eval`: `emittedConstraintCount`, the number of constraints an
evaluation emitted before equal ones are made one. It is there for tests,
which have no other way to count the applications of a function.

## 0.3.0.0

Implements v2 of the language. This is a rewrite of the semantic core, not an
extension of it -- every program, every host and every fixture is affected.
`../specs/decisions.md` records the design conflicts this resolved and which
reading of the specs won.

**Documents are expressions.** `Element` and `Fragment` are ordinary `Expr`
constructors, so a document node is a value that can be bound, passed to a
lambda and returned from one -- which is what makes the JSX-children pattern
work. The separate template-phase AST (`TemplateNode`, `TElement`, `TValue`,
`TMap`, `TBranch`) and its duplicated `map`/`branch` forms are gone, and so is
`JsonProgram`: `Program` is now `DocumentProgram`/`ExpressionProgram`, told
apart by the root's own form.

**New `Tramaj.Node`**, the normative interchange format specified in
`../specs/node-json.md` and implemented with both an encoder and a strict
decoder. `Text` carries a `Value` rather than a `String` and `Element` gains a
value slot, so a scalar child stays a scalar instead of being stringified;
attribute values are arbitrary values; an element may carry any number of
actions rather than at most one; fragments are real nodes; and every node
carries an annotations map that transformations preserve.

**New `Tramaj.Analysis`**: `staticImportNames`, `staticActionKeys` and
`contextHoles`, each with a deep variant that follows imports through a
library table and cuts cycles, plus `contextReads` (every path a program
reads from its own context) and `unsuppliedParams` (per import, what its
library reads and the import does not supply). `staticActionKeys` applies
action adaptation rather than ignoring it, so a prefixed subtree's keys are
known without evaluating anything.

**Imports take their parameters three ways.** One
`import(name, {k: expr, k2: ctx(path)})` form; `partial-import` is gone, and
so is v1's `tryPartial` heuristic, which inferred partiality by running a
library and catching a `PathNotFound` that started with `ctx`. A parameter is
supplied by an expression, by `ctx(path)` -- which reads the *importing*
program's own `$ctx.path` where the import is written, and exists as its own
form so `contextHoles` can enumerate the holes statically -- or by being left
out and supplied later, by calling the import with more parameters
(right-biased). An import runs when a field is read off it, `.rendered` or
`.vals`, not where it is written, so one wired-up import can serve a whole
`map`. A parameter nobody supplies is the library's own `PathNotFound`,
wrapped in the new `InLibrary` error naming the library that raised it.
`../specs/decisions.md` #10 has the reasoning.

**Other language changes.** `a <> b` concatenates strings, arrays or objects
(mixed types rejected, objects right-biased). `branch` is uniformly lazy and a
core constructor rather than a builtin, so an unreached arm's errors never
surface. `remap-actions` becomes `adapt-actions(node, prefix("ns:") | identity
[, fn])`, its prefix a static literal, its optional closure limited to the
event type and payload. Fragments are written `.(a, b)`. An action's event
joins its key in being a static literal. String literals gain escape sequences
including `\u{...}`; objects gain shorthand `{foo}` and bare keys. Builtins are
values in the initial environment, so one can be passed by reference; `str` is
new and `branch` is no longer among them.

Two parser bugs found while porting, both of the shape the 2026-08-26 pass
fixed for special forms: a malformed `action(...)`/`value(...)` in an element
argument backtracked into a meaningless `Call` instead of failing, and a
backtick in a static string position was silently taken as a literal rather
than reported as an interpolation that cannot go there.

Not yet ported to the PureScript `tramaj` package, which still implements v1.

Adds `fold(arr, init, fn)`, a fourth functional array primitive alongside
`map`/`filter`/`scan`: same `(acc, item)` step and `scanl` iteration order as
`scan`, but returns only the final accumulator instead of every intermediate
step. Also adds `concat(a, b, ...)` (variadic array-joining) and
`append(arr, item)` (add a single element at the end) as ordinary builtins.
Purely additive; no existing behavior changed. Ported in lockstep to the
PureScript `tramaj` package.

Adds object destructuring patterns (`../specs/decisions.md` \S17) in `@`
bindings and lambda parameters: `@{title, meta: {owner}} = $ctx.item`,
`({a, b}) => ...`. Purely a parser lowering to plain `Let`/`Lambda`; defaults,
rest, array patterns, empty patterns, duplicate names and annotated patterns
are parse errors. Adds `Tramaj.Ast.isHiddenName`: the hidden names the
lowering invents (`#src`, `#argN`) are never a symbol's `"binding"` and never
appear in a library's `.vals`.

## 0.2.0.0

Adds a JSON-producing mode for hosts that want the data half of the language on
its own: `Tramaj.Ast.JsonProgram`, `Tramaj.Parser.parseJsonProgram` and
`Tramaj.Eval.evalJsonProgram`. Same computation block and same expression
language as `parseProgram`/`evalProgram`, but the root is an expression rather
than an element, so the result is an aeson `Value` instead of a document `Node`
-- nothing is stringified through `jsonToDisplayString`. Purely additive; no
existing behavior changed. Haskell-only, with no counterpart in the PureScript
`tramaj` package.

## 0.1.0.0

Initial release, extracted from the repository it was written in. Ports the
PureScript `tramaj` package's `Tramaj.Ast` / `Tramaj.Parser` /
`Tramaj.Eval` to megaparsec + aeson.