ludic/tools/ludic-cli/main.ludic
Orkuncakilkaya 2ad6edaae3 ludic syntax, and ludic-dev syntax: every grammar written from the compiler's vocabulary and checked against it
`ludic syntax [--json] [-o FILE]` prints what `ludicc --emit-syntax` does (a line
per entry, or the JSON). `ludic-dev syntax` writes, between "ludic-dev syntax:
begin" / "end" lines, the keyword, type, phase and attribute tables of
ludic_syntax.h, LudicVocabulary's sets (JetBrains), ludic-mode.el's lists and the
language server's word tests (is_keyword_word and the rest; is_contextual_word is
every word the parser does not reserve, and every declaring or modifying one),
and every TextMate pattern marked "comment": "ludic-dev syntax: <group>" (shared
and the VS Code copy). The grammars gain module uses port bind action reducer
dispatch registry def open component prop view alias friend unsafe numbers of as
from mut system; import and extern colour as declarations; the phase clause
knows Overlay; the bitwise pattern matches | and ^ on their own again.

`ludic-dev syntax --check` - and check-vocabulary, whose old parser comparison it
replaces, and the regression suite (syntax_cases, one line per file) - fails when
a written list is behind, when a grammar lacks a keyword, type or phase, when
docs/language has no page for a keyword, type, phase or attribute, or when the
parser (a scan of selfhost/frontend: is_id / text == words, a == / ann ==
attributes) tests a word or reads an attribute vocab.ludic lacks, or the reverse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:59:30 +03:00

176 lines
8.8 KiB
Text

# ludic — the command-line interface to the Ludic language. One native binary
# (bin/ludic) written in Ludic and compiled by Ludic; the unix tools, clang and
# the compiler it drives are invoked through the `run` intrinsic.
#
# This is everything a user of the language does: create a project, build it, run
# it, test it, add a package, format, upgrade. Nothing here is about the
# toolchain's own repository — building the compiler, reseeding it, generating
# the docs site, cutting a release live in a separate tool (dev.ludic ->
# bin/ludic-dev) that only exists in a checkout and is never installed. Someone
# who installs Ludic should not have to read past commands they cannot use.
#
# Installing (macOS, Linux):
#
# curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
#
# Building from a checkout — the one step Ludic cannot do for itself, since
# compiling Ludic needs a compiler:
#
# mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
# bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
# bin/ludic-dev build
program Ludic {
import "prelude.ludic"
import "build.ludic"
import "split.ludic"
import "project.ludic"
import "pkg.ludic"
import "assets.ludic"
import "packignore.ludic"
import "pack.ludic"
import "bundle.ludic"
import "scripts.ludic"
import "deps.ludic"
import "schema.ludic"
import "syntax_cli.ludic"
import "ui_preview.ludic"
import "testpar.ludic"
import "migrate.ludic"
function usage() -> void {
print("ludic — the toolchain for the Ludic language")
print("")
print("usage: ludic <command> [arguments]")
print("")
print("start a game:")
print(" new <name> create a project that builds and runs as it stands")
print(" run [file] [--headless] build the project and run it")
print(" build [file] [--headless] [-o out]")
print(" compile to build/<name> (a native binary, nothing to ship beside it)")
print(" build [file] --check only check it: types, modules, uses and ports; nothing emitted")
print(" build [file] --check --diagnostics=json every error as a JSON array on stdout")
print(" test [file|dir...] [-j N] [-v] [--test NAME]")
print(" compile and run the project's tests (-v: every test's line)")
print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]")
print(" the module graph as the compiler sees it, and how tangled it is")
print(" schema [file] [-o FILE] records, registries and their entries, consts, as JSON (for editors)")
print(" syntax [--json] [-o FILE] the language's vocabulary: keywords, declarations, types, phases, attributes, operators")
print(" migrate state [file] [--prune] [--runtime] module-level vars into states, passed as parameters (0.S); --prune drops states nothing uses, --tighten also mut nothing writes")
print(" clean remove build/")
print("")
print("packages:")
print(" add <module>[@version] add a dependency, then resolve + fetch + link")
print(" get resolve every package.ludic dependency and write the lock")
print(" update [module] bump a dependency (or all) to its latest published version")
print(" verify check every locked package against the store by content hash")
print(" vendor copy the resolved packages into ./vendor for offline builds")
print(" assets [--force] fetch the CC0 materials the renderer needs into assets/polyhaven/")
print(" build-lib <module.ludic> compile a package's module to a prebuilt dylib in lib/<target>/")
print(" link-flags print the clang flags to link this project's prebuilt module dylibs")
print("")
print("shipping:")
print(" pack [--out FILE] [dirs...] pack the assets into build/<name>.lpak (honouring .packignore)")
print(" pack --no-ignore ... pack everything, ignoring .packignore")
print(" pack --list FILE print what is in a pack")
print(" pack --verify FILE re-hash every entry against the table")
print(" bundle [file] build a macOS .app: icon, metadata, assets, splash")
print("")
print("the toolchain:")
print(" version print the toolchain version")
print(" upgrade [version] reinstall from the docs site (the same script that installed it)")
print(" doctor check that the install is complete and usable")
print(" fmt [--check] [paths...] format Ludic source in place (--check: report, write nothing)")
print(" lsp run the language server on stdio (what editors spawn)")
print(" ui-preview [--font DIR] preview a game's ludic.ui components for a studio, on stdio")
print(" help this message")
print("")
print("package scripts (package.ludic):")
print(" script <name> [args] run a `script \"name\" \"command\"` line (or just: ludic <name>)")
print(" scripts list the package's scripts and hooks")
print(" `hook before|after <command> \"…\"` wraps build, run, test, bundle, …")
print("")
print("A file argument runs it: ludic mygame.ludic")
}
# does `s` end in ".ludic"? — `ludic mygame.ludic` runs that file, the shape
# the compiler-runner had before the CLI grew subcommands.
function is_ludic_file(s: pointer) -> bool {
let n = slen(s)
if n <= 6 { return false }
return sslice(s, n - 6, n) == ".ludic"
}
# route one subcommand to its implementation and return its exit code; -1 when
# the name is unknown. Returning (rather than exiting in place) lets entry tear
# down the per-process scratch directory before the process ends.
function dispatch(cmd: pointer) -> int {
if (cmd == "new") { return cmd_new() }
if (cmd == "build") { return cmd_build() }
if (cmd == "run") { return cmd_run() }
if (cmd == "test") { return cmd_test() }
if (cmd == "deps") { return cmd_deps() }
if (cmd == "schema") { return cmd_schema() }
if (cmd == "syntax") { return cmd_syntax() }
if (cmd == "migrate") { return cmd_migrate() }
if (cmd == "clean") { return cmd_clean() }
if (cmd == "fmt") { return cmd_fmt() }
if (cmd == "lsp") { return cmd_lsp() }
if (cmd == "ui-preview") { return cmd_ui_preview() }
if (cmd == "doctor") { return cmd_doctor() }
if (cmd == "upgrade") { return cmd_upgrade() }
if (cmd == "script") { return cmd_script() }
if (cmd == "scripts") { return cmd_scripts() }
if (cmd == "add") { return cmd_pkg_add() }
if (cmd == "get") { return cmd_pkg_get() }
if (cmd == "update") { return cmd_pkg_update() }
if (cmd == "verify") { return cmd_pkg_verify() }
if (cmd == "vendor") { return cmd_pkg_vendor() }
if (cmd == "assets") { return cmd_fetch_assets() }
if (cmd == "pack") { return cmd_pack() }
if (cmd == "bundle") { return cmd_bundle() }
if (cmd == "build-lib") { return cmd_pkg_build_lib() }
if (cmd == "link-flags") { return cmd_pkg_link_flags() }
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
# The toolchain's own tasks used to live here under `ludic dev`. Say where
# they went rather than printing "unknown command" at someone who read an
# older README or a commit message.
if (cmd == "dev") {
err("ludic: the toolchain's own tasks live in `ludic-dev`, which is built from a\n")
err(" checkout of the Ludic repository and is not part of an install:\n")
err(" clang selfhost/ludicc.seed.ll -o bin/ludicc\n")
err(" bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev help\n")
return 1
}
return -1
}
entry {
if (arg_count() < 2) { usage(); exit(1) }
let cmd = arg(1)
# `ludic mygame.ludic` — build and run that file
if is_ludic_file(cmd) {
let rc = cmd_run_file()
tmp_cleanup()
exit(rc)
}
# package.ludic hooks wrap the built-in commands: `before` can veto,
# `after` runs only when the command succeeded
if is_hookable(cmd) {
let pre = run_hooks("before", cmd)
if pre != 0 { tmp_cleanup(); exit(pre) }
}
var code = dispatch(cmd)
if code == 0 and is_hookable(cmd) { code = run_hooks("after", cmd) }
# an unknown command may be one of the package's scripts
if code == -1 { code = run_script(cmd, 2) }
tmp_cleanup()
if code >= 0 { exit(code) }
err(`ludic: unknown command '{cmd}'\n`)
usage()
exit(1)
}
}