Getting started meant cloning the repository, bootstrapping a compiler and
learning a task runner called `x`. That is a contributor's workflow handed to
everyone who wants to try the language.
Installing is now one command:
curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
install.sh puts a complete toolchain — compiler, CLI, engine runtime, bundled
ludic.* packages, formatter, language server — in ~/.ludic and adds it to PATH.
Prebuilt artifacts are checksum-verified; where a platform has none, or the
release predates this layout, it bootstraps from the compiler's own IR seed with
clang. The docs site publishes the script beside the pages that quote it, so the
page and the script can never come from different releases.
`x` becomes `ludic`, and the surface splits by audience. A user of the language
sees `new`, `run`, `build`, `test`, `add`, `fmt`, `lsp`, `doctor`, `upgrade`;
`ludic new` scaffolds a project that builds and plays as it stands. Everything
the toolchain repo needs moved under `ludic dev` — build, test, reseed,
bootstrap-cfree, docs-gen, release — unchanged apart from the namespace. Those
tasks read arguments one position further along, so dispatch_dev sets a shift
and commands use arg_n()/arg_total() rather than each knowing its own depth.
Release artifacts become complete install roots (bin/ beside runtime/, packages/
and VERSION) rather than bare binaries, which is what the installer unpacks.
`ludic dev test` asserts the whole shape: it stages an install, puts it on PATH
with no LUDIC_HOME, and runs new -> build -> test through it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2.6 KiB
| id | name | category | kind | tokens | sig | tip | order |
|---|---|---|---|---|---|---|---|
| kw-test | test | testing | keyword | test | test "name" { … } | A named test block, run automatically with pass/fail reporting. | 1 |
A test "name" { … } block declares a named test. Every test in a file is discovered automatically and run by a generated entry point — there is no entry to write and nothing to register. The runner prints ok - name for a test whose assertions all held and FAIL - name for one that had a failure, then a == N passed, M failed == summary, and the program exits non-zero if any test failed. Writing a test should feel like writing a function: put a few assertions in a block and run it.
Assertions (each records a failure and prints file:line: … failed but keeps going, so one run reports every failure):
expect(cond)— the booleancondmust be true.expect_eq(a, b)—amust equalb; on failure prints(got a, want b).expect_near(a, b, tol)—amust be withintolofb(absolute). Use it forfixed-point results and accumulated integer math, where an exact match is too brittle.
Run a spec file directly with the compiler-runner — ludic mymath_test.ludic compiles it to a native binary, runs it, and forwards the pass/fail exit code — so it drops straight into bin/ludic and CI.
Line coverage. Compile with the --coverage flag and the compiler instruments every statement with a per-source-line hit counter; at exit the counts are written to the file named by $LUDIC_COVERAGE (default ludic.cov) as a FILE <name> header followed by one <line> <hits> row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical. bin/ludic dev test --coverage compiles the test specs this way, runs them, and aggregates the dumps into a per-file report that names the lines your tests never reached:
== line coverage (bin/ludic dev test --coverage) ==
examples/library/coverage.ludic 16/17 lines 94% uncovered: 33
examples/library/testing.ludic 17/17 lines 100%
----
total: 33/34 lines 97%
program MathSpec {
function add(a: int, b: int) -> int { return a + b }
test "addition adds" {
expect_eq(add(2, 3), 5)
expect(add(1, 1) == 2)
}
test "fixed math is close enough" {
let half = fixed(1) / 2 # 0.5 in Q16.16
expect_near(half, 32768, 2) # within 2 raw units of 0.5
}
}