refactor(x): per-process scratch dirs, Ludic ports of the LSP test and Forgejo release

- every scratch file lives in `$TMPDIR/x_<pid>/` (tmp_dir/tmp_path in
  prelude.ludic), removed by main's new dispatch() → tmp_cleanup();
  X_KEEP_TMP=1 keeps it. `x test` and `x check-*` may now run together.
- `x test-lsp` (tools/x/lsp_test.ludic) replaces tools/test-lsp.py: the
  whole request stream is framed into one stdin file, the server runs to
  `exit`, and the response stream is parsed back by request id; adds a
  check that ludicc's own error is published on save
- tools/x/forgejo.ludic replaces tools/ci/forgejo_release.py (curl with a
  0600 header file; the token is no longer on the command line;
  LUDIC_FORGEJO_API for forks)
- `x docs-palette --check` regenerates into scratch and compares, so the
  drift guard judges the working tree rather than git HEAD; the generator
  no longer emits a trailing blank line the formatter rejects
- `x test-tools`: exit 2 from test-lsp / test-grammar.js is a visible
  skip, never a pass; the widget-prop test uses the `id: Root` syntax
- tools/test-grammar.js: current vocabulary (property/model/handler/
  prefab/scene/event/become/@Queries), LUDIC_NODE_MODULES, exit 2 on skip
- tools/atlas.ludic rewritten in the current language (it did not compile)
- operators.ludic / os.ludic registered in the suite
- json.ludic: j_quote() writer helper

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 01:12:26 +03:00
parent 647dfec334
commit 8fed9add66
19 changed files with 1154 additions and 614 deletions

View file

@ -1,56 +1,41 @@
# ============================================================================
# atlas.ludic — render a Kenney tile pack as one index-labeled sheet, so you can # atlas.ludic — render a Kenney tile pack as one index-labeled sheet, so you can
# see which sprite id is which before referring to it in a game. # see which sprite id is which before referring to it in a game.
# #
# ludicc tools/atlas.ludic --headless -o build/atlas # bin/ludicc --headless tools/atlas.ludic -o build/atlas
# ./build/atlas && sips -s format png out.ppm --out atlas.png # ./build/atlas < /dev/null && sips -s format png build/out.ppm --out atlas.png
# #
# Replaces the old tools/atlas.c, which used the C runtime that no longer # A tool written in the language it is a tool for: one Start handler draws every
# exists. It is the same program, in the language it is a tool for. # tile with its number underneath and quits, and the headless build leaves the
# ============================================================================ # frame in build/out.ppm.
game Atlas { program Atlas {
const DIR: str = "assets/kenney/tiny-dungeon/Tiles" const DIR: string = "assets/kenney/tiny-dungeon/Tiles"
const N: int = 132 const TILE_COUNT: int = 132 # tile_0000.png … tile_0131.png
const COLS: int = 12 const COLUMNS: int = 12
const CELL: int = 24 const CELL: int = 24
const SPRITE_INSET_X: int = 4 # centre a 16px sprite in a 24px cell
const SPRITE_INSET_Y: int = 2
const LABEL_INSET_X: int = 1
const LABEL_INSET_Y: int = 18
const BACKDROP: int = 0x101018
const LABEL_COLOR: int = 0x8090b0
system Sheet phase Start { # "<DIR>/tile_0007.png" — the id is the file's zero-padded number
clear(0x101018) function tile_path(i: int) -> string {
for i in 0 .. N { var digits = Text.from_int(i)
let col = i % COLS while Text.length(digits) < 4 { digits = "0" + digits }
let row = i / COLS return `{DIR}/tile_{digits}.png`
let x = col * CELL
let y = row * CELL
# tile_0000.png … tile_0131.png — the id is the file's number
let id = load_png(tile_path(i))
if id >= 0 {
draw_sprite(id, x + 4, y + 2)
} }
text_int(x + 1, y + 18, i, 0x8090b0, 1)
handler Sheet phase Start {
Screen.clear(BACKDROP)
for i in 0 .. TILE_COUNT {
let x = (i % COLUMNS) * CELL
let y = (i / COLUMNS) * CELL
let id = Assets.image(tile_path(i))
if id >= 0 { Screen.sprite(id, x + SPRITE_INSET_X, y + SPRITE_INSET_Y) }
Screen.draw_number(x + LABEL_INSET_X, y + LABEL_INSET_Y, i, LABEL_COLOR, 1)
} }
present() Screen.show()
quit() quit()
} }
# build "<DIR>/tile_%04d.png" without a formatter: the runtime's string
# helpers write into a scratch buffer we own
function tile_path(i: int) -> string {
let buf = atlas_buf()
let n = rt_put_str(buf, 0, DIR)
n = rt_put_str(buf, n, "/tile_")
if i < 1000 { poke8(buf, n, 48) n = n + 1 }
if i < 100 { poke8(buf, n, 48) n = n + 1 }
if i < 10 { poke8(buf, n, 48) n = n + 1 }
n = rt_put_int(buf, n, i)
n = rt_put_str(buf, n, ".png")
poke8(buf, n, 0)
return buf
}
var atlas_scratch: pointer = ptr_null()
function atlas_buf() -> pointer {
if ptr_is_null(atlas_scratch) { atlas_scratch = mem_alloc(512) }
return atlas_scratch
}
} }

View file

