diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..151c7aff --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to the purescript-lua package set + +This set is a collection of PureScript libraries forked to run on Lua 5.1 through +the [pslua](https://github.com/Unisay/purescript-lua) compiler. Each fork keeps +the upstream PureScript sources and replaces the JavaScript FFI with Lua. This +document is the practical canon for maintaining a fork; the reasoning behind each +rule lives in [`docs/adr/`](docs/adr/). + +## Toolchain + +Pinned through purescript-overlay ([ADR 0001](docs/adr/0001-overlay-flake-toolchain.md)): +purs 0.15.16, spago 0.21.0, Lua 5.1. Enter the dev shell with `nix develop`. Keep +`flake.lock` reasonably current with `nix flake update`; a long-stale pslua pin +breaks the build. + +## Commands + +- Build: `nix develop -c ./scripts/build` +- Test: `nix develop -c bash ./scripts/test` (forks that ship Lua regression tests) +- Lint: `nix develop -c luacheck --quiet --std lua51 --no-unused-args src/` +- Format: `nix fmt` ([ADR 0007](docs/adr/0007-formatting-treefmt-purs-tidy-lua-format.md)) + +## Lua 5.1 and FFI ([ADR 0003](docs/adr/0003-lua-51-target-and-ffi-parens.md)) + +- No `table.unpack`, `bit32`, `utf8`, or `//`. `math.pow` and `math.atan2` exist. + Array-style tables are 1-indexed. Keep string escapes 5.1-safe. +- Parenthesise every FFI export: `return { name = (), ... }`. +- `unit` is `{}`, never nil ([ADR 0004](docs/adr/0004-unit-is-empty-table.md)). + +## CI ([ADR 0002](docs/adr/0002-hardened-ci-canon.md)) + +All forks share one workflow: substituters pinned in `extra_nix_config` (no +`accept-flake-config`), build, optional test via `bash`, and luacheck with +`--std lua51 --no-unused-args`. + +## Agent instructions ([ADR 0005](docs/adr/0005-agents-md-single-source.md)) + +Each fork has an `AGENTS.md` (the single source) and a one-line `CLAUDE.md` that +imports it with `@AGENTS.md`. Edit `AGENTS.md`; never duplicate. + +## Releasing ([ADR 0006](docs/adr/0006-fork-release-by-annotated-tag.md)) + +Annotated git tag on `master` → bump the fork's version in `src/packages.dhall` → +refresh `latest-compatible-sets.json` → push a `psc-*` set tag. A tooling-only PR +needs no release. + +## Decisions and ADRs + +Cross-cutting decisions are recorded as ADRs in [`docs/adr/`](docs/adr/). + +- **Read them before a decision** that affects the set (toolchain, CI, FFI + conventions, release, formatting). The relevant ADR is usually linked from the + section above. +- **Add one after a decision.** Copy the shape of an existing record (status, + context, decision, consequences), give it the next number, and add it to the + ADR index. Supersede rather than rewrite an accepted record. + +Decisions about the pslua compiler itself live in the +[pslua repository](https://github.com/Unisay/purescript-lua) under its own +`docs/adr/`. diff --git a/docs/adr/0001-overlay-flake-toolchain.md b/docs/adr/0001-overlay-flake-toolchain.md new file mode 100644 index 00000000..30af34eb --- /dev/null +++ b/docs/adr/0001-overlay-flake-toolchain.md @@ -0,0 +1,27 @@ +# 0001 — Pin the toolchain with purescript-overlay + +Status: Accepted + +## Context + +Each fork needs a reproducible PureScript toolchain (purs, spago) and a Lua 5.1 +runtime for the FFI checks. Early forks pinned their tools through +easy-purescript-nix, which tracked purs 0.15.15 and pulled an unpinned spago. +The compiler and the later forks moved to `thomashoneyman/purescript-overlay`, +which packages exact purs and spago releases as nix attributes. + +## Decision + +Every fork pins its toolchain through purescript-overlay: purs 0.15.16 +(`purs-bin.purs-0_15_16`), spago 0.21.0 (`spago-bin.spago-0_21_0`), and a Lua 5.1 +toolchain (`lua51Packages`). The dev shell also carries dhall, luacheck, +luaformatter, nixfmt, and treefmt. The pslua compiler is a flake input tracking +`github:Unisay/purescript-lua`. + +## Consequences + +- One flake.nix shape across the set. Converting a fork is copying the canonical + flake.nix and running `nix flake update`. +- purs and spago versions are exact and shared through the binary caches. +- Keep flake.lock reasonably current. A long-stale pslua pin predates the + output-directory and codegen fixes and breaks CI (see 0002, 0003). diff --git a/docs/adr/0002-hardened-ci-canon.md b/docs/adr/0002-hardened-ci-canon.md new file mode 100644 index 00000000..77ea327c --- /dev/null +++ b/docs/adr/0002-hardened-ci-canon.md @@ -0,0 +1,33 @@ +# 0002 — Shared hardened CI workflow + +Status: Accepted + +## Context + +Forks drifted across CI shapes: some on the old npm/setup-purescript flow, some +on an early nix workflow that set `accept-flake-config = true` and ran luacheck +with `--std min`. `accept-flake-config` lets a pull request's own flake nixConfig +inject substituters and signing keys, which is a supply-chain risk. `--std min` +is the intersection of all Lua versions and flags `math.pow`/`math.atan2`, which +are valid on the Lua 5.1 target. + +## Decision + +All forks share one CI workflow: + +- `cachix/install-nix-action@v27`, with substituters and keys pinned in + `extra_nix_config`. `accept-flake-config` is dropped. +- Build: `nix develop -c ./scripts/build`. +- Test: `if [ -f scripts/test ]; then nix develop -c bash ./scripts/test; fi`, + run via `bash` so it does not depend on the execute bit. +- Lint: `nix develop -c luacheck --quiet --std lua51 --no-unused-args src/` + (`dist/` for forks without hand-written FFI). `--no-unused-args` tolerates the + curried fallback arguments the native FFI stubs ignore. +- `scripts/build` starts with `set -euo pipefail`. + +## Consequences + +- A pull request's flake config can no longer add caches or keys. +- luacheck matches the real target and the FFI idiom, so it stops flagging + `math.pow`/`math.atan2` and starts catching real Lua 5.1 violations (see 0003). +- A stale pslua pin still breaks the build step; keep flake.lock current (0001). diff --git a/docs/adr/0003-lua-51-target-and-ffi-parens.md b/docs/adr/0003-lua-51-target-and-ffi-parens.md new file mode 100644 index 00000000..84e61766 --- /dev/null +++ b/docs/adr/0003-lua-51-target-and-ffi-parens.md @@ -0,0 +1,29 @@ +# 0003 — Target Lua 5.1 and parenthesise FFI values + +Status: Accepted + +## Context + +The set targets Lua 5.1. pslua reads each fork's foreign file (`src/*.lua`) and +extracts the exported values. Its reader does not parse Lua: it requires every +exported value to be wrapped in parentheses and finds the end of a value by +matching those parentheses (`Foreign.hs`, `valueParser`). + +## Decision + +- Generated and hand-written Lua must run on Lua 5.1: no `table.unpack`, `bit32`, + `utf8`, or the `//` operator; `math.pow` and `math.atan2` do exist; array-style + tables are 1-indexed; Lua 5.1 mangles some Lua 5.3 string escapes, so keep FFI + escapes 5.1-safe. +- Every FFI export is parenthesised: `return { name = (), ... }`. A bare + `function ... end` or an unparenthesised expression fails to parse. + +## Consequences + +- luacheck `--std lua51` is the gate that catches 5.1 violations, for example + `table.pack`/`unpack`/`move` slipping in from Lua 5.2/5.3 habits. +- Lua formatters that strip "redundant" parentheses (StyLua) break the FFI + reader; the set uses lua-format, which preserves them (see 0007). +- Relaxing the parenthesis requirement would mean teaching pslua to find a value's + end without the parentheses, which needs real Lua parsing. That is a compiler + decision, recorded in the pslua repository, not here. diff --git a/docs/adr/0004-unit-is-empty-table.md b/docs/adr/0004-unit-is-empty-table.md new file mode 100644 index 00000000..427e85b6 --- /dev/null +++ b/docs/adr/0004-unit-is-empty-table.md @@ -0,0 +1,20 @@ +# 0004 — Represent unit as an empty table, never nil + +Status: Accepted + +## Context + +Lua tables cannot hold nil values: assigning nil removes the key. If the prelude +defines `unit = nil`, an `Array Unit` collapses to an empty table and a length +check silently returns 0. + +## Decision + +`unit` is `{}` (an empty table), never nil. This requires +`Unisay/purescript-lua-prelude` v7.2.0 or newer. + +## Consequences + +- `Array Unit` keeps its length. +- If eval goldens for unit arrays start printing 0, a package set has downgraded + the prelude below v7.2.0. Do not accept such goldens. diff --git a/docs/adr/0005-agents-md-single-source.md b/docs/adr/0005-agents-md-single-source.md new file mode 100644 index 00000000..221297b3 --- /dev/null +++ b/docs/adr/0005-agents-md-single-source.md @@ -0,0 +1,26 @@ +# 0005 — AGENTS.md is the single agent instruction file + +Status: Accepted + +## Context + +Several AI coding agents read repository-level instructions. Most of them +(Codex, Cursor, Copilot, Gemini CLI, Aider) read `AGENTS.md` natively; Claude +Code reads `CLAUDE.md`. Keeping two instruction files per fork drifts them out of +sync. + +## Decision + +Each fork has one `AGENTS.md` with the build/test/lint commands, the Lua 5.1 +constraints, the FFI parenthesisation rule, the toolchain pins, and a pointer to +the ADRs. `CLAUDE.md` is a one-line `@AGENTS.md` import, not a symlink (symlinks +break on Windows and on some CI checkouts). `AGENTS.md` stays short and +command-first; the full rationale lives in these ADRs and in `CONTRIBUTING.md`. + +## Consequences + +- One file to edit; every agent reads the same content. +- `AGENTS.md` links to the package-set `CONTRIBUTING.md` for the release process + and the ADRs, so neither is duplicated across forks. +- Agents are pointed at the ADRs: read them before a cross-cutting decision, add + one after making such a decision (see the ADR index README). diff --git a/docs/adr/0006-fork-release-by-annotated-tag.md b/docs/adr/0006-fork-release-by-annotated-tag.md new file mode 100644 index 00000000..a9c123cd --- /dev/null +++ b/docs/adr/0006-fork-release-by-annotated-tag.md @@ -0,0 +1,26 @@ +# 0006 — Release forks with annotated tags + +Status: Accepted + +## Context + +The forks are consumed by spago through git refs and are aggregated into the +package set. They inherit upstream CHANGELOGs and do not track fork versions +there. The existing fork tags (for example effect v4.1.0) are bare annotated +tags with no GitHub Release attached. + +## Decision + +A fork release is an annotated git tag on `master`, pushed to the fork. No GitHub +Release and no per-fork changelog entry. After tagging, bump the fork's `version` +in this repository's `src/packages.dhall`, refresh `latest-compatible-sets.json`, +and push a `psc-0.15.15-[-N]` set tag, which the release workflow turns +into the set asset. A pull request that touches only tooling, CI, or the flake +(not `src/`) needs no tag or set bump, because spago consumes `src/` only. + +## Consequences + +- Consumers resolve a fork by its tag; the set tag pins a compatible combination. +- Tooling-only sweeps (for example the AGENTS.md and CI alignment) ship without a + release. +- A `src/*.lua` or `src/*.purs` change does require a tag and a set bump. diff --git a/docs/adr/0007-formatting-treefmt-purs-tidy-lua-format.md b/docs/adr/0007-formatting-treefmt-purs-tidy-lua-format.md new file mode 100644 index 00000000..9fa18b9e --- /dev/null +++ b/docs/adr/0007-formatting-treefmt-purs-tidy-lua-format.md @@ -0,0 +1,41 @@ +# 0007 — Formatting via treefmt: purs-tidy and lua-format + +Status: Accepted + +## Context + +Formatting was unconfigured across the set: treefmt and luaformatter were in the +dev shell, but there was no treefmt config. We want one setup that runs the same +way locally (`nix fmt`), as a pre-commit guard, and as a CI gate +(`treefmt --ci`). Two formatter choices needed care. + +- PureScript: purs-tidy is the ecosystem standard. purty is deprecated, and + purescript-contrib mandates purs-tidy. The forks' `.purs` are vendored from + upstream and are mostly already purs-tidy-formatted, so reformatting them is a + small, layout-only diff (mostly import and export lists; measured at 0 changed + files for arrays, 1 of 50 for prelude, more for foldable-traversable). Running + purs-tidy over the upstream snapshot before an upstream sync keeps that sync + free of formatting conflicts. +- Lua: StyLua is modern and lightweight but removes parentheses it considers + redundant, including the wrapping parentheses the FFI reader depends on (0003). + Tested on a real FFI file, StyLua turned `name = (function ... end)` into + `name = function ... end`, which fails to parse. There is no StyLua option to + preserve them (only per-node `-- stylua: ignore`, which skips formatting that + node entirely). lua-format preserves the parentheses. + +## Decision + +Configure treefmt with treefmt-nix: nixfmt (`*.nix`), purs-tidy with a shared +`.tidyrc.json` (`*.purs`), dhall format (`*.dhall`), and lua-format (the FFI +`src/*.lua`). The flake exposes `formatter` (so `nix fmt` works) and a +`checks.formatting` (so `nix flake check` / `treefmt --ci` gates CI), and a +pre-commit hook runs `nix fmt`. Generated and vendored trees are excluded: +`dist/`, `output/`, `.spago/`, `*.lock`, `.tidyrc.json`. + +## Consequences + +- One config drives local formatting, the pre-commit guard, and the CI gate. +- StyLua stays unusable until pslua's foreign reader no longer requires grouping + parentheses, which is a compiler change recorded in the pslua repository. +- Vendored `.purs` take a small one-time reflow; future upstream syncs normalise + both sides with purs-tidy to stay conflict-free. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..f53d3915 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,37 @@ +# Architecture Decision Records + +Decisions that apply across the whole purescript-lua package set live here. Each +record is a short Markdown file in the MADR style: a status, the context, the +decision, and its consequences. + +Scope: these ADRs cover the set as a whole, its forks, their toolchain, CI, FFI +conventions, release process, and formatting. Decisions about the pslua compiler +itself live in its own repository (`Unisay/purescript-lua`, under `docs/adr/`). +The rule of thumb: a decision about the compiler goes there; a decision about +the forks, the set, or the shared tooling goes here. + +Status values: `Proposed`, `Accepted`, `Superseded by NNNN`, `Deprecated`. + +The practical, do-this summary of these decisions is in the repository root +[`CONTRIBUTING.md`](../../CONTRIBUTING.md); the records here hold the reasoning. + +## Maintaining these records + +- **Read before deciding.** Before making a change that affects the set as a + whole, check whether an ADR already covers it. The relevant record is usually + linked from `CONTRIBUTING.md`. +- **Write after deciding.** Once a cross-cutting decision is made, add a record: + copy the shape of an existing one (Status, Context, Decision, Consequences), + give it the next number, and add a line to the index below. +- **Supersede, do not rewrite.** When a decision changes, write a new ADR and set + the old one's status to `Superseded by NNNN`. The history stays readable. + +## Index + +- [0001](0001-overlay-flake-toolchain.md) — Pin the toolchain with purescript-overlay +- [0002](0002-hardened-ci-canon.md) — Shared hardened CI workflow +- [0003](0003-lua-51-target-and-ffi-parens.md) — Target Lua 5.1 and parenthesise FFI values +- [0004](0004-unit-is-empty-table.md) — Represent unit as an empty table, never nil +- [0005](0005-agents-md-single-source.md) — AGENTS.md is the single agent instruction file +- [0006](0006-fork-release-by-annotated-tag.md) — Release forks with annotated tags +- [0007](0007-formatting-treefmt-purs-tidy-lua-format.md) — Formatting via treefmt: purs-tidy and lua-format