Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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 = (<value>), ... }`.
- `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/`.
27 changes: 27 additions & 0 deletions docs/adr/0001-overlay-flake-toolchain.md
Original file line number Diff line number Diff line change
@@ -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).
33 changes: 33 additions & 0 deletions docs/adr/0002-hardened-ci-canon.md
Original file line number Diff line number Diff line change
@@ -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).
29 changes: 29 additions & 0 deletions docs/adr/0003-lua-51-target-and-ffi-parens.md
Original file line number Diff line number Diff line change
@@ -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 = (<value>), ... }`. 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.
20 changes: 20 additions & 0 deletions docs/adr/0004-unit-is-empty-table.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 26 additions & 0 deletions docs/adr/0005-agents-md-single-source.md
Original file line number Diff line number Diff line change
@@ -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).
26 changes: 26 additions & 0 deletions docs/adr/0006-fork-release-by-annotated-tag.md
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +9 to +10

## 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-<YYYYMMDD>[-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.
41 changes: 41 additions & 0 deletions docs/adr/0007-formatting-treefmt-purs-tidy-lua-format.md
Original file line number Diff line number Diff line change
@@ -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.
37 changes: 37 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -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