@ -10,8 +10,9 @@
* *
* node tools/test-grammar.js * node tools/test-grammar.js
* *
* Needs vscode-textmate and vscode-oniguruma; skips cleanly when they are not * Needs vscode-textmate and vscode-oniguruma (from the VS Code extension's
* installed, so it never blocks the suite on a machine without npm. * node_modules, or a directory named by $LUDIC_NODE_MODULES); exits 2 when they
* are not installed so the caller can report a skip rather than a pass.
* ==========================================================================*/ * ==========================================================================*/
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -22,12 +23,12 @@ const SHARED = path.join(ROOT, 'tools', 'editors', 'shared');
let vsctm, oniguruma; let vsctm, oniguruma;
try { try {
const local = path.join(ROOT, 'tools', 'editors', 'vscode', 'node_modules'); const local = path.join(ROOT, 'tools', 'editors', 'vscode', 'node_modules');
const paths = [local, '/tmp/tmgrammar/node_modules', ...module.paths]; const paths = [local, ...(process.env.LUDIC_NODE_MODULES ? [process.env.LUDIC_NODE_MODULES] : []), ...module.paths];
vsctm = require(require.resolve('vscode-textmate', { paths })); vsctm = require(require.resolve('vscode-textmate', { paths }));
oniguruma = require(require.resolve('vscode-oniguruma', { paths })); oniguruma = require(require.resolve('vscode-oniguruma', { paths }));
} catch (e) { } catch (e) {
console.log(' skip TextMate grammar (npm i vscode-textmate vscode-oniguruma to run)'); console.log(' skip TextMate grammar (npm i vscode-textmate vscode-oniguruma to run)');
process.exit(0); process.exit(2);
} }
let passed = 0; let passed = 0;
@ -38,7 +39,7 @@ function bad(name, detail) { failed++; console.log(` FAIL ${name}\n ${d
async function main() { async function main() {
const wasmPath = require.resolve('vscode-oniguruma/release/onig.wasm', { const wasmPath = require.resolve('vscode-oniguruma/release/onig.wasm', {
paths: [path.join(ROOT, 'tools', 'editors', 'vscode', 'node_modules'), '/tmp/tmgrammar/node_modules'] paths: [path.join(ROOT, 'tools', 'editors', 'vscode', 'node_modules'), ...(process.env.LUDIC_NODE_MODULES ? [process.env.LUDIC_NODE_MODULES] : []), ...module.paths]
}); });
await oniguruma.loadWASM(fs.readFileSync(wasmPath).buffer); await oniguruma.loadWASM(fs.readFileSync(wasmPath).buffer);
@ -96,27 +97,31 @@ async function main() {
} }
expect('comment', '# a note', '# a note', 'comment.line'); expect('comment', '# a note', '# a note', 'comment.line');
expect('string', 'const S: str = "hi"', '"hi"', 'string.quoted.double'); expect('string', 'const S: string = "hi"', '"hi"', 'string.quoted.double');
expect('hex literal', 'clear(0x0e0e16)', '0x0e0e16', 'constant.numeric.hex'); expect('interpolated string', 'let m = `hp {hp}`', '`hp {hp}`', 'string.interpolated');
expect('hex literal', 'Screen.clear(0x0e0e16)', '0x0e0e16', 'constant.numeric.hex');
expect('fixed literal', 'let f = 1.5', '1.5', 'constant.numeric.fixed'); expect('fixed literal', 'let f = 1.5', '1.5', 'constant.numeric.fixed');
expect('declaration keyword', 'component Pos { x: int = 0 }', 'component', 'storage.type.component'); expect('declaration keyword', 'property Pos { x: int = 0 }', 'property', 'storage.type.component');
expect('component name', 'component Pos { x: int = 0 }', 'Pos', 'entity.name.type.component'); expect('property name', 'property Pos { x: int = 0 }', 'Pos', 'entity.name.type.component');
expect('primitive type', 'component Pos { x: int = 0 }', 'int', 'support.type.primitive'); expect('primitive type', 'property Pos { x: int = 0 }', 'int', 'support.type.primitive');
expect('system name', 'system Move phase Update {', 'Move', 'entity.name.function.system'); expect('model keyword', 'model Hero { Pos, Stats }', 'model', 'storage.type.archetype');
expect('phase name', 'system Move phase Update {', 'Update', 'constant.language.phase'); expect('model name', 'model Hero { Pos, Stats }', 'Hero', 'entity.name.type.archetype');
expect('clause keyword', ' reads [Vel]', 'reads', 'keyword.other.clause'); expect('handler name', 'handler Move phase Update {', 'Move', 'entity.name.function.system');
expect('annotation', 'system Move @deterministic', '@deterministic', 'entity.name.function.decorator'); expect('phase name', 'handler Move phase Update {', 'Update', 'constant.language.phase');
expect('builtin call', ' clear(0x000000)', 'clear', 'support.function.builtin'); expect('clause keyword', 'handler Move phase Update {', 'phase', 'keyword.other.clause');
expect('intrinsic call', ' mem_alloc(64)', 'mem_alloc', 'support.function.intrinsic'); expect('annotation', '@Queries(Pos, Vel) handler Move phase Update {', '@Queries', 'entity.name.function.decorator');
expect('builtin call', ' print(1)', 'print', 'support.function.builtin');
expect('control keyword', ' if a == 1 { }', 'if', 'keyword.control'); expect('control keyword', ' if a == 1 { }', 'if', 'keyword.control');
expect('become keyword', ' become Playing', 'become', 'keyword.control');
expect('member access', ' p.x = 1', 'x', 'variable.other.property'); expect('member access', ' p.x = 1', 'x', 'variable.other.property');
expect('query tag filter', 'for (p) in query [Pos, {Player}] {', 'Player', 'entity.name.type.tag'); expect('query tag filter', 'for (p) in query [Pos, {Player}] {', 'Player', 'entity.name.type.tag');
expect('query component', 'for (p) in query [Pos, {Player}] {', 'Pos', 'entity.name.type.component'); expect('query property', 'for (p) in query [Pos, {Player}] {', 'Pos', 'entity.name.type.component');
expect('widget type', ' button id=Go text="go"', 'button', 'support.class.widget'); expect('widget type', ' button id: Go text: "go"', 'button', 'support.class.widget');
expect('widget prop', ' button id=Go text="go"', 'id', 'variable.parameter.widget');
expect('user function call', ' my_helper(1)', 'my_helper', 'entity.name.function'); expect('user function call', ' my_helper(1)', 'my_helper', 'entity.name.function');
expect('archetype in spawn', ' spawn Hero {', 'Hero', 'entity.name.type.archetype'); expect('model in spawn', ' spawn Hero {', 'Hero', 'entity.name.type.archetype');
expect('scene in enter', ' enter Battle', 'Battle', 'entity.name.type.scene'); expect('scene declaration', 'scene Battle {', 'Battle', 'entity.name.type');
expect('prefab declaration', 'prefab Orc {', 'Orc', 'entity.name.type');
expect('event declaration', 'event Hurt { amount: int = 0 }', 'Hurt', 'entity.name.type');
// A whole file must tokenise without the engine bailing out. // A whole file must tokenise without the engine bailing out.
const sample = fs.readFileSync(path.join(ROOT, 'examples', 'games', 'menu.ludic'), 'utf8'); const sample = fs.readFileSync(path.join(ROOT, 'examples', 'games', 'menu.ludic'), 'utf8');
@ -140,7 +145,7 @@ async function main() {
// Drive the injection directly: feed it the fence, then the body, and check // Drive the injection directly: feed it the fence, then the body, and check
// the body is handed to source.ludic. // the body is handed to source.ludic.
const lines = ['```ludic', 'component Pos { x: int = 0 }', '```']; const lines = ['```ludic', 'property Pos { x: int = 0 }', '```'];
let mdState = vsctm.INITIAL; let mdState = vsctm.INITIAL;
const bodyScopes = []; const bodyScopes = [];
lines.forEach((line, i) => { lines.forEach((line, i) => {

View file

@ -56,8 +56,8 @@ function cmd_app() -> int {
if (a == "--headless") { mode = 2 } if (a == "--headless") { mode = 2 }
else if (a == "--windowed") { mode = 1 } else if (a == "--windowed") { mode = 1 }
else if (a == "--save-temps") { save = true } else if (a == "--save-temps") { save = true }
else if (a[0] != 45) { src = a } # 45 = '-', so skip other flags else if (a[0] != '-') { src = a } # 45 = '-', so skip other flags
ai = ai + 1 ai += 1
} }
if (src == null) { err("usage: x app <file.ludic> [--headless] [--save-temps]\n"); return 1 } if (src == null) { err("usage: x app <file.ludic> [--headless] [--save-temps]\n"); return 1 }
let name = capture_line(`basename {src} .ludic`) let name = capture_line(`basename {src} .ludic`)

View file

@ -12,7 +12,7 @@
# ---- small string helpers --------------------------------------------------- # ---- small string helpers ---------------------------------------------------
# length of a NUL-terminated buffer # length of a NUL-terminated buffer
function slen(s: pointer) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n } function slen(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n }
# a fresh NUL-terminated copy of s[start .. end) (end exclusive) # a fresh NUL-terminated copy of s[start .. end) (end exclusive)
function sslice(s: pointer, start: int, end: int) -> pointer { function sslice(s: pointer, start: int, end: int) -> pointer {
@ -20,7 +20,7 @@ function sslice(s: pointer, start: int, end: int) -> pointer {
let n = end - start let n = end - start
let b = bytes(n + 1) let b = bytes(n + 1)
var i = 0 var i = 0
while i < n { b[i] = s[start + i]; i = i + 1 } while i < n { b[i] = s[start + i]; i += 1 }
b[n] = 0 b[n] = 0
return b return b
} }
@ -33,11 +33,11 @@ function s_index(hay: pointer, needle: pointer, from: int) -> int {
var i = from var i = from
while i + nn <= hn { while i + nn <= hn {
var j = 0 var j = 0
while j < nn and hay[i + j] == needle[j] { j = j + 1 } while j < nn and hay[i + j] == needle[j] { j += 1 }
if j == nn { return i } if j == nn { return i }
i = i + 1 i += 1
} }
return 0 - 1 return -1
} }
function s_contains(hay: pointer, needle: pointer) -> bool { return s_index(hay, needle, 0) >= 0 } function s_contains(hay: pointer, needle: pointer) -> bool { return s_index(hay, needle, 0) >= 0 }
@ -46,7 +46,7 @@ function s_contains(hay: pointer, needle: pointer) -> bool { return s_index(hay,
function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool { function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool {
let nn = slen(needle) let nn = slen(needle)
var i = 0 var i = 0
while i < nn { if hay[at + i] != needle[i] { return false }; i = i + 1 } while i < nn { if hay[at + i] != needle[i] { return false }; i += 1 }
return true return true
} }
@ -54,17 +54,17 @@ function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool {
function s_starts(s: pointer, pre: pointer) -> bool { function s_starts(s: pointer, pre: pointer) -> bool {
let pn = slen(pre) let pn = slen(pre)
var i = 0 var i = 0
while i < pn { if s[i] != pre[i] { return false }; i = i + 1 } while i < pn { if s[i] != pre[i] { return false }; i += 1 }
return true return true
} }
# is byte c an ASCII space/tab? # is byte c an ASCII space/tab?
function is_ws(c: int) -> bool { return c == 32 or c == 9 } function is_ws(c: int) -> bool { return c == ' ' or c == '\t' }
# the substring from `start` up to the next '\n' (or end) # the substring from `start` up to the next '\n' (or end)
function line_at(s: pointer, start: int) -> pointer { function line_at(s: pointer, start: int) -> pointer {
var e = start var e = start
while s[e] != 0 and s[e] != 10 { e = e + 1 } while s[e] != 0 and s[e] != '\n' { e += 1 }
return sslice(s, start, e) return sslice(s, start, e)
} }
@ -72,9 +72,9 @@ function line_at(s: pointer, start: int) -> pointer {
function s_trim(s: pointer) -> pointer { function s_trim(s: pointer) -> pointer {
let n = slen(s) let n = slen(s)
var a = 0 var a = 0
while a < n and (is_ws(s[a]) or s[a] == 10 or s[a] == 13) { a = a + 1 } while a < n and (is_ws(s[a]) or s[a] == '\n' or s[a] == '\r') { a += 1 }
var b = n var b = n
while b > a and (is_ws(s[b - 1]) or s[b - 1] == 10 or s[b - 1] == 13) { b = b - 1 } while b > a and (is_ws(s[b - 1]) or s[b - 1] == '\n' or s[b - 1] == '\r') { b -= 1 }
return sslice(s, a, b) return sslice(s, a, b)
} }
@ -101,11 +101,11 @@ function classify_fence(body: pointer) -> pointer {
while i < n { while i < n {
let ln = line_at(body, i) let ln = line_at(body, i)
let t = s_trim(ln) let t = s_trim(ln)
if slen(t) > 0 and t[0] != 35 { # non-empty, not a '#' comment if slen(t) > 0 and t[0] != '#' { # non-empty, not a '#' comment
# head = first token, split on '(' or whitespace # head = first token, split on '(' or whitespace
var e = 0 var e = 0
let tn = slen(t) let tn = slen(t)
while e < tn and t[e] != 40 and not is_ws(t[e]) { e = e + 1 } while e < tn and t[e] != '(' and not is_ws(t[e]) { e += 1 }
let head = sslice(t, 0, e) let head = sslice(t, 0, e)
if head == "program" { return "whole" } if head == "program" { return "whole" }
if is_decl_head(head) { return "decls" } if is_decl_head(head) { return "decls" }
@ -118,8 +118,8 @@ function classify_fence(body: pointer) -> pointer {
# does the wrapped source at `path` parse? (ludicc --fmt = lex+parse gate) # does the wrapped source at `path` parse? (ludicc --fmt = lex+parse gate)
function fence_parses(src: pointer) -> bool { function fence_parses(src: pointer) -> bool {
write_file("/tmp/x_docchk.ludic", src) write_file(`{tmp_dir()}/docchk.ludic`, src)
return shq("bin/ludicc /tmp/x_docchk.ludic --fmt > /dev/null 2>&1") return shq(`bin/ludicc {tmp_dir()}/docchk.ludic --fmt > /dev/null 2>&1`)
} }
# try the candidate framings for a fence; true if any parses # try the candidate framings for a fence; true if any parses
@ -149,21 +149,21 @@ function check_docs_file(path: pointer) -> void {
let open = s_index(t, "```ludic\n", i) let open = s_index(t, "```ludic\n", i)
if open < 0 { break } if open < 0 { break }
# require the fence to sit at a line start # require the fence to sit at a line start
if open != 0 and t[open - 1] != 10 { i = open + 1; continue } if open != 0 and t[open - 1] != '\n' { i = open + 1; continue }
let bstart = open + 9 # past "```ludic\n" let bstart = open + 9 # past "```ludic\n"
let close = s_index(t, "\n```", bstart) let close = s_index(t, "\n```", bstart)
if close < 0 { break } if close < 0 { break }
let body = sslice(t, bstart, close + 1) # include trailing newline let body = sslice(t, bstart, close + 1) # include trailing newline
let line = 1 + count_nl(t, open) let line = 1 + count_nl(t, open)
i = close + 4 i = close + 4
if s_contains(body, "# doc-check: skip") { DC_SKIP = DC_SKIP + 1; continue } if s_contains(body, "# doc-check: skip") { DC_SKIP += 1; continue }
let expect_err = s_contains(body, "# doc-check: expect-error") let expect_err = s_contains(body, "# doc-check: expect-error")
let kind = classify_fence(body) let kind = classify_fence(body)
let parsed = fence_body_parses(body, kind) let parsed = fence_body_parses(body, kind)
if parsed == (not expect_err) { if parsed == (not expect_err) {
DC_OK = DC_OK + 1 DC_OK += 1
} else { } else {
DC_BAD = DC_BAD + 1 DC_BAD += 1
if expect_err { DC_FAILS = DC_FAILS + " " + path + ":" + string(line) + " expected to be rejected\n" } if expect_err { DC_FAILS = DC_FAILS + " " + path + ":" + string(line) + " expected to be rejected\n" }
else { DC_FAILS = DC_FAILS + " " + path + ":" + string(line) + " expected to parse\n" } else { DC_FAILS = DC_FAILS + " " + path + ":" + string(line) + " expected to parse\n" }
} }
@ -173,7 +173,7 @@ function check_docs_file(path: pointer) -> void {
# number of '\n' in s[0 .. upto) # number of '\n' in s[0 .. upto)
function count_nl(s: pointer, upto: int) -> int { function count_nl(s: pointer, upto: int) -> int {
var n = 0; var i = 0 var n = 0; var i = 0
while i < upto { if s[i] == 10 { n = n + 1 }; i = i + 1 } while i < upto { if s[i] == '\n' { n += 1 }; i += 1 }
return n return n
} }
@ -205,12 +205,12 @@ function cmd_check_docs() -> int {
# asserts they agree. Replaces the former tools/docgen/check-impl.py. # asserts they agree. Replaces the former tools/docgen/check-impl.py.
# a double-quote byte as a string (the lexer has no \" escape we rely on) # a double-quote byte as a string (the lexer has no \" escape we rely on)
function dq() -> pointer { let b = bytes(2); b[0] = 34; b[1] = 0; return b } function dq() -> pointer { let b = bytes(2); b[0] = '"'; b[1] = 0; return b }
# set-style string list # set-style string list
function set_has(l: []pointer, s: pointer) -> bool { function set_has(l: []pointer, s: pointer) -> bool {
var i = 0 var i = 0
while i < len(l) { if l[i] == s { return true }; i = i + 1 } while i < len(l) { if l[i] == s { return true }; i += 1 }
return false return false
} }
function set_add(l: []pointer, s: pointer) -> void { if not set_has(l, s) { push(l, s) } } function set_add(l: []pointer, s: pointer) -> void { if not set_has(l, s) { push(l, s) } }
@ -218,7 +218,7 @@ function set_add(l: []pointer, s: pointer) -> void { if not set_has(l, s) { push
# the body of `function <name>(` up to the next top-level `function ` (or EOF) # the body of `function <name>(` up to the next top-level `function ` (or EOF)
function fn_body(src: pointer, name: pointer) -> pointer { function fn_body(src: pointer, name: pointer) -> pointer {
let needle = "function " + name + "(" let needle = "function " + name + "("
var p = 0 - 1 var p = -1
if s_starts(src, needle) { p = 0 } if s_starts(src, needle) { p = 0 }
else { else {
let q = s_index(src, "\n" + needle, 0) let q = s_index(src, "\n" + needle, 0)
@ -239,7 +239,7 @@ function collect_after(src: pointer, marker: pointer) -> []pointer {
let p = s_index(src, marker, i) let p = s_index(src, marker, i)
if p < 0 { break } if p < 0 { break }
var e = p + mn var e = p + mn
while src[e] != 0 and src[e] != 34 { e = e + 1 } while src[e] != 0 and src[e] != '"' { e += 1 }
push(out, sslice(src, p + mn, e)) push(out, sslice(src, p + mn, e))
i = e + 1 i = e + 1
} }
@ -256,20 +256,20 @@ function table_set(h: pointer, name: pointer) -> []pointer {
if b < 0 or e < 0 { return out } if b < 0 or e < 0 { return out }
var i = b var i = b
while i < e { while i < e {
if h[i] == 34 { if h[i] == '"' {
var j = i + 1 var j = i + 1
while j < e and h[j] != 34 { j = j + 1 } while j < e and h[j] != '"' { j += 1 }
set_add(out, sslice(h, i + 1, j)) set_add(out, sslice(h, i + 1, j))
i = j + 1 i = j + 1
} else { i = i + 1 } } else { i += 1 }
} }
return out return out
} }
# concatenate every selfhost/*.ludic into one buffer (via cat, then read) # concatenate every selfhost/*.ludic into one buffer (via cat, then read)
function read_all_selfhost() -> pointer { function read_all_selfhost() -> pointer {
run("find selfhost -name '*.ludic' | sort | xargs cat > /tmp/x_allsh.txt 2>/dev/null") run(`find selfhost -name '*.ludic' | sort | xargs cat > {tmp_dir()}/allsh.txt 2>/dev/null`)
let s = read_file("/tmp/x_allsh.txt") let s = read_file(`{tmp_dir()}/allsh.txt`)
if s == null { return "" } if s == null { return "" }
return s return s
} }
@ -285,7 +285,7 @@ function collect_impl_pairs(allsrc: pointer, pairs: []pointer) -> void {
let p = s_index(body, mns, i) let p = s_index(body, mns, i)
if p < 0 { break } if p < 0 { break }
var ne = p + mnslen var ne = p + mnslen
while body[ne] != 0 and body[ne] != 34 { ne = ne + 1 } while body[ne] != 0 and body[ne] != '"' { ne += 1 }
let nsname = sslice(body, p + mnslen, ne) let nsname = sslice(body, p + mnslen, ne)
let nxt = s_index(body, mns, ne) let nxt = s_index(body, mns, ne)
var chunkEnd = slen(body) var chunkEnd = slen(body)
@ -296,13 +296,13 @@ function collect_impl_pairs(allsrc: pointer, pairs: []pointer) -> void {
let dp = s_index(chunk, "is_", 0) let dp = s_index(chunk, "is_", 0)
if dp >= 0 { if dp >= 0 {
var pe = dp var pe = dp
while chunk[pe] != 0 and chunk[pe] != 40 { pe = pe + 1 } while chunk[pe] != 0 and chunk[pe] != '(' { pe += 1 }
let predname = sslice(chunk, dp, pe) let predname = sslice(chunk, dp, pe)
methods = collect_after(fn_body(allsrc, predname), mmeth) methods = collect_after(fn_body(allsrc, predname), mmeth)
} }
} }
var k = 0 var k = 0
while k < len(methods) { set_add(pairs, nsname + "." + methods[k]); k = k + 1 } while k < len(methods) { set_add(pairs, nsname + "." + methods[k]); k += 1 }
if nxt < 0 { break } if nxt < 0 { break }
i = nxt i = nxt
} }
@ -340,9 +340,9 @@ function collect_docs(tokens: []pointer, docpairs: []pointer) -> void {
var a = 0 var a = 0
let vn = slen(val) let vn = slen(val)
while a < vn { while a < vn {
while a < vn and val[a] == 32 { a = a + 1 } while a < vn and val[a] == ' ' { a += 1 }
var e = a var e = a
while e < vn and val[e] != 32 { e = e + 1 } while e < vn and val[e] != ' ' { e += 1 }
if e > a { set_add(tokens, sslice(val, a, e)) } if e > a { set_add(tokens, sslice(val, a, e)) }
a = e a = e
} }
@ -359,7 +359,7 @@ function collect_docs(tokens: []pointer, docpairs: []pointer) -> void {
var CI_PROB: pointer = "" var CI_PROB: pointer = ""
var CI_NPROB: int = 0 var CI_NPROB: int = 0
function ci_problem(msg: pointer) -> void { CI_PROB = CI_PROB + " - " + msg + "\n"; CI_NPROB = CI_NPROB + 1 } function ci_problem(msg: pointer) -> void { CI_PROB = CI_PROB + " - " + msg + "\n"; CI_NPROB += 1 }
# usage: x check-impl # usage: x check-impl
function cmd_check_impl() -> int { function cmd_check_impl() -> int {
@ -377,13 +377,13 @@ function cmd_check_impl() -> int {
let pair = impl[i] let pair = impl[i]
if not set_has(docpairs, pair) { ci_problem("undocumented " + pair + " — add a docs/language page (tokens: " + pair + ")") } if not set_has(docpairs, pair) { ci_problem("undocumented " + pair + " — add a docs/language page (tokens: " + pair + ")") }
else { if not set_has(tokens, pair) { ci_problem(pair + " documented but its page lacks that tokens entry") } } else { if not set_has(tokens, pair) { ci_problem(pair + " documented but its page lacks that tokens entry") } }
i = i + 1 i += 1
} }
# 2) every documented ns-method corresponds to real dispatch # 2) every documented ns-method corresponds to real dispatch
i = 0 i = 0
while i < len(docpairs) { while i < len(docpairs) {
if not set_has(impl, docpairs[i]) { ci_problem("stale doc: " + docpairs[i] + " is not dispatched by the compiler") } if not set_has(impl, docpairs[i]) { ci_problem("stale doc: " + docpairs[i] + " is not dispatched by the compiler") }
i = i + 1 i += 1
} }
# 3) every implemented keyword / type / phase is documented # 3) every implemented keyword / type / phase is documented
let h = read_file("tools/ludic-tools/ludic_syntax.h") let h = read_file("tools/ludic-tools/ludic_syntax.h")
@ -410,14 +410,14 @@ function cmd_check_impl() -> int {
# add every element of `src` not in `deny` to `dst` (set semantics) # add every element of `src` not in `deny` to `dst` (set semantics)
function add_all_except(dst: []pointer, src: []pointer, deny: []pointer) -> void { function add_all_except(dst: []pointer, src: []pointer, deny: []pointer) -> void {
var i = 0 var i = 0
while i < len(src) { if not set_has(deny, src[i]) { set_add(dst, src[i]) }; i = i + 1 } while i < len(src) { if not set_has(deny, src[i]) { set_add(dst, src[i]) }; i += 1 }
} }
# every name must appear in the token set, else a problem # every name must appear in the token set, else a problem
function check_all_documented(names: []pointer, tokens: []pointer, label: pointer) -> void { function check_all_documented(names: []pointer, tokens: []pointer, label: pointer) -> void {
var i = 0 var i = 0
while i < len(names) { while i < len(names) {
if not set_has(tokens, names[i]) { ci_problem("undocumented " + label + ": " + names[i] + " (no docs/language page lists it in tokens)") } if not set_has(tokens, names[i]) { ci_problem("undocumented " + label + ": " + names[i] + " (no docs/language page lists it in tokens)") }
i = i + 1 i += 1
} }
} }
@ -435,7 +435,7 @@ function all_lower(s: pointer) -> bool {
let n = slen(s) let n = slen(s)
if n == 0 { return false } if n == 0 { return false }
var i = 0 var i = 0
while i < n { if s[i] < 97 or s[i] > 122 { return false }; i = i + 1 } while i < n { if s[i] < 'a' or s[i] > 'z' { return false }; i += 1 }
return true return true
} }
@ -446,7 +446,7 @@ function split_pipe(s: pointer) -> []pointer {
var a = 0 var a = 0
while a <= n { while a <= n {
var e = a var e = a
while e < n and s[e] != 124 { e = e + 1 } while e < n and s[e] != '|' { e += 1 }
set_add(out, sslice(s, a, e)) set_add(out, sslice(s, a, e))
a = e + 1 a = e + 1
} }
@ -463,16 +463,16 @@ function c_table_names(h: pointer, name: pointer) -> []pointer {
if b < 0 or e < 0 { return out } if b < 0 or e < 0 { return out }
var i = b + 1 var i = b + 1
while i < e { while i < e {
if h[i] == 123 { # '{' opens a row if h[i] == '{' { # '{' opens a row
var j = i + 1 var j = i + 1
while j < e and (h[j] == 32 or h[j] == 9 or h[j] == 10) { j = j + 1 } while j < e and (h[j] == ' ' or h[j] == '\t' or h[j] == '\n') { j += 1 }
if h[j] == 34 { if h[j] == '"' {
var k = j + 1 var k = j + 1
while k < e and h[k] != 34 { k = k + 1 } while k < e and h[k] != '"' { k += 1 }
set_add(out, sslice(h, j + 1, k)) set_add(out, sslice(h, j + 1, k))
} }
} }
i = i + 1 i += 1
} }
return out return out
} }
@ -488,22 +488,22 @@ function kotlin_set(text: pointer, name: pointer) -> []pointer {
let region_start = i let region_start = i
let tn = slen(text) let tn = slen(text)
while i < tn and depth > 0 { while i < tn and depth > 0 {
if text[i] == 40 { depth = depth + 1 } if text[i] == '(' { depth += 1 }
if text[i] == 41 { depth = depth - 1 } if text[i] == ')' { depth -= 1 }
if depth == 0 { break } if depth == 0 { break }
i = i + 1 i += 1
} }
# collect quoted strings in [region_start, i), skipping //-comments # collect quoted strings in [region_start, i), skipping //-comments
var k = region_start var k = region_start
while k < i { while k < i {
if text[k] == 47 and text[k + 1] == 47 { # '//' if text[k] == '/' and text[k + 1] == '/' { # '//'
while k < i and text[k] != 10 { k = k + 1 } while k < i and text[k] != '\n' { k += 1 }
} else { if text[k] == 34 { } else { if text[k] == '"' {
var e = k + 1 var e = k + 1
while e < i and text[e] != 34 { e = e + 1 } while e < i and text[e] != '"' { e += 1 }
set_add(out, sslice(text, k + 1, e)) set_add(out, sslice(text, k + 1, e))
k = e + 1 k = e + 1
} else { k = k + 1 } } } else { k += 1 } }
} }
return out return out
} }
@ -517,13 +517,13 @@ function grammar_alt(root: JVal, node: pointer, marker: pointer) -> []pointer {
while i < len(pats.kids) { while i < len(pats.kids) {
let m = j_get(pats.kids[i], "match") let m = j_get(pats.kids[i], "match")
if m.t == JV_STR and s_contains(m.s, marker) { if m.t == JV_STR and s_contains(m.s, marker) {
let op = s_index(m.s, bx3(92, 98, 40), 0) # \b( let op = s_index(m.s, bx3('\\', 'b', '('), 0) # \b(
if op >= 0 { if op >= 0 {
let cl = s_index(m.s, bx3(41, 92, 98), op) # )\b let cl = s_index(m.s, bx3(')', '\\', 'b'), op) # )\b
if cl >= 0 { return split_pipe(sslice(m.s, op + 3, cl)) } if cl >= 0 { return split_pipe(sslice(m.s, op + 3, cl)) }
} }
} }
i = i + 1 i += 1
} }
return out return out
} }
@ -543,28 +543,28 @@ function add_parser_kw(out: []pointer, path: pointer) -> void {
} }
function add_lower(out: []pointer, src: []pointer) -> void { function add_lower(out: []pointer, src: []pointer) -> void {
var i = 0 var i = 0
while i < len(src) { if all_lower(src[i]) { set_add(out, src[i]) }; i = i + 1 } while i < len(src) { if all_lower(src[i]) { set_add(out, src[i]) }; i += 1 }
} }
var CV_PROB: pointer = "" var CV_PROB: pointer = ""
var CV_N: int = 0 var CV_N: int = 0
function cv_problem(msg: pointer) -> void { CV_PROB = CV_PROB + " - " + msg + "\n"; CV_N = CV_N + 1 } function cv_problem(msg: pointer) -> void { CV_PROB = CV_PROB + " - " + msg + "\n"; CV_N += 1 }
# report names in `ref` missing from `other`, and names in `other` not in `ref` # report names in `ref` missing from `other`, and names in `other` not in `ref`
function cmp_sets(label: pointer, ref: []pointer, other: []pointer, other_label: pointer) -> void { function cmp_sets(label: pointer, ref: []pointer, other: []pointer, other_label: pointer) -> void {
var i = 0 var i = 0
while i < len(ref) { if not set_has(other, ref[i]) { cv_problem(other_label + " is missing " + label + ": " + ref[i]) }; i = i + 1 } while i < len(ref) { if not set_has(other, ref[i]) { cv_problem(other_label + " is missing " + label + ": " + ref[i]) }; i += 1 }
i = 0 i = 0
while i < len(other) { if not set_has(ref, other[i]) { cv_problem(other_label + " has unknown " + label + ": " + other[i]) }; i = i + 1 } while i < len(other) { if not set_has(ref, other[i]) { cv_problem(other_label + " has unknown " + label + ": " + other[i]) }; i += 1 }
} }
# union of two sets # union of two sets
function set_union(a: []pointer, b: []pointer) -> []pointer { function set_union(a: []pointer, b: []pointer) -> []pointer {
let out = new []pointer let out = new []pointer
var i = 0 var i = 0
while i < len(a) { set_add(out, a[i]); i = i + 1 } while i < len(a) { set_add(out, a[i]); i += 1 }
i = 0 i = 0
while i < len(b) { set_add(out, b[i]); i = i + 1 } while i < len(b) { set_add(out, b[i]); i += 1 }
return out return out
} }
@ -614,7 +614,7 @@ function cmd_check_vocab() -> int {
cmp_unparsed("clause keywords", h_clause, pkw, h_reserved) cmp_unparsed("clause keywords", h_clause, pkw, h_reserved)
# reserved words the parser now accepts should be promoted # reserved words the parser now accepts should be promoted
var i = 0 var i = 0
while i < len(h_reserved) { if set_has(pkw, h_reserved[i]) { cv_problem("LUDIC_KW_RESERVED lists a keyword the parser now accepts — promote it: " + h_reserved[i]) }; i = i + 1 } while i < len(h_reserved) { if set_has(pkw, h_reserved[i]) { cv_problem("LUDIC_KW_RESERVED lists a keyword the parser now accepts — promote it: " + h_reserved[i]) }; i += 1 }
} }
if CV_N > 0 { if CV_N > 0 {
@ -632,7 +632,7 @@ function cmp_unparsed(label: pointer, kws: []pointer, pkw: []pointer, reserved:
if not set_has(pkw, kws[i]) { if not set_has(pkw, kws[i]) {
if not set_has(reserved, kws[i]) { cv_problem("ludic_syntax.h lists a " + label + " the selfhost parser never dispatches on: " + kws[i]) } if not set_has(reserved, kws[i]) { cv_problem("ludic_syntax.h lists a " + label + " the selfhost parser never dispatches on: " + kws[i]) }
} }
i = i + 1 i += 1
} }
} }
@ -648,14 +648,14 @@ function xml_valid(text: pointer) -> bool {
var sp = 0 var sp = 0
var i = 0 var i = 0
while i < n { while i < n {
if text[i] != 60 { i = i + 1; continue } # seek '<' if text[i] != '<' { i += 1; continue } # seek '<'
if text[i + 1] == 63 { # <? ... ?> if text[i + 1] == '?' { # <? ... ?>
let e = s_index(text, "?>", i) let e = s_index(text, "?>", i)
if e < 0 { return false } if e < 0 { return false }
i = e + 2; continue i = e + 2; continue
} }
if text[i + 1] == 33 { # <! comment / cdata / doctype if text[i + 1] == '!' { # <! comment / cdata / doctype
if text[i + 2] == 45 and text[i + 3] == 45 { # <!-- ... --> if text[i + 2] == '-' and text[i + 3] == '-' { # <!-- ... -->
let e = s_index(text, "-->", i) let e = s_index(text, "-->", i)
if e < 0 { return false } if e < 0 { return false }
i = e + 3; continue i = e + 3; continue
@ -669,33 +669,33 @@ function xml_valid(text: pointer) -> bool {
if e < 0 { return false } if e < 0 { return false }
i = e + 1; continue i = e + 1; continue
} }
if text[i + 1] == 47 { # closing </name> if text[i + 1] == '/' { # closing </name>
var j = i + 2 var j = i + 2
while j < n and text[j] != 62 and text[j] != 32 and text[j] != 9 and text[j] != 10 { j = j + 1 } while j < n and text[j] != '>' and text[j] != ' ' and text[j] != '\t' and text[j] != '\n' { j += 1 }
let name = sslice(text, i + 2, j) let name = sslice(text, i + 2, j)
let e = s_index(text, ">", j) let e = s_index(text, ">", j)
if e < 0 { return false } if e < 0 { return false }
if sp == 0 { return false } if sp == 0 { return false }
if stack[sp - 1] != name { return false } if stack[sp - 1] != name { return false }
sp = sp - 1 sp -= 1
i = e + 1; continue i = e + 1; continue
} }
# opening tag: read the name # opening tag: read the name
var j = i + 1 var j = i + 1
while j < n and text[j] != 62 and text[j] != 32 and text[j] != 9 and text[j] != 10 and text[j] != 47 { j = j + 1 } while j < n and text[j] != '>' and text[j] != ' ' and text[j] != '\t' and text[j] != '\n' and text[j] != '/' { j += 1 }
let name = sslice(text, i + 1, j) let name = sslice(text, i + 1, j)
if slen(name) == 0 { return false } if slen(name) == 0 { return false }
# advance to '>', skipping quoted attribute values # advance to '>', skipping quoted attribute values
var k = j var k = j
while k < n and text[k] != 62 { while k < n and text[k] != '>' {
if text[k] == 34 { k = k + 1; while k < n and text[k] != 34 { k = k + 1 } } if text[k] == '"' { k += 1; while k < n and text[k] != '"' { k += 1 } }
else { if text[k] == 39 { k = k + 1; while k < n and text[k] != 39 { k = k + 1 } } } else { if text[k] == '\'' { k += 1; while k < n and text[k] != '\'' { k += 1 } } }
k = k + 1 k += 1
} }
if k >= n { return false } if k >= n { return false }
if text[k - 1] != 47 { # not self-closing -> push if text[k - 1] != '/' { # not self-closing -> push
if sp < len(stack) { stack[sp] = name } else { push(stack, name) } if sp < len(stack) { stack[sp] = name } else { push(stack, name) }
sp = sp + 1 sp += 1
} }
i = k + 1 i = k + 1
} }

View file

@ -26,10 +26,10 @@ function sb_new() -> Sb {
function sb_grow(s: Sb, need: int) -> void { function sb_grow(s: Sb, need: int) -> void {
if s.len + need + 1 <= s.cap { return } if s.len + need + 1 <= s.cap { return }
var nc = s.cap * 2 var nc = s.cap * 2
while nc < s.len + need + 1 { nc = nc * 2 } while nc < s.len + need + 1 { nc *= 2 }
let nd = bytes(nc) let nd = bytes(nc)
var i = 0 var i = 0
while i < s.len { nd[i] = s.data[i]; i = i + 1 } while i < s.len { nd[i] = s.data[i]; i += 1 }
s.data = nd s.data = nd
s.cap = nc s.cap = nc
} }
@ -37,12 +37,12 @@ function sb_puts(s: Sb, t: pointer) -> void {
let n = slen(t) let n = slen(t)
sb_grow(s, n) sb_grow(s, n)
var i = 0 var i = 0
while i < n { s.data[s.len] = t[i]; s.len = s.len + 1; i = i + 1 } while i < n { s.data[s.len] = t[i]; s.len += 1; i += 1 }
} }
function sb_putc(s: Sb, c: int) -> void { function sb_putc(s: Sb, c: int) -> void {
sb_grow(s, 1) sb_grow(s, 1)
s.data[s.len] = c s.data[s.len] = c
s.len = s.len + 1 s.len += 1
} }
function sb_str(s: Sb) -> pointer { function sb_str(s: Sb) -> pointer {
s.data[s.len] = 0 s.data[s.len] = 0
@ -59,13 +59,13 @@ function esc_q(s: pointer, quote: bool) -> pointer {
var i = 0 var i = 0
while i < n { while i < n {
let c = s[i] let c = s[i]
if c == 38 { sb_puts(b, "&amp;") } if c == '&' { sb_puts(b, "&amp;") }
else if c == 60 { sb_puts(b, "&lt;") } else if c == '<' { sb_puts(b, "&lt;") }
else if c == 62 { sb_puts(b, "&gt;") } else if c == '>' { sb_puts(b, "&gt;") }
else if quote and c == 34 { sb_puts(b, "&quot;") } else if quote and c == '"' { sb_puts(b, "&quot;") }
else if quote and c == 39 { sb_puts(b, "&#x27;") } else if quote and c == '\'' { sb_puts(b, "&#x27;") }
else { sb_putc(b, c) } else { sb_putc(b, c) }
i = i + 1 i += 1
} }
return sb_str(b) return sb_str(b)
} }
@ -75,16 +75,16 @@ function hescattr(s: pointer) -> pointer { return esc_q(s, true) }
# ---- generic byte/character predicates -------------------------------------- # ---- generic byte/character predicates --------------------------------------
# Python \s = [ \t\n\r\f\v] # Python \s = [ \t\n\r\f\v]
function is_space_all(c: int) -> bool { function is_space_all(c: int) -> bool {
return c == 32 or c == 9 or c == 10 or c == 13 or c == 12 or c == 11 return c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == 12 or c == 11
} }
# Python \w = [A-Za-z0-9_] # Python \w = [A-Za-z0-9_]
function is_word(c: int) -> bool { function is_word(c: int) -> bool {
return (c >= 48 and c <= 57) or (c >= 65 and c <= 90) or (c >= 97 and c <= 122) or c == 95 return (c >= '0' and c <= '9') or (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or c == '_'
} }
# strip only leading '\n' (Python str.lstrip("\n")) # strip only leading '\n' (Python str.lstrip("\n"))
function lstrip_nl(s: pointer) -> pointer { function lstrip_nl(s: pointer) -> pointer {
var i = 0 var i = 0
while s[i] == 10 { i = i + 1 } while s[i] == '\n' { i += 1 }
return sslice(s, i, slen(s)) return sslice(s, i, slen(s))
} }
# lowercase ASCII A-Z # lowercase ASCII A-Z
@ -94,9 +94,9 @@ function lower_ascii(s: pointer) -> pointer {
var i = 0 var i = 0
while i < n { while i < n {
var c = s[i] var c = s[i]
if c >= 65 and c <= 90 { c = c + 32 } if c >= 'A' and c <= 'Z' { c += 32 }
b[i] = c b[i] = c
i = i + 1 i += 1
} }
b[n] = 0 b[n] = 0
return b return b
@ -109,14 +109,14 @@ function title_case(s: pointer) -> pointer {
var prev_alpha = false var prev_alpha = false
while i < n { while i < n {
var c = s[i] var c = s[i]
let al = (c >= 65 and c <= 90) or (c >= 97 and c <= 122) let al = (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z')
if al { if al {
if prev_alpha { if c >= 65 and c <= 90 { c = c + 32 } } if prev_alpha { if c >= 'A' and c <= 'Z' { c += 32 } }
else { if c >= 97 and c <= 122 { c = c - 32 } } else { if c >= 'a' and c <= 'z' { c -= 32 } }
} }
b[i] = c b[i] = c
prev_alpha = al prev_alpha = al
i = i + 1 i += 1
} }
b[n] = 0 b[n] = 0
return b return b
@ -128,7 +128,7 @@ function str_cmp(a: pointer, b: pointer) -> int {
let ca = a[i] & 255 let ca = a[i] & 255
let cb = b[i] & 255 let cb = b[i] & 255
if ca != cb { return ca - cb } if ca != cb { return ca - cb }
i = i + 1 i += 1
} }
return (a[i] & 255) - (b[i] & 255) return (a[i] & 255) - (b[i] & 255)
} }
@ -146,9 +146,9 @@ function split_ws(s: pointer) -> []pointer {
let n = slen(s) let n = slen(s)
var i = 0 var i = 0
while i < n { while i < n {
while i < n and is_space_all(s[i]) { i = i + 1 } while i < n and is_space_all(s[i]) { i += 1 }
var e = i var e = i
while e < n and not is_space_all(s[e]) { e = e + 1 } while e < n and not is_space_all(s[e]) { e += 1 }
if e > i { push(out, sslice(s, i, e)) } if e > i { push(out, sslice(s, i, e)) }
i = e i = e
} }
@ -160,9 +160,9 @@ function strs_sort(a: []pointer) -> void {
while i < len(a) { while i < len(a) {
let cur = a[i] let cur = a[i]
var j = i - 1 var j = i - 1
while j >= 0 and str_gt(a[j], cur) { a[j + 1] = a[j]; j = j - 1 } while j >= 0 and str_gt(a[j], cur) { a[j + 1] = a[j]; j -= 1 }
a[j + 1] = cur a[j + 1] = cur
i = i + 1 i += 1
} }
} }
@ -173,22 +173,22 @@ function codeify(s: pointer) -> pointer {
let b = sb_new() let b = sb_new()
var i = 0 var i = 0
while i < n { while i < n {
if s[i] == 96 { if s[i] == '`' {
var j = i + 1 var j = i + 1
while j < n and s[j] != 96 { j = j + 1 } while j < n and s[j] != '`' { j += 1 }
if j < n and j > i + 1 { if j < n and j > i + 1 {
sb_puts(b, "<code>") sb_puts(b, "<code>")
var k = i + 1 var k = i + 1
while k < j { sb_putc(b, s[k]); k = k + 1 } while k < j { sb_putc(b, s[k]); k += 1 }
sb_puts(b, "</code>") sb_puts(b, "</code>")
i = j + 1 i = j + 1
} else { } else {
sb_putc(b, 96) sb_putc(b, '`')
i = i + 1 i += 1
} }
} else { } else {
sb_putc(b, s[i]) sb_putc(b, s[i])
i = i + 1 i += 1
} }
} }
return sb_str(b) return sb_str(b)
@ -204,7 +204,7 @@ function jva() -> JVal { return jval_new(JV_ARR) }
# token collides across kinds (e.g. `exit`/`string`/`fixed`) in sym["tips"]. # token collides across kinds (e.g. `exit`/`string`/`fixed`) in sym["tips"].
function jvput(o: JVal, k: pointer, v: JVal) -> void { function jvput(o: JVal, k: pointer, v: JVal) -> void {
var i = 0 var i = 0
while i < len(o.keys) { if o.keys[i] == k { o.kids[i] = v; return }; i = i + 1 } while i < len(o.keys) { if o.keys[i] == k { o.kids[i] = v; return }; i += 1 }
push(o.keys, k) push(o.keys, k)
push(o.kids, v) push(o.kids, v)
} }
@ -215,7 +215,7 @@ function hex_lower(n: int) -> int {
return 87 + n return 87 + n
} }
function jemit_u(b: Sb, cp: int) -> void { function jemit_u(b: Sb, cp: int) -> void {
sb_putc(b, 92); sb_putc(b, 117) # \u sb_putc(b, '\\'); sb_putc(b, 'u') # \u
sb_putc(b, hex_lower((cp >> 12) & 15)) sb_putc(b, hex_lower((cp >> 12) & 15))
sb_putc(b, hex_lower((cp >> 8) & 15)) sb_putc(b, hex_lower((cp >> 8) & 15))
sb_putc(b, hex_lower((cp >> 4) & 15)) sb_putc(b, hex_lower((cp >> 4) & 15))
@ -228,20 +228,20 @@ function json_escape_into(b: Sb, s: pointer, ensure_ascii: bool) -> void {
var i = 0 var i = 0
while i < n { while i < n {
let c = s[i] & 255 let c = s[i] & 255
if c == 34 { sb_putc(b, 92); sb_putc(b, 34); i = i + 1 } if c == '"' { sb_putc(b, '\\'); sb_putc(b, '"'); i += 1 }
else if c == 92 { sb_putc(b, 92); sb_putc(b, 92); i = i + 1 } else if c == '\\' { sb_putc(b, '\\'); sb_putc(b, '\\'); i += 1 }
else if c == 8 { sb_putc(b, 92); sb_putc(b, 98); i = i + 1 } else if c == 8 { sb_putc(b, '\\'); sb_putc(b, 'b'); i += 1 }
else if c == 9 { sb_putc(b, 92); sb_putc(b, 116); i = i + 1 } else if c == '\t' { sb_putc(b, '\\'); sb_putc(b, 't'); i += 1 }
else if c == 10 { sb_putc(b, 92); sb_putc(b, 110); i = i + 1 } else if c == '\n' { sb_putc(b, '\\'); sb_putc(b, 'n'); i += 1 }
else if c == 12 { sb_putc(b, 92); sb_putc(b, 102); i = i + 1 } else if c == 12 { sb_putc(b, '\\'); sb_putc(b, 'f'); i += 1 }
else if c == 13 { sb_putc(b, 92); sb_putc(b, 114); i = i + 1 } else if c == '\r' { sb_putc(b, '\\'); sb_putc(b, 'r'); i += 1 }
else if c < 32 { else if c < ' ' {
sb_putc(b, 92); sb_putc(b, 117); sb_putc(b, 48); sb_putc(b, 48) sb_putc(b, '\\'); sb_putc(b, 'u'); sb_putc(b, '0'); sb_putc(b, '0')
sb_putc(b, hex_lower((c >> 4) & 15)); sb_putc(b, hex_lower(c & 15)) sb_putc(b, hex_lower((c >> 4) & 15)); sb_putc(b, hex_lower(c & 15))
i = i + 1 i += 1
} }
else if c < 128 { sb_putc(b, c); i = i + 1 } else if c < 128 { sb_putc(b, c); i += 1 }
else if not ensure_ascii { sb_putc(b, c); i = i + 1 } else if not ensure_ascii { sb_putc(b, c); i += 1 }
else { else {
var cp = 0 var cp = 0
var nb = 1 var nb = 1
@ -249,8 +249,8 @@ function json_escape_into(b: Sb, s: pointer, ensure_ascii: bool) -> void {
else if c < 240 { cp = c - 224; nb = 3 } else if c < 240 { cp = c - 224; nb = 3 }
else { cp = c - 240; nb = 4 } else { cp = c - 240; nb = 4 }
var k = 1 var k = 1
while k < nb and i + k < n { cp = cp * 64 + ((s[i + k] & 255) - 128); k = k + 1 } while k < nb and i + k < n { cp = cp * 64 + ((s[i + k] & 255) - 128); k += 1 }
i = i + nb i += nb
if cp <= 65535 { jemit_u(b, cp) } if cp <= 65535 { jemit_u(b, cp) }
else { else {
let c2 = cp - 65536 let c2 = cp - 65536
@ -262,7 +262,7 @@ function json_escape_into(b: Sb, s: pointer, ensure_ascii: bool) -> void {
} }
function json_indent(b: Sb, depth: int) -> void { function json_indent(b: Sb, depth: int) -> void {
var i = 0 var i = 0
while i < depth { sb_putc(b, 32); sb_putc(b, 32); i = i + 1 } while i < depth { sb_putc(b, ' '); sb_putc(b, ' '); i += 1 }
} }
# pretty=true reproduces json.dumps(indent=2); pretty=false the compact default # pretty=true reproduces json.dumps(indent=2); pretty=false the compact default
# (item sep ", ", key sep ": "). ensure_ascii toggles \uXXXX for non-ASCII. # (item sep ", ", key sep ": "). ensure_ascii toggles \uXXXX for non-ASCII.
@ -273,36 +273,36 @@ function jwrite(b: Sb, v: JVal, pretty: bool, depth: int, ea: bool) -> void {
return return
} }
if v.t == JV_NUM { sb_puts(b, string(v.num)); return } if v.t == JV_NUM { sb_puts(b, string(v.num)); return }
if v.t == JV_STR { sb_putc(b, 34); json_escape_into(b, v.s, ea); sb_putc(b, 34); return } if v.t == JV_STR { sb_putc(b, '"'); json_escape_into(b, v.s, ea); sb_putc(b, '"'); return }
if v.t == JV_ARR { if v.t == JV_ARR {
let m = len(v.kids) let m = len(v.kids)
if m == 0 { sb_puts(b, "[]"); return } if m == 0 { sb_puts(b, "[]"); return }
sb_putc(b, 91) sb_putc(b, '[')
var i = 0 var i = 0
while i < m { while i < m {
if pretty { sb_putc(b, 10); json_indent(b, depth + 1) } if pretty { sb_putc(b, '\n'); json_indent(b, depth + 1) }
jwrite(b, v.kids[i], pretty, depth + 1, ea) jwrite(b, v.kids[i], pretty, depth + 1, ea)
if i < m - 1 { sb_putc(b, 44); if not pretty { sb_putc(b, 32) } } if i < m - 1 { sb_putc(b, ','); if not pretty { sb_putc(b, ' ') } }
i = i + 1 i += 1
} }
if pretty { sb_putc(b, 10); json_indent(b, depth) } if pretty { sb_putc(b, '\n'); json_indent(b, depth) }
sb_putc(b, 93) sb_putc(b, ']')
return return
} }
let m = len(v.keys) let m = len(v.keys)
if m == 0 { sb_puts(b, "{}"); return } if m == 0 { sb_puts(b, "{}"); return }
sb_putc(b, 123) sb_putc(b, '{')
var i = 0 var i = 0
while i < m { while i < m {
if pretty { sb_putc(b, 10); json_indent(b, depth + 1) } if pretty { sb_putc(b, '\n'); json_indent(b, depth + 1) }
sb_putc(b, 34); json_escape_into(b, v.keys[i], ea); sb_putc(b, 34) sb_putc(b, '"'); json_escape_into(b, v.keys[i], ea); sb_putc(b, '"')
sb_putc(b, 58); sb_putc(b, 32) sb_putc(b, ':'); sb_putc(b, ' ')
jwrite(b, v.kids[i], pretty, depth + 1, ea) jwrite(b, v.kids[i], pretty, depth + 1, ea)
if i < m - 1 { sb_putc(b, 44); if not pretty { sb_putc(b, 32) } } if i < m - 1 { sb_putc(b, ','); if not pretty { sb_putc(b, ' ') } }
i = i + 1 i += 1
} }
if pretty { sb_putc(b, 10); json_indent(b, depth) } if pretty { sb_putc(b, '\n'); json_indent(b, depth) }
sb_putc(b, 125) sb_putc(b, '}')
} }
function json_dumps(v: JVal, pretty: bool, ea: bool) -> pointer { function json_dumps(v: JVal, pretty: bool, ea: bool) -> pointer {
let b = sb_new() let b = sb_new()
@ -571,11 +571,10 @@ function palette_emit_ludic(path: pointer) -> bool {
var i = 0 var i = 0
while i < len(pal_names) { while i < len(pal_names) {
sb_puts(b, ` if (name == "{pal_names[i]}") {{ return 0x{pal_hexes[i]} }}\n`) sb_puts(b, ` if (name == "{pal_names[i]}") {{ return 0x{pal_hexes[i]} }}\n`)
i = i + 1 i += 1
} }
sb_puts(b, " return -1\n") sb_puts(b, " return -1\n")
sb_puts(b, "}\n") sb_puts(b, "}\n")
sb_puts(b, "\n")
return write_file(path, sb_str(b)) return write_file(path, sb_str(b))
} }
@ -609,19 +608,23 @@ function palette_emit_json(path: pointer) -> bool {
sb_puts(b, ` "hex": "{pal_hexes[ci]}"\n`) sb_puts(b, ` "hex": "{pal_hexes[ci]}"\n`)
sb_puts(b, " }") sb_puts(b, " }")
} }
ci = ci + 1 ci += 1
} }
sb_puts(b, "\n ]\n") sb_puts(b, "\n ]\n")
sb_puts(b, " }") sb_puts(b, " }")
} }
gi = gi + 1 gi += 1
} }
sb_puts(b, "\n ]\n") sb_puts(b, "\n ]\n")
sb_puts(b, "}") sb_puts(b, "}")
return write_file(path, sb_str(b)) return write_file(path, sb_str(b))
} }
# `x docs-palette` rewrites the two tracked outputs; `x docs-palette --check`
# regenerates into scratch files and compares them to the tracked ones instead,
# so the drift guard judges the working tree, not git HEAD.
function cmd_docs_palette() -> int { function cmd_docs_palette() -> int {
let check = arg_count() > 2 and arg(2) == "--check"
palette_init() palette_init()
# guard: duplicate names # guard: duplicate names
let seen = new []pointer let seen = new []pointer
@ -629,10 +632,17 @@ function cmd_docs_palette() -> int {
while i < len(pal_names) { while i < len(pal_names) {
if set_has(seen, pal_names[i]) { err(`DUPLICATE NAME: {pal_names[i]}\n`); return 1 } if set_has(seen, pal_names[i]) { err(`DUPLICATE NAME: {pal_names[i]}\n`); return 1 }
push(seen, pal_names[i]) push(seen, pal_names[i])
i = i + 1 i += 1
}
var ludic_out = "selfhost/backend/stdlib/emit_color.ludic"
var json_out = "docs/language/colors/palette.json"
if check { ludic_out = tmp_path("pal_emit_color.ludic"); json_out = tmp_path("pal_palette.json") }
if not palette_emit_ludic(ludic_out) { err("cannot write emit_color.ludic\n"); return 1 }
if not palette_emit_json(json_out) { err("cannot write palette.json\n"); return 1 }
if check {
if not shq(`cmp -s {ludic_out} selfhost/backend/stdlib/emit_color.ludic`) { err("emit_color.ludic drifted from the palette table (run: x docs-palette)\n"); return 1 }
if not shq(`cmp -s {json_out} docs/language/colors/palette.json`) { err("palette.json drifted from the palette table (run: x docs-palette)\n"); return 1 }
} }
if not palette_emit_ludic("selfhost/backend/stdlib/emit_color.ludic") { err("cannot write emit_color.ludic\n"); return 1 }
if not palette_emit_json("docs/language/colors/palette.json") { err("cannot write palette.json\n"); return 1 }
print(`OK {string(len(pal_names))} colors`) print(`OK {string(len(pal_names))} colors`)
return 0 return 0
} }

View file

@ -11,7 +11,7 @@ function bucket_get(bs: []Bucket, k1: pointer, k2: pointer) -> Bucket {
var i = 0 var i = 0
while i < len(bs) { while i < len(bs) {
if bs[i].k1 == k1 and bs[i].k2 == k2 { return bs[i] } if bs[i].k1 == k1 and bs[i].k2 == k2 { return bs[i] }
i = i + 1 i += 1
} }
let b = new Bucket let b = new Bucket
b.k1 = k1 b.k1 = k1
@ -23,7 +23,7 @@ function bucket_get(bs: []Bucket, k1: pointer, k2: pointer) -> Bucket {
function bucket_add(bs: []Bucket, k1: pointer, k2: pointer, v: pointer) -> void { function bucket_add(bs: []Bucket, k1: pointer, k2: pointer, v: pointer) -> void {
let b = bucket_get(bs, k1, k2) let b = bucket_get(bs, k1, k2)
var i = 0 var i = 0
while i < len(b.vals) { if b.vals[i] == v { return }; i = i + 1 } while i < len(b.vals) { if b.vals[i] == v { return }; i += 1 }
push(b.vals, v) push(b.vals, v)
} }
# ", ".join(sorted(vals)) # ", ".join(sorted(vals))
@ -34,7 +34,7 @@ function join_sorted(vals: []pointer) -> pointer {
while i < len(vals) { while i < len(vals) {
if i > 0 { sb_puts(b, ", ") } if i > 0 { sb_puts(b, ", ") }
sb_puts(b, vals[i]) sb_puts(b, vals[i])
i = i + 1 i += 1
} }
return sb_str(b) return sb_str(b)
} }
@ -70,7 +70,7 @@ function cp_len(s: pointer) -> int {
let n = slen(s) let n = slen(s)
var c = 0 var c = 0
var i = 0 var i = 0
while i < n { if (s[i] & 192) != 128 { c = c + 1 }; i = i + 1 } while i < n { if (s[i] & 192) != 128 { c += 1 }; i += 1 }
return c return c
} }
function replace_all(hay: pointer, needle: pointer, repl: pointer) -> pointer { function replace_all(hay: pointer, needle: pointer, repl: pointer) -> pointer {
@ -80,8 +80,8 @@ function replace_all(hay: pointer, needle: pointer, repl: pointer) -> pointer {
let n = slen(hay) let n = slen(hay)
var i = 0 var i = 0
while i < n { while i < n {
if s_starts_at(hay, i, needle) { sb_puts(b, repl); i = i + nl } if s_starts_at(hay, i, needle) { sb_puts(b, repl); i += nl }
else { sb_putc(b, hay[i]); i = i + 1 } else { sb_putc(b, hay[i]); i += 1 }
} }
return sb_str(b) return sb_str(b)
} }
@ -110,14 +110,14 @@ function cmd_docs_check() -> int {
var ci = 0 var ci = 0
while ci < len(cats) { while ci < len(cats) {
let cat = cats[ci] let cat = cats[ci]
ci = ci + 1 ci += 1
let cdir = "docs/language/" + cat let cdir = "docs/language/" + cat
if not is_dir(cdir) { continue } if not is_dir(cdir) { continue }
let files = list_sorted(cdir) let files = list_sorted(cdir)
var fi = 0 var fi = 0
while fi < len(files) { while fi < len(files) {
let fn = files[fi] let fn = files[fi]
fi = fi + 1 fi += 1
if not s_ends(fn, ".md") { continue } if not s_ends(fn, ".md") { continue }
if fn == "_section.md" { continue } if fn == "_section.md" { continue }
let d = parse_front(cdir + "/" + fn) let d = parse_front(cdir + "/" + fn)
@ -143,12 +143,12 @@ function cmd_docs_check() -> int {
while k < len(inv.keys) { while k < len(inv.keys) {
let cat = inv.keys[k] let cat = inv.keys[k]
let ids = inv.kids[k] let ids = inv.kids[k]
k = k + 1 k += 1
if ids.t != JV_ARR { continue } if ids.t != JV_ARR { continue }
var m = 0 var m = 0
while m < len(ids.kids) { while m < len(ids.kids) {
let sid = ids.kids[m].s let sid = ids.kids[m].s
m = m + 1 m += 1
if meta_get(id2src, sid, "") == "" { push(problems, `no source file for inventory symbol: {sid} ({cat})`) } if meta_get(id2src, sid, "") == "" { push(problems, `no source file for inventory symbol: {sid} ({cat})`) }
else if not file_exists(`{site}/{sid}.html`) { push(problems, `no generated page for symbol: {sid}.html`) } else if not file_exists(`{site}/{sid}.html`) { push(problems, `no generated page for symbol: {sid}.html`) }
} }
@ -164,7 +164,7 @@ function cmd_docs_check() -> int {
ci = 0 ci = 0
while ci < len(cats) { while ci < len(cats) {
let cat = cats[ci] let cat = cats[ci]
ci = ci + 1 ci += 1
let cdir = "docs/language/" + cat let cdir = "docs/language/" + cat
if not is_dir(cdir) { continue } if not is_dir(cdir) { continue }
let secp = cdir + "/_section.md" let secp = cdir + "/_section.md"
@ -177,7 +177,7 @@ function cmd_docs_check() -> int {
var fi = 0 var fi = 0
while fi < len(files) { while fi < len(files) {
let fn = files[fi] let fn = files[fi]
fi = fi + 1 fi += 1
if not s_ends(fn, ".md") { continue } if not s_ends(fn, ".md") { continue }
if fn == "_section.md" { continue } if fn == "_section.md" { continue }
let d = parse_front(cdir + "/" + fn) let d = parse_front(cdir + "/" + fn)
@ -185,7 +185,7 @@ function cmd_docs_check() -> int {
let id = meta_get(d.m, "id", "") let id = meta_get(d.m, "id", "")
let toks = split_ws(meta_get(d.m, "tokens", "")) let toks = split_ws(meta_get(d.m, "tokens", ""))
var ti = 0 var ti = 0
while ti < len(toks) { bucket_add(tok2ids, kind, toks[ti], id); ti = ti + 1 } while ti < len(toks) { bucket_add(tok2ids, kind, toks[ti], id); ti += 1 }
let ns = meta_get(d.m, "ns", "") let ns = meta_get(d.m, "ns", "")
if slen(ns) > 0 { bucket_add(ns2dirs, "", ns, cat) } if slen(ns) > 0 { bucket_add(ns2dirs, "", ns, cat) }
} }
@ -193,25 +193,25 @@ function cmd_docs_check() -> int {
var bi = 0 var bi = 0
while bi < len(tok2ids) { while bi < len(tok2ids) {
let b = tok2ids[bi] let b = tok2ids[bi]
bi = bi + 1 bi += 1
if len(b.vals) > 1 { push(problems, `token '{b.k2}' ({b.k1}) documented on multiple pages: {join_sorted(b.vals)}`) } if len(b.vals) > 1 { push(problems, `token '{b.k2}' ({b.k1}) documented on multiple pages: {join_sorted(b.vals)}`) }
} }
bi = 0 bi = 0
while bi < len(ns2dirs) { while bi < len(ns2dirs) {
let b = ns2dirs[bi] let b = ns2dirs[bi]
bi = bi + 1 bi += 1
if len(b.vals) > 1 { push(problems, `namespace '{b.k2}' documented from multiple dirs: {join_sorted(b.vals)}`) } if len(b.vals) > 1 { push(problems, `namespace '{b.k2}' documented from multiple dirs: {join_sorted(b.vals)}`) }
} }
bi = 0 bi = 0
while bi < len(id2dirs) { while bi < len(id2dirs) {
let b = id2dirs[bi] let b = id2dirs[bi]
bi = bi + 1 bi += 1
if len(b.vals) > 1 { push(problems, `section id '{b.k2}' declared by multiple dirs: {join_sorted(b.vals)}`) } if len(b.vals) > 1 { push(problems, `section id '{b.k2}' declared by multiple dirs: {join_sorted(b.vals)}`) }
} }
bi = 0 bi = 0
while bi < len(title2dirs) { while bi < len(title2dirs) {
let b = title2dirs[bi] let b = title2dirs[bi]
bi = bi + 1 bi += 1
if slen(b.k2) > 0 and len(b.vals) > 1 { push(problems, `section title '{b.k2}' shared by multiple dirs: {join_sorted(b.vals)}`) } if slen(b.k2) > 0 and len(b.vals) > 1 { push(problems, `section title '{b.k2}' shared by multiple dirs: {join_sorted(b.vals)}`) }
} }
@ -223,7 +223,7 @@ function cmd_docs_check() -> int {
while i < len(thin) and i < 12 { while i < len(thin) and i < 12 {
if i > 0 { sb_puts(b, ", ") } if i > 0 { sb_puts(b, ", ") }
sb_puts(b, thin[i]) sb_puts(b, thin[i])
i = i + 1 i += 1
} }
var tail = "" var tail = ""
if len(thin) > 12 { tail = " …" } if len(thin) > 12 { tail = " …" }
@ -247,18 +247,18 @@ function cmd_docs_check() -> int {
var i = 0 var i = 0
while i < len(targets) { while i < len(targets) {
let tgt = targets[i] let tgt = targets[i]
i = i + 1 i += 1
if not file_exists(`{site}/{tgt}.html`) { push(problems, `highlighter links to {tgt}.html but it was not generated`) } if not file_exists(`{site}/{tgt}.html`) { push(problems, `highlighter links to {tgt}.html but it was not generated`) }
} }
} }
# report # report
var wi = 0 var wi = 0
while wi < len(warnings) { print(` warning: {warnings[wi]}`); wi = wi + 1 } while wi < len(warnings) { print(` warning: {warnings[wi]}`); wi += 1 }
if len(problems) > 0 { if len(problems) > 0 {
print("docs check FAILED:") print("docs check FAILED:")
var pi = 0 var pi = 0
while pi < len(problems) { print(` - {problems[pi]}`); pi = pi + 1 } while pi < len(problems) { print(` - {problems[pi]}`); pi += 1 }
return 1 return 1
} }
print(`docs check OK: {site} ({string(len(id2src.keys))} symbols documented)`) print(`docs check OK: {site} ({string(len(id2src.keys))} symbols documented)`)
@ -271,7 +271,7 @@ function collect_targets_str(o: JVal, out: []pointer) -> void {
var i = 0 var i = 0
while i < len(o.kids) { while i < len(o.kids) {
let v = o.kids[i] let v = o.kids[i]
i = i + 1 i += 1
if v.t == JV_STR { set_add(out, v.s) } if v.t == JV_STR { set_add(out, v.s) }
} }
} }
@ -281,7 +281,7 @@ function collect_targets_id(o: JVal, out: []pointer) -> void {
var i = 0 var i = 0
while i < len(o.kids) { while i < len(o.kids) {
let v = o.kids[i] let v = o.kids[i]
i = i + 1 i += 1
let id = j_get(v, "id") let id = j_get(v, "id")
if id.t == JV_STR { set_add(out, id.s) } if id.t == JV_STR { set_add(out, id.s) }
} }

View file

@ -33,13 +33,13 @@ function meta_new() -> Meta {
} }
function meta_put(m: Meta, k: pointer, v: pointer) -> void { function meta_put(m: Meta, k: pointer, v: pointer) -> void {
var i = 0 var i = 0
while i < len(m.keys) { if m.keys[i] == k { m.vals[i] = v; return }; i = i + 1 } while i < len(m.keys) { if m.keys[i] == k { m.vals[i] = v; return }; i += 1 }
push(m.keys, k) push(m.keys, k)
push(m.vals, v) push(m.vals, v)
} }
function meta_get(m: Meta, k: pointer, dflt: pointer) -> pointer { function meta_get(m: Meta, k: pointer, dflt: pointer) -> pointer {
var i = 0 var i = 0
while i < len(m.keys) { if m.keys[i] == k { return m.vals[i] }; i = i + 1 } while i < len(m.keys) { if m.keys[i] == k { return m.vals[i] }; i += 1 }
return dflt return dflt
} }
@ -54,7 +54,7 @@ function read_snippet(rel: pointer) -> pointer {
let s = read_file(rel) let s = read_file(rel)
if s == null { return "" } if s == null { return "" }
var n = slen(s) var n = slen(s)
while n > 0 and s[n - 1] == 10 { n = n - 1 } while n > 0 and s[n - 1] == '\n' { n -= 1 }
return sslice(s, 0, n) return sslice(s, 0, n)
} }
@ -90,23 +90,23 @@ function parse_doc(path: pointer) -> Doc {
function param_match(line: pointer, p: Param) -> bool { function param_match(line: pointer, p: Param) -> bool {
let n = slen(line) let n = slen(line)
var i = 0 var i = 0
if i < n and line[i] == 45 { i = i + 1 } else { return false } if i < n and line[i] == '-' { i += 1 } else { return false }
while i < n and is_ws(line[i]) { i = i + 1 } while i < n and is_ws(line[i]) { i += 1 }
if i < n and line[i] == 96 { i = i + 1 } if i < n and line[i] == '`' { i += 1 }
let ws = i let ws = i
while i < n and is_word(line[i]) { i = i + 1 } while i < n and is_word(line[i]) { i += 1 }
if i == ws { return false } if i == ws { return false }
let name = sslice(line, ws, i) let name = sslice(line, ws, i)
if i < n and line[i] == 96 { i = i + 1 } if i < n and line[i] == '`' { i += 1 }
while i < n and is_ws(line[i]) { i = i + 1 } while i < n and is_ws(line[i]) { i += 1 }
var sep = 0 var sep = 0
while i < n { while i < n {
if line[i] == 45 { i = i + 1; sep = sep + 1 } if line[i] == '-' { i += 1; sep += 1 }
else if (line[i] & 255) == 226 and i + 2 < n and (line[i + 1] & 255) == 128 and (line[i + 2] & 255) == 148 { i = i + 3; sep = sep + 1 } else if (line[i] & 255) == 226 and i + 2 < n and (line[i + 1] & 255) == 128 and (line[i + 2] & 255) == 148 { i += 3; sep += 1 }
else { break } else { break }
} }
if sep == 0 { return false } if sep == 0 { return false }
while i < n and is_ws(line[i]) { i = i + 1 } while i < n and is_ws(line[i]) { i += 1 }
p.name = name p.name = name
p.desc = codeify(s_trim(sslice(line, i, n))) p.desc = codeify(s_trim(sslice(line, i, n)))
return true return true
@ -119,10 +119,10 @@ function split_paras(desc: pointer) -> []pointer {
var start = 0 var start = 0
var i = 0 var i = 0
while i < n { while i < n {
if desc[i] == 10 { if desc[i] == '\n' {
var jj = i var jj = i
var nl = 0 var nl = 0
while jj < n and is_space_all(desc[jj]) { if desc[jj] == 10 { nl = nl + 1 }; jj = jj + 1 } while jj < n and is_space_all(desc[jj]) { if desc[jj] == '\n' { nl += 1 }; jj += 1 }
if nl >= 2 { if nl >= 2 {
let part = s_trim(sslice(desc, start, i)) let part = s_trim(sslice(desc, start, i))
if slen(part) > 0 { push(out, part) } if slen(part) > 0 { push(out, part) }
@ -132,7 +132,7 @@ function split_paras(desc: pointer) -> []pointer {
} }
i = jj i = jj
} else { } else {
i = i + 1 i += 1
} }
} }
let last = s_trim(sslice(desc, start, n)) let last = s_trim(sslice(desc, start, n))
@ -152,16 +152,16 @@ function split_body(body: pointer) -> Split {
let p = s_index(body, "```ludic\n", i) let p = s_index(body, "```ludic\n", i)
if p < 0 { if p < 0 {
var k = i var k = i
while k < n { sb_putc(wo, body[k]); k = k + 1 } while k < n { sb_putc(wo, body[k]); k += 1 }
i = n i = n
} else { } else {
var k = i var k = i
while k < p { sb_putc(wo, body[k]); k = k + 1 } while k < p { sb_putc(wo, body[k]); k += 1 }
let cs = p + 9 let cs = p + 9
let q = s_index(body, "\n```", cs) let q = s_index(body, "\n```", cs)
if q < 0 { if q < 0 {
var k2 = p var k2 = p
while k2 < cs { sb_putc(wo, body[k2]); k2 = k2 + 1 } while k2 < cs { sb_putc(wo, body[k2]); k2 += 1 }
i = cs i = cs
} else { } else {
push(r.examples, sslice(body, cs, q)) push(r.examples, sslice(body, cs, q))
@ -191,10 +191,10 @@ function split_body(body: pointer) -> Split {
if param_match(t2, pp) { push(r.params, pp) } if param_match(t2, pp) { push(r.params, pp) }
} }
} else { } else {
if not firstkeep { sb_putc(keep, 10) } if not firstkeep { sb_putc(keep, '\n') }
firstkeep = false firstkeep = false
var kk = 0 var kk = 0
while kk < lnlen { sb_putc(keep, ln[kk]); kk = kk + 1 } while kk < lnlen { sb_putc(keep, ln[kk]); kk += 1 }
} }
} }
# 3) desc -> paragraphs joined </p><p>, each codeified # 3) desc -> paragraphs joined </p><p>, each codeified
@ -205,7 +205,7 @@ function split_body(body: pointer) -> Split {
while pi < len(paras) { while pi < len(paras) {
if pi > 0 { sb_puts(ob, "</p><p>") } if pi > 0 { sb_puts(ob, "</p><p>") }
sb_puts(ob, codeify(paras[pi])) sb_puts(ob, codeify(paras[pi]))
pi = pi + 1 pi += 1
} }
r.desc = sb_str(ob) r.desc = sb_str(ob)
return r return r
@ -217,26 +217,26 @@ function plain_text(s: pointer) -> pointer {
let n = slen(s) let n = slen(s)
var i = 0 var i = 0
while i < n { while i < n {
if s[i] == 60 { if s[i] == '<' {
while i < n and s[i] != 62 { i = i + 1 } while i < n and s[i] != '>' { i += 1 }
if i < n { i = i + 1 } if i < n { i += 1 }
} else { } else {
sb_putc(sb, s[i]) sb_putc(sb, s[i])
i = i + 1 i += 1
} }
} }
let t0 = s_trim(sb_str(sb)) let t0 = s_trim(sb_str(sb))
let tn = slen(t0) let tn = slen(t0)
var cut = 0 - 1 var cut = -1
var j = 0 var j = 0
while j < tn { while j < tn {
let c = t0[j] let c = t0[j]
if c == 10 { break } if c == '\n' { break }
if (c == 46 or c == 33 or c == 63) and j >= 1 { if (c == '.' or c == '!' or c == '?') and j >= 1 {
if j + 1 >= tn { cut = j + 1; break } if j + 1 >= tn { cut = j + 1; break }
if is_space_all(t0[j + 1]) { cut = j + 1; break } if is_space_all(t0[j + 1]) { cut = j + 1; break }
} }
j = j + 1 j += 1
} }
var t = t0 var t = t0
if cut >= 0 { t = sslice(t0, 0, cut) } if cut >= 0 { t = sslice(t0, 0, cut) }
@ -245,18 +245,18 @@ function plain_text(s: pointer) -> pointer {
let tl = slen(t) let tl = slen(t)
var cpc = 0 var cpc = 0
var bi = 0 var bi = 0
while bi < tl { if (t[bi] & 192) != 128 { cpc = cpc + 1 }; bi = bi + 1 } while bi < tl { if (t[bi] & 192) != 128 { cpc += 1 }; bi += 1 }
if cpc <= 160 { return t } if cpc <= 160 { return t }
var idx = 0 var idx = 0
var seen = 0 var seen = 0
while idx < tl and seen < 160 { while idx < tl and seen < 160 {
idx = idx + 1 idx += 1
while idx < tl and (t[idx] & 192) == 128 { idx = idx + 1 } while idx < tl and (t[idx] & 192) == 128 { idx += 1 }
seen = seen + 1 seen += 1
} }
let pre = sslice(t, 0, idx) let pre = sslice(t, 0, idx)
var sp = slen(pre) - 1 var sp = slen(pre) - 1
while sp >= 0 and pre[sp] != 32 { sp = sp - 1 } while sp >= 0 and pre[sp] != ' ' { sp -= 1 }
var head = pre var head = pre
if sp >= 0 { head = sslice(pre, 0, sp) } if sp >= 0 { head = sslice(pre, 0, sp) }
return head + "…" return head + "…"
@ -305,9 +305,9 @@ function entries_sort(es: []Entry) -> void {
while i < len(es) { while i < len(es) {
let cur = es[i] let cur = es[i]
var j = i - 1 var j = i - 1
while j >= 0 and entry_gt(es[j], cur) { es[j + 1] = es[j]; j = j - 1 } while j >= 0 and entry_gt(es[j], cur) { es[j + 1] = es[j]; j -= 1 }
es[j + 1] = cur es[j + 1] = cur
i = i + 1 i += 1
} }
} }
function section_gt(a: Section, b: Section) -> bool { function section_gt(a: Section, b: Section) -> bool {
@ -319,9 +319,9 @@ function sections_sort(ss: []Section) -> void {
while i < len(ss) { while i < len(ss) {
let cur = ss[i] let cur = ss[i]
var j = i - 1 var j = i - 1
while j >= 0 and section_gt(ss[j], cur) { ss[j + 1] = ss[j]; j = j - 1 } while j >= 0 and section_gt(ss[j], cur) { ss[j + 1] = ss[j]; j -= 1 }
ss[j + 1] = cur ss[j + 1] = cur
i = i + 1 i += 1
} }
} }
@ -331,7 +331,7 @@ function load_sections() -> []Section {
var ci = 0 var ci = 0
while ci < len(cats) { while ci < len(cats) {
let cat = cats[ci] let cat = cats[ci]
ci = ci + 1 ci += 1
let cdir = "docs/language/" + cat let cdir = "docs/language/" + cat
if not is_dir(cdir) { continue } if not is_dir(cdir) { continue }
var sid = cat var sid = cat
@ -351,7 +351,7 @@ function load_sections() -> []Section {
var fi = 0 var fi = 0
while fi < len(files) { while fi < len(files) {
let fn = files[fi] let fn = files[fi]
fi = fi + 1 fi += 1
if not s_ends(fn, ".md") { continue } if not s_ends(fn, ".md") { continue }
if fn == "_section.md" { continue } if fn == "_section.md" { continue }
push(entries, build_entry(cdir + "/" + fn)) push(entries, build_entry(cdir + "/" + fn))
@ -362,7 +362,7 @@ function load_sections() -> []Section {
while ei < len(entries) { while ei < len(entries) {
let e = entries[ei] let e = entries[ei]
if e.kind == "namespace-method" and slen(e.ns) > 0 { nsname = e.ns; break } if e.kind == "namespace-method" and slen(e.ns) > 0 { nsname = e.ns; break }
ei = ei + 1 ei += 1
} }
let sec = new Section let sec = new Section
sec.id = sid sec.id = sid
@ -384,7 +384,7 @@ function ns_tip(nn: pointer, sections: []Section) -> pointer {
var i = 0 var i = 0
while i < len(sections) { while i < len(sections) {
if sections[i].ns_name == nn { return plain_text(sections[i].blurb) } if sections[i].ns_name == nn { return plain_text(sections[i].blurb) }
i = i + 1 i += 1
} }
return "" return ""
} }
@ -414,11 +414,11 @@ function build_symbols(sections: []Section) -> Symbols {
var si = 0 var si = 0
while si < len(sections) { while si < len(sections) {
let s = sections[si] let s = sections[si]
si = si + 1 si += 1
var ei = 0 var ei = 0
while ei < len(s.entries) { while ei < len(s.entries) {
let e = s.entries[ei] let e = s.entries[ei]
ei = ei + 1 ei += 1
let eid = e.id let eid = e.id
let kind = e.kind let kind = e.kind
let tip = e.tip let tip = e.tip
@ -444,7 +444,7 @@ function build_symbols(sections: []Section) -> Symbols {
var ti = 0 var ti = 0
while ti < len(e.tokens_list) { while ti < len(e.tokens_list) {
let tok = e.tokens_list[ti] let tok = e.tokens_list[ti]
ti = ti + 1 ti += 1
if kind == "keyword" { jvput(kw, tok, jvs(eid)) } if kind == "keyword" { jvput(kw, tok, jvs(eid)) }
else if kind == "type" { jvput(types, tok, jvs(eid)) } else if kind == "type" { jvput(types, tok, jvs(eid)) }
else if kind == "phase" { jvput(phases, tok, jvs(eid)) } else if kind == "phase" { jvput(phases, tok, jvs(eid)) }
@ -453,7 +453,7 @@ function build_symbols(sections: []Section) -> Symbols {
jvput(o, "id", jvs(eid)) jvput(o, "id", jvs(eid))
let pa = jva() let pa = jva()
var qi = 0 var qi = 0
while qi < len(e.params) { jvpush(pa, jvs(e.params[qi].name)); qi = qi + 1 } while qi < len(e.params) { jvpush(pa, jvs(e.params[qi].name)); qi += 1 }
jvput(o, "params", pa) jvput(o, "params", pa)
jvput(builtins, tok, o) jvput(builtins, tok, o)
} }
@ -462,7 +462,7 @@ function build_symbols(sections: []Section) -> Symbols {
jvput(o, "id", jvs(eid)) jvput(o, "id", jvs(eid))
let pa = jva() let pa = jva()
var qi = 0 var qi = 0
while qi < len(e.params) { jvpush(pa, jvs(e.params[qi].name)); qi = qi + 1 } while qi < len(e.params) { jvpush(pa, jvs(e.params[qi].name)); qi += 1 }
jvput(o, "params", pa) jvput(o, "params", pa)
jvput(nsm, tok, o) jvput(nsm, tok, o)
} }
@ -475,7 +475,7 @@ function build_symbols(sections: []Section) -> Symbols {
var ki = 0 var ki = 0
while ki < len(nsm.keys) { while ki < len(nsm.keys) {
let tok = nsm.keys[ki] let tok = nsm.keys[ki]
ki = ki + 1 ki += 1
let dot = s_index(tok, ".", 0) let dot = s_index(tok, ".", 0)
if dot >= 0 { set_add(nsnames, sslice(tok, 0, dot)) } if dot >= 0 { set_add(nsnames, sslice(tok, 0, dot)) }
} }
@ -484,7 +484,7 @@ function build_symbols(sections: []Section) -> Symbols {
var ni = 0 var ni = 0
while ni < len(nsnames) { while ni < len(nsnames) {
let nn = nsnames[ni] let nn = nsnames[ni]
ni = ni + 1 ni += 1
let pg = "ns-" + lower_ascii(nn) let pg = "ns-" + lower_ascii(nn)
jvput(namespaces, nn, jvs(pg)) jvput(namespaces, nn, jvs(pg))
let cd = jvo() let cd = jvo()
@ -511,7 +511,7 @@ function js_get(v: JVal, key: pointer) -> pointer {
function j_has(v: JVal, key: pointer) -> bool { function j_has(v: JVal, key: pointer) -> bool {
if v.t != JV_OBJ { return false } if v.t != JV_OBJ { return false }
var i = 0 var i = 0
while i < len(v.keys) { if v.keys[i] == key { return true }; i = i + 1 } while i < len(v.keys) { if v.keys[i] == key { return true }; i += 1 }
return false return false
} }
@ -527,7 +527,7 @@ function nav_html(b: Sb, links: JVal, cta_href: pointer) -> void {
var i = 0 var i = 0
while i < len(links.kids) { while i < len(links.kids) {
let nlk = links.kids[i] let nlk = links.kids[i]
i = i + 1 i += 1
let href = js_get(nlk, "href") let href = js_get(nlk, "href")
let label = js_get(nlk, "label") let label = js_get(nlk, "label")
if href == cta_href { sb_puts(b, `<a class="nav-cta" href="{href}">{hesc(label)}</a>`) } if href == cta_href { sb_puts(b, `<a class="nav-cta" href="{href}">{hesc(label)}</a>`) }
@ -551,7 +551,7 @@ function render_params(e: Entry) -> pointer {
var i = 0 var i = 0
while i < len(e.params) { while i < len(e.params) {
let p = e.params[i] let p = e.params[i]
i = i + 1 i += 1
sb_puts(b, `<div class="param" id="param-{p.name}"><code class="pname">{hesc(p.name)}</code><span class="pdesc">{p.desc}</span></div>`) sb_puts(b, `<div class="param" id="param-{p.name}"><code class="pname">{hesc(p.name)}</code><span class="pdesc">{p.desc}</span></div>`)
} }
sb_puts(b, `</div>`) sb_puts(b, `</div>`)
@ -564,7 +564,7 @@ function render_examples(e: Entry) -> pointer {
var i = 0 var i = 0
while i < len(e.examples) { while i < len(e.examples) {
sb_puts(b, `<pre data-lang="ludic">{hesc(e.examples[i])}</pre>`) sb_puts(b, `<pre data-lang="ludic">{hesc(e.examples[i])}</pre>`)
i = i + 1 i += 1
} }
sb_puts(b, `</div>`) sb_puts(b, `</div>`)
return sb_str(b) return sb_str(b)
@ -575,11 +575,11 @@ function render_related(e: Entry, items: JVal) -> pointer {
var i = 0 var i = 0
while i < len(e.related_list) { while i < len(e.related_list) {
let r = e.related_list[i] let r = e.related_list[i]
i = i + 1 i += 1
let it = j_get(items, r) let it = j_get(items, r)
if it.t == JV_NULL { continue } if it.t == JV_NULL { continue }
if cnt == 0 { sb_puts(b, `<div class="related"><h2>Related</h2><div class="rel-row">`) } if cnt == 0 { sb_puts(b, `<div class="related"><h2>Related</h2><div class="rel-row">`) }
cnt = cnt + 1 cnt += 1
sb_puts(b, `<a class="rel" href="{r}.html">{hesc(js_get(it,"name"))}</a>`) sb_puts(b, `<a class="rel" href="{r}.html">{hesc(js_get(it,"name"))}</a>`)
} }
if cnt == 0 { return "" } if cnt == 0 { return "" }
@ -591,7 +591,7 @@ function render_item(b: Sb, e: Entry, s: Section, cfg: JVal, items: JVal) -> voi
emit_head(b, e.name + " — Ludic", e.tip, "item.css") emit_head(b, e.name + " — Ludic", e.tip, "item.css")
sb_puts(b, "\n<body>\n") sb_puts(b, "\n<body>\n")
topbar(b, cfg) topbar(b, cfg)
sb_putc(b, 10) sb_putc(b, '\n')
var sechref = "api.html#" + s.id var sechref = "api.html#" + s.id
if slen(s.ns_name) > 0 { sechref = "ns-" + lower_ascii(s.ns_name) + ".html" } if slen(s.ns_name) > 0 { sechref = "ns-" + lower_ascii(s.ns_name) + ".html" }
var kindc = e.kind var kindc = e.kind
@ -624,7 +624,7 @@ function render_ns(b: Sb, nsname: pointer, s: Section, cfg: JVal) -> void {
emit_head(b, nsname + " — Ludic", plain_text(s.blurb), "item.css") emit_head(b, nsname + " — Ludic", plain_text(s.blurb), "item.css")
sb_puts(b, "\n<body>\n") sb_puts(b, "\n<body>\n")
topbar(b, cfg) topbar(b, cfg)
sb_putc(b, 10) sb_putc(b, '\n')
sb_puts(b, `<main class="wrap item">\n`) sb_puts(b, `<main class="wrap item">\n`)
sb_puts(b, ` <div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">{hesc(nsname)}</span></div>\n`) sb_puts(b, ` <div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">{hesc(nsname)}</span></div>\n`)
sb_puts(b, ` <div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">{hesc(nsname)}</h1></div>\n`) sb_puts(b, ` <div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">{hesc(nsname)}</h1></div>\n`)
@ -633,7 +633,7 @@ function render_ns(b: Sb, nsname: pointer, s: Section, cfg: JVal) -> void {
var i = 0 var i = 0
while i < len(s.entries) { while i < len(s.entries) {
let e = s.entries[i] let e = s.entries[i]
i = i + 1 i += 1
sb_puts(b, `<a class="ns-method" href="{e.id}.html"><code class="nm-sig">{hesc(e.sig)}</code><span class="nm-tip">{hesc(e.tip)}</span></a>`) sb_puts(b, `<a class="ns-method" href="{e.id}.html"><code class="nm-sig">{hesc(e.sig)}</code><span class="nm-tip">{hesc(e.tip)}</span></a>`)
} }
sb_puts(b, `</div>\n`) sb_puts(b, `</div>\n`)
@ -650,7 +650,7 @@ function render_color(b: Sb, cfg: JVal) -> void {
emit_head(b, "Color — Ludic", "The Ludic named-color palette.", "item.css") emit_head(b, "Color — Ludic", "The Ludic named-color palette.", "item.css")
sb_puts(b, "\n<body>\n") sb_puts(b, "\n<body>\n")
topbar(b, cfg) topbar(b, cfg)
sb_putc(b, 10) sb_putc(b, '\n')
sb_puts(b, `<main class="wrap item">\n`) sb_puts(b, `<main class="wrap item">\n`)
sb_puts(b, ` <div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">Color</span></div>\n`) sb_puts(b, ` <div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">Color</span></div>\n`)
sb_puts(b, ` <div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">Color</h1></div>\n`) sb_puts(b, ` <div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">Color</h1></div>\n`)
@ -661,7 +661,7 @@ function render_color(b: Sb, cfg: JVal) -> void {
var gi = 0 var gi = 0
while gi < len(pal_groups) { while gi < len(pal_groups) {
let g = pal_groups[gi] let g = pal_groups[gi]
gi = gi + 1 gi += 1
if set_has(seen, g) { continue } if set_has(seen, g) { continue }
push(seen, g) push(seen, g)
sb_puts(b, `<div class="swatch-group"><h3>{hesc(g)}</h3><div class="swatch-row">`) sb_puts(b, `<div class="swatch-group"><h3>{hesc(g)}</h3><div class="swatch-row">`)
@ -672,7 +672,7 @@ function render_color(b: Sb, cfg: JVal) -> void {
let hx = pal_hexes[ci] let hx = pal_hexes[ci]
sb_puts(b, `<div class="swatch" id="{lower_ascii(nm)}"><span class="chip" style="background:#{hx}"></span><span class="cname">Color.{hesc(nm)}</span><span class="chex">#{hx}</span></div>`) sb_puts(b, `<div class="swatch" id="{lower_ascii(nm)}"><span class="chip" style="background:#{hx}"></span><span class="cname">Color.{hesc(nm)}</span><span class="chex">#{hx}</span></div>`)
} }
ci = ci + 1 ci += 1
} }
sb_puts(b, `</div></div>`) sb_puts(b, `</div></div>`)
} }
@ -689,7 +689,7 @@ function render_api(b: Sb, sections: []Section, cfg: JVal) -> void {
emit_head(b, "Ludic — API Reference", "The complete Ludic API Reference: every keyword, type, builtin, namespace, annotation and color, each on its own page, with fuzzy search.", "item.css") emit_head(b, "Ludic — API Reference", "The complete Ludic API Reference: every keyword, type, builtin, namespace, annotation and color, each on its own page, with fuzzy search.", "item.css")
sb_puts(b, "\n<body>\n") sb_puts(b, "\n<body>\n")
topbar(b, cfg) topbar(b, cfg)
sb_putc(b, 10) sb_putc(b, '\n')
sb_puts(b, `<main class="wrap ref">\n`) sb_puts(b, `<main class="wrap ref">\n`)
sb_puts(b, ` <div class="ref-intro">\n`) sb_puts(b, ` <div class="ref-intro">\n`)
sb_puts(b, ` <div class="kicker">Reference</div>\n`) sb_puts(b, ` <div class="kicker">Reference</div>\n`)
@ -702,7 +702,7 @@ function render_api(b: Sb, sections: []Section, cfg: JVal) -> void {
var si = 0 var si = 0
while si < len(sections) { while si < len(sections) {
let s = sections[si] let s = sections[si]
si = si + 1 si += 1
if s.cat == "colors" { if s.cat == "colors" {
sb_puts(b, `<section class="idx-sec" data-sec="{s.id}"><h2 id="{s.id}">{hesc(s.title)}</h2><p class="sec-blurb">{s.blurb}</p><div class="idx-grid"><a class="idx-item" href="ns-color.html" data-name="Color" data-tip="The named color palette."><code>Color</code><span>The named color palette (221 names).</span></a></div></section>`) sb_puts(b, `<section class="idx-sec" data-sec="{s.id}"><h2 id="{s.id}">{hesc(s.title)}</h2><p class="sec-blurb">{s.blurb}</p><div class="idx-grid"><a class="idx-item" href="ns-color.html" data-name="Color" data-tip="The named color palette."><code>Color</code><span>The named color palette (221 names).</span></a></div></section>`)
continue continue
@ -711,7 +711,7 @@ function render_api(b: Sb, sections: []Section, cfg: JVal) -> void {
var ei = 0 var ei = 0
while ei < len(s.entries) { while ei < len(s.entries) {
let e = s.entries[ei] let e = s.entries[ei]
ei = ei + 1 ei += 1
sb_puts(b, `<a class="idx-item" href="{e.id}.html" data-name="{hescattr(e.name)}" data-tip="{hescattr(e.tip)}"><code>{hesc(e.name)}</code><span>{hesc(e.tip)}</span></a>`) sb_puts(b, `<a class="idx-item" href="{e.id}.html" data-name="{hescattr(e.name)}" data-tip="{hescattr(e.tip)}"><code>{hesc(e.name)}</code><span>{hesc(e.tip)}</span></a>`)
} }
sb_puts(b, `</div></section>`) sb_puts(b, `</div></section>`)
@ -733,20 +733,20 @@ function pct(tmpl: pointer, mk: []pointer, mv: []pointer) -> pointer {
let b = sb_new() let b = sb_new()
var i = 0 var i = 0
while i < n { while i < n {
if tmpl[i] == 37 and i + 1 < n and tmpl[i + 1] == 40 { if tmpl[i] == '%' and i + 1 < n and tmpl[i + 1] == '(' {
var j = i + 2 var j = i + 2
while j < n and is_word(tmpl[j]) { j = j + 1 } while j < n and is_word(tmpl[j]) { j += 1 }
if j > i + 2 and j + 1 < n and tmpl[j] == 41 and tmpl[j + 1] == 115 { if j > i + 2 and j + 1 < n and tmpl[j] == ')' and tmpl[j + 1] == 's' {
let key = sslice(tmpl, i + 2, j) let key = sslice(tmpl, i + 2, j)
var k = 0 var k = 0
var found = false var found = false
while k < len(mk) { if mk[k] == key { sb_puts(b, mv[k]); found = true; break }; k = k + 1 } while k < len(mk) { if mk[k] == key { sb_puts(b, mv[k]); found = true; break }; k += 1 }
i = j + 2 i = j + 2
continue continue
} }
} }
sb_putc(b, tmpl[i]) sb_putc(b, tmpl[i])
i = i + 1 i += 1
} }
return sb_str(b) return sb_str(b)
} }
@ -763,7 +763,7 @@ function build_stages(hero: JVal) -> pointer {
if st == "ludicc" { hl = " hl" } if st == "ludicc" { hl = " hl" }
sb_puts(b, `<span class="stage{hl}">{hesc(st)}</span>`) sb_puts(b, `<span class="stage{hl}">{hesc(st)}</span>`)
if i < m - 1 { sb_puts(b, `<span class="arrow">→</span>`) } if i < m - 1 { sb_puts(b, `<span class="arrow">→</span>`) }
i = i + 1 i += 1
} }
return sb_str(b) return sb_str(b)
} }
@ -774,7 +774,7 @@ function build_feats(feats: JVal) -> pointer {
var i = 0 var i = 0
while i < len(cards.kids) { while i < len(cards.kids) {
let c = cards.kids[i] let c = cards.kids[i]
i = i + 1 i += 1
sb_puts(b, `<div class="feat reveal"><div class="ico">{js_get(c,"icon")}</div><h3>{js_get(c,"title")}</h3><p>{js_get(c,"html")}</p></div>`) sb_puts(b, `<div class="feat reveal"><div class="ico">{js_get(c,"icon")}</div><h3>{js_get(c,"title")}</h3><p>{js_get(c,"html")}</p></div>`)
} }
return sb_str(b) return sb_str(b)
@ -784,7 +784,7 @@ function build_phil_paras(phil: JVal) -> pointer {
let ps = j_get(phil, "paras") let ps = j_get(phil, "paras")
if ps.t != JV_ARR { return "" } if ps.t != JV_ARR { return "" }
var i = 0 var i = 0
while i < len(ps.kids) { sb_puts(b, `<p>{ps.kids[i].s}</p>`); i = i + 1 } while i < len(ps.kids) { sb_puts(b, `<p>{ps.kids[i].s}</p>`); i += 1 }
return sb_str(b) return sb_str(b)
} }
function build_phil_stats(phil: JVal) -> pointer { function build_phil_stats(phil: JVal) -> pointer {
@ -794,7 +794,7 @@ function build_phil_stats(phil: JVal) -> pointer {
var i = 0 var i = 0
while i < len(ss.kids) { while i < len(ss.kids) {
let s = ss.kids[i] let s = ss.kids[i]
i = i + 1 i += 1
sb_puts(b, `<div class="stat"><div class="big">{js_get(s,"big")}</div><div class="lbl">{hesc(js_get(s,"lbl"))}</div></div>`) sb_puts(b, `<div class="stat"><div class="big">{js_get(s,"big")}</div><div class="lbl">{hesc(js_get(s,"lbl"))}</div></div>`)
} }
return sb_str(b) return sb_str(b)
@ -807,7 +807,7 @@ function build_steps(start: JVal) -> pointer {
while i < len(st.kids) { while i < len(st.kids) {
let s = st.kids[i] let s = st.kids[i]
sb_puts(b, `<div class="step reveal"><div class="n">{string(i + 1)}</div><div><h4>{js_get(s,"title")}</h4><p>{js_get(s,"html")}</p></div></div>`) sb_puts(b, `<div class="step reveal"><div class="n">{string(i + 1)}</div><div><h4>{js_get(s,"title")}</h4><p>{js_get(s,"html")}</p></div></div>`)
i = i + 1 i += 1
} }
return sb_str(b) return sb_str(b)
} }
@ -818,9 +818,9 @@ function build_term(start: JVal) -> pointer {
var i = 0 var i = 0
while i < len(tm.kids) { while i < len(tm.kids) {
let t = tm.kids[i] let t = tm.kids[i]
i = i + 1 i += 1
let bl = j_get(t, "blank") let bl = j_get(t, "blank")
if bl.t == JV_BOOL and bl.b == 1 { sb_putc(b, 10) } if bl.t == JV_BOOL and bl.b == 1 { sb_putc(b, '\n') }
else if j_has(t, "comment") { sb_puts(b, `<span class="t-com"># {hesc(js_get(t,"comment"))}</span>\n`) } else if j_has(t, "comment") { sb_puts(b, `<span class="t-com"># {hesc(js_get(t,"comment"))}</span>\n`) }
else if j_has(t, "cmd") { sb_puts(b, `<span class="prompt">$</span> {hesc(js_get(t,"cmd"))}\n`) } else if j_has(t, "cmd") { sb_puts(b, `<span class="prompt">$</span> {hesc(js_get(t,"cmd"))}\n`) }
else if j_has(t, "out") { sb_puts(b, `<span class="out">{hesc(js_get(t,"out"))}</span>\n`) } else if j_has(t, "out") { sb_puts(b, `<span class="out">{hesc(js_get(t,"out"))}</span>\n`) }
@ -832,7 +832,7 @@ function build_eds(ed: JVal) -> pointer {
let ls = j_get(ed, "list") let ls = j_get(ed, "list")
if ls.t != JV_ARR { return "" } if ls.t != JV_ARR { return "" }
var i = 0 var i = 0
while i < len(ls.kids) { sb_puts(b, `<div class="ed"><span class="k">◆</span> {hesc(ls.kids[i].s)}</div>`); i = i + 1 } while i < len(ls.kids) { sb_puts(b, `<div class="ed"><span class="k">◆</span> {hesc(ls.kids[i].s)}</div>`); i += 1 }
return sb_str(b) return sb_str(b)
} }
function build_footlinks(cfg: JVal) -> pointer { function build_footlinks(cfg: JVal) -> pointer {
@ -842,7 +842,7 @@ function build_footlinks(cfg: JVal) -> pointer {
var i = 0 var i = 0
while i < len(nl.kids) { while i < len(nl.kids) {
let n = nl.kids[i] let n = nl.kids[i]
i = i + 1 i += 1
sb_puts(b, `<a href="{js_get(n,"href")}">{hesc(js_get(n,"label"))}</a>`) sb_puts(b, `<a href="{js_get(n,"href")}">{hesc(js_get(n,"label"))}</a>`)
} }
} }
@ -857,7 +857,7 @@ function build_samples(cfg: JVal) -> pointer {
var i = 0 var i = 0
while i < len(sm.kids) { while i < len(sm.kids) {
let s = sm.kids[i] let s = sm.kids[i]
i = i + 1 i += 1
let o = jvo() let o = jvo()
jvput(o, "name", jvs(js_get(s, "name"))) jvput(o, "name", jvs(js_get(s, "name")))
jvput(o, "label", jvs(js_get(s, "label"))) jvput(o, "label", jvs(js_get(s, "label")))
@ -938,11 +938,11 @@ function render_highlighter(sym: JVal) -> pointer {
if p < 0 { return tmpl } if p < 0 { return tmpl }
let b = sb_new() let b = sb_new()
var i = 0 var i = 0
while i < p { sb_putc(b, tmpl[i]); i = i + 1 } while i < p { sb_putc(b, tmpl[i]); i += 1 }
sb_puts(b, payload) sb_puts(b, payload)
var j = p + slen("/*__SYMBOLS__*/{}") var j = p + slen("/*__SYMBOLS__*/{}")
let n = slen(tmpl) let n = slen(tmpl)
while j < n { sb_putc(b, tmpl[j]); j = j + 1 } while j < n { sb_putc(b, tmpl[j]); j += 1 }
return sb_str(b) return sb_str(b)
} }
@ -953,10 +953,10 @@ function cmd_docs_gen() -> int {
while i < arg_count() { while i < arg_count() {
let a = arg(i) let a = arg(i)
if a == "--out" { if a == "--out" {
i = i + 1 i += 1
if i < arg_count() { out = arg(i) } if i < arg_count() { out = arg(i) }
} }
i = i + 1 i += 1
} }
run(`mkdir -p {out}`) run(`mkdir -p {out}`)
palette_init() palette_init()
@ -969,33 +969,33 @@ function cmd_docs_gen() -> int {
var si = 0 var si = 0
while si < len(sections) { while si < len(sections) {
let s = sections[si] let s = sections[si]
si = si + 1 si += 1
if s.cat == "colors" { continue } if s.cat == "colors" { continue }
var ei = 0 var ei = 0
while ei < len(s.entries) { while ei < len(s.entries) {
let e = s.entries[ei] let e = s.entries[ei]
ei = ei + 1 ei += 1
let pb = sb_new() let pb = sb_new()
render_item(pb, e, s, cfg, syms.items) render_item(pb, e, s, cfg, syms.items)
write_file(`{out}/{e.id}.html`, sb_str(pb)) write_file(`{out}/{e.id}.html`, sb_str(pb))
npages = npages + 1 npages += 1
} }
} }
si = 0 si = 0
while si < len(sections) { while si < len(sections) {
let s = sections[si] let s = sections[si]
si = si + 1 si += 1
if slen(s.ns_name) > 0 { if slen(s.ns_name) > 0 {
let pb = sb_new() let pb = sb_new()
render_ns(pb, s.ns_name, s, cfg) render_ns(pb, s.ns_name, s, cfg)
write_file(`{out}/ns-{lower_ascii(s.ns_name)}.html`, sb_str(pb)) write_file(`{out}/ns-{lower_ascii(s.ns_name)}.html`, sb_str(pb))
npages = npages + 1 npages += 1
} }
} }
let cb = sb_new() let cb = sb_new()
render_color(cb, cfg) render_color(cb, cfg)
write_file(`{out}/ns-color.html`, sb_str(cb)) write_file(`{out}/ns-color.html`, sb_str(cb))
npages = npages + 1 npages += 1
let ab = sb_new() let ab = sb_new()
render_api(ab, sections, cfg) render_api(ab, sections, cfg)
write_file(`{out}/api.html`, sb_str(ab)) write_file(`{out}/api.html`, sb_str(ab))

93
tools/x/forgejo.ludic Normal file
View file

@ -0,0 +1,93 @@
# forgejo.ludic — publish a release on the Forgejo instance: create the release
# for a tag (or reuse it), then attach every file from an assets directory.
# Replaces tools/ci/forgejo_release.py: the one piece of the release flow that
# speaks HTTP now does so through curl, driven from Ludic like every other
# external tool the runner uses.
#
# The API root comes from $LUDIC_FORGEJO_API (default: this project's repo);
# the token from $FORGEJO_TOKEN, passed to curl through a 0600 header file so
# it never appears on a command line.
#
# Idempotent-ish: an existing release for the tag is reused, and an asset whose
# name is already attached is skipped, not fatal.
const FORGEJO_API_DEFAULT: string = "https://git.workshopsoft.io/api/v1/repos/workshopsoft/ludic"
function forgejo_api() -> pointer { return getenv_or("LUDIC_FORGEJO_API", FORGEJO_API_DEFAULT) }
# the curl header file carrying the token (written once per process)
var forgejo_hdr: pointer = null
function forgejo_headers() -> pointer {
if forgejo_hdr == null {
forgejo_hdr = tmp_path("forgejo_headers.txt")
run(`umask 077 && printf 'Authorization: token %s\n' "$FORGEJO_TOKEN" > {forgejo_hdr}`)
}
return forgejo_hdr
}
# one API call: `extra` carries the method/body flags; returns the HTTP status
# and leaves the response body in `out`
function forgejo_call(path: pointer, extra: pointer, out: pointer) -> int {
let code = capture_line(`curl -sS -o {out} -w '%{{http_code}}' -H @{forgejo_headers()} {extra} '{forgejo_api()}{path}'`)
return str_to_int(code)
}
# create-or-reuse the release for `tag`, with `body_file` as its notes; returns
# the release JSON, or null after printing why
function forgejo_release(tag: pointer, body_file: pointer) -> JVal {
let out = tmp_path("forgejo_release.json")
let code = forgejo_call(`/releases/tags/{tag}`, "", out)
if code == 200 {
let rel = json_parse(read_file(out))
print(`release {tag} already exists (id {string(j_get(rel, "id").num)}); uploading assets`)
return rel
}
if code != 404 { err(`forgejo: GET /releases/tags/{tag} -> HTTP {string(code)}\n`); return null }
var body = ""
if shq(`test -f {body_file}`) { body = read_file(body_file) }
var prerelease = "false"
if Text.starts_with(tag, "v0.") { prerelease = "true" }
let req = tmp_path("forgejo_create.json")
write_file(req, `{{"tag_name":{j_quote(tag)},"name":{j_quote(tag)},"body":{j_quote(body)},"draft":false,"prerelease":{prerelease}}}`)
let ccode = forgejo_call("/releases", `-X POST -H 'Content-Type: application/json' --data-binary @{req}`, out)
if ccode != 201 { err(`forgejo: POST /releases -> HTTP {string(ccode)}\n`); return null }
let rel = json_parse(read_file(out))
print(`created release {tag} id {string(j_get(rel, "id").num)}`)
return rel
}
# attach every regular file in `dir` that the release does not already carry
function forgejo_upload_assets(rel: JVal, dir: pointer) -> bool {
if not shq(`test -d {dir}`) { return true }
let rid = j_get(rel, "id").num
let existing = new []pointer
let assets = j_get(rel, "assets")
var i = 0
while i < len(assets.kids) { push(existing, j_get(assets.kids[i], "name").s); i += 1 }
let names = Text.split(capture(`ls -1 {dir}`), "\n")
var ok_all = true
i = 0
while i < len(names) {
let name = Text.trim(names[i])
i += 1
if name == "" { continue }
if set_has(existing, name) { print(` skip (exists): {name}`); continue }
let path = `{dir}/{name}`
if not shq(`test -f {path}`) { continue }
let out = tmp_path("forgejo_asset.json")
let code = forgejo_call(`/releases/{string(rid)}/assets?name={name}`, `-X POST -F 'attachment=@{path}'`, out)
if code == 201 { print(` uploaded: {name} (HTTP {string(code)})`) }
else { err(` upload failed: {name} (HTTP {string(code)})\n`); ok_all = false }
}
return ok_all
}
# the whole flow; 0 on success
function forgejo_publish(tag: pointer, body_file: pointer, assets_dir: pointer) -> int {
if getenv_or("FORGEJO_TOKEN", "") == "" { err("forgejo: FORGEJO_TOKEN is not set\n"); return 1 }
let rel = forgejo_release(tag, body_file)
if rel == null { return 1 }
if not forgejo_upload_assets(rel, assets_dir) { return 1 }
print(`done: {forgejo_api()}/releases/tag/{tag}`)
return 0
}

View file

@ -26,18 +26,18 @@ var jpos: int = 0
var jerr: bool = false var jerr: bool = false
function j_ws() -> void { function j_ws() -> void {
while jbuf[jpos] == 32 or jbuf[jpos] == 9 or jbuf[jpos] == 10 or jbuf[jpos] == 13 { jpos = jpos + 1 } while jbuf[jpos] == ' ' or jbuf[jpos] == '\t' or jbuf[jpos] == '\n' or jbuf[jpos] == '\r' { jpos += 1 }
} }
function j_hex(c: int) -> int { function j_hex(c: int) -> int {
if c >= 48 and c <= 57 { return c - 48 } if c >= '0' and c <= '9' { return c - 48 }
if c >= 97 and c <= 102 { return c - 97 + 10 } if c >= 'a' and c <= 'f' { return c - 97 + 10 }
if c >= 65 and c <= 70 { return c - 65 + 10 } if c >= 'A' and c <= 'F' { return c - 65 + 10 }
return 0 return 0
} }
function j_hex4() -> int { function j_hex4() -> int {
var v = 0; var i = 0 var v = 0; var i = 0
while i < 4 { v = v * 16 + j_hex(jbuf[jpos]); jpos = jpos + 1; i = i + 1 } while i < 4 { v = v * 16 + j_hex(jbuf[jpos]); jpos += 1; i += 1 }
return v return v
} }
@ -52,32 +52,32 @@ function utf8_put(b: pointer, n: int, cp: int) -> int {
# parse a JSON string starting at the opening quote; returns a fresh buffer # parse a JSON string starting at the opening quote; returns a fresh buffer
function j_string() -> pointer { function j_string() -> pointer {
jpos = jpos + 1 # opening quote jpos += 1 # opening quote
# over-allocate: escapes only ever shrink the byte count # over-allocate: escapes only ever shrink the byte count
let cap = slen(jbuf) - jpos + 1 let cap = slen(jbuf) - jpos + 1
let out = bytes(cap + 4) let out = bytes(cap + 4)
var n = 0 var n = 0
while jbuf[jpos] != 0 and jbuf[jpos] != 34 { while jbuf[jpos] != 0 and jbuf[jpos] != '"' {
if jbuf[jpos] == 92 { if jbuf[jpos] == '\\' {
jpos = jpos + 1 jpos += 1
let c = jbuf[jpos]; jpos = jpos + 1 let c = jbuf[jpos]; jpos += 1
if c == 110 { out[n] = 10; n = n + 1 } if c == 'n' { out[n] = '\n'; n += 1 }
else { if c == 116 { out[n] = 9; n = n + 1 } else { if c == 't' { out[n] = '\t'; n += 1 }
else { if c == 114 { out[n] = 13; n = n + 1 } else { if c == 'r' { out[n] = '\r'; n += 1 }
else { if c == 98 { out[n] = 8; n = n + 1 } else { if c == 'b' { out[n] = 8; n += 1 }
else { if c == 102 { out[n] = 12; n = n + 1 } else { if c == 'f' { out[n] = 12; n += 1 }
else { if c == 117 { else { if c == 'u' {
var cp = j_hex4() var cp = j_hex4()
if cp >= 55296 and cp < 56320 and jbuf[jpos] == 92 and jbuf[jpos + 1] == 117 { if cp >= 55296 and cp < 56320 and jbuf[jpos] == '\\' and jbuf[jpos + 1] == 'u' {
jpos = jpos + 2 jpos += 2
let lo = j_hex4() let lo = j_hex4()
cp = 65536 + ((cp - 55296) << 10) + (lo - 56320) cp = 65536 + ((cp - 55296) << 10) + (lo - 56320)
} }
n = utf8_put(out, n, cp) n = utf8_put(out, n, cp)
} else { out[n] = c; n = n + 1 } } } } } } } else { out[n] = c; n += 1 } } } } } }
} else { out[n] = jbuf[jpos]; n = n + 1; jpos = jpos + 1 } } else { out[n] = jbuf[jpos]; n += 1; jpos += 1 }
} }
if jbuf[jpos] == 34 { jpos = jpos + 1 } if jbuf[jpos] == '"' { jpos += 1 }
out[n] = 0 out[n] = 0
return out return out
} }
@ -85,55 +85,55 @@ function j_string() -> pointer {
function j_value() -> JVal { function j_value() -> JVal {
j_ws() j_ws()
let c = jbuf[jpos] let c = jbuf[jpos]
if c == 123 { # '{' if c == '{' { # '{'
jpos = jpos + 1 jpos += 1
let v = jval_new(JV_OBJ) let v = jval_new(JV_OBJ)
j_ws() j_ws()
if jbuf[jpos] == 125 { jpos = jpos + 1; return v } if jbuf[jpos] == '}' { jpos += 1; return v }
while true { while true {
j_ws() j_ws()
var key = "" var key = ""
if jbuf[jpos] == 34 { key = j_string() } else { jerr = true } if jbuf[jpos] == '"' { key = j_string() } else { jerr = true }
j_ws(); if jbuf[jpos] == 58 { jpos = jpos + 1 } else { jerr = true } j_ws(); if jbuf[jpos] == ':' { jpos += 1 } else { jerr = true }
let kid = j_value() let kid = j_value()
push(v.keys, key); push(v.kids, kid) push(v.keys, key); push(v.kids, kid)
j_ws() j_ws()
if jbuf[jpos] == 44 { jpos = jpos + 1 } else { if jbuf[jpos] == 125 { jpos = jpos + 1; return v } else { return v } } if jbuf[jpos] == ',' { jpos += 1 } else { if jbuf[jpos] == '}' { jpos += 1; return v } else { return v } }
} }
return v return v
} }
if c == 91 { # '[' if c == '[' { # '['
jpos = jpos + 1 jpos += 1
let v = jval_new(JV_ARR) let v = jval_new(JV_ARR)
j_ws() j_ws()
if jbuf[jpos] == 93 { jpos = jpos + 1; return v } if jbuf[jpos] == ']' { jpos += 1; return v }
while true { while true {
let kid = j_value() let kid = j_value()
push(v.kids, kid) push(v.kids, kid)
j_ws() j_ws()
if jbuf[jpos] == 44 { jpos = jpos + 1 } else { if jbuf[jpos] == 93 { jpos = jpos + 1; return v } else { return v } } if jbuf[jpos] == ',' { jpos += 1 } else { if jbuf[jpos] == ']' { jpos += 1; return v } else { return v } }
} }
return v return v
} }
if c == 34 { # '"' if c == '"' { # '"'
let v = jval_new(JV_STR) let v = jval_new(JV_STR)
v.s = j_string() v.s = j_string()
return v return v
} }
if c == 116 { # true if c == 't' { # true
jpos = jpos + 4; let v = jval_new(JV_BOOL); v.b = 1; return v jpos += 4; let v = jval_new(JV_BOOL); v.b = 1; return v
} }
if c == 102 { # false if c == 'f' { # false
jpos = jpos + 5; let v = jval_new(JV_BOOL); v.b = 0; return v jpos += 5; let v = jval_new(JV_BOOL); v.b = 0; return v
} }
if c == 110 { # null if c == 'n' { # null
jpos = jpos + 4; return jval_new(JV_NULL) jpos += 4; return jval_new(JV_NULL)
} }
# number # number
let start = jpos let start = jpos
if jbuf[jpos] == 45 { jpos = jpos + 1 } if jbuf[jpos] == '-' { jpos += 1 }
if jbuf[jpos] < 48 or jbuf[jpos] > 57 { jerr = true; if jbuf[jpos] != 0 { jpos = jpos + 1 }; return jval_new(JV_NULL) } if jbuf[jpos] < '0' or jbuf[jpos] > '9' { jerr = true; if jbuf[jpos] != 0 { jpos += 1 }; return jval_new(JV_NULL) }
while (jbuf[jpos] >= 48 and jbuf[jpos] <= 57) or jbuf[jpos] == 46 or jbuf[jpos] == 101 or jbuf[jpos] == 69 or jbuf[jpos] == 43 or jbuf[jpos] == 45 { jpos = jpos + 1 } while (jbuf[jpos] >= '0' and jbuf[jpos] <= '9') or jbuf[jpos] == '.' or jbuf[jpos] == 'e' or jbuf[jpos] == 'E' or jbuf[jpos] == '+' or jbuf[jpos] == '-' { jpos += 1 }
let v = jval_new(JV_NUM) let v = jval_new(JV_NUM)
v.s = sslice(jbuf, start, jpos) v.s = sslice(jbuf, start, jpos)
v.num = str_to_int(v.s) v.num = str_to_int(v.s)
@ -158,17 +158,43 @@ function json_valid(text: pointer) -> bool {
# integer value of a decimal string (ignores any fractional part) # integer value of a decimal string (ignores any fractional part)
function str_to_int(s: pointer) -> int { function str_to_int(s: pointer) -> int {
var i = 0; var neg = false var i = 0; var neg = false
if s[0] == 45 { neg = true; i = 1 } if s[0] == '-' { neg = true; i = 1 }
var v = 0 var v = 0
while s[i] >= 48 and s[i] <= 57 { v = v * 10 + (s[i] - 48); i = i + 1 } while s[i] >= '0' and s[i] <= '9' { v = v * 10 + (s[i] - 48); i += 1 }
if neg { return 0 - v } if neg { return -v }
return v return v
} }
# the lowercase hex digit for 0..15
function j_hexch(v: int) -> int {
if v < 10 { return '0' + v }
return 'a' + (v - 10)
}
# `s` as a JSON string literal: quoted, with " \\ and control bytes escaped
function j_quote(s: pointer) -> pointer {
let b = sb_new()
sb_putc(b, '"')
var i = 0
while s[i] != 0 {
let c = s[i]
if c == '"' { sb_puts(b, "\\\"") }
else if c == '\\' { sb_puts(b, "\\\\") }
else if c == '\n' { sb_puts(b, "\\n") }
else if c == '\r' { sb_puts(b, "\\r") }
else if c == '\t' { sb_puts(b, "\\t") }
else if c >= 0 and c < ' ' { sb_puts(b, "\\u00"); sb_putc(b, j_hexch(c / 16)); sb_putc(b, j_hexch(c % 16)) }
else { sb_putc(b, c) }
i += 1
}
sb_putc(b, '"')
return sb_str(b)
}
# object member lookup by key (JV_NULL if absent or not an object) # object member lookup by key (JV_NULL if absent or not an object)
function j_get(v: JVal, key: pointer) -> JVal { function j_get(v: JVal, key: pointer) -> JVal {
if v.t != JV_OBJ { return jval_new(JV_NULL) } if v.t != JV_OBJ { return jval_new(JV_NULL) }
var i = 0 var i = 0
while i < len(v.keys) { if v.keys[i] == key { return v.kids[i] }; i = i + 1 } while i < len(v.keys) { if v.keys[i] == key { return v.kids[i] }; i += 1 }
return jval_new(JV_NULL) return jval_new(JV_NULL)
} }

373
tools/x/lsp_test.ludic Normal file
View file

@ -0,0 +1,373 @@
# lsp_test.ludic — drive bin/ludic-lsp over real LSP traffic against the real
# source tree (`x test-lsp`; replaces tools/test-lsp.py). Every assertion is
# about a feature an editor actually calls: if this passes, VS Code, JetBrains,
# Neovim and the rest are exercising code paths that have been checked, because
# they all speak to this one binary.
#
# The server is driven in one batch: every request is framed into a single
# stdin stream (positions are computed from the same files the server reads),
# the server runs to `exit`, and its stdout is parsed back into a result table
# keyed by request id plus the last publishDiagnostics per file — exactly what
# an interactive client would have observed, without a bidirectional pipe.
# ---- the request stream -------------------------------------------------------
var lsp_req: Sb = null
var lsp_next_id: int = 0
# one framed JSON-RPC message appended to the stream
function lsp_frame(body: pointer) -> void {
sb_puts(lsp_req, "Content-Length: "); sb_puts(lsp_req, string(len(body)))
sb_puts(lsp_req, "\r\n\r\n"); sb_puts(lsp_req, body)
}
# queue a request; returns its id (the key into lsp_res after the run)
function lsp_request(method: pointer, params: pointer) -> int {
lsp_next_id += 1
lsp_frame(`{{"jsonrpc":"2.0","id":{lsp_next_id},"method":"{method}","params":{params}}}`)
return lsp_next_id
}
function lsp_notify(method: pointer, params: pointer) -> void {
lsp_frame(`{{"jsonrpc":"2.0","method":"{method}","params":{params}}}`)
}
# didOpen for a file on disk; returns its text (for position lookups)
function lsp_open(path: pointer, lang: pointer) -> pointer {
let text = read_file(path)
lsp_notify("textDocument/didOpen", `{{"textDocument":{{"uri":"file://{path}","languageId":"{lang}","version":1,"text":{j_quote(text)}}}}}`)
return text
}
function lsp_doc(path: pointer) -> pointer { return `{{"uri":"file://{path}"}}` }
# the LSP position of the first occurrence of `needle` (+ `off` characters)
function position_of(text: pointer, needle: pointer, off: int) -> pointer {
let n = len(text)
var line = 0; var a = 0
while a <= n {
var b = a
while b < n and text[b] != '\n' { b += 1 }
let row = text[a..b]
let at = Text.index_of(row, needle)
if at >= 0 { return `{{"line":{line},"character":{at + off}}}` }
line += 1; a = b + 1
}
err(`lsp_test: {needle} not found\n`)
exit(1)
return ""
}
# ---- the response stream ------------------------------------------------------
property DiagSet { file: pointer = "", msgs: []pointer }
var lsp_res: []JVal = new []JVal # index = request id (0 unused)
var lsp_diags: []DiagSet = new []DiagSet # the last publish per file basename
function lsp_result(id: int) -> JVal {
if id < len(lsp_res) { return lsp_res[id] }
return jval_new(JV_NULL)
}
function lsp_diag_set(file: pointer) -> DiagSet {
var i = 0
while i < len(lsp_diags) { if lsp_diags[i].file == file { return lsp_diags[i] }; i += 1 }
return null
}
# messages published for `file` (an empty slice when nothing was published)
function lsp_diag_msgs(file: pointer) -> []pointer {
let d = lsp_diag_set(file)
if d == null { return new []pointer }
return d.msgs
}
function basename_of(uri: pointer) -> pointer {
var i = len(uri)
while i > 0 and uri[i - 1] != '/' { i -= 1 }
return uri[i..len(uri)]
}
function lsp_record(msg: JVal) -> void {
let id = j_get(msg, "id")
if id.t == JV_NUM {
while len(lsp_res) <= id.num { push(lsp_res, jval_new(JV_NULL)) }
lsp_res[id.num] = j_get(msg, "result")
return
}
if j_get(msg, "method").s == "textDocument/publishDiagnostics" {
let p = j_get(msg, "params")
let file = basename_of(j_get(p, "uri").s)
let msgs = new []pointer
let list = j_get(p, "diagnostics")
var i = 0
while i < len(list.kids) { push(msgs, j_get(list.kids[i], "message").s); i += 1 }
var d = lsp_diag_set(file)
if d == null { d = new DiagSet; d.file = file; push(lsp_diags, d) }
d.msgs = msgs
}
}
# split a Content-Length framed stream into messages
function lsp_parse_stream(s: pointer) -> void {
let n = len(s)
var p = 0
while p < n {
let h = Text.index_of(s[p..n], "Content-Length:")
if h < 0 { return }
var q = p + h + 15
while s[q] == ' ' { q += 1 }
var size = 0
while s[q] >= '0' and s[q] <= '9' { size = size * 10 + (s[q] - '0'); q += 1 }
let body_at = Text.index_of(s[q..n], "\r\n\r\n")
if body_at < 0 { return }
let start = q + body_at + 4
lsp_record(json_parse(s[start..start + size]))
p = start + size
}
}
# ---- JVal conveniences --------------------------------------------------------
function j_has_key(v: JVal, key: pointer) -> bool { return j_get(v, key).t != JV_NULL }
# does any element of array `arr` have field `key` equal to `want`?
function j_any_field(arr: JVal, key: pointer, want: pointer) -> bool {
var i = 0
while i < len(arr.kids) { if j_get(arr.kids[i], key).s == want { return true }; i += 1 }
return false
}
# the sorted, de-duplicated start lines of a TextEdit array, as "1,7"
function lines_key(edits: JVal) -> pointer {
let lines = new []int
var i = 0
while i < len(edits.kids) {
let l = j_get(j_get(j_get(edits.kids[i], "range"), "start"), "line").num
if not List.contains(lines, l) { push(lines, l) }
i += 1
}
List.sort(lines)
var out = ""
i = 0
while i < len(lines) { if i > 0 { out += "," }; out += string(lines[i]); i += 1 }
return out
}
# the sorted, de-duplicated start lines of a Location array, as "2,11"
function loc_lines_key(locs: JVal) -> pointer {
let lines = new []int
var i = 0
while i < len(locs.kids) {
let l = j_get(j_get(j_get(locs.kids[i], "range"), "start"), "line").num
if not List.contains(lines, l) { push(lines, l) }
i += 1
}
List.sort(lines)
var out = ""
i = 0
while i < len(lines) { if i > 0 { out += "," }; out += string(lines[i]); i += 1 }
return out
}
# the edits of a WorkspaceEdit's first (only) changed document
function first_change(ws: JVal) -> JVal {
let changes = j_get(ws, "changes")
if len(changes.kids) == 0 { return jval_new(JV_ARR) }
return changes.kids[0]
}
# how many distinct file basenames a Location array spans
function distinct_files(locs: JVal) -> int {
let seen = new []pointer
var i = 0
while i < len(locs.kids) {
let f = basename_of(j_get(locs.kids[i], "uri").s)
if not set_has(seen, f) { push(seen, f) }
i += 1
}
return len(seen)
}
function check_true(label: pointer, cond: bool) -> void {
if cond { ok(label) } else { bad(label) }
}
# ---- the suite ----------------------------------------------------------------
# Exit 2 when there is no server to test (the caller reports a skip, not a pass).
function cmd_test_lsp() -> int {
let server = getenv_or("LUDIC_LSP", "bin/ludic-lsp")
if not is_exec(server) { skip("ludic-lsp missing"); return 2 }
let root = capture_line("pwd")
lsp_req = sb_new()
let init = lsp_request("initialize", `{{"rootUri":"file://{root}","capabilities":{{}},"initializationOptions":{{"compilerDiagnostics":true,"compilerPath":"{root}/bin/ludicc"}}}}`)
lsp_notify("initialized", "{}")
# ---- a fragment file: no `program` block, only reachable through an import --
let frag = `{root}/examples/games/chronorift/combat.ludic`
let text = lsp_open(frag, "ludic")
let uri = lsp_doc(frag)
let syms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{uri}}}`)
# a field reached through a query binding, declared in ANOTHER file
let pos = position_of(text, "st.guard", 3)
let hov = lsp_request("textDocument/hover", `{{"textDocument":{uri},"position":{pos}}}`)
let def = lsp_request("textDocument/definition", `{{"textDocument":{uri},"position":{pos}}}`)
# a binding is not yet inside its own scope at the point it is written, so
# resolving it has to go through the declaration table, not scope lookup
let qpos = position_of(text, "for (e) in query", 5)
let qhov = lsp_request("textDocument/hover", `{{"textDocument":{uri},"position":{qpos}}}`)
let comp = lsp_request("textDocument/completion", `{{"textDocument":{uri},"position":{pos}}}`)
# a query's term list admits properties and models and nothing else —
# not the loop variables, not builtins, not keywords
let qsrc = "program QT {\n property Pos { x: int = 0 }\n property Vel { dx: int = 0 }\n handler S phase Update {\n for (a, b) in query [Pos, ] { }\n }\n}\n"
let qfile = tmp_path("lsp_query.ludic")
write_file(qfile, qsrc)
lsp_open(qfile, "ludic")
let quri = lsp_doc(qfile)
let qcomp = lsp_request("textDocument/completion", `{{"textDocument":{quri},"position":{position_of(qsrc, "[Pos, ]", 6)}}}`)
let body = lsp_request("textDocument/completion", `{{"textDocument":{quri},"position":{position_of(qsrc, "] { }", 3)}}}`)
let refs = lsp_request("textDocument/references", `{{"textDocument":{uri},"position":{position_of(text, "query [Stats", 7)},"context":{{"includeDeclaration":true}}}}`)
let hints = lsp_request("textDocument/inlayHint", `{{"textDocument":{uri},"range":{{"start":{{"line":0,"character":0}},"end":{{"line":9999,"character":0}}}}}}`)
# a namespaced call resolves to its underlying bare-builtin signature
let sig = lsp_request("textDocument/signatureHelp", `{{"textDocument":{uri},"position":{position_of(text, "Random.chance(", 14)}}}`)
let toks = lsp_request("textDocument/semanticTokens/full", `{{"textDocument":{uri}}}`)
let folds = lsp_request("textDocument/foldingRange", `{{"textDocument":{uri}}}`)
# ---- structural diagnostics arrive while typing --------------------------
lsp_notify("textDocument/didChange", `{{"textDocument":{{"uri":"file://{frag}","version":2}},"contentChanges":[{{"text":{j_quote(text + "\n{ oops")}}}]}}`)
lsp_request("textDocument/documentSymbol", `{{"textDocument":{uri}}}`)
# ---- the LSP analyses opened files on its own ----------------------------
let badf = tmp_path("lsp_bad.ludic")
write_file(badf, "program Bad {\n property P { x: nosuchtype = 0 }\n}\n")
lsp_open(badf, "ludic")
let bsyms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(badf)}}}`)
# ---- markdown: ```ludic fences are real Ludic ---------------------------
let md = tmp_path("lsp_test.md")
write_file(md, "# Title\n\nprose\n\n```ludic\nprogram D { property Pt { x: int = 0 } }\n```\n\n```python\nz = 1\n```\n")
lsp_open(md, "markdown")
let mduri = lsp_doc(md)
let mdsyms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{mduri}}}`)
let mdtoks = lsp_request("textDocument/semanticTokens/full", `{{"textDocument":{mduri}}}`)
let fmt = lsp_request("textDocument/formatting", `{{"textDocument":{mduri},"options":{{"tabSize":2,"insertSpaces":true}}}}`)
# the repository's own documentation must not light up with false errors
let docs = ["LANGUAGE.md", "README.md", "COMPILING.md"]
var di = 0
while di < len(docs) {
let path = `{root}/{docs[di]}`
if shq(`test -f {path}`) {
lsp_open(path, "markdown")
lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(path)}}}`)
}
di += 1
}
let langdoc = lsp_doc(`{root}/LANGUAGE.md`)
let langtoks = lsp_request("textDocument/semanticTokens/full", `{{"textDocument":{langdoc}}}`)
# ---- rename has to be resolution-based, not textual ----------------------
# `x` here is a field of Pos, a field of Vel, a local, and a parameter.
# Renaming any one of them must leave the other three alone.
let amb = tmp_path("lsp_rename.ludic")
write_file(amb, "program R {\n property Pos { x: int = 0, y: int = 0 }\n property Vel { x: int = 0 }\n function helper(x: int) -> int { return x + 1 }\n handler A phase Update {\n for (p) in query [Pos] {\n let x = 5\n p.x = p.x + x\n }\n }\n handler B phase Update {\n for (v) in query [Vel] { v.x = v.x + 1 }\n }\n}\n")
let atext = lsp_open(amb, "ludic")
let auri = lsp_doc(amb)
let field = lsp_request("textDocument/rename", `{{"textDocument":{auri},"position":{position_of(atext, "property Pos { x", 15)},"newName":"ZZZ"}}`)
let local = lsp_request("textDocument/rename", `{{"textDocument":{auri},"position":{position_of(atext, "let x = 5", 4)},"newName":"ZZZ"}}`)
let param = lsp_request("textDocument/rename", `{{"textDocument":{auri},"position":{position_of(atext, "function helper(x", 16)},"newName":"ZZZ"}}`)
# find-usages must be equally precise
let refs_x = lsp_request("textDocument/references", `{{"textDocument":{auri},"position":{position_of(atext, "property Vel { x", 15)},"context":{{"includeDeclaration":true}}}}`)
# ---- the compiler's own errors arrive on save ----------------------------
let ccf = tmp_path("lsp_cc.ludic")
write_file(ccf, "program CcErr {\n entry {\n let x = (5 +\n }\n}\n")
lsp_open(ccf, "ludic")
lsp_notify("textDocument/didSave", `{{"textDocument":{lsp_doc(ccf)}}}`)
lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(ccf)}}}`)
# ---- workspace-wide symbol search ---------------------------------------
let ws = lsp_request("workspace/symbol", `{{"query":"Stats"}}`)
lsp_request("shutdown", "null")
lsp_notify("exit", "null")
# ---- run the server over the whole stream --------------------------------
let req_path = tmp_path("lsp_req.bin")
let res_path = tmp_path("lsp_res.bin")
write_file(req_path, sb_str(lsp_req))
run(`{server} --stdio < {req_path} > {res_path} 2>{tmp_path("lsp_err.txt")}`)
let stream = read_file(res_path)
if stream == null { bad("ludic-lsp produced no output"); return report() }
lsp_parse_stream(stream)
# ---- and judge ----------------------------------------------------------
let caps = j_get(lsp_result(init), "capabilities")
let wanted = ["completionProvider", "hoverProvider", "definitionProvider", "referencesProvider", "renameProvider", "documentSymbolProvider", "workspaceSymbolProvider", "documentFormattingProvider", "semanticTokensProvider", "foldingRangeProvider", "signatureHelpProvider", "inlayHintProvider", "documentLinkProvider"]
var wi = 0
while wi < len(wanted) { check_true(`advertises {wanted[wi]}`, j_has_key(caps, wanted[wi])); wi += 1 }
check_true("document symbols in an imported fragment", len(lsp_result(syms).kids) >= 3)
let hov_text = j_get(j_get(lsp_result(hov), "contents"), "value").s
check_true("hover resolves a field through a query binding", Text.contains(hov_text, "Stats.guard: int"))
check_true("go-to-definition crosses files", Text.ends_with(j_get(lsp_result(def), "uri").s, "world.ludic"))
let qhov_text = j_get(j_get(lsp_result(qhov), "contents"), "value").s
check_true("hover works on a query binding at its declaration site", Text.contains(qhov_text, "query binding"))
let items = j_get(lsp_result(comp), "items")
check_true("completion after '.' offers only that property's fields", j_any_field(items, "label", "guard") and j_any_field(items, "label", "hp") and not j_any_field(items, "label", "clear"))
let qitems = j_get(lsp_result(qcomp), "items")
check_true("completion inside query terms lists properties only", len(qitems.kids) == 2 and j_any_field(qitems, "label", "Pos") and j_any_field(qitems, "label", "Vel"))
let bitems = j_get(lsp_result(body), "items")
check_true("completion in the loop body still has locals and builtins", j_any_field(bitems, "label", "a") and j_any_field(bitems, "label", "clear"))
check_true("find-usages spans the compilation unit", distinct_files(lsp_result(refs)) >= 3)
check_true("inlay hints name query binding types", j_any_field(lsp_result(hints), "label", ": Stats"))
let sigs = j_get(lsp_result(sig), "signatures")
check_true("signature help for a namespaced builtin", len(sigs.kids) > 0 and Text.starts_with(j_get(sigs.kids[0], "label").s, "rng_chance("))
let tdata = j_get(lsp_result(toks), "data")
check_true("semantic tokens produced", len(tdata.kids) / 5 > 100)
check_true("semantic token stream is well formed", len(tdata.kids) % 5 == 0)
check_true("folding ranges produced", len(lsp_result(folds).kids) > 5)
let cmsgs = lsp_diag_msgs("combat.ludic")
var unclosed = false
var ci = 0
while ci < len(cmsgs) { if Text.contains(cmsgs[ci], "unclosed") { unclosed = true }; ci += 1 }
check_true("unbalanced brace reported while typing", unclosed)
check_true("LSP opens and analyses a file with no compiler present", j_any_field(lsp_result(bsyms), "name", "Bad"))
let mdnames = lsp_result(mdsyms)
check_true("symbols found inside a markdown fence", len(mdnames.kids) == 1 and j_get(mdnames.kids[0], "name").s == "D")
let mdata = j_get(lsp_result(mdtoks), "data")
var first_tok = -1
if len(mdata.kids) > 0 { first_tok = mdata.kids[0].num }
check("markdown tokens start on the fence body line", string(first_tok), "5")
# A fence is a snippet: elisions and half-shown blocks are the documentation
# doing its job, so errors there would be noise. Highlighting still applies.
check_true("no diagnostics are reported for markdown", lsp_diag_set("lsp_test.md") != null and len(lsp_diag_msgs("lsp_test.md")) == 0)
let fedits = lsp_result(fmt)
var ftext = ""
if len(fedits.kids) > 0 { ftext = j_get(fedits.kids[0], "newText").s }
check_true("markdown formatting leaves prose and other fences alone", Text.contains(ftext, "prose") and Text.contains(ftext, "z = 1"))
di = 0
while di < len(docs) {
check(`{docs[di]} opens with no diagnostics`, string(len(lsp_diag_msgs(docs[di]))), "0")
di += 1
}
check_true("LANGUAGE.md fences are highlighted", len(j_get(lsp_result(langtoks), "data").kids) / 5 > 100)
check("renaming Pos.x touches only its declaration and its uses", lines_key(first_change(lsp_result(field))), "1,7")
check("renaming a local touches only that local", lines_key(first_change(lsp_result(local))), "6,7")
check("renaming a parameter touches only that function", lines_key(first_change(lsp_result(param))), "3")
check("renaming a parameter makes exactly two edits", string(len(first_change(lsp_result(param)).kids)), "2")
check("find-usages of Vel.x does not pick up Pos.x", loc_lines_key(lsp_result(refs_x)), "2,11")
check_true("workspace symbol search finds a property", j_any_field(lsp_result(ws), "name", "Stats"))
let ccmsgs = lsp_diag_msgs("lsp_cc.ludic")
var cc_seen = false
ci = 0
while ci < len(ccmsgs) { if Text.contains(ccmsgs[ci], "expected expression") { cc_seen = true }; ci += 1 }
check_true("ludicc's own error is published on save (compilerDiagnostics)", cc_seen)
return report()
}

View file

@ -18,6 +18,8 @@ program X {
import "selfhost_test.ludic" import "selfhost_test.ludic"
import "test.ludic" import "test.ludic"
import "json.ludic" import "json.ludic"
import "lsp_test.ludic"
import "forgejo.ludic"
import "checks.ludic" import "checks.ludic"
import "docgen.ludic" import "docgen.ludic"
import "docgen_gen.ludic" import "docgen_gen.ludic"
@ -52,6 +54,7 @@ program X {
print(" x test --coverage per-file line coverage over the test specs") print(" x test --coverage per-file line coverage over the test specs")
print(" x selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)") print(" x selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
print(" x test-tools the editor-toolchain suite") print(" x test-tools the editor-toolchain suite")
print(" x test-lsp drive bin/ludic-lsp over real LSP traffic (exit 2 = no server)")
print(" x golden regenerate selfhost/golden/renders.sha256 (review with git diff)") print(" x golden regenerate selfhost/golden/renders.sha256 (review with git diff)")
print("") print("")
print("doc / lint checks (Ludic, no Python):") print("doc / lint checks (Ludic, no Python):")
@ -61,7 +64,7 @@ program X {
print(" x lint-asset <file> validate one editor .json / .xml asset") print(" x lint-asset <file> validate one editor .json / .xml asset")
print(" x docs-gen [--out DIR] generate the documentation site (default build/pages)") print(" x docs-gen [--out DIR] generate the documentation site (default build/pages)")
print(" x docs-check [DIR] coverage/integrity guard over a generated docs site") print(" x docs-check [DIR] coverage/integrity guard over a generated docs site")
print(" x docs-palette regenerate emit_color.ludic + palette.json from the palette table") print(" x docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table (--check: compare only)")
print("") print("")
print("release:") print("release:")
print(" x version print the toolchain version (ludicc --version)") print(" x version print the toolchain version (ludicc --version)")
@ -83,53 +86,63 @@ program X {
return dflt return dflt
} }
entry { # route one subcommand to its implementation and return its exit code; -1 when
if (arg_count() < 2) { usage(); exit(1) } # the name is unknown. Returning (rather than exiting in place) lets entry tear
let cmd = arg(1) # down the per-process scratch directory before the process ends.
function dispatch(cmd: pointer) -> int {
if (cmd == "build") { exit(cmd_build()) } if (cmd == "build") { return cmd_build() }
if (cmd == "build-cli") { exit(cmd_build_cli()) } if (cmd == "build-cli") { return cmd_build_cli() }
if (cmd == "app") { exit(cmd_app()) } if (cmd == "app") { return cmd_app() }
if (cmd == "tools") { exit(cmd_tools()) } if (cmd == "tools") { return cmd_tools() }
if (cmd == "clean") { exit(cmd_clean()) } if (cmd == "clean") { return cmd_clean() }
if (cmd == "test") { if (cmd == "test") {
if (argn(2, "") == "--coverage") { exit(cmd_test_coverage()) } if (argn(2, "") == "--coverage") { return cmd_test_coverage() }
exit(cmd_test()) return cmd_test()
} }
if (cmd == "selfhost-test") { exit(cmd_selfhost_test()) } if (cmd == "selfhost-test") { return cmd_selfhost_test() }
if (cmd == "test-tools") { exit(cmd_test_tools()) } if (cmd == "test-tools") { return cmd_test_tools() }
if (cmd == "check-docs") { exit(cmd_check_docs()) } if (cmd == "test-lsp") { return cmd_test_lsp() }
if (cmd == "check-impl") { exit(cmd_check_impl()) } if (cmd == "check-docs") { return cmd_check_docs() }
if (cmd == "check-vocabulary") { exit(cmd_check_vocab()) } if (cmd == "check-impl") { return cmd_check_impl() }
if (cmd == "lint-asset") { exit(cmd_lint_asset()) } if (cmd == "check-vocabulary") { return cmd_check_vocab() }
if (cmd == "docs-palette") { exit(cmd_docs_palette()) } if (cmd == "lint-asset") { return cmd_lint_asset() }
if (cmd == "docs-gen") { exit(cmd_docs_gen()) } if (cmd == "docs-palette") { return cmd_docs_palette() }
if (cmd == "docs-check") { exit(cmd_docs_check()) } if (cmd == "docs-gen") { return cmd_docs_gen() }
if (cmd == "golden") { exit(cmd_golden()) } if (cmd == "docs-check") { return cmd_docs_check() }
if (cmd == "bootstrap") { exit(cmd_bootstrap()) } if (cmd == "golden") { return cmd_golden() }
if (cmd == "bootstrap-cfree") { exit(cmd_bootstrap_cfree()) } if (cmd == "bootstrap") { return cmd_bootstrap() }
if (cmd == "reseed") { exit(cmd_reseed()) } if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() }
if (cmd == "selfhost-build") { exit(cmd_selfhost_build(argn(2, "bin/ludicc"), argn(3, "build/selfhost"))) } if (cmd == "reseed") { return cmd_reseed() }
if (cmd == "selfhost-build") { return cmd_selfhost_build(argn(2, "bin/ludicc"), argn(3, "build/selfhost")) }
if (cmd == "sh-compile") { if (cmd == "sh-compile") {
if (arg_count() < 5) { err("usage: x sh-compile <ludicc> <in.ludic> <out>\n"); exit(1) } if (arg_count() < 5) { err("usage: x sh-compile <ludicc> <in.ludic> <out>\n"); exit(1) }
exit(cmd_sh_compile(arg(2), arg(3), arg(4))) return cmd_sh_compile(arg(2), arg(3), arg(4))
} }
if (cmd == "game-build") { if (cmd == "game-build") {
if (arg_count() < 5) { err("usage: x game-build <ludicc> <game.ludic> <out>\n"); exit(1) } if (arg_count() < 5) { err("usage: x game-build <ludicc> <game.ludic> <out>\n"); exit(1) }
exit(cmd_game_build(arg(2), arg(3), arg(4))) return cmd_game_build(arg(2), arg(3), arg(4))
} }
if (cmd == "add") { exit(cmd_pkg_add()) } if (cmd == "add") { return cmd_pkg_add() }
if (cmd == "get") { exit(cmd_pkg_get()) } if (cmd == "get") { return cmd_pkg_get() }
if (cmd == "update") { exit(cmd_pkg_update()) } if (cmd == "update") { return cmd_pkg_update() }
if (cmd == "verify") { exit(cmd_pkg_verify()) } if (cmd == "verify") { return cmd_pkg_verify() }
if (cmd == "vendor") { exit(cmd_pkg_vendor()) } if (cmd == "vendor") { return cmd_pkg_vendor() }
if (cmd == "build-lib") { exit(cmd_pkg_build_lib()) } if (cmd == "build-lib") { return cmd_pkg_build_lib() }
if (cmd == "link-flags") { exit(cmd_pkg_link_flags()) } if (cmd == "link-flags") { return cmd_pkg_link_flags() }
if (cmd == "test-pkg") { exit(cmd_test_pkg()) } if (cmd == "test-pkg") { return cmd_test_pkg() }
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { exit(cmd_version()) } if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() }
if (cmd == "release") { exit(cmd_release()) } if (cmd == "release") { return cmd_release() }
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); exit(0) } if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); return 0 }
return -1
}
entry {
if (arg_count() < 2) { usage(); exit(1) }
let cmd = arg(1)
let code = dispatch(cmd)
tmp_cleanup()
if code >= 0 { exit(code) }
err(`x: unknown command '{cmd}'\n`) err(`x: unknown command '{cmd}'\n`)
usage() usage()
exit(1) exit(1)

View file

@ -47,13 +47,13 @@ function manifest_new() -> Manifest {
} }
# a bare newline string (the lexer has no reliable \n inside interpolation) # a bare newline string (the lexer has no reliable \n inside interpolation)
function nl() -> pointer { let b = bytes(2); b[0] = 10; b[1] = 0; return b } function nl() -> pointer { let b = bytes(2); b[0] = '\n'; b[1] = 0; return b }
# ---- version arithmetic (semver major.minor.patch, pre-release ignored) ------ # ---- version arithmetic (semver major.minor.patch, pre-release ignored) ------
# drop a leading 'v' (118) from a tag/version string # drop a leading 'v' (118) from a tag/version string
function strip_v(s: pointer) -> pointer { function strip_v(s: pointer) -> pointer {
if slen(s) > 0 and s[0] == 118 { return sslice(s, 1, slen(s)) } if slen(s) > 0 and s[0] == 'v' { return sslice(s, 1, slen(s)) }
return s return s
} }
@ -66,10 +66,10 @@ function ver_nums(s: pointer) -> []int {
var i = 0 var i = 0
while i < n { while i < n {
let c = v[i] let c = v[i]
if c >= 48 and c <= 57 { cur = cur * 10 + (c - 48) } if c >= '0' and c <= '9' { cur = cur * 10 + (c - 48) }
else if c == 46 { push(parts, cur); cur = 0 } else if c == '.' { push(parts, cur); cur = 0 }
else { i = n } # a '-'/'+' ends the numeric core else { i = n } # a '-'/'+' ends the numeric core
i = i + 1 i += 1
} }
push(parts, cur) push(parts, cur)
while len(parts) < 3 { push(parts, 0) } while len(parts) < 3 { push(parts, 0) }
@ -82,9 +82,9 @@ function ver_cmp(a: pointer, b: pointer) -> int {
let pb = ver_nums(b) let pb = ver_nums(b)
var i = 0 var i = 0
while i < 3 { while i < 3 {
if pa[i] < pb[i] { return 0 - 1 } if pa[i] < pb[i] { return -1 }
if pa[i] > pb[i] { return 1 } if pa[i] > pb[i] { return 1 }
i = i + 1 i += 1
} }
return 0 return 0
} }
@ -94,7 +94,7 @@ function ver_gt(a: pointer, b: pointer) -> bool { return ver_cmp(a, b) > 0 }
function is_ver_tag(s: pointer) -> bool { function is_ver_tag(s: pointer) -> bool {
let v = strip_v(s) let v = strip_v(s)
if slen(v) == 0 { return false } if slen(v) == 0 { return false }
return v[0] >= 48 and v[0] <= 57 return v[0] >= '0' and v[0] <= '9'
} }
# ---- tiny line/token scanners for the manifest & lock ------------------------ # ---- tiny line/token scanners for the manifest & lock ------------------------
@ -107,17 +107,17 @@ function tok_line(line: pointer) -> []pointer {
var i = 0 var i = 0
while i < n { while i < n {
let c = line[i] let c = line[i]
if c == 35 { i = n } # '#' comment if c == '#' { i = n } # '#' comment
else if c == 32 or c == 9 { i = i + 1 } # whitespace else if c == ' ' or c == '\t' { i += 1 } # whitespace
else if c == 34 { # "quoted" else if c == '"' { # "quoted"
i = i + 1 i += 1
let start = i let start = i
while i < n and line[i] != 34 { i = i + 1 } while i < n and line[i] != '"' { i += 1 }
push(toks, sslice(line, start, i)) push(toks, sslice(line, start, i))
if i < n { i = i + 1 } if i < n { i += 1 }
} else { # bare word } else { # bare word
let start = i let start = i
while i < n and line[i] != 32 and line[i] != 9 and line[i] != 35 { i = i + 1 } while i < n and line[i] != ' ' and line[i] != '\t' and line[i] != '#' { i += 1 }
push(toks, sslice(line, start, i)) push(toks, sslice(line, start, i))
} }
} }
@ -153,8 +153,8 @@ function parse_manifest(text: pointer) -> Manifest {
if head == "package" { if len(ts) > 1 { m.module = ts[1] } } if head == "package" { if len(ts) > 1 { m.module = ts[1] } }
else if head == "version" { if len(ts) > 1 { m.ver = ts[1] } } else if head == "version" { if len(ts) > 1 { m.ver = ts[1] } }
else if head == "kind" { if len(ts) > 1 { m.kind = ts[1] } } else if head == "kind" { if len(ts) > 1 { m.kind = ts[1] } }
else if head == "provides" { var k = 1; while k < len(ts) { push(m.provides, ts[k]); k = k + 1 } } else if head == "provides" { var k = 1; while k < len(ts) { push(m.provides, ts[k]); k += 1 } }
else if head == "targets" { var k = 1; while k < len(ts) { push(m.targets, ts[k]); k = k + 1 } } else if head == "targets" { var k = 1; while k < len(ts) { push(m.targets, ts[k]); k += 1 } }
else if head == "require" { else if head == "require" {
if len(ts) > 2 { let d = new Dep; d.module = ts[1]; d.ver = ts[2]; push(m.deps, d) } if len(ts) > 2 { let d = new Dep; d.module = ts[1]; d.ver = ts[2]; push(m.deps, d) }
} }
@ -178,11 +178,11 @@ function parse_lock(text: pointer) -> []Manifest {
var k = 2 var k = 2
while k < len(ts) { while k < len(ts) {
let key = ts[k] let key = ts[k]
if key == "version" and k + 1 < len(ts) { m.ver = ts[k + 1]; k = k + 2 } if key == "version" and k + 1 < len(ts) { m.ver = ts[k + 1]; k += 2 }
else if key == "hash" and k + 1 < len(ts) { m.hash = ts[k + 1]; k = k + 2 } else if key == "hash" and k + 1 < len(ts) { m.hash = ts[k + 1]; k += 2 }
else if key == "kind" and k + 1 < len(ts) { m.kind = ts[k + 1]; k = k + 2 } else if key == "kind" and k + 1 < len(ts) { m.kind = ts[k + 1]; k += 2 }
else if key == "provides" and k + 1 < len(ts) { push(m.provides, ts[k + 1]); k = k + 2 } else if key == "provides" and k + 1 < len(ts) { push(m.provides, ts[k + 1]); k += 2 }
else { k = k + 1 } else { k += 1 }
} }
push(out, m) push(out, m)
} }
@ -201,7 +201,7 @@ function store_root() -> pointer {
function ensure_dir_slash(d: pointer) -> pointer { function ensure_dir_slash(d: pointer) -> pointer {
let n = slen(d) let n = slen(d)
if n == 0 { return d } if n == 0 { return d }
if d[n - 1] == 47 { return d } if d[n - 1] == '/' { return d }
return d + "/" return d + "/"
} }
@ -248,7 +248,7 @@ function latest_version(module: pointer) -> pointer {
let cand = strip_v(tg) let cand = strip_v(tg)
if best == "" or ver_gt(cand, best) { best = cand } if best == "" or ver_gt(cand, best) { best = cand }
} }
k = k + 1 k += 1
} }
return best return best
} }
@ -317,8 +317,8 @@ function link_module(module: pointer, hash: pointer) -> void {
function find_mod(mods: []pointer, m: pointer) -> int { function find_mod(mods: []pointer, m: pointer) -> int {
var i = 0 var i = 0
while i < len(mods) { if mods[i] == m { return i }; i = i + 1 } while i < len(mods) { if mods[i] == m { return i }; i += 1 }
return 0 - 1 return -1
} }
# resolve a root manifest to the selected build list (one Manifest per module, # resolve a root manifest to the selected build list (one Manifest per module,
@ -330,7 +330,7 @@ function resolve(root: Manifest) -> []Manifest {
var fm = new []pointer # the current requirement frontier var fm = new []pointer # the current requirement frontier
var fv = new []pointer var fv = new []pointer
var di = 0 var di = 0
while di < len(root.deps) { push(fm, root.deps[di].module); push(fv, root.deps[di].ver); di = di + 1 } while di < len(root.deps) { push(fm, root.deps[di].module); push(fv, root.deps[di].ver); di += 1 }
while len(fm) > 0 { while len(fm) > 0 {
var nm = new []pointer var nm = new []pointer
@ -346,9 +346,9 @@ function resolve(root: Manifest) -> []Manifest {
if take { if take {
let man = fetch_manifest(m, v) # expand the chosen version's requires let man = fetch_manifest(m, v) # expand the chosen version's requires
var k = 0 var k = 0
while k < len(man.deps) { push(nm, man.deps[k].module); push(nv, man.deps[k].ver); k = k + 1 } while k < len(man.deps) { push(nm, man.deps[k].module); push(nv, man.deps[k].ver); k += 1 }
} }
i = i + 1 i += 1
} }
fm = nm fm = nm
fv = nv fv = nv
@ -361,11 +361,11 @@ function resolve(root: Manifest) -> []Manifest {
var seen = new []pointer var seen = new []pointer
var q = new []pointer var q = new []pointer
var d2 = 0 var d2 = 0
while d2 < len(root.deps) { push(q, root.deps[d2].module); d2 = d2 + 1 } while d2 < len(root.deps) { push(q, root.deps[d2].module); d2 += 1 }
var qi = 0 var qi = 0
while qi < len(q) { while qi < len(q) {
let m = q[qi] let m = q[qi]
qi = qi + 1 qi += 1
if find_mod(seen, m) < 0 { if find_mod(seen, m) < 0 {
push(seen, m) push(seen, m)
let idx = find_mod(mods, m) let idx = find_mod(mods, m)
@ -374,7 +374,7 @@ function resolve(root: Manifest) -> []Manifest {
let man = fetch_manifest(m, ver) let man = fetch_manifest(m, ver)
push(out, man) push(out, man)
var k = 0 var k = 0
while k < len(man.deps) { push(q, man.deps[k].module); k = k + 1 } while k < len(man.deps) { push(q, man.deps[k].module); k += 1 }
} }
} }
} }
@ -387,9 +387,9 @@ function sort_mans(a: []Manifest) -> []Manifest {
while i < len(a) { while i < len(a) {
let x = a[i] let x = a[i]
var j = i - 1 var j = i - 1
while j >= 0 and (a[j].module > x.module) { a[j + 1] = a[j]; j = j - 1 } while j >= 0 and (a[j].module > x.module) { a[j + 1] = a[j]; j -= 1 }
a[j + 1] = x a[j + 1] = x
i = i + 1 i += 1
} }
return a return a
} }
@ -409,9 +409,9 @@ function collision(sels: []Manifest) -> pointer {
if at >= 0 { return `namespace {name} provided by both {owner[at]} and {sels[i].module}` } if at >= 0 { return `namespace {name} provided by both {owner[at]} and {sels[i].module}` }
push(ns, name) push(ns, name)
push(owner, sels[i].module) push(owner, sels[i].module)
p = p + 1 p += 1
} }
i = i + 1 i += 1
} }
return "" return ""
} }
@ -427,9 +427,9 @@ function write_lock(sels: []Manifest) -> bool {
let m = sorted[i] let m = sorted[i]
var line = `module "{m.module}" version "{m.ver}" hash "{m.hash}" kind "{m.kind}"` var line = `module "{m.module}" version "{m.ver}" hash "{m.hash}" kind "{m.kind}"`
var p = 0 var p = 0
while p < len(m.provides) { line = line + ` provides "{m.provides[p]}"`; p = p + 1 } while p < len(m.provides) { line += ` provides "{m.provides[p]}"`; p += 1 }
body = body + line + nl() body = body + line + nl()
i = i + 1 i += 1
} }
return write_file("package.lock.ludic", body) return write_file("package.lock.ludic", body)
} }
@ -462,7 +462,7 @@ function do_install(root: Manifest) -> int {
m.hash = `sha256:{h}` m.hash = `sha256:{h}`
link_module(m.module, h) link_module(m.module, h)
print(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`) print(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`)
i = i + 1 i += 1
} }
if not write_lock(sels) { err("x: cannot write package.lock.ludic\n"); return 1 } if not write_lock(sels) { err("x: cannot write package.lock.ludic\n"); return 1 }
print(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`) print(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`)
@ -480,7 +480,7 @@ function target_id() -> pointer {
} }
function has_target(m: Manifest, t: pointer) -> bool { function has_target(m: Manifest, t: pointer) -> bool {
var i = 0 var i = 0
while i < len(m.targets) { if m.targets[i] == t { return true }; i = i + 1 } while i < len(m.targets) { if m.targets[i] == t { return true }; i += 1 }
return false return false
} }
@ -498,9 +498,9 @@ function read_root_manifest() -> Manifest {
function split_spec(spec: pointer) -> []pointer { function split_spec(spec: pointer) -> []pointer {
var out = new []pointer var out = new []pointer
let n = slen(spec) let n = slen(spec)
var at = 0 - 1 var at = -1
var i = 0 var i = 0
while i < n { if spec[i] == 64 { at = i }; i = i + 1 } # '@' = 64 while i < n { if spec[i] == '@' { at = i }; i += 1 } # '@' = 64
if at < 0 { push(out, spec); push(out, "") } if at < 0 { push(out, spec); push(out, "") }
else { push(out, sslice(spec, 0, at)); push(out, sslice(spec, at + 1, n)) } else { push(out, sslice(spec, 0, at)); push(out, sslice(spec, at + 1, n)) }
return out return out
@ -575,7 +575,7 @@ function cmd_pkg_update() -> int {
set_require(d.module, latest) set_require(d.module, latest)
} }
} }
i = i + 1 i += 1
} }
return do_install(read_root_manifest()) return do_install(read_root_manifest())
} }
@ -596,19 +596,19 @@ function cmd_pkg_verify() -> int {
let dest = `{store_root()}{raw}` let dest = `{store_root()}{raw}`
if not file_exists(dest) { if not file_exists(dest) {
print(` MISSING {m.module}@{m.ver} (store entry {want} absent)`) print(` MISSING {m.module}@{m.ver} (store entry {want} absent)`)
bad_count = bad_count + 1 bad_count += 1
} else { } else {
let got = dir_hash(dest) let got = dir_hash(dest)
if got == raw { if got == raw {
let link = `ludic_modules/{m.module}` let link = `ludic_modules/{m.module}`
if file_exists(link) { print(` ok {m.module}@{m.ver}`) } if file_exists(link) { print(` ok {m.module}@{m.ver}`) }
else { print(` UNLINKED {m.module}@{m.ver} (ludic_modules view missing — run x get)`); bad_count = bad_count + 1 } else { print(` UNLINKED {m.module}@{m.ver} (ludic_modules view missing — run x get)`); bad_count += 1 }
} else { } else {
print(` TAMPERED {m.module}@{m.ver} (want sha256:{sslice(raw, 0, 12)}… got sha256:{sslice(got, 0, 12)}…)`) print(` TAMPERED {m.module}@{m.ver} (want sha256:{sslice(raw, 0, 12)}… got sha256:{sslice(got, 0, 12)}…)`)
bad_count = bad_count + 1 bad_count += 1
} }
} }
i = i + 1 i += 1
} }
if bad_count == 0 { print(`verified {string(len(locked))} package(s) against the store`); return 0 } if bad_count == 0 { print(`verified {string(len(locked))} package(s) against the store`); return 0 }
err(`x: {string(bad_count)} package(s) failed verification\n`) err(`x: {string(bad_count)} package(s) failed verification\n`)
@ -632,7 +632,7 @@ function cmd_pkg_vendor() -> int {
run(`mkdir -p "$(dirname {vdir})"`) run(`mkdir -p "$(dirname {vdir})"`)
run(`cp -R {dest} {vdir}`) run(`cp -R {dest} {vdir}`)
print(` vendored {m.module}@{m.ver}`) print(` vendored {m.module}@{m.ver}`)
i = i + 1 i += 1
} }
print(`vendored {string(len(locked))} package(s) into ./vendor — build offline with LUDIC_MODULES=vendor`) print(`vendored {string(len(locked))} package(s) into ./vendor — build offline with LUDIC_MODULES=vendor`)
return 0 return 0
@ -656,14 +656,14 @@ function cmd_pkg_build_lib() -> int {
let t = target_id() let t = target_id()
run(`mkdir -p lib/{t}`) run(`mkdir -p lib/{t}`)
let ludicc = getenv_or("LUDICC", "bin/ludicc") let ludicc = getenv_or("LUDICC", "bin/ludicc")
let ll = "/tmp/x_buildlib.ll" let ll = `{tmp_dir()}/buildlib.ll`
if not shq(`{ludicc} --emit-module {src} -o {ll} 2>/tmp/x_bl.err`) { if not shq(`{ludicc} --emit-module {src} -o {ll} 2>{tmp_dir()}/bl.err`) {
err(`x: build-lib compile failed — {capture_line("tail -1 /tmp/x_bl.err")}\n`); return 1 err(`x: build-lib compile failed — {capture_line("tail -1 {tmp_dir()}/bl.err")}\n`); return 1
} }
let out = `lib/{t}/lib{name}.dylib` let out = `lib/{t}/lib{name}.dylib`
# @rpath install name so a consumer resolves it via -rpath to the store dir # @rpath install name so a consumer resolves it via -rpath to the store dir
if not shq(`{cc()} -O2 -Wno-override-module -dynamiclib -undefined dynamic_lookup -Wl,-install_name,@rpath/lib{name}.dylib {ll} -o {out} 2>/tmp/x_bl.err`) { if not shq(`{cc()} -O2 -Wno-override-module -dynamiclib -undefined dynamic_lookup -Wl,-install_name,@rpath/lib{name}.dylib {ll} -o {out} 2>{tmp_dir()}/bl.err`) {
err(`x: build-lib link failed — {capture_line("tail -1 /tmp/x_bl.err")}\n`); return 1 err(`x: build-lib link failed — {capture_line("tail -1 {tmp_dir()}/bl.err")}\n`); return 1
} }
run(`rm -f {ll}`) run(`rm -f {ll}`)
print(`built {out} (target {t})`) print(`built {out} (target {t})`)
@ -700,12 +700,12 @@ function prebuilt_link_flags() -> pointer {
let dir = `{store_root()}{raw}/lib/{t}` let dir = `{store_root()}{raw}/lib/{t}`
let libs = split_lines(capture(`ls {dir}/*.dylib 2>/dev/null`)) let libs = split_lines(capture(`ls {dir}/*.dylib 2>/dev/null`))
var k = 0 var k = 0
while k < len(libs) { flags = flags + " " + libs[k]; any = true; k = k + 1 } while k < len(libs) { flags = flags + " " + libs[k]; any = true; k += 1 }
if len(libs) > 0 { flags = flags + ` -Wl,-rpath,{dir}` } if len(libs) > 0 { flags += ` -Wl,-rpath,{dir}` }
} }
i = i + 1 i += 1
} }
if any { flags = flags + " -Wl,-export_dynamic" } if any { flags += " -Wl,-export_dynamic" }
return flags return flags
} }

View file

@ -30,7 +30,7 @@ function cmd_test_pkg() -> int {
} }
let root = capture_line("pwd") let root = capture_line("pwd")
let work = "/tmp/x_pkg_test" let work = `{tmp_dir()}/pkg_test`
let proxy = `{work}/proxy` let proxy = `{work}/proxy`
let store = `{work}/store` let store = `{work}/store`
let proj = `{work}/proj` let proj = `{work}/proj`

View file

@ -49,9 +49,37 @@ function sh(cmd: pointer) -> int { return exit_code(run(cmd)) }
# run a command, true when it succeeded # run a command, true when it succeeded
function shq(cmd: pointer) -> bool { return exit_code(run(cmd)) == 0 } function shq(cmd: pointer) -> bool { return exit_code(run(cmd)) == 0 }
# ---- scratch files ------------------------------------------------------------
# Every scratch file the runner writes lives under one per-process directory
# (`$TMPDIR/x_<pid>`), so `x test` and an `x check-*` can run side by side without
# clobbering each other's captures. main removes it on the way out.
var x_tmp: pointer = null
function tmp_dir() -> pointer {
if x_tmp == null {
var base = Os.temp_dir()
if len(base) > 1 and base[len(base) - 1] == '/' { base = base[0..len(base) - 1] } # macOS: TMPDIR ends in '/'
x_tmp = `{base}/x_{Os.pid()}`
run(`mkdir -p {x_tmp}`)
}
return x_tmp
}
# a scratch path under tmp_dir(): tmp_path("foo.out")
function tmp_path(name: pointer) -> pointer { return `{tmp_dir()}/{name}` }
# remove the scratch directory (idempotent; a no-op when nothing was written).
# X_KEEP_TMP=1 leaves it in place, and says where, for debugging a failing case.
function tmp_cleanup() -> void {
if x_tmp == null { return }
if getenv_or("X_KEEP_TMP", "0") == "1" { print(`scratch kept: {x_tmp}`) }
else { run(`rm -rf {x_tmp}`) }
x_tmp = null
}
# run `cmd` and return its stdout (stderr discarded). Never null. # run `cmd` and return its stdout (stderr discarded). Never null.
function capture(cmd: pointer) -> pointer { function capture(cmd: pointer) -> pointer {
let tmp = "/tmp/x_capture.out" let tmp = tmp_path("capture.out")
run(`{cmd} > {tmp} 2>/dev/null`) run(`{cmd} > {tmp} 2>/dev/null`)
let s = read_file(tmp) let s = read_file(tmp)
if (s == null) { return "" } if (s == null) { return "" }
@ -83,7 +111,7 @@ function flat(p: pointer) -> pointer {
let b = bytes(n + 1) let b = bytes(n + 1)
for i in 0 .. n { for i in 0 .. n {
var c = p[i] var c = p[i]
if (c == 47) { c = 95 } # '/' (47) -> '_' (95) if (c == '/') { c = 95 } # '/' (47) -> '_' (95)
b[i] = c b[i] = c
} }
b[n] = 0 b[n] = 0
@ -108,11 +136,11 @@ var PASS: int = 0
var FAIL: int = 0 var FAIL: int = 0
function ok(msg: pointer) -> void { function ok(msg: pointer) -> void {
PASS = PASS + 1 PASS += 1
print(` {c_green()}PASS{c_reset()} {msg}`) print(` {c_green()}PASS{c_reset()} {msg}`)
} }
function bad(msg: pointer) -> void { function bad(msg: pointer) -> void {
FAIL = FAIL + 1 FAIL += 1
print(` {c_red()}FAIL{c_reset()} {msg}`) print(` {c_red()}FAIL{c_reset()} {msg}`)
} }
function bad2(msg: pointer, detail: pointer) -> void { function bad2(msg: pointer, detail: pointer) -> void {

View file

@ -59,16 +59,16 @@ function cmd_version() -> int {
} }
# assemble the CHANGELOG.md section for `ver` from the pending changesets into # assemble the CHANGELOG.md section for `ver` from the pending changesets into
# /tmp/x_rel_section.md (header + one bullet per changeset, sorted by type). # the scratch file rel_section.md (header + one bullet per changeset, sorted by type).
function build_section(ver: pointer) -> void { function build_section(ver: pointer) -> void {
let date = capture_line("date +%Y-%m-%d") let date = capture_line("date +%Y-%m-%d")
run(`printf '## v%s — %s\n\n' '{ver}' '{date}' > /tmp/x_rel_section.md`) run(`printf '## v%s — %s\n\n' '{ver}' '{date}' > {tmp_dir()}/rel_section.md`)
# one bullet per changeset: "- **<type>**: <body>" (body = the non-header text) # one bullet per changeset: "- **<type>**: <body>" (body = the non-header text)
run("( for f in changes/*.md; do case \"$f\" in */README.md) continue;; esac; t=$(sed -n 's/^type:[[:space:]]*//p' \"$f\" | head -1); b=$(grep -vE '^(type|bump):' \"$f\" | sed '/^[[:space:]]*$/d' | tr '\\n' ' ' | sed 's/[[:space:]]*$//'); printf -- '- **%s**: %s\\n' \"$t\" \"$b\"; done | sort ) >> /tmp/x_rel_section.md") run(`( for f in changes/*.md; do case \"$f\" in */README.md) continue;; esac; t=$(sed -n 's/^type:[[:space:]]*//p' \"$f\" | head -1); b=$(grep -vE '^(type|bump):' \"$f\" | sed '/^[[:space:]]*$/d' | tr '\\n' ' ' | sed 's/[[:space:]]*$//'); printf -- '- **%s**: %s\\n' \"$t\" \"$b\"; done | sort ) >> {tmp_dir()}/rel_section.md`)
run("printf '\n' >> /tmp/x_rel_section.md") run(`printf '\n' >> {tmp_dir()}/rel_section.md`)
} }
# prepend /tmp/x_rel_section.md into CHANGELOG.md, above the first existing # prepend the scratch rel_section.md into CHANGELOG.md, above the first existing
# release section (or at the end of the header if this is the first release). # release section (or at the end of the header if this is the first release).
function prepend_changelog() -> void { function prepend_changelog() -> void {
if not file_exists("CHANGELOG.md") { if not file_exists("CHANGELOG.md") {
@ -76,19 +76,18 @@ function prepend_changelog() -> void {
} }
let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1") let ln = capture_line("grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1")
if (ln == "") { if (ln == "") {
run("cat CHANGELOG.md /tmp/x_rel_section.md > /tmp/x_rel_new.md") run(`cat CHANGELOG.md {tmp_dir()}/rel_section.md > {tmp_dir()}/rel_new.md`)
} else { } else {
run(`head -n $(expr {ln} - 1) CHANGELOG.md > /tmp/x_rel_new.md`) run(`head -n $(expr {ln} - 1) CHANGELOG.md > {tmp_dir()}/rel_new.md`)
run("cat /tmp/x_rel_section.md >> /tmp/x_rel_new.md") run(`cat {tmp_dir()}/rel_section.md >> {tmp_dir()}/rel_new.md`)
run(`tail -n +{ln} CHANGELOG.md >> /tmp/x_rel_new.md`) run(`tail -n +{ln} CHANGELOG.md >> {tmp_dir()}/rel_new.md`)
} }
run("cp /tmp/x_rel_new.md CHANGELOG.md") run(`cp {tmp_dir()}/rel_new.md CHANGELOG.md`)
} }
# push main + the tag and create the Forgejo release with build artifacts. # push main + the tag and create the Forgejo release with build artifacts.
function publish_release(ver: pointer) -> int { function publish_release(ver: pointer) -> int {
let tok = getenv_or("FORGEJO_TOKEN", "") if getenv_or("FORGEJO_TOKEN", "") == "" { err("release --publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 }
if (tok == "") { err("release --publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 }
if not shq("git push origin HEAD") { err("release: git push (main) failed\n"); return 1 } if not shq("git push origin HEAD") { err("release: git push (main) failed\n"); return 1 }
if not shq(`git push origin v{ver}`) { err("release: git push (tag) failed\n"); return 1 } if not shq(`git push origin v{ver}`) { err("release: git push (tag) failed\n"); return 1 }
# artifacts: a reproducible source+seed tarball, and the built macOS toolchain. # artifacts: a reproducible source+seed tarball, and the built macOS toolchain.
@ -100,7 +99,7 @@ function publish_release(ver: pointer) -> int {
run(`tar -czf dist/ludic-{ver}-{plat}-{arch}.tar.gz bin selfhost/ludicc.seed.ll VERSION`) run(`tar -czf dist/ludic-{ver}-{plat}-{arch}.tar.gz bin selfhost/ludicc.seed.ll VERSION`)
} }
# the changelog section written by build_section is the release body # the changelog section written by build_section is the release body
if not shq(`FORGEJO_TOKEN='{tok}' python3 tools/ci/forgejo_release.py v{ver} /tmp/x_rel_section.md dist`) { if forgejo_publish(`v{ver}`, tmp_path("rel_section.md"), "dist") != 0 {
err("release: creating the Forgejo release failed\n"); return 1 err("release: creating the Forgejo release failed\n"); return 1
} }
return 0 return 0
@ -116,7 +115,7 @@ function cmd_release() -> int {
if (a == "--publish") { publish = true } if (a == "--publish") { publish = true }
else { if (a == "major") or (a == "minor") or (a == "patch") { level = a } else { if (a == "major") or (a == "minor") or (a == "patch") { level = a }
else { err(`release: unknown argument {a}\n`); return 1 } } else { err(`release: unknown argument {a}\n`); return 1 } }
ai = ai + 1 ai += 1
} }
if not has_changesets() { if not has_changesets() {

View file

@ -79,7 +79,7 @@ function write_selfhost_src(outpath: pointer) -> bool {
let s = read_file(frags[i]) let s = read_file(frags[i])
if (s != null) { file_write(f, s, len(s)) } if (s != null) { file_write(f, s, len(s)) }
file_write(f, "\n", 1) file_write(f, "\n", 1)
i = i + 1 i += 1
} }
let ftr = "}\n" let ftr = "}\n"
file_write(f, ftr, len(ftr)) file_write(f, ftr, len(ftr))
@ -131,11 +131,11 @@ function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
# strips the dead windowing branches so a headless link needs no win_* defs. # strips the dead windowing branches so a headless link needs no win_* defs.
# the quiet core, reused by the test suites; returns true on success. On failure # the quiet core, reused by the test suites; returns true on success. On failure
# the self-host/link diagnostics are left in /tmp/x_gb.err. # the self-host/link diagnostics are left in tmp_path("gb.err").
function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool { function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
let ll = `{outbin}.ll` let ll = `{outbin}.ll`
if not shq(`{shbin} {game} > {ll} 2>/tmp/x_gb.err`) { return false } if not shq(`{shbin} {game} > {ll} 2>{tmp_dir()}/gb.err`) { return false }
if not shq(`{cc()} -O2 {ll} -o {outbin} 2>/tmp/x_gb.err`) { return false } if not shq(`{cc()} -O2 {ll} -o {outbin} 2>{tmp_dir()}/gb.err`) { return false }
run(`rm -f {ll}`) run(`rm -f {ll}`)
return true return true
} }
@ -144,7 +144,7 @@ function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int { function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int {
if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 } if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 }
print("build failed:") print("build failed:")
out(capture("grep -i error /tmp/x_gb.err | head -5")) out(capture(`grep -i error {tmp_dir()}/gb.err | head -5`))
return 1 return 1
} }

View file

@ -4,9 +4,9 @@
# compile selfhost/tests/<name>.ludic with bin/ludicc, assemble, run, compare # compile selfhost/tests/<name>.ludic with bin/ludicc, assemble, run, compare
function sh_case(name: pointer, exp: pointer) -> void { function sh_case(name: pointer, exp: pointer) -> void {
let ll = `/tmp/x_sh_{name}.ll` let ll = `{tmp_dir()}/sh_{name}.ll`
let er = `/tmp/x_sh_{name}.err` let er = `{tmp_dir()}/sh_{name}.err`
let bn = `/tmp/x_sh_{name}` let bn = `{tmp_dir()}/sh_{name}`
if not shq(`bin/ludicc selfhost/tests/{name}.ludic > {ll} 2>{er}`) { if not shq(`bin/ludicc selfhost/tests/{name}.ludic > {ll} 2>{er}`) {
bad2(`{name}: self-host errored`, capture_line(`head -1 {er}`)); return bad2(`{name}: self-host errored`, capture_line(`head -1 {er}`)); return
} }
@ -22,9 +22,9 @@ function sh_case(name: pointer, exp: pointer) -> void {
# `bin/x golden` (see cmd_golden below). # `bin/x golden` (see cmd_golden below).
function game_case(path: pointer, keys: pointer) -> void { function game_case(path: pointer, keys: pointer) -> void {
let nm = flat(path) let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_g_{nm}`) { bad(`{path}: build`); return } if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/g_{nm}`) { bad(`{path}: build`); return }
run("mkdir -p build") run("mkdir -p build")
run(`printf '%s' '{keys}' | /tmp/x_g_{nm} >/dev/null 2>&1`) run(`printf '%s' '{keys}' | {tmp_dir()}/g_{nm} >/dev/null 2>&1`)
# The build + headless run above guard against a compile or runtime break on # The build + headless run above guard against a compile or runtime break on
# every host. The pixel-exact golden comparison, though, is against a render # every host. The pixel-exact golden comparison, though, is against a render
# blessed on macOS; text rasterization differs by a hair on other hosts, so # blessed on macOS; text rasterization differs by a hair on other hosts, so
@ -41,11 +41,11 @@ function game_case(path: pointer, keys: pointer) -> void {
# string on build failure). Shared by `bin/x golden` and nothing else. # string on build failure). Shared by `bin/x golden` and nothing else.
function bless_render(path: pointer, keys: pointer) -> pointer { function bless_render(path: pointer, keys: pointer) -> pointer {
let nm = flat(path) let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_bg_{nm}`) { if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/bg_{nm}`) {
err(`golden: {path} failed to build\n`); return "" err(`golden: {path} failed to build\n`); return ""
} }
run("mkdir -p build") run("mkdir -p build")
run(`printf '%s' '{keys}' | /tmp/x_bg_{nm} >/dev/null 2>&1`) run(`printf '%s' '{keys}' | {tmp_dir()}/bg_{nm} >/dev/null 2>&1`)
let h = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1") let h = capture_line("shasum -a 256 build/out.ppm | cut -d' ' -f1")
return `{path} {h}\n` return `{path} {h}\n`
} }
@ -58,10 +58,10 @@ function cmd_golden() -> int {
ensure_ludicc() ensure_ludicc()
if not is_exec("bin/ludicc") { err("golden: bin/ludicc unavailable\n"); return 1 } if not is_exec("bin/ludicc") { err("golden: bin/ludicc unavailable\n"); return 1 }
var manifest = "" var manifest = ""
manifest = manifest + bless_render("games/snake", "ddssaawwddss") manifest += bless_render("games/snake", "ddssaawwddss")
manifest = manifest + bless_render("games/menu", "ssss") manifest += bless_render("games/menu", "ssss")
manifest = manifest + bless_render("games/chronorift", "ddddwwwwaassK") manifest += bless_render("games/chronorift", "ddddwwwwaassK")
manifest = manifest + bless_render("rendering/draw_prims", "") manifest += bless_render("rendering/draw_prims", "")
if not write_file("selfhost/golden/renders.sha256", manifest) { if not write_file("selfhost/golden/renders.sha256", manifest) {
err("golden: could not write selfhost/golden/renders.sha256\n"); return 1 err("golden: could not write selfhost/golden/renders.sha256\n"); return 1
} }
@ -117,12 +117,12 @@ function cmd_selfhost_test() -> int {
game_case("rendering/draw_prims", "") game_case("rendering/draw_prims", "")
print("== the bootstrap fixpoint (seeded from bin/ludicc) ==") print("== the bootstrap fixpoint (seeded from bin/ludicc) ==")
if shq("bin/x bootstrap > /tmp/x_boot.out 2>&1") { ok("gen2.ll == gen3.ll (compiler reproduces itself)") } if shq(`bin/x bootstrap > {tmp_dir()}/boot.out 2>&1`) { ok("gen2.ll == gen3.ll (compiler reproduces itself)") }
else { bad("fixpoint not reached"); out(capture("cat /tmp/x_boot.out")) } else { bad("fixpoint not reached"); out(capture(`cat {tmp_dir()}/boot.out`)) }
print("== the C-free bootstrap (from the checked-in IR seed, no C compiler) ==") print("== the C-free bootstrap (from the checked-in IR seed, no C compiler) ==")
if shq("bin/x bootstrap-cfree > /tmp/x_cfree.out 2>&1") { ok("seed.ll rebuilds the compiler and is its own fixed point") } if shq(`bin/x bootstrap-cfree > {tmp_dir()}/cfree.out 2>&1`) { ok("seed.ll rebuilds the compiler and is its own fixed point") }
else { bad("C-free bootstrap failed (seed stale? run: x reseed)"); out(capture("cat /tmp/x_cfree.out")) } else { bad("C-free bootstrap failed (seed stale? run: x reseed)"); out(capture(`cat {tmp_dir()}/cfree.out`)) }
return report() return report()
} }

View file

@ -11,12 +11,12 @@
# its stdout against the expected space-joined output. # its stdout against the expected space-joined output.
function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void { function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
let nm = flat(path) let nm = flat(path)
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_e_{nm}`) { if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/e_{nm}`) {
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return bad2(label, capture_line(`grep -i error {tmp_dir()}/gb.err | head -1`)); return
} }
var got = "" var got = ""
if (keys == "") { got = capture_line(`/tmp/x_e_{nm} < /dev/null`) } if (keys == "") { got = capture_line(`{tmp_dir()}/e_{nm} < /dev/null`) }
else { got = capture_line(`printf '{keys}' | /tmp/x_e_{nm}`) } else { got = capture_line(`printf '{keys}' | {tmp_dir()}/e_{nm}`) }
if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) } if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
} }
@ -28,16 +28,16 @@ function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -
function controller_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void { function controller_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
let nm = flat(path) let nm = flat(path)
let root = capture_line("pwd") let root = capture_line("pwd")
let ll = `/tmp/x_c_{nm}.ll` let ll = `{tmp_dir()}/c_{nm}.ll`
if not shq(`LUDIC_HOME={root} LUDIC_MODULES={root}/packages bin/ludicc examples/{path}.ludic > {ll} 2>/tmp/x_gb.err`) { if not shq(`LUDIC_HOME={root} LUDIC_MODULES={root}/packages bin/ludicc examples/{path}.ludic > {ll} 2>{tmp_dir()}/gb.err`) {
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return bad2(label, capture_line(`grep -i error {tmp_dir()}/gb.err | head -1`)); return
} }
if not shq(`{cc()} -O2 {ll} -o /tmp/x_c_{nm} 2>/tmp/x_gb.err`) { if not shq(`{cc()} -O2 {ll} -o {tmp_dir()}/c_{nm} 2>{tmp_dir()}/gb.err`) {
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return bad2(label, capture_line(`grep -i error {tmp_dir()}/gb.err | head -1`)); return
} }
var got = "" var got = ""
if (keys == "") { got = capture_line(`/tmp/x_c_{nm} < /dev/null`) } if (keys == "") { got = capture_line(`{tmp_dir()}/c_{nm} < /dev/null`) }
else { got = capture_line(`printf '{keys}' | /tmp/x_c_{nm}`) } else { got = capture_line(`printf '{keys}' | {tmp_dir()}/c_{nm}`) }
if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) } if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
} }
@ -46,7 +46,7 @@ function controller_case(path: pointer, keys: pointer, exp: pointer, label: poin
# from $LUDIC_HOME (the toolchain install), independent of the package module root. # from $LUDIC_HOME (the toolchain install), independent of the package module root.
function runtime_home_case() -> void { function runtime_home_case() -> void {
let root = capture_line("pwd") let root = capture_line("pwd")
let dir = "/tmp/x_ext75" let dir = `{tmp_dir()}/ext75`
shq(`rm -rf {dir} && mkdir -p {dir}`) shq(`rm -rf {dir} && mkdir -p {dir}`)
write_file(`{dir}/game.ludic`, "program Ext {\n property Mob { hp: int = 0 }\n model M { Mob }\n entry {\n spawn M { Mob { hp: 7 } }\n let pm = World.prop_id(\"Mob\")\n let e = World.query_next(pm, 0)\n print(World.get(e, pm, World.field_id(pm, \"hp\")))\n }\n}\n") write_file(`{dir}/game.ludic`, "program Ext {\n property Mob { hp: int = 0 }\n model M { Mob }\n entry {\n spawn M { Mob { hp: 7 } }\n let pm = World.prop_id(\"Mob\")\n let e = World.query_next(pm, 0)\n print(World.get(e, pm, World.field_id(pm, \"hp\")))\n }\n}\n")
let lbl = "runtime ships with the toolchain, resolved from LUDIC_HOME (#75)" let lbl = "runtime ships with the toolchain, resolved from LUDIC_HOME (#75)"
@ -62,17 +62,17 @@ function runtime_home_case() -> void {
# a "does it still compile" smoke test (parse -> lower -> link), no run # a "does it still compile" smoke test (parse -> lower -> link), no run
function qsmoke(path: pointer) -> void { function qsmoke(path: pointer) -> void {
let nm = flat(path) let nm = flat(path)
if game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_q_{nm}`) { if game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/q_{nm}`) {
ok(`{path} compiles (@Queries desugars to S_QUERY)`) ok(`{path} compiles (@Queries desugars to S_QUERY)`)
} else { bad2(path, capture_line("grep -i error /tmp/x_gb.err | head -1")) } } else { bad2(path, capture_line(`grep -i error {tmp_dir()}/gb.err | head -1`)) }
} }
# a plain compile-only rot guard for a showcase example that has no self-asserting # a plain compile-only rot guard for a showcase example that has no self-asserting
# `entry` (so net_case can't drive it): prove it still parses, lowers and links. # `entry` (so net_case can't drive it): prove it still parses, lowers and links.
function smoke(path: pointer) -> void { function smoke(path: pointer) -> void {
let nm = flat(path) let nm = flat(path)
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o /tmp/x_sm_{nm}.ll > /tmp/x_sm_{nm}.err 2>&1`) { if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {tmp_dir()}/sm_{nm}.ll > {tmp_dir()}/sm_{nm}.err 2>&1`) {
bad2(path, capture_line(`tail -1 /tmp/x_sm_{nm}.err`)); return bad2(path, capture_line(`tail -1 {tmp_dir()}/sm_{nm}.err`)); return
} }
ok(`{path} still compiles`) ok(`{path} still compiles`)
} }
@ -81,17 +81,17 @@ function smoke(path: pointer) -> void {
# let clang assemble it, run, and compare stdout. No C host and no C compiled. # let clang assemble it, run, and compare stdout. No C host and no C compiled.
function net_case(path: pointer, exp: pointer) -> void { function net_case(path: pointer, exp: pointer) -> void {
let nm = flat(path) let nm = flat(path)
let ll = `/tmp/x_n_{nm}.ll` let ll = `{tmp_dir()}/n_{nm}.ll`
let log = `/tmp/x_n_{nm}.out` let log = `{tmp_dir()}/n_{nm}.out`
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) { if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
let t = capture_line(`tail -1 {log}`) let t = capture_line(`tail -1 {log}`)
bad2(path, `build ({t})`); return bad2(path, `build ({t})`); return
} }
if not shq(`{cc()} -O2 {ll} -o /tmp/x_nb_{nm} >> {log} 2>&1`) { if not shq(`{cc()} -O2 {ll} -o {tmp_dir()}/nb_{nm} >> {log} 2>&1`) {
let t = capture_line(`tail -1 {log}`) let t = capture_line(`tail -1 {log}`)
bad2(path, `build ({t})`); return bad2(path, `build ({t})`); return
} }
let got = capture_line(`/tmp/x_nb_{nm} < /dev/null`) let got = capture_line(`{tmp_dir()}/nb_{nm} < /dev/null`)
if (got == exp) { ok(`{path} ({got})`) } else { bad2(path, `got [{got}] want [{exp}]`) } if (got == exp) { ok(`{path} ({got})`) } else { bad2(path, `got [{got}] want [{exp}]`) }
} }
@ -100,16 +100,16 @@ function net_case(path: pointer, exp: pointer) -> void {
# exit (0) plus the expected `== N passed, M failed ==` summary on the last line. # exit (0) plus the expected `== N passed, M failed ==` summary on the last line.
function spec_case(path: pointer, exp: pointer) -> void { function spec_case(path: pointer, exp: pointer) -> void {
let nm = flat(path) let nm = flat(path)
let ll = `/tmp/x_spec_{nm}.ll` let ll = `{tmp_dir()}/spec_{nm}.ll`
let er = `/tmp/x_spec_{nm}.err` let er = `{tmp_dir()}/spec_{nm}.err`
if not shq(`bin/ludicc examples/{path}.ludic > {ll} 2>{er}`) { if not shq(`bin/ludicc examples/{path}.ludic > {ll} 2>{er}`) {
bad2(path, capture_line(`tail -1 {er}`)); return bad2(path, capture_line(`tail -1 {er}`)); return
} }
if not shq(`{cc()} -O2 {ll} -o /tmp/x_spec_{nm} 2>{er}`) { if not shq(`{cc()} -O2 {ll} -o {tmp_dir()}/spec_{nm} 2>{er}`) {
bad2(path, capture_line(`tail -1 {er}`)); return bad2(path, capture_line(`tail -1 {er}`)); return
} }
let rc = sh(`/tmp/x_spec_{nm} > /tmp/x_spec_{nm}.out 2>&1`) let rc = sh(`{tmp_dir()}/spec_{nm} > {tmp_dir()}/spec_{nm}.out 2>&1`)
let summary = capture_line(`tail -1 /tmp/x_spec_{nm}.out`) let summary = capture_line(`tail -1 {tmp_dir()}/spec_{nm}.out`)
if (rc == 0) and (summary == exp) { ok(`{path} ({summary})`) } if (rc == 0) and (summary == exp) { ok(`{path} ({summary})`) }
else { bad2(path, `rc={string(rc)} last=[{summary}]`) } else { bad2(path, `rc={string(rc)} last=[{summary}]`) }
} }
@ -117,12 +117,12 @@ function spec_case(path: pointer, exp: pointer) -> void {
# issue #8: the failure path — a panic must abort with a non-zero exit and a # issue #8: the failure path — a panic must abort with a non-zero exit and a
# `file:line: panic: <msg>` line on stderr (stdout is unaffected up to the panic). # `file:line: panic: <msg>` line on stderr (stdout is unaffected up to the panic).
function panic_case() -> void { function panic_case() -> void {
write_file("/tmp/x_panic.ludic", "program P { entry { print(7); panic(\"boom\") } }\n") write_file(`{tmp_dir()}/panic.ludic`, "program P { entry { print(7); panic(\"boom\") } }\n")
if not shq("bin/ludicc /tmp/x_panic.ludic > /tmp/x_panic.ll 2>/dev/null") { bad("panic example did not compile"); return } if not shq(`bin/ludicc {tmp_dir()}/panic.ludic > {tmp_dir()}/panic.ll 2>/dev/null`) { bad("panic example did not compile"); return }
if not shq(`{cc()} -O2 /tmp/x_panic.ll -o /tmp/x_panic 2>/dev/null`) { bad("panic example did not link"); return } if not shq(`{cc()} -O2 {tmp_dir()}/panic.ll -o {tmp_dir()}/panic 2>/dev/null`) { bad("panic example did not link"); return }
let rc = sh("/tmp/x_panic > /tmp/x_panic.out 2>/tmp/x_panic.err") let rc = sh(`{tmp_dir()}/panic > {tmp_dir()}/panic.out 2>{tmp_dir()}/panic.err`)
let msg = capture_line("cat /tmp/x_panic.err") let msg = capture_line(`cat {tmp_dir()}/panic.err`)
if (rc != 0) and shq("grep -q 'panic: boom' /tmp/x_panic.err") { ok(`panic aborts non-zero with a located stderr message ({msg})`) } if (rc != 0) and shq(`grep -q 'panic: boom' {tmp_dir()}/panic.err`) { ok(`panic aborts non-zero with a located stderr message ({msg})`) }
else { bad2("panic", `rc={string(rc)} err=[{msg}]`) } else { bad2("panic", `rc={string(rc)} err=[{msg}]`) }
} }
@ -132,14 +132,14 @@ function panic_case() -> void {
# so this reuses the same seed-built bin/ludicc the rest of the suite does. # so this reuses the same seed-built bin/ludicc the rest of the suite does.
function cov_one(path: pointer, dir: pointer) -> int { function cov_one(path: pointer, dir: pointer) -> int {
let nm = flat(path) let nm = flat(path)
let ll = `/tmp/x_cov_{nm}.ll` let ll = `{tmp_dir()}/cov_{nm}.ll`
let bin = `/tmp/x_cov_{nm}` let bin = `{tmp_dir()}/cov_{nm}`
let cov = `{dir}/{nm}.cov` let cov = `{dir}/{nm}.cov`
if not shq(`bin/ludicc --coverage examples/{path}.ludic > {ll} 2>/tmp/x_cov.err`) { if not shq(`bin/ludicc --coverage examples/{path}.ludic > {ll} 2>{tmp_dir()}/cov.err`) {
bad2(path, capture_line("tail -1 /tmp/x_cov.err")); return 1 bad2(path, capture_line(`tail -1 {tmp_dir()}/cov.err`)); return 1
} }
if not shq(`{cc()} -O2 {ll} -o {bin} 2>/tmp/x_cov.err`) { if not shq(`{cc()} -O2 {ll} -o {bin} 2>{tmp_dir()}/cov.err`) {
bad2(path, capture_line("tail -1 /tmp/x_cov.err")); return 1 bad2(path, capture_line(`tail -1 {tmp_dir()}/cov.err`)); return 1
} }
run(`rm -f {ll}`) run(`rm -f {ll}`)
# run the spec; its atexit hook writes the dump to $LUDIC_COVERAGE # run the spec; its atexit hook writes the dump to $LUDIC_COVERAGE
@ -159,8 +159,8 @@ function cov_one(path: pointer, dir: pointer) -> int {
var line = ` examples/{path}.ludic {string(covered)}/{string(total)} lines {string(pct)}%` var line = ` examples/{path}.ludic {string(covered)}/{string(total)} lines {string(pct)}%`
if not (missed == "") { line = `{line} uncovered: {missed}` } if not (missed == "") { line = `{line} uncovered: {missed}` }
print(line) print(line)
COV_COVERED = COV_COVERED + covered COV_COVERED += covered
COV_TOTAL = COV_TOTAL + total COV_TOTAL += total
return 0 return 0
} }
@ -170,15 +170,15 @@ var COV_TOTAL: int = 0
function cmd_test_coverage() -> int { function cmd_test_coverage() -> int {
# the suite normally rebuilds bin/ludicc from the seed; do the same here so the # the suite normally rebuilds bin/ludicc from the seed; do the same here so the
# instrumentation path is exactly what a clean checkout ships. # instrumentation path is exactly what a clean checkout ships.
run("mkdir -p bin build /tmp/x_cov_out") run(`mkdir -p bin build {tmp_dir()}/cov_out`)
if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") { if (not is_exec("bin/ludicc")) or newer("selfhost/ludicc.seed.ll", "bin/ludicc") {
if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("x: cannot build bin/ludicc from the seed\n"); return 1 } if not shq(`{cc()} selfhost/ludicc.seed.ll -o bin/ludicc`) { err("x: cannot build bin/ludicc from the seed\n"); return 1 }
} }
COV_COVERED = 0 COV_COVERED = 0
COV_TOTAL = 0 COV_TOTAL = 0
print("== line coverage (bin/x test --coverage) ==") print("== line coverage (bin/x test --coverage) ==")
cov_one("library/coverage", "/tmp/x_cov_out") cov_one("library/coverage", `{tmp_dir()}/cov_out`)
cov_one("library/testing", "/tmp/x_cov_out") cov_one("library/testing", `{tmp_dir()}/cov_out`)
var pct = 100 var pct = 100
if COV_TOTAL > 0 { pct = (COV_COVERED * 100) / COV_TOTAL } if COV_TOTAL > 0 { pct = (COV_COVERED * 100) / COV_TOTAL }
print(` ----`) print(` ----`)
@ -209,10 +209,10 @@ function cmd_test() -> int {
game_case("games/chronorift", "ddddwwwwaassK") game_case("games/chronorift", "ddddwwwwaassK")
print("== self-hosting and the bootstrap fixpoints ==") print("== self-hosting and the bootstrap fixpoints ==")
run("bin/x selfhost-test > /tmp/x_sh.out 2>&1") run(`bin/x selfhost-test > {tmp_dir()}/sh.out 2>&1`)
if shq("grep -q 'gen2.ll == gen3.ll' /tmp/x_sh.out") { ok("the compiler reproduces itself (fixpoint)") } else { bad("fixpoint") } if shq(`grep -q 'gen2.ll == gen3.ll' {tmp_dir()}/sh.out`) { ok("the compiler reproduces itself (fixpoint)") } else { bad("fixpoint") }
if shq("grep -q 'no C compiler' /tmp/x_sh.out") { ok("the compiler rebuilds itself from the seed, C-free") } else { bad("C-free bootstrap") } if shq(`grep -q 'no C compiler' {tmp_dir()}/sh.out`) { ok("the compiler rebuilds itself from the seed, C-free") } else { bad("C-free bootstrap") }
let corr = capture_line("grep -c PASS /tmp/x_sh.out") let corr = capture_line(`grep -c PASS {tmp_dir()}/sh.out`)
ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`) ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`)
print("== documented syntax stays compilable (guards against spec/compiler drift) ==") print("== documented syntax stays compilable (guards against spec/compiler drift) ==")
@ -221,6 +221,7 @@ function cmd_test() -> int {
feat_case("lang/lifecycle", "", "1 700 50 950 2", "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)") feat_case("lang/lifecycle", "", "1 700 50 950 2", "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)")
feat_case("lang/toggle", "", "6 0 7 1 0", "toggle.ludic (enable/disable + @OnDisable/@OnEnable across property/model)") feat_case("lang/toggle", "", "6 0 7 1 0", "toggle.ludic (enable/disable + @OnDisable/@OnEnable across property/model)")
feat_case("lang/strings", "", "1 2 3 4 5 6 7 8 9", "strings.ludic (str ops, interpolation, slicing)") feat_case("lang/strings", "", "1 2 3 4 5 6 7 8 9", "strings.ludic (str ops, interpolation, slicing)")
feat_case("lang/operators", "", "1 2 3 4 5 6 7 8 9 10", "operators.ludic (compound assignment on fixed/string/long, -fixed, char escapes, list literals)")
feat_case("lang/rng_demo", "", "69 89 6 -1 0", "rng_demo.ludic (Random.value/int/sign)") feat_case("lang/rng_demo", "", "69 89 6 -1 0", "rng_demo.ludic (Random.value/int/sign)")
feat_case("lang/time_demo", "aaaaaa", "0 1 2 3 4 16", "time_demo.ludic (Time.frame/elapsed/delta)") feat_case("lang/time_demo", "aaaaaa", "0 1 2 3 4 16", "time_demo.ludic (Time.frame/elapsed/delta)")
feat_case("lang/scenes", "aaaaaaaaaa", "1000 1 101 102 2 3 900 201 900 202 900", "scenes.ludic (scene/layer/on enter/on exit/become)") feat_case("lang/scenes", "aaaaaaaaaa", "1000 1 101 102 2 3 900 201 900 202 900", "scenes.ludic (scene/layer/on enter/on exit/become)")
@ -289,7 +290,7 @@ function cmd_test() -> int {
# Os known-folders/arch and Fs.list read the BSD utsname/dirent layout, so # Os known-folders/arch and Fs.list read the BSD utsname/dirent layout, so
# their asserted values are macOS-specific; skip off Darwin (see is_darwin). # their asserted values are macOS-specific; skip off Darwin (see is_darwin).
if is_darwin() { if is_darwin() {
feat_case("library/os", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "os.ludic (Os args/env round-trip, platform/arch, known dirs)") feat_case("library/os", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "os.ludic (Os args/env round-trip, platform/arch, known dirs, pid)")
} else { skip("os.ludic (Os known-folder / uname ABI is macOS/BSD-specific)") } } else { skip("os.ludic (Os known-folder / uname ABI is macOS/BSD-specific)") }
feat_case("library/unicode", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "unicode.ludic (UTF-8 len/validate/char_at/chars/case/truncate/graphemes)") feat_case("library/unicode", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "unicode.ludic (UTF-8 len/validate/char_at/chars/case/truncate/graphemes)")
if is_darwin() { if is_darwin() {
@ -342,11 +343,11 @@ function cmd_test() -> int {
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The # canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
# run exercises the pure-Ludic response parser; no network is touched. # run exercises the pure-Ludic response parser; no network is touched.
if is_darwin() { if is_darwin() {
if shq("LUDIC_HOME=. bin/ludicc examples/library/http.ludic -o /tmp/x_http_demo > /tmp/x_http.out 2>&1") { if shq(`LUDIC_HOME=. bin/ludicc examples/library/http.ludic -o {tmp_dir()}/http_demo > {tmp_dir()}/http.out 2>&1`) {
let got = capture_line("/tmp/x_http_demo < /dev/null") let got = capture_line(`{tmp_dir()}/http_demo < /dev/null`)
if (got == "200 1 5 hello 42 text/plain 404 0") { ok("http.ludic (#6 Http.parse/status/ok/text/header — pure response parser)") } if (got == "200 1 5 hello 42 text/plain 404 0") { ok("http.ludic (#6 Http.parse/status/ok/text/header — pure response parser)") }
else { bad2("http.ludic", `got [{got}]`) } else { bad2("http.ludic", `got [{got}]`) }
} else { bad2("http.ludic build", capture_line("tail -1 /tmp/x_http.out")) } } else { bad2("http.ludic build", capture_line(`tail -1 {tmp_dir()}/http.out`)) }
} }
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)") feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
@ -369,37 +370,37 @@ function cmd_test() -> int {
print("== self-hosted front-end binaries (ludicc / ludic) ==") print("== self-hosted front-end binaries (ludicc / ludic) ==")
# the two commands are one multi-call native binary built from the seed with # the two commands are one multi-call native binary built from the seed with
# clang alone. ludicc compiles; ludic compiles-and-runs. # clang alone. ludicc compiles; ludic compiles-and-runs.
if shq("bin/x build-cli > /tmp/x_cli.out 2>&1") and is_exec("bin/ludicc") and is_exec("bin/ludic") { if shq(`bin/x build-cli > {tmp_dir()}/cli.out 2>&1`) and is_exec("bin/ludicc") and is_exec("bin/ludic") {
ok("x build-cli builds bin/ludicc and bin/ludic from the seed (clang-only)") ok("x build-cli builds bin/ludicc and bin/ludic from the seed (clang-only)")
} else { bad2("x build-cli", capture_line("tail -1 /tmp/x_cli.out")) } } else { bad2("x build-cli", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
# ludicc -o drives clang to a native binary. A game links windowed by default # ludicc -o drives clang to a native binary. A game links windowed by default
# (LUDIC_HOME points the linker at runtime/native/cocoa.ll) — that path is # (LUDIC_HOME points the linker at runtime/native/cocoa.ll) — that path is
# macOS-only (Cocoa), so off Darwin we prove the same "-o yields a runnable # macOS-only (Cocoa), so off Darwin we prove the same "-o yields a runnable
# native binary" with a headless link, which needs no window backend. # native binary" with a headless link, which needs no window backend.
if is_darwin() { if is_darwin() {
if shq("LUDIC_HOME=. bin/ludicc examples/games/snake.ludic -o /tmp/x_cli_snake > /tmp/x_cli.out 2>&1") and shq("file /tmp/x_cli_snake 2>/dev/null | grep -q Mach-O") { if shq(`LUDIC_HOME=. bin/ludicc examples/games/snake.ludic -o {tmp_dir()}/cli_snake > {tmp_dir()}/cli.out 2>&1`) and shq(`file {tmp_dir()}/cli_snake 2>/dev/null | grep -q Mach-O`) {
ok("ludicc app.ludic -o bin -> native executable") ok("ludicc app.ludic -o bin -> native executable")
} else { bad2("ludicc -o", capture_line("tail -1 /tmp/x_cli.out")) } } else { bad2("ludicc -o", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
# #51 — a windowed build must load GameController so the native gamepad # #51 — a windowed build must load GameController so the native gamepad
# polling (win_pad, reached on the is_windowed branch) can see controllers. # polling (win_pad, reached on the is_windowed branch) can see controllers.
if shq("otool -L /tmp/x_cli_snake 2>/dev/null | grep -q GameController") { if shq(`otool -L {tmp_dir()}/cli_snake 2>/dev/null | grep -q GameController`) {
ok("windowed link loads GameController (#51 gamepad binding)") ok("windowed link loads GameController (#51 gamepad binding)")
} else { bad("windowed binary is missing the GameController load command") } } else { bad("windowed binary is missing the GameController load command") }
} else { } else {
if shq("LUDIC_HOME=. bin/ludicc --headless examples/games/snake.ludic -o /tmp/x_cli_snake > /tmp/x_cli.out 2>&1") and shq("test -x /tmp/x_cli_snake") { if shq(`LUDIC_HOME=. bin/ludicc --headless examples/games/snake.ludic -o {tmp_dir()}/cli_snake > {tmp_dir()}/cli.out 2>&1`) and shq(`test -x {tmp_dir()}/cli_snake`) {
ok("ludicc app.ludic -o bin -> native executable (headless)") ok("ludicc app.ludic -o bin -> native executable (headless)")
} else { bad2("ludicc -o", capture_line("tail -1 /tmp/x_cli.out")) } } else { bad2("ludicc -o", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
} }
# --emit-llvm stops at the IR. # --emit-llvm stops at the IR.
if shq("bin/ludicc examples/games/snake.ludic --emit-llvm -o /tmp/x_cli_snake.ll > /tmp/x_cli.out 2>&1") and shq("head -1 /tmp/x_cli_snake.ll | grep -q 'LLVM IR'") { if shq(`bin/ludicc examples/games/snake.ludic --emit-llvm -o {tmp_dir()}/cli_snake.ll > {tmp_dir()}/cli.out 2>&1`) and shq(`head -1 {tmp_dir()}/cli_snake.ll | grep -q 'LLVM IR'`) {
ok("ludicc --emit-llvm -> LLVM IR") ok("ludicc --emit-llvm -> LLVM IR")
} else { bad2("ludicc --emit-llvm", capture_line("tail -1 /tmp/x_cli.out")) } } else { bad2("ludicc --emit-llvm", capture_line(`tail -1 {tmp_dir()}/cli.out`)) }
# ludic compiles, runs, and forwards the program's exit code. # ludic compiles, runs, and forwards the program's exit code.
write_file("/tmp/x_cli_exit.ludic", "entry { exit(42) }\n") write_file(`{tmp_dir()}/cli_exit.ludic`, "entry { exit(42) }\n")
let rc = sh("LUDIC_HOME=. bin/ludic /tmp/x_cli_exit.ludic > /tmp/x_cli.out 2>&1") let rc = sh(`LUDIC_HOME=. bin/ludic {tmp_dir()}/cli_exit.ludic > {tmp_dir()}/cli.out 2>&1`)
if (rc == 42) { ok("ludic app.ludic -> compiles, runs, forwards exit code") } if (rc == 42) { ok("ludic app.ludic -> compiles, runs, forwards exit code") }
else { bad2("ludic run: expected exit 42", `got {string(rc)}`) } else { bad2("ludic run: expected exit 42", `got {string(rc)}`) }
@ -408,26 +409,26 @@ function cmd_test() -> int {
# stands up throwaway git repos, so like selfhost-test it runs as a sub-process # stands up throwaway git repos, so like selfhost-test it runs as a sub-process
# and this stage checks its footer). # and this stage checks its footer).
print("== package manager (x test-pkg) ==") print("== package manager (x test-pkg) ==")
run("bin/x test-pkg > /tmp/x_pkg.out 2>&1") run(`bin/x test-pkg > {tmp_dir()}/pkg.out 2>&1`)
if shq("grep -q '0 failed' /tmp/x_pkg.out") { if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) {
let pc = capture_line("grep -c PASS /tmp/x_pkg.out") let pc = capture_line(`grep -c PASS {tmp_dir()}/pkg.out`)
ok(`package-manager suite: {pc} checks passed (see: x test-pkg)`) ok(`package-manager suite: {pc} checks passed (see: x test-pkg)`)
} else { bad2("x test-pkg", capture_line("grep -i fail /tmp/x_pkg.out | head -1")) } } else { bad2("x test-pkg", capture_line(`grep -i fail {tmp_dir()}/pkg.out | head -1`)) }
# docs site generator + guard, in Ludic (no Python). docs-gen emits the whole # docs site generator + guard, in Ludic (no Python). docs-gen emits the whole
# pages payload; docs-check is its coverage/integrity guard; docs-palette is # pages payload; docs-check is its coverage/integrity guard; docs-palette is
# the named-colour source of truth (its output is the tracked emit_color.ludic # the named-colour source of truth (its --check mode regenerates the tracked
# + palette.json, so a clean git diff proves it stayed byte-identical). # emit_color.ludic + palette.json into scratch files and compares).
print("== docs site (Ludic generator, no Python) ==") print("== docs site (Ludic generator, no Python) ==")
if shq("bin/x docs-gen --out /tmp/x_docs > /tmp/x_docs.out 2>&1") { if shq(`bin/x docs-gen --out {tmp_dir()}/docs > {tmp_dir()}/docs.out 2>&1`) {
ok("x docs-gen -> the pages-branch payload") ok("x docs-gen -> the pages-branch payload")
} else { bad2("x docs-gen", capture_line("tail -1 /tmp/x_docs.out")) } } else { bad2("x docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) }
if shq("bin/x docs-check /tmp/x_docs > /tmp/x_docs_chk.out 2>&1") { if shq(`bin/x docs-check {tmp_dir()}/docs > {tmp_dir()}/docs_chk.out 2>&1`) {
ok("x docs-check passes on the generated site") ok("x docs-check passes on the generated site")
} else { bad2("x docs-check", capture_line("tail -1 /tmp/x_docs_chk.out")) } } else { bad2("x docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) }
if shq("bin/x docs-palette > /dev/null 2>&1") and shq("git diff --quiet selfhost/backend/stdlib/emit_color.ludic docs/language/colors/palette.json") { if shq(`bin/x docs-palette --check > {tmp_dir()}/pal.out 2>&1`) {
ok("x docs-palette regenerates emit_color.ludic + palette.json byte-identically") ok("x docs-palette regenerates emit_color.ludic + palette.json byte-identically")
} else { bad("x docs-palette drifted (git diff not empty)") } } else { bad2("x docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) }
return report() return report()
} }

View file

@ -27,7 +27,7 @@ function sync_vscode_grammar() -> void {
# is `flag` present anywhere in argv? # is `flag` present anywhere in argv?
function has_flag(flag: pointer) -> bool { function has_flag(flag: pointer) -> bool {
var i = 2 var i = 2
while i < arg_count() { if (arg(i) == flag) { return true }; i = i + 1 } while i < arg_count() { if (arg(i) == flag) { return true }; i += 1 }
return false return false
} }
@ -64,12 +64,12 @@ function cmd_test_tools() -> int {
# --- idempotence: formatting twice equals formatting once ------------------ # --- idempotence: formatting twice equals formatting once ------------------
let nsrc = capture_line("find examples runtime -name '*.ludic' | wc -l") let nsrc = capture_line("find examples runtime -name '*.ludic' | wc -l")
if shq(`fail=0; for f in $(find examples runtime -name '*.ludic' | sort); do {fmt} "$f" > /tmp/x_a 2>/dev/null; {fmt} /tmp/x_a > /tmp/x_b 2>/dev/null; cmp -s /tmp/x_a /tmp/x_b || fail=1; done; test $fail -eq 0`) { if shq(`fail=0; for f in $(find examples runtime -name '*.ludic' | sort); do {fmt} "$f" > {tmp_dir()}/a 2>/dev/null; {fmt} {tmp_dir()}/a > {tmp_dir()}/b 2>/dev/null; cmp -s {tmp_dir()}/a {tmp_dir()}/b || fail=1; done; test $fail -eq 0`) {
ok(`idempotent on {nsrc} files`) ok(`idempotent on {nsrc} files`)
} else { bad("idempotent on the source tree") } } else { bad("idempotent on the source tree") }
# --- meaning preserved: reformatting a formatted file must be a no-op ------- # --- meaning preserved: reformatting a formatted file must be a no-op -------
if shq(`fail=0; for f in examples/ecs/hello.ludic examples/games/snake.ludic examples/games/menu.ludic examples/lang/qdecl.ludic examples/lang/scenes.ludic; do test -f "$f" || continue; {fmt} "$f" > /tmp/x_f1 2>/dev/null; {fmt} /tmp/x_f1 > /tmp/x_f2 2>/dev/null; cmp -s /tmp/x_f1 /tmp/x_f2 || fail=1; done; test $fail -eq 0`) { if shq(`fail=0; for f in examples/ecs/hello.ludic examples/games/snake.ludic examples/games/menu.ludic examples/lang/qdecl.ludic examples/lang/scenes.ludic; do test -f "$f" || continue; {fmt} "$f" > {tmp_dir()}/f1 2>/dev/null; {fmt} {tmp_dir()}/f1 > {tmp_dir()}/f2 2>/dev/null; cmp -s {tmp_dir()}/f1 {tmp_dir()}/f2 || fail=1; done; test $fail -eq 0`) {
ok("formatting is idempotent (fmt(fmt(x)) == fmt(x))") ok("formatting is idempotent (fmt(fmt(x)) == fmt(x))")
} else { bad("formatting is not idempotent") } } else { bad("formatting is not idempotent") }
@ -81,47 +81,47 @@ function cmd_test_tools() -> int {
} else { bad("formatter altered non-whitespace bytes") } } else { bad("formatter altered non-whitespace bytes") }
# --- multi-byte characters must survive intact ----------------------------- # --- multi-byte characters must survive intact -----------------------------
write_file("/tmp/x_utf8.ludic", "program E {\n # … ellipsis in a comment\n const S: string = \"café → naïve\"\n}\n") write_file(`{tmp_dir()}/utf8.ludic`, "program E {\n # … ellipsis in a comment\n const S: string = \"café → naïve\"\n}\n")
if shq(`{fmt} /tmp/x_utf8.ludic > /tmp/x_utf8.out 2>/dev/null; cmp -s /tmp/x_utf8.ludic /tmp/x_utf8.out`) { if shq(`{fmt} {tmp_dir()}/utf8.ludic > {tmp_dir()}/utf8.out 2>/dev/null; cmp -s {tmp_dir()}/utf8.ludic {tmp_dir()}/utf8.out`) {
ok("UTF-8 preserved byte for byte") ok("UTF-8 preserved byte for byte")
} else { bad("UTF-8 mangled") } } else { bad("UTF-8 mangled") }
write_file("/tmp/x_utf8b.ludic", "program F {\nfn f() -> int { return … }\n}\n") write_file(`{tmp_dir()}/utf8b.ludic`, "program F {\nfn f() -> int { return … }\n}\n")
let ua = capture_line("tr -d ' \t\n' < /tmp/x_utf8b.ludic | shasum | cut -d' ' -f1") let ua = capture_line(`tr -d ' \t\n' < {tmp_dir()}/utf8b.ludic | shasum | cut -d' ' -f1`)
let ub = capture_line(`{fmt} /tmp/x_utf8b.ludic | tr -d ' \t\n' | shasum | cut -d' ' -f1`) let ub = capture_line(`{fmt} {tmp_dir()}/utf8b.ludic | tr -d ' \t\n' | shasum | cut -d' ' -f1`)
check("unrecognised characters pass through whole", ub, ua) check("unrecognised characters pass through whole", ub, ua)
# --- comments survive, the whole reason this is not `ludicc --fmt` ---------- # --- comments survive, the whole reason this is not `ludicc --fmt` ----------
write_file("/tmp/x_c.ludic", "program C {\n # keep me\n property P { x: int = 0 } # and me\n}\n") write_file(`{tmp_dir()}/c.ludic`, "program C {\n # keep me\n property P { x: int = 0 } # and me\n}\n")
let ncmt = capture_line(`{fmt} /tmp/x_c.ludic | grep -c '#'`) let ncmt = capture_line(`{fmt} {tmp_dir()}/c.ludic | grep -c '#'`)
check("comments preserved", ncmt, "2") check("comments preserved", ncmt, "2")
# --- messy input is normalised --------------------------------------------- # --- messy input is normalised ---------------------------------------------
write_file("/tmp/x_m.ludic", "program M{\nproperty P{x:int=0}\nhandler S phase Update{let a=1+2\nif a== -1 {a=-a}}\n}\n") write_file(`{tmp_dir()}/m.ludic`, "program M{\nproperty P{x:int=0}\nhandler S phase Update{let a=1+2\nif a== -1 {a=-a}}\n}\n")
run(`{fmt} /tmp/x_m.ludic > /tmp/x_m.out 2>/dev/null`) run(`{fmt} {tmp_dir()}/m.ludic > {tmp_dir()}/m.out 2>/dev/null`)
if shq("grep -q '^ property P { x: int = 0 }$' /tmp/x_m.out") { ok("reindents and respaces") } else { bad("reindents and respaces") } if shq(`grep -q '^ property P {{ x: int = 0 }}$' {tmp_dir()}/m.out`) { ok("reindents and respaces") } else { bad("reindents and respaces") }
if shq("grep -q 'a = -a' /tmp/x_m.out") { ok("unary minus not spaced") } else { bad("unary minus not spaced") } if shq(`grep -q 'a = -a' {tmp_dir()}/m.out`) { ok("unary minus not spaced") } else { bad("unary minus not spaced") }
# --- language conventions are respected, not overwritten ------------------- # --- language conventions are respected, not overwritten -------------------
write_file("/tmp/x_u.ludic", "program U {\n ui M {\n panel id=Root w=10 {\n button id=Go text=\"go\"\n }\n }\n}\n") write_file(`{tmp_dir()}/u.ludic`, "program U {\n ui M {\n panel id: Root w: 10 {\n button id: Go text: \"go\"\n }\n }\n}\n")
if shq(`{fmt} /tmp/x_u.ludic | grep -q 'id=Root'`) { ok("widget props stay tight (id=Root)") } else { bad("widget props stay tight") } if shq(`{fmt} {tmp_dir()}/u.ludic | grep -q 'panel id: Root w: 10 {{'`) { ok("widget props keep their `key: value` spacing") } else { bad("widget props keep their `key: value` spacing") }
write_file("/tmp/x_q.ludic", "program Q {\n handler S phase Update {\n for (p) in query [Pos, {Foe}] { }\n }\n property Pos { x: int = 0 }\n property Foe { }\n}\n") write_file(`{tmp_dir()}/q.ludic`, "program Q {\n handler S phase Update {\n for (p) in query [Pos, {Foe}] { }\n }\n property Pos { x: int = 0 }\n property Foe { }\n}\n")
if shq(`{fmt} /tmp/x_q.ludic | grep -q '\\[Pos, {{Foe}}\\]'`) { ok("query tags stay tight ({Foe})") } else { bad("query tags stay tight") } if shq(`{fmt} {tmp_dir()}/q.ludic | grep -q '\\[Pos, {{Foe}}\\]'`) { ok("query tags stay tight ({Foe})") } else { bad("query tags stay tight") }
write_file("/tmp/x_al.ludic", "program A {\n const R_A: int = 0 # one\n const R_BBB: int = 1 # two\n}\n") write_file(`{tmp_dir()}/al.ludic`, "program A {\n const R_A: int = 0 # one\n const R_BBB: int = 1 # two\n}\n")
if shq(`{fmt} /tmp/x_al.ludic | grep -q 'R_A: int = 0 # one'`) { ok("hand alignment preserved") } else { bad("hand alignment preserved") } if shq(`{fmt} {tmp_dir()}/al.ludic | grep -q 'R_A: int = 0 # one'`) { ok("hand alignment preserved") } else { bad("hand alignment preserved") }
# --- --check exit status, for CI and pre-commit hooks ---------------------- # --- --check exit status, for CI and pre-commit hooks ----------------------
let chk1 = sh(`{fmt} --check -q /tmp/x_m.ludic`) let chk1 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`)
check("--check exits 1 on unformatted input", string(chk1), "1") check("--check exits 1 on unformatted input", string(chk1), "1")
run(`{fmt} -w /tmp/x_m.ludic`) run(`{fmt} -w {tmp_dir()}/m.ludic`)
let chk2 = sh(`{fmt} --check -q /tmp/x_m.ludic`) let chk2 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`)
check("--check exits 0 after -w", string(chk2), "0") check("--check exits 0 after -w", string(chk2), "0")
# --- markdown: fences formatted, prose untouched --------------------------- # --- markdown: fences formatted, prose untouched ---------------------------
write_file("/tmp/x_d.md", "# T\n\nprose with spaces\n\n```ludic\nprogram D{property P{x:int=0}}\n```\n\n```python\nz = 1\n```\n") write_file(`{tmp_dir()}/d.md`, "# T\n\nprose with spaces\n\n```ludic\nprogram D{property P{x:int=0}}\n```\n\n```python\nz = 1\n```\n")
run(`{fmt} /tmp/x_d.md > /tmp/x_d.out 2>/dev/null`) run(`{fmt} {tmp_dir()}/d.md > {tmp_dir()}/d.out 2>/dev/null`)
if shq("grep -q 'prose with spaces' /tmp/x_d.out") { ok("markdown prose untouched") } else { bad("markdown prose untouched") } if shq(`grep -q 'prose with spaces' {tmp_dir()}/d.out`) { ok("markdown prose untouched") } else { bad("markdown prose untouched") }
if shq("grep -q 'z = 1' /tmp/x_d.out") { ok("non-ludic fences untouched") } else { bad("non-ludic fences untouched") } if shq(`grep -q 'z = 1' {tmp_dir()}/d.out`) { ok("non-ludic fences untouched") } else { bad("non-ludic fences untouched") }
if shq("grep -q 'program D { property P { x: int = 0 } }' /tmp/x_d.out") { ok("ludic fences formatted") } else { bad("ludic fences formatted") } if shq(`grep -q 'program D {{ property P {{ x: int = 0 }} }}' {tmp_dir()}/d.out`) { ok("ludic fences formatted") } else { bad("ludic fences formatted") }
print("language server") print("language server")
# The LSP indexes the workspace by walking directories, which reads the BSD # The LSP indexes the workspace by walking directories, which reads the BSD
@ -129,7 +129,10 @@ function cmd_test_tools() -> int {
# features (go-to-def, find-usages, workspace symbols) can't resolve. Gate the # features (go-to-def, find-usages, workspace symbols) can't resolve. Gate the
# end-to-end protocol test on Darwin until the directory walk is portable. # end-to-end protocol test on Darwin until the directory walk is portable.
if is_darwin() { if is_darwin() {
if shq("python3 tools/test-lsp.py") { ok("language server protocol") } else { bad("language server protocol") } let lrc = sh("bin/x test-lsp")
if lrc == 0 { ok("language server protocol") }
else if lrc == 2 { skip("language server protocol (bin/ludic-lsp not built)") }
else { bad("language server protocol") }
} else { skip("language server protocol (workspace scan uses the macOS/BSD dirent layout)") } } else { skip("language server protocol (workspace scan uses the macOS/BSD dirent layout)") }
print("") print("")
@ -145,7 +148,11 @@ function cmd_test_tools() -> int {
if shq("command -v node >/dev/null 2>&1") { if shq("command -v node >/dev/null 2>&1") {
if shq("node --check tools/editors/vscode/src/extension.js 2>/dev/null") { ok("extension.js parses") } else { bad("extension.js parses") } if shq("node --check tools/editors/vscode/src/extension.js 2>/dev/null") { ok("extension.js parses") } else { bad("extension.js parses") }
print("textmate grammar") print("textmate grammar")
if shq("node tools/test-grammar.js") { ok("textmate grammar") } else { bad("textmate grammar") } # exit 2 = the TextMate engine is not installed: a skip, never a silent pass
let grc = sh("node tools/test-grammar.js")
if grc == 0 { ok("textmate grammar") }
else if grc == 2 { skip("textmate grammar (npm i vscode-textmate vscode-oniguruma, or set LUDIC_NODE_MODULES)") }
else { bad("textmate grammar") }
} else { } else {
print(" skip extension.js and grammar tests (node not installed)") print(" skip extension.js and grammar tests (node not installed)")
} }