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:
parent
647dfec334
commit
8fed9add66
19 changed files with 1154 additions and 614 deletions
|
|
@ -1,56 +1,41 @@
|
|||
# ============================================================================
|
||||
# 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.
|
||||
#
|
||||
# ludicc tools/atlas.ludic --headless -o build/atlas
|
||||
# ./build/atlas && sips -s format png out.ppm --out atlas.png
|
||||
# bin/ludicc --headless tools/atlas.ludic -o build/atlas
|
||||
# ./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
|
||||
# exists. It is the same program, in the language it is a tool for.
|
||||
# ============================================================================
|
||||
game Atlas {
|
||||
const DIR: str = "assets/kenney/tiny-dungeon/Tiles"
|
||||
const N: int = 132
|
||||
const COLS: int = 12
|
||||
# A tool written in the language it is a tool for: one Start handler draws every
|
||||
# tile with its number underneath and quits, and the headless build leaves the
|
||||
# frame in build/out.ppm.
|
||||
program Atlas {
|
||||
const DIR: string = "assets/kenney/tiny-dungeon/Tiles"
|
||||
const TILE_COUNT: int = 132 # tile_0000.png … tile_0131.png
|
||||
const COLUMNS: int = 12
|
||||
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 {
|
||||
clear(0x101018)
|
||||
for i in 0 .. N {
|
||||
let col = i % COLS
|
||||
let row = i / COLS
|
||||
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)
|
||||
# "<DIR>/tile_0007.png" — the id is the file's zero-padded number
|
||||
function tile_path(i: int) -> string {
|
||||
var digits = Text.from_int(i)
|
||||
while Text.length(digits) < 4 { digits = "0" + digits }
|
||||
return `{DIR}/tile_{digits}.png`
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
*
|
||||
* node tools/test-grammar.js
|
||||
*
|
||||
* Needs vscode-textmate and vscode-oniguruma; skips cleanly when they are not
|
||||
* installed, so it never blocks the suite on a machine without npm.
|
||||
* Needs vscode-textmate and vscode-oniguruma (from the VS Code extension's
|
||||
* 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 path = require('path');
|
||||
|
|
@ -22,12 +23,12 @@ const SHARED = path.join(ROOT, 'tools', 'editors', 'shared');
|
|||
let vsctm, oniguruma;
|
||||
try {
|
||||
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 }));
|
||||
oniguruma = require(require.resolve('vscode-oniguruma', { paths }));
|
||||
} catch (e) {
|
||||
console.log(' skip TextMate grammar (npm i vscode-textmate vscode-oniguruma to run)');
|
||||
process.exit(0);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
|
|
@ -38,7 +39,7 @@ function bad(name, detail) { failed++; console.log(` FAIL ${name}\n ${d
|
|||
|
||||
async function main() {
|
||||
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);
|
||||
|
||||
|
|
@ -96,27 +97,31 @@ async function main() {
|
|||
}
|
||||
|
||||
expect('comment', '# a note', '# a note', 'comment.line');
|
||||
expect('string', 'const S: str = "hi"', '"hi"', 'string.quoted.double');
|
||||
expect('hex literal', 'clear(0x0e0e16)', '0x0e0e16', 'constant.numeric.hex');
|
||||
expect('string', 'const S: string = "hi"', '"hi"', 'string.quoted.double');
|
||||
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('declaration keyword', 'component Pos { x: int = 0 }', 'component', 'storage.type.component');
|
||||
expect('component name', 'component Pos { x: int = 0 }', 'Pos', 'entity.name.type.component');
|
||||
expect('primitive type', 'component Pos { x: int = 0 }', 'int', 'support.type.primitive');
|
||||
expect('system name', 'system Move phase Update {', 'Move', 'entity.name.function.system');
|
||||
expect('phase name', 'system Move phase Update {', 'Update', 'constant.language.phase');
|
||||
expect('clause keyword', ' reads [Vel]', 'reads', 'keyword.other.clause');
|
||||
expect('annotation', 'system Move @deterministic', '@deterministic', 'entity.name.function.decorator');
|
||||
expect('builtin call', ' clear(0x000000)', 'clear', 'support.function.builtin');
|
||||
expect('intrinsic call', ' mem_alloc(64)', 'mem_alloc', 'support.function.intrinsic');
|
||||
expect('declaration keyword', 'property Pos { x: int = 0 }', 'property', 'storage.type.component');
|
||||
expect('property name', 'property Pos { x: int = 0 }', 'Pos', 'entity.name.type.component');
|
||||
expect('primitive type', 'property Pos { x: int = 0 }', 'int', 'support.type.primitive');
|
||||
expect('model keyword', 'model Hero { Pos, Stats }', 'model', 'storage.type.archetype');
|
||||
expect('model name', 'model Hero { Pos, Stats }', 'Hero', 'entity.name.type.archetype');
|
||||
expect('handler name', 'handler Move phase Update {', 'Move', 'entity.name.function.system');
|
||||
expect('phase name', 'handler Move phase Update {', 'Update', 'constant.language.phase');
|
||||
expect('clause keyword', 'handler Move phase Update {', 'phase', 'keyword.other.clause');
|
||||
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('become keyword', ' become Playing', 'become', 'keyword.control');
|
||||
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 component', '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 prop', ' button id=Go text="go"', 'id', 'variable.parameter.widget');
|
||||
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('user function call', ' my_helper(1)', 'my_helper', 'entity.name.function');
|
||||
expect('archetype in spawn', ' spawn Hero {', 'Hero', 'entity.name.type.archetype');
|
||||
expect('scene in enter', ' enter Battle', 'Battle', 'entity.name.type.scene');
|
||||
expect('model in spawn', ' spawn Hero {', 'Hero', 'entity.name.type.archetype');
|
||||
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.
|
||||
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
|
||||
// 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;
|
||||
const bodyScopes = [];
|
||||
lines.forEach((line, i) => {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ function cmd_app() -> int {
|
|||
if (a == "--headless") { mode = 2 }
|
||||
else if (a == "--windowed") { mode = 1 }
|
||||
else if (a == "--save-temps") { save = true }
|
||||
else if (a[0] != 45) { src = a } # 45 = '-', so skip other flags
|
||||
ai = ai + 1
|
||||
else if (a[0] != '-') { src = a } # 45 = '-', so skip other flags
|
||||
ai += 1
|
||||
}
|
||||
if (src == null) { err("usage: x app <file.ludic> [--headless] [--save-temps]\n"); return 1 }
|
||||
let name = capture_line(`basename {src} .ludic`)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
# ---- small string helpers ---------------------------------------------------
|
||||
|
||||
# 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)
|
||||
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 b = bytes(n + 1)
|
||||
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
|
||||
return b
|
||||
}
|
||||
|
|
@ -33,11 +33,11 @@ function s_index(hay: pointer, needle: pointer, from: int) -> int {
|
|||
var i = from
|
||||
while i + nn <= hn {
|
||||
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 }
|
||||
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 }
|
||||
|
|
@ -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 {
|
||||
let nn = slen(needle)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -54,17 +54,17 @@ function s_starts_at(hay: pointer, at: int, needle: pointer) -> bool {
|
|||
function s_starts(s: pointer, pre: pointer) -> bool {
|
||||
let pn = slen(pre)
|
||||
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
|
||||
}
|
||||
|
||||
# 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)
|
||||
function line_at(s: pointer, start: int) -> pointer {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -72,9 +72,9 @@ function line_at(s: pointer, start: int) -> pointer {
|
|||
function s_trim(s: pointer) -> pointer {
|
||||
let n = slen(s)
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -101,11 +101,11 @@ function classify_fence(body: pointer) -> pointer {
|
|||
while i < n {
|
||||
let ln = line_at(body, i)
|
||||
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
|
||||
var e = 0
|
||||
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)
|
||||
if head == "program" { return "whole" }
|
||||
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)
|
||||
function fence_parses(src: pointer) -> bool {
|
||||
write_file("/tmp/x_docchk.ludic", src)
|
||||
return shq("bin/ludicc /tmp/x_docchk.ludic --fmt > /dev/null 2>&1")
|
||||
write_file(`{tmp_dir()}/docchk.ludic`, src)
|
||||
return shq(`bin/ludicc {tmp_dir()}/docchk.ludic --fmt > /dev/null 2>&1`)
|
||||
}
|
||||
|
||||
# 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)
|
||||
if open < 0 { break }
|
||||
# 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 close = s_index(t, "\n```", bstart)
|
||||
if close < 0 { break }
|
||||
let body = sslice(t, bstart, close + 1) # include trailing newline
|
||||
let line = 1 + count_nl(t, open)
|
||||
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 kind = classify_fence(body)
|
||||
let parsed = fence_body_parses(body, kind)
|
||||
if parsed == (not expect_err) {
|
||||
DC_OK = DC_OK + 1
|
||||
DC_OK += 1
|
||||
} else {
|
||||
DC_BAD = DC_BAD + 1
|
||||
DC_BAD += 1
|
||||
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" }
|
||||
}
|
||||
|
|
@ -173,7 +173,7 @@ function check_docs_file(path: pointer) -> void {
|
|||
# number of '\n' in s[0 .. upto)
|
||||
function count_nl(s: pointer, upto: int) -> int {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -205,12 +205,12 @@ function cmd_check_docs() -> int {
|
|||
# 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)
|
||||
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
|
||||
function set_has(l: []pointer, s: pointer) -> bool {
|
||||
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
|
||||
}
|
||||
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)
|
||||
function fn_body(src: pointer, name: pointer) -> pointer {
|
||||
let needle = "function " + name + "("
|
||||
var p = 0 - 1
|
||||
var p = -1
|
||||
if s_starts(src, needle) { p = 0 }
|
||||
else {
|
||||
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)
|
||||
if p < 0 { break }
|
||||
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))
|
||||
i = e + 1
|
||||
}
|
||||
|
|
@ -256,20 +256,20 @@ function table_set(h: pointer, name: pointer) -> []pointer {
|
|||
if b < 0 or e < 0 { return out }
|
||||
var i = b
|
||||
while i < e {
|
||||
if h[i] == 34 {
|
||||
if h[i] == '"' {
|
||||
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))
|
||||
i = j + 1
|
||||
} else { i = i + 1 }
|
||||
} else { i += 1 }
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
# concatenate every selfhost/*.ludic into one buffer (via cat, then read)
|
||||
function read_all_selfhost() -> pointer {
|
||||
run("find selfhost -name '*.ludic' | sort | xargs cat > /tmp/x_allsh.txt 2>/dev/null")
|
||||
let s = read_file("/tmp/x_allsh.txt")
|
||||
run(`find selfhost -name '*.ludic' | sort | xargs cat > {tmp_dir()}/allsh.txt 2>/dev/null`)
|
||||
let s = read_file(`{tmp_dir()}/allsh.txt`)
|
||||
if s == null { return "" }
|
||||
return s
|
||||
}
|
||||
|
|
@ -285,7 +285,7 @@ function collect_impl_pairs(allsrc: pointer, pairs: []pointer) -> void {
|
|||
let p = s_index(body, mns, i)
|
||||
if p < 0 { break }
|
||||
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 nxt = s_index(body, mns, ne)
|
||||
var chunkEnd = slen(body)
|
||||
|
|
@ -296,13 +296,13 @@ function collect_impl_pairs(allsrc: pointer, pairs: []pointer) -> void {
|
|||
let dp = s_index(chunk, "is_", 0)
|
||||
if dp >= 0 {
|
||||
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)
|
||||
methods = collect_after(fn_body(allsrc, predname), mmeth)
|
||||
}
|
||||
}
|
||||
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 }
|
||||
i = nxt
|
||||
}
|
||||
|
|
@ -340,9 +340,9 @@ function collect_docs(tokens: []pointer, docpairs: []pointer) -> void {
|
|||
var a = 0
|
||||
let vn = slen(val)
|
||||
while a < vn {
|
||||
while a < vn and val[a] == 32 { a = a + 1 }
|
||||
while a < vn and val[a] == ' ' { a += 1 }
|
||||
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)) }
|
||||
a = e
|
||||
}
|
||||
|
|
@ -359,7 +359,7 @@ function collect_docs(tokens: []pointer, docpairs: []pointer) -> void {
|
|||
|
||||
var CI_PROB: pointer = ""
|
||||
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
|
||||
function cmd_check_impl() -> int {
|
||||
|
|
@ -377,13 +377,13 @@ function cmd_check_impl() -> int {
|
|||
let pair = impl[i]
|
||||
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") } }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
# 2) every documented ns-method corresponds to real dispatch
|
||||
i = 0
|
||||
while i < len(docpairs) {
|
||||
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
|
||||
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)
|
||||
function add_all_except(dst: []pointer, src: []pointer, deny: []pointer) -> void {
|
||||
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
|
||||
function check_all_documented(names: []pointer, tokens: []pointer, label: pointer) -> void {
|
||||
var i = 0
|
||||
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)") }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,7 +435,7 @@ function all_lower(s: pointer) -> bool {
|
|||
let n = slen(s)
|
||||
if n == 0 { return false }
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +446,7 @@ function split_pipe(s: pointer) -> []pointer {
|
|||
var a = 0
|
||||
while a <= n {
|
||||
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))
|
||||
a = e + 1
|
||||
}
|
||||
|
|
@ -463,16 +463,16 @@ function c_table_names(h: pointer, name: pointer) -> []pointer {
|
|||
if b < 0 or e < 0 { return out }
|
||||
var i = b + 1
|
||||
while i < e {
|
||||
if h[i] == 123 { # '{' opens a row
|
||||
if h[i] == '{' { # '{' opens a row
|
||||
var j = i + 1
|
||||
while j < e and (h[j] == 32 or h[j] == 9 or h[j] == 10) { j = j + 1 }
|
||||
if h[j] == 34 {
|
||||
while j < e and (h[j] == ' ' or h[j] == '\t' or h[j] == '\n') { j += 1 }
|
||||
if h[j] == '"' {
|
||||
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))
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -488,22 +488,22 @@ function kotlin_set(text: pointer, name: pointer) -> []pointer {
|
|||
let region_start = i
|
||||
let tn = slen(text)
|
||||
while i < tn and depth > 0 {
|
||||
if text[i] == 40 { depth = depth + 1 }
|
||||
if text[i] == 41 { depth = depth - 1 }
|
||||
if text[i] == '(' { depth += 1 }
|
||||
if text[i] == ')' { depth -= 1 }
|
||||
if depth == 0 { break }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
# collect quoted strings in [region_start, i), skipping //-comments
|
||||
var k = region_start
|
||||
while k < i {
|
||||
if text[k] == 47 and text[k + 1] == 47 { # '//'
|
||||
while k < i and text[k] != 10 { k = k + 1 }
|
||||
} else { if text[k] == 34 {
|
||||
if text[k] == '/' and text[k + 1] == '/' { # '//'
|
||||
while k < i and text[k] != '\n' { k += 1 }
|
||||
} else { if text[k] == '"' {
|
||||
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))
|
||||
k = e + 1
|
||||
} else { k = k + 1 } }
|
||||
} else { k += 1 } }
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -517,13 +517,13 @@ function grammar_alt(root: JVal, node: pointer, marker: pointer) -> []pointer {
|
|||
while i < len(pats.kids) {
|
||||
let m = j_get(pats.kids[i], "match")
|
||||
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 {
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -543,28 +543,28 @@ function add_parser_kw(out: []pointer, path: pointer) -> void {
|
|||
}
|
||||
function add_lower(out: []pointer, src: []pointer) -> void {
|
||||
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_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`
|
||||
function cmp_sets(label: pointer, ref: []pointer, other: []pointer, other_label: pointer) -> void {
|
||||
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
|
||||
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
|
||||
function set_union(a: []pointer, b: []pointer) -> []pointer {
|
||||
let out = new []pointer
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -614,7 +614,7 @@ function cmd_check_vocab() -> int {
|
|||
cmp_unparsed("clause keywords", h_clause, pkw, h_reserved)
|
||||
# reserved words the parser now accepts should be promoted
|
||||
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 {
|
||||
|
|
@ -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(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 i = 0
|
||||
while i < n {
|
||||
if text[i] != 60 { i = i + 1; continue } # seek '<'
|
||||
if text[i + 1] == 63 { # <? ... ?>
|
||||
if text[i] != '<' { i += 1; continue } # seek '<'
|
||||
if text[i + 1] == '?' { # <? ... ?>
|
||||
let e = s_index(text, "?>", i)
|
||||
if e < 0 { return false }
|
||||
i = e + 2; continue
|
||||
}
|
||||
if text[i + 1] == 33 { # <! comment / cdata / doctype
|
||||
if text[i + 2] == 45 and text[i + 3] == 45 { # <!-- ... -->
|
||||
if text[i + 1] == '!' { # <! comment / cdata / doctype
|
||||
if text[i + 2] == '-' and text[i + 3] == '-' { # <!-- ... -->
|
||||
let e = s_index(text, "-->", i)
|
||||
if e < 0 { return false }
|
||||
i = e + 3; continue
|
||||
|
|
@ -669,33 +669,33 @@ function xml_valid(text: pointer) -> bool {
|
|||
if e < 0 { return false }
|
||||
i = e + 1; continue
|
||||
}
|
||||
if text[i + 1] == 47 { # closing </name>
|
||||
if text[i + 1] == '/' { # closing </name>
|
||||
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 e = s_index(text, ">", j)
|
||||
if e < 0 { return false }
|
||||
if sp == 0 { return false }
|
||||
if stack[sp - 1] != name { return false }
|
||||
sp = sp - 1
|
||||
sp -= 1
|
||||
i = e + 1; continue
|
||||
}
|
||||
# opening tag: read the name
|
||||
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)
|
||||
if slen(name) == 0 { return false }
|
||||
# advance to '>', skipping quoted attribute values
|
||||
var k = j
|
||||
while k < n and text[k] != 62 {
|
||||
if text[k] == 34 { k = k + 1; while k < n and text[k] != 34 { k = k + 1 } }
|
||||
else { if text[k] == 39 { k = k + 1; while k < n and text[k] != 39 { k = k + 1 } } }
|
||||
k = k + 1
|
||||
while k < n and text[k] != '>' {
|
||||
if text[k] == '"' { k += 1; while k < n and text[k] != '"' { k += 1 } }
|
||||
else { if text[k] == '\'' { k += 1; while k < n and text[k] != '\'' { k += 1 } } }
|
||||
k += 1
|
||||
}
|
||||
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) }
|
||||
sp = sp + 1
|
||||
sp += 1
|
||||
}
|
||||
i = k + 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ function sb_new() -> Sb {
|
|||
function sb_grow(s: Sb, need: int) -> void {
|
||||
if s.len + need + 1 <= s.cap { return }
|
||||
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)
|
||||
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.cap = nc
|
||||
}
|
||||
|
|
@ -37,12 +37,12 @@ function sb_puts(s: Sb, t: pointer) -> void {
|
|||
let n = slen(t)
|
||||
sb_grow(s, n)
|
||||
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 {
|
||||
sb_grow(s, 1)
|
||||
s.data[s.len] = c
|
||||
s.len = s.len + 1
|
||||
s.len += 1
|
||||
}
|
||||
function sb_str(s: Sb) -> pointer {
|
||||
s.data[s.len] = 0
|
||||
|
|
@ -59,13 +59,13 @@ function esc_q(s: pointer, quote: bool) -> pointer {
|
|||
var i = 0
|
||||
while i < n {
|
||||
let c = s[i]
|
||||
if c == 38 { sb_puts(b, "&") }
|
||||
else if c == 60 { sb_puts(b, "<") }
|
||||
else if c == 62 { sb_puts(b, ">") }
|
||||
else if quote and c == 34 { sb_puts(b, """) }
|
||||
else if quote and c == 39 { sb_puts(b, "'") }
|
||||
if c == '&' { sb_puts(b, "&") }
|
||||
else if c == '<' { sb_puts(b, "<") }
|
||||
else if c == '>' { sb_puts(b, ">") }
|
||||
else if quote and c == '"' { sb_puts(b, """) }
|
||||
else if quote and c == '\'' { sb_puts(b, "'") }
|
||||
else { sb_putc(b, c) }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return sb_str(b)
|
||||
}
|
||||
|
|
@ -75,16 +75,16 @@ function hescattr(s: pointer) -> pointer { return esc_q(s, true) }
|
|||
# ---- generic byte/character predicates --------------------------------------
|
||||
# Python \s = [ \t\n\r\f\v]
|
||||
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_]
|
||||
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"))
|
||||
function lstrip_nl(s: pointer) -> pointer {
|
||||
var i = 0
|
||||
while s[i] == 10 { i = i + 1 }
|
||||
while s[i] == '\n' { i += 1 }
|
||||
return sslice(s, i, slen(s))
|
||||
}
|
||||
# lowercase ASCII A-Z
|
||||
|
|
@ -94,9 +94,9 @@ function lower_ascii(s: pointer) -> pointer {
|
|||
var i = 0
|
||||
while i < n {
|
||||
var c = s[i]
|
||||
if c >= 65 and c <= 90 { c = c + 32 }
|
||||
if c >= 'A' and c <= 'Z' { c += 32 }
|
||||
b[i] = c
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
b[n] = 0
|
||||
return b
|
||||
|
|
@ -109,14 +109,14 @@ function title_case(s: pointer) -> pointer {
|
|||
var prev_alpha = false
|
||||
while i < n {
|
||||
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 prev_alpha { if c >= 65 and c <= 90 { c = c + 32 } }
|
||||
else { if c >= 97 and c <= 122 { c = c - 32 } }
|
||||
if prev_alpha { if c >= 'A' and c <= 'Z' { c += 32 } }
|
||||
else { if c >= 'a' and c <= 'z' { c -= 32 } }
|
||||
}
|
||||
b[i] = c
|
||||
prev_alpha = al
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
b[n] = 0
|
||||
return b
|
||||
|
|
@ -128,7 +128,7 @@ function str_cmp(a: pointer, b: pointer) -> int {
|
|||
let ca = a[i] & 255
|
||||
let cb = b[i] & 255
|
||||
if ca != cb { return ca - cb }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return (a[i] & 255) - (b[i] & 255)
|
||||
}
|
||||
|
|
@ -146,9 +146,9 @@ function split_ws(s: pointer) -> []pointer {
|
|||
let n = slen(s)
|
||||
var i = 0
|
||||
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
|
||||
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)) }
|
||||
i = e
|
||||
}
|
||||
|
|
@ -160,9 +160,9 @@ function strs_sort(a: []pointer) -> void {
|
|||
while i < len(a) {
|
||||
let cur = a[i]
|
||||
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
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,22 +173,22 @@ function codeify(s: pointer) -> pointer {
|
|||
let b = sb_new()
|
||||
var i = 0
|
||||
while i < n {
|
||||
if s[i] == 96 {
|
||||
if s[i] == '`' {
|
||||
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 {
|
||||
sb_puts(b, "<code>")
|
||||
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>")
|
||||
i = j + 1
|
||||
} else {
|
||||
sb_putc(b, 96)
|
||||
i = i + 1
|
||||
sb_putc(b, '`')
|
||||
i += 1
|
||||
}
|
||||
} else {
|
||||
sb_putc(b, s[i])
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
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"].
|
||||
function jvput(o: JVal, k: pointer, v: JVal) -> void {
|
||||
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.kids, v)
|
||||
}
|
||||
|
|
@ -215,7 +215,7 @@ function hex_lower(n: int) -> int {
|
|||
return 87 + n
|
||||
}
|
||||
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 >> 8) & 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
|
||||
while i < n {
|
||||
let c = s[i] & 255
|
||||
if c == 34 { sb_putc(b, 92); sb_putc(b, 34); i = i + 1 }
|
||||
else if c == 92 { sb_putc(b, 92); sb_putc(b, 92); i = i + 1 }
|
||||
else if c == 8 { sb_putc(b, 92); sb_putc(b, 98); i = i + 1 }
|
||||
else if c == 9 { sb_putc(b, 92); sb_putc(b, 116); i = i + 1 }
|
||||
else if c == 10 { sb_putc(b, 92); sb_putc(b, 110); i = i + 1 }
|
||||
else if c == 12 { sb_putc(b, 92); sb_putc(b, 102); i = i + 1 }
|
||||
else if c == 13 { sb_putc(b, 92); sb_putc(b, 114); i = i + 1 }
|
||||
else if c < 32 {
|
||||
sb_putc(b, 92); sb_putc(b, 117); sb_putc(b, 48); sb_putc(b, 48)
|
||||
if c == '"' { sb_putc(b, '\\'); sb_putc(b, '"'); i += 1 }
|
||||
else if c == '\\' { sb_putc(b, '\\'); sb_putc(b, '\\'); i += 1 }
|
||||
else if c == 8 { sb_putc(b, '\\'); sb_putc(b, 'b'); i += 1 }
|
||||
else if c == '\t' { sb_putc(b, '\\'); sb_putc(b, 't'); i += 1 }
|
||||
else if c == '\n' { sb_putc(b, '\\'); sb_putc(b, 'n'); i += 1 }
|
||||
else if c == 12 { sb_putc(b, '\\'); sb_putc(b, 'f'); i += 1 }
|
||||
else if c == '\r' { sb_putc(b, '\\'); sb_putc(b, 'r'); i += 1 }
|
||||
else if c < ' ' {
|
||||
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))
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
else if c < 128 { sb_putc(b, c); i = i + 1 }
|
||||
else if not ensure_ascii { 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 += 1 }
|
||||
else {
|
||||
var cp = 0
|
||||
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 { cp = c - 240; nb = 4 }
|
||||
var k = 1
|
||||
while k < nb and i + k < n { cp = cp * 64 + ((s[i + k] & 255) - 128); k = k + 1 }
|
||||
i = i + nb
|
||||
while k < nb and i + k < n { cp = cp * 64 + ((s[i + k] & 255) - 128); k += 1 }
|
||||
i += nb
|
||||
if cp <= 65535 { jemit_u(b, cp) }
|
||||
else {
|
||||
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 {
|
||||
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
|
||||
# (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
|
||||
}
|
||||
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 {
|
||||
let m = len(v.kids)
|
||||
if m == 0 { sb_puts(b, "[]"); return }
|
||||
sb_putc(b, 91)
|
||||
sb_putc(b, '[')
|
||||
var i = 0
|
||||
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)
|
||||
if i < m - 1 { sb_putc(b, 44); if not pretty { sb_putc(b, 32) } }
|
||||
i = i + 1
|
||||
if i < m - 1 { sb_putc(b, ','); if not pretty { sb_putc(b, ' ') } }
|
||||
i += 1
|
||||
}
|
||||
if pretty { sb_putc(b, 10); json_indent(b, depth) }
|
||||
sb_putc(b, 93)
|
||||
if pretty { sb_putc(b, '\n'); json_indent(b, depth) }
|
||||
sb_putc(b, ']')
|
||||
return
|
||||
}
|
||||
let m = len(v.keys)
|
||||
if m == 0 { sb_puts(b, "{}"); return }
|
||||
sb_putc(b, 123)
|
||||
sb_putc(b, '{')
|
||||
var i = 0
|
||||
while i < m {
|
||||
if pretty { sb_putc(b, 10); json_indent(b, depth + 1) }
|
||||
sb_putc(b, 34); json_escape_into(b, v.keys[i], ea); sb_putc(b, 34)
|
||||
sb_putc(b, 58); sb_putc(b, 32)
|
||||
if pretty { sb_putc(b, '\n'); json_indent(b, depth + 1) }
|
||||
sb_putc(b, '"'); json_escape_into(b, v.keys[i], ea); sb_putc(b, '"')
|
||||
sb_putc(b, ':'); sb_putc(b, ' ')
|
||||
jwrite(b, v.kids[i], pretty, depth + 1, ea)
|
||||
if i < m - 1 { sb_putc(b, 44); if not pretty { sb_putc(b, 32) } }
|
||||
i = i + 1
|
||||
if i < m - 1 { sb_putc(b, ','); if not pretty { sb_putc(b, ' ') } }
|
||||
i += 1
|
||||
}
|
||||
if pretty { sb_putc(b, 10); json_indent(b, depth) }
|
||||
sb_putc(b, 125)
|
||||
if pretty { sb_putc(b, '\n'); json_indent(b, depth) }
|
||||
sb_putc(b, '}')
|
||||
}
|
||||
function json_dumps(v: JVal, pretty: bool, ea: bool) -> pointer {
|
||||
let b = sb_new()
|
||||
|
|
@ -571,11 +571,10 @@ function palette_emit_ludic(path: pointer) -> bool {
|
|||
var i = 0
|
||||
while i < len(pal_names) {
|
||||
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, "}\n")
|
||||
sb_puts(b, "\n")
|
||||
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, " }")
|
||||
}
|
||||
ci = ci + 1
|
||||
ci += 1
|
||||
}
|
||||
sb_puts(b, "\n ]\n")
|
||||
sb_puts(b, " }")
|
||||
}
|
||||
gi = gi + 1
|
||||
gi += 1
|
||||
}
|
||||
sb_puts(b, "\n ]\n")
|
||||
sb_puts(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 {
|
||||
let check = arg_count() > 2 and arg(2) == "--check"
|
||||
palette_init()
|
||||
# guard: duplicate names
|
||||
let seen = new []pointer
|
||||
|
|
@ -629,10 +632,17 @@ function cmd_docs_palette() -> int {
|
|||
while i < len(pal_names) {
|
||||
if set_has(seen, pal_names[i]) { err(`DUPLICATE NAME: {pal_names[i]}\n`); return 1 }
|
||||
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`)
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function bucket_get(bs: []Bucket, k1: pointer, k2: pointer) -> Bucket {
|
|||
var i = 0
|
||||
while i < len(bs) {
|
||||
if bs[i].k1 == k1 and bs[i].k2 == k2 { return bs[i] }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
let b = new Bucket
|
||||
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 {
|
||||
let b = bucket_get(bs, k1, k2)
|
||||
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)
|
||||
}
|
||||
# ", ".join(sorted(vals))
|
||||
|
|
@ -34,7 +34,7 @@ function join_sorted(vals: []pointer) -> pointer {
|
|||
while i < len(vals) {
|
||||
if i > 0 { sb_puts(b, ", ") }
|
||||
sb_puts(b, vals[i])
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return sb_str(b)
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ function cp_len(s: pointer) -> int {
|
|||
let n = slen(s)
|
||||
var c = 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
|
||||
}
|
||||
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)
|
||||
var i = 0
|
||||
while i < n {
|
||||
if s_starts_at(hay, i, needle) { sb_puts(b, repl); i = i + nl }
|
||||
else { sb_putc(b, hay[i]); i = i + 1 }
|
||||
if s_starts_at(hay, i, needle) { sb_puts(b, repl); i += nl }
|
||||
else { sb_putc(b, hay[i]); i += 1 }
|
||||
}
|
||||
return sb_str(b)
|
||||
}
|
||||
|
|
@ -110,14 +110,14 @@ function cmd_docs_check() -> int {
|
|||
var ci = 0
|
||||
while ci < len(cats) {
|
||||
let cat = cats[ci]
|
||||
ci = ci + 1
|
||||
ci += 1
|
||||
let cdir = "docs/language/" + cat
|
||||
if not is_dir(cdir) { continue }
|
||||
let files = list_sorted(cdir)
|
||||
var fi = 0
|
||||
while fi < len(files) {
|
||||
let fn = files[fi]
|
||||
fi = fi + 1
|
||||
fi += 1
|
||||
if not s_ends(fn, ".md") { continue }
|
||||
if fn == "_section.md" { continue }
|
||||
let d = parse_front(cdir + "/" + fn)
|
||||
|
|
@ -143,12 +143,12 @@ function cmd_docs_check() -> int {
|
|||
while k < len(inv.keys) {
|
||||
let cat = inv.keys[k]
|
||||
let ids = inv.kids[k]
|
||||
k = k + 1
|
||||
k += 1
|
||||
if ids.t != JV_ARR { continue }
|
||||
var m = 0
|
||||
while m < len(ids.kids) {
|
||||
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})`) }
|
||||
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
|
||||
while ci < len(cats) {
|
||||
let cat = cats[ci]
|
||||
ci = ci + 1
|
||||
ci += 1
|
||||
let cdir = "docs/language/" + cat
|
||||
if not is_dir(cdir) { continue }
|
||||
let secp = cdir + "/_section.md"
|
||||
|
|
@ -177,7 +177,7 @@ function cmd_docs_check() -> int {
|
|||
var fi = 0
|
||||
while fi < len(files) {
|
||||
let fn = files[fi]
|
||||
fi = fi + 1
|
||||
fi += 1
|
||||
if not s_ends(fn, ".md") { continue }
|
||||
if fn == "_section.md" { continue }
|
||||
let d = parse_front(cdir + "/" + fn)
|
||||
|
|
@ -185,7 +185,7 @@ function cmd_docs_check() -> int {
|
|||
let id = meta_get(d.m, "id", "")
|
||||
let toks = split_ws(meta_get(d.m, "tokens", ""))
|
||||
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", "")
|
||||
if slen(ns) > 0 { bucket_add(ns2dirs, "", ns, cat) }
|
||||
}
|
||||
|
|
@ -193,25 +193,25 @@ function cmd_docs_check() -> int {
|
|||
var bi = 0
|
||||
while bi < len(tok2ids) {
|
||||
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)}`) }
|
||||
}
|
||||
bi = 0
|
||||
while bi < len(ns2dirs) {
|
||||
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)}`) }
|
||||
}
|
||||
bi = 0
|
||||
while bi < len(id2dirs) {
|
||||
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)}`) }
|
||||
}
|
||||
bi = 0
|
||||
while bi < len(title2dirs) {
|
||||
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)}`) }
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +223,7 @@ function cmd_docs_check() -> int {
|
|||
while i < len(thin) and i < 12 {
|
||||
if i > 0 { sb_puts(b, ", ") }
|
||||
sb_puts(b, thin[i])
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
var tail = ""
|
||||
if len(thin) > 12 { tail = " …" }
|
||||
|
|
@ -247,18 +247,18 @@ function cmd_docs_check() -> int {
|
|||
var i = 0
|
||||
while i < len(targets) {
|
||||
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`) }
|
||||
}
|
||||
}
|
||||
|
||||
# report
|
||||
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 {
|
||||
print("docs check FAILED:")
|
||||
var pi = 0
|
||||
while pi < len(problems) { print(` - {problems[pi]}`); pi = pi + 1 }
|
||||
while pi < len(problems) { print(` - {problems[pi]}`); pi += 1 }
|
||||
return 1
|
||||
}
|
||||
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
|
||||
while i < len(o.kids) {
|
||||
let v = o.kids[i]
|
||||
i = i + 1
|
||||
i += 1
|
||||
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
|
||||
while i < len(o.kids) {
|
||||
let v = o.kids[i]
|
||||
i = i + 1
|
||||
i += 1
|
||||
let id = j_get(v, "id")
|
||||
if id.t == JV_STR { set_add(out, id.s) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ function meta_new() -> Meta {
|
|||
}
|
||||
function meta_put(m: Meta, k: pointer, v: pointer) -> void {
|
||||
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.vals, v)
|
||||
}
|
||||
function meta_get(m: Meta, k: pointer, dflt: pointer) -> pointer {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ function read_snippet(rel: pointer) -> pointer {
|
|||
let s = read_file(rel)
|
||||
if s == null { return "" }
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -90,23 +90,23 @@ function parse_doc(path: pointer) -> Doc {
|
|||
function param_match(line: pointer, p: Param) -> bool {
|
||||
let n = slen(line)
|
||||
var i = 0
|
||||
if i < n and line[i] == 45 { i = i + 1 } else { return false }
|
||||
while i < n and is_ws(line[i]) { i = i + 1 }
|
||||
if i < n and line[i] == 96 { i = i + 1 }
|
||||
if i < n and line[i] == '-' { i += 1 } else { return false }
|
||||
while i < n and is_ws(line[i]) { i += 1 }
|
||||
if i < n and line[i] == '`' { i += 1 }
|
||||
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 }
|
||||
let name = sslice(line, ws, i)
|
||||
if i < n and line[i] == 96 { i = i + 1 }
|
||||
while i < n and is_ws(line[i]) { i = i + 1 }
|
||||
if i < n and line[i] == '`' { i += 1 }
|
||||
while i < n and is_ws(line[i]) { i += 1 }
|
||||
var sep = 0
|
||||
while i < n {
|
||||
if line[i] == 45 { i = i + 1; 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 = i + 3; 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 += 3; sep += 1 }
|
||||
else { break }
|
||||
}
|
||||
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.desc = codeify(s_trim(sslice(line, i, n)))
|
||||
return true
|
||||
|
|
@ -119,10 +119,10 @@ function split_paras(desc: pointer) -> []pointer {
|
|||
var start = 0
|
||||
var i = 0
|
||||
while i < n {
|
||||
if desc[i] == 10 {
|
||||
if desc[i] == '\n' {
|
||||
var jj = i
|
||||
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 {
|
||||
let part = s_trim(sslice(desc, start, i))
|
||||
if slen(part) > 0 { push(out, part) }
|
||||
|
|
@ -132,7 +132,7 @@ function split_paras(desc: pointer) -> []pointer {
|
|||
}
|
||||
i = jj
|
||||
} else {
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
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)
|
||||
if p < 0 {
|
||||
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
|
||||
} else {
|
||||
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 q = s_index(body, "\n```", cs)
|
||||
if q < 0 {
|
||||
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
|
||||
} else {
|
||||
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) }
|
||||
}
|
||||
} else {
|
||||
if not firstkeep { sb_putc(keep, 10) }
|
||||
if not firstkeep { sb_putc(keep, '\n') }
|
||||
firstkeep = false
|
||||
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
|
||||
|
|
@ -205,7 +205,7 @@ function split_body(body: pointer) -> Split {
|
|||
while pi < len(paras) {
|
||||
if pi > 0 { sb_puts(ob, "</p><p>") }
|
||||
sb_puts(ob, codeify(paras[pi]))
|
||||
pi = pi + 1
|
||||
pi += 1
|
||||
}
|
||||
r.desc = sb_str(ob)
|
||||
return r
|
||||
|
|
@ -217,26 +217,26 @@ function plain_text(s: pointer) -> pointer {
|
|||
let n = slen(s)
|
||||
var i = 0
|
||||
while i < n {
|
||||
if s[i] == 60 {
|
||||
while i < n and s[i] != 62 { i = i + 1 }
|
||||
if i < n { i = i + 1 }
|
||||
if s[i] == '<' {
|
||||
while i < n and s[i] != '>' { i += 1 }
|
||||
if i < n { i += 1 }
|
||||
} else {
|
||||
sb_putc(sb, s[i])
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
let t0 = s_trim(sb_str(sb))
|
||||
let tn = slen(t0)
|
||||
var cut = 0 - 1
|
||||
var cut = -1
|
||||
var j = 0
|
||||
while j < tn {
|
||||
let c = t0[j]
|
||||
if c == 10 { break }
|
||||
if (c == 46 or c == 33 or c == 63) and j >= 1 {
|
||||
if c == '\n' { break }
|
||||
if (c == '.' or c == '!' or c == '?') and j >= 1 {
|
||||
if j + 1 >= tn { cut = j + 1; break }
|
||||
if is_space_all(t0[j + 1]) { cut = j + 1; break }
|
||||
}
|
||||
j = j + 1
|
||||
j += 1
|
||||
}
|
||||
var t = t0
|
||||
if cut >= 0 { t = sslice(t0, 0, cut) }
|
||||
|
|
@ -245,18 +245,18 @@ function plain_text(s: pointer) -> pointer {
|
|||
let tl = slen(t)
|
||||
var cpc = 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 }
|
||||
var idx = 0
|
||||
var seen = 0
|
||||
while idx < tl and seen < 160 {
|
||||
idx = idx + 1
|
||||
while idx < tl and (t[idx] & 192) == 128 { idx = idx + 1 }
|
||||
seen = seen + 1
|
||||
idx += 1
|
||||
while idx < tl and (t[idx] & 192) == 128 { idx += 1 }
|
||||
seen += 1
|
||||
}
|
||||
let pre = sslice(t, 0, idx)
|
||||
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
|
||||
if sp >= 0 { head = sslice(pre, 0, sp) }
|
||||
return head + "…"
|
||||
|
|
@ -305,9 +305,9 @@ function entries_sort(es: []Entry) -> void {
|
|||
while i < len(es) {
|
||||
let cur = es[i]
|
||||
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
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function section_gt(a: Section, b: Section) -> bool {
|
||||
|
|
@ -319,9 +319,9 @@ function sections_sort(ss: []Section) -> void {
|
|||
while i < len(ss) {
|
||||
let cur = ss[i]
|
||||
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
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +331,7 @@ function load_sections() -> []Section {
|
|||
var ci = 0
|
||||
while ci < len(cats) {
|
||||
let cat = cats[ci]
|
||||
ci = ci + 1
|
||||
ci += 1
|
||||
let cdir = "docs/language/" + cat
|
||||
if not is_dir(cdir) { continue }
|
||||
var sid = cat
|
||||
|
|
@ -351,7 +351,7 @@ function load_sections() -> []Section {
|
|||
var fi = 0
|
||||
while fi < len(files) {
|
||||
let fn = files[fi]
|
||||
fi = fi + 1
|
||||
fi += 1
|
||||
if not s_ends(fn, ".md") { continue }
|
||||
if fn == "_section.md" { continue }
|
||||
push(entries, build_entry(cdir + "/" + fn))
|
||||
|
|
@ -362,7 +362,7 @@ function load_sections() -> []Section {
|
|||
while ei < len(entries) {
|
||||
let e = entries[ei]
|
||||
if e.kind == "namespace-method" and slen(e.ns) > 0 { nsname = e.ns; break }
|
||||
ei = ei + 1
|
||||
ei += 1
|
||||
}
|
||||
let sec = new Section
|
||||
sec.id = sid
|
||||
|
|
@ -384,7 +384,7 @@ function ns_tip(nn: pointer, sections: []Section) -> pointer {
|
|||
var i = 0
|
||||
while i < len(sections) {
|
||||
if sections[i].ns_name == nn { return plain_text(sections[i].blurb) }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -414,11 +414,11 @@ function build_symbols(sections: []Section) -> Symbols {
|
|||
var si = 0
|
||||
while si < len(sections) {
|
||||
let s = sections[si]
|
||||
si = si + 1
|
||||
si += 1
|
||||
var ei = 0
|
||||
while ei < len(s.entries) {
|
||||
let e = s.entries[ei]
|
||||
ei = ei + 1
|
||||
ei += 1
|
||||
let eid = e.id
|
||||
let kind = e.kind
|
||||
let tip = e.tip
|
||||
|
|
@ -444,7 +444,7 @@ function build_symbols(sections: []Section) -> Symbols {
|
|||
var ti = 0
|
||||
while ti < len(e.tokens_list) {
|
||||
let tok = e.tokens_list[ti]
|
||||
ti = ti + 1
|
||||
ti += 1
|
||||
if kind == "keyword" { jvput(kw, tok, jvs(eid)) }
|
||||
else if kind == "type" { jvput(types, 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))
|
||||
let pa = jva()
|
||||
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(builtins, tok, o)
|
||||
}
|
||||
|
|
@ -462,7 +462,7 @@ function build_symbols(sections: []Section) -> Symbols {
|
|||
jvput(o, "id", jvs(eid))
|
||||
let pa = jva()
|
||||
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(nsm, tok, o)
|
||||
}
|
||||
|
|
@ -475,7 +475,7 @@ function build_symbols(sections: []Section) -> Symbols {
|
|||
var ki = 0
|
||||
while ki < len(nsm.keys) {
|
||||
let tok = nsm.keys[ki]
|
||||
ki = ki + 1
|
||||
ki += 1
|
||||
let dot = s_index(tok, ".", 0)
|
||||
if dot >= 0 { set_add(nsnames, sslice(tok, 0, dot)) }
|
||||
}
|
||||
|
|
@ -484,7 +484,7 @@ function build_symbols(sections: []Section) -> Symbols {
|
|||
var ni = 0
|
||||
while ni < len(nsnames) {
|
||||
let nn = nsnames[ni]
|
||||
ni = ni + 1
|
||||
ni += 1
|
||||
let pg = "ns-" + lower_ascii(nn)
|
||||
jvput(namespaces, nn, jvs(pg))
|
||||
let cd = jvo()
|
||||
|
|
@ -511,7 +511,7 @@ function js_get(v: JVal, key: pointer) -> pointer {
|
|||
function j_has(v: JVal, key: pointer) -> bool {
|
||||
if v.t != JV_OBJ { return false }
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -527,7 +527,7 @@ function nav_html(b: Sb, links: JVal, cta_href: pointer) -> void {
|
|||
var i = 0
|
||||
while i < len(links.kids) {
|
||||
let nlk = links.kids[i]
|
||||
i = i + 1
|
||||
i += 1
|
||||
let href = js_get(nlk, "href")
|
||||
let label = js_get(nlk, "label")
|
||||
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
|
||||
while i < len(e.params) {
|
||||
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>`)
|
||||
|
|
@ -564,7 +564,7 @@ function render_examples(e: Entry) -> pointer {
|
|||
var i = 0
|
||||
while i < len(e.examples) {
|
||||
sb_puts(b, `<pre data-lang="ludic">{hesc(e.examples[i])}</pre>`)
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
sb_puts(b, `</div>`)
|
||||
return sb_str(b)
|
||||
|
|
@ -575,11 +575,11 @@ function render_related(e: Entry, items: JVal) -> pointer {
|
|||
var i = 0
|
||||
while i < len(e.related_list) {
|
||||
let r = e.related_list[i]
|
||||
i = i + 1
|
||||
i += 1
|
||||
let it = j_get(items, r)
|
||||
if it.t == JV_NULL { continue }
|
||||
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>`)
|
||||
}
|
||||
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")
|
||||
sb_puts(b, "\n<body>\n")
|
||||
topbar(b, cfg)
|
||||
sb_putc(b, 10)
|
||||
sb_putc(b, '\n')
|
||||
var sechref = "api.html#" + s.id
|
||||
if slen(s.ns_name) > 0 { sechref = "ns-" + lower_ascii(s.ns_name) + ".html" }
|
||||
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")
|
||||
sb_puts(b, "\n<body>\n")
|
||||
topbar(b, cfg)
|
||||
sb_putc(b, 10)
|
||||
sb_putc(b, '\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="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
|
||||
while i < len(s.entries) {
|
||||
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, `</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")
|
||||
sb_puts(b, "\n<body>\n")
|
||||
topbar(b, cfg)
|
||||
sb_putc(b, 10)
|
||||
sb_putc(b, '\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="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
|
||||
while gi < len(pal_groups) {
|
||||
let g = pal_groups[gi]
|
||||
gi = gi + 1
|
||||
gi += 1
|
||||
if set_has(seen, g) { continue }
|
||||
push(seen, g)
|
||||
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]
|
||||
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>`)
|
||||
}
|
||||
|
|
@ -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")
|
||||
sb_puts(b, "\n<body>\n")
|
||||
topbar(b, cfg)
|
||||
sb_putc(b, 10)
|
||||
sb_putc(b, '\n')
|
||||
sb_puts(b, `<main class="wrap ref">\n`)
|
||||
sb_puts(b, ` <div class="ref-intro">\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
|
||||
while si < len(sections) {
|
||||
let s = sections[si]
|
||||
si = si + 1
|
||||
si += 1
|
||||
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>`)
|
||||
continue
|
||||
|
|
@ -711,7 +711,7 @@ function render_api(b: Sb, sections: []Section, cfg: JVal) -> void {
|
|||
var ei = 0
|
||||
while ei < len(s.entries) {
|
||||
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, `</div></section>`)
|
||||
|
|
@ -733,20 +733,20 @@ function pct(tmpl: pointer, mk: []pointer, mv: []pointer) -> pointer {
|
|||
let b = sb_new()
|
||||
var i = 0
|
||||
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
|
||||
while j < n and is_word(tmpl[j]) { j = j + 1 }
|
||||
if j > i + 2 and j + 1 < n and tmpl[j] == 41 and tmpl[j + 1] == 115 {
|
||||
while j < n and is_word(tmpl[j]) { j += 1 }
|
||||
if j > i + 2 and j + 1 < n and tmpl[j] == ')' and tmpl[j + 1] == 's' {
|
||||
let key = sslice(tmpl, i + 2, j)
|
||||
var k = 0
|
||||
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
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb_putc(b, tmpl[i])
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return sb_str(b)
|
||||
}
|
||||
|
|
@ -763,7 +763,7 @@ function build_stages(hero: JVal) -> pointer {
|
|||
if st == "ludicc" { hl = " hl" }
|
||||
sb_puts(b, `<span class="stage{hl}">{hesc(st)}</span>`)
|
||||
if i < m - 1 { sb_puts(b, `<span class="arrow">→</span>`) }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return sb_str(b)
|
||||
}
|
||||
|
|
@ -774,7 +774,7 @@ function build_feats(feats: JVal) -> pointer {
|
|||
var i = 0
|
||||
while i < len(cards.kids) {
|
||||
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>`)
|
||||
}
|
||||
return sb_str(b)
|
||||
|
|
@ -784,7 +784,7 @@ function build_phil_paras(phil: JVal) -> pointer {
|
|||
let ps = j_get(phil, "paras")
|
||||
if ps.t != JV_ARR { return "" }
|
||||
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)
|
||||
}
|
||||
function build_phil_stats(phil: JVal) -> pointer {
|
||||
|
|
@ -794,7 +794,7 @@ function build_phil_stats(phil: JVal) -> pointer {
|
|||
var i = 0
|
||||
while i < len(ss.kids) {
|
||||
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>`)
|
||||
}
|
||||
return sb_str(b)
|
||||
|
|
@ -807,7 +807,7 @@ function build_steps(start: JVal) -> pointer {
|
|||
while i < len(st.kids) {
|
||||
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>`)
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return sb_str(b)
|
||||
}
|
||||
|
|
@ -818,9 +818,9 @@ function build_term(start: JVal) -> pointer {
|
|||
var i = 0
|
||||
while i < len(tm.kids) {
|
||||
let t = tm.kids[i]
|
||||
i = i + 1
|
||||
i += 1
|
||||
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, "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`) }
|
||||
|
|
@ -832,7 +832,7 @@ function build_eds(ed: JVal) -> pointer {
|
|||
let ls = j_get(ed, "list")
|
||||
if ls.t != JV_ARR { return "" }
|
||||
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)
|
||||
}
|
||||
function build_footlinks(cfg: JVal) -> pointer {
|
||||
|
|
@ -842,7 +842,7 @@ function build_footlinks(cfg: JVal) -> pointer {
|
|||
var i = 0
|
||||
while i < len(nl.kids) {
|
||||
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>`)
|
||||
}
|
||||
}
|
||||
|
|
@ -857,7 +857,7 @@ function build_samples(cfg: JVal) -> pointer {
|
|||
var i = 0
|
||||
while i < len(sm.kids) {
|
||||
let s = sm.kids[i]
|
||||
i = i + 1
|
||||
i += 1
|
||||
let o = jvo()
|
||||
jvput(o, "name", jvs(js_get(s, "name")))
|
||||
jvput(o, "label", jvs(js_get(s, "label")))
|
||||
|
|
@ -938,11 +938,11 @@ function render_highlighter(sym: JVal) -> pointer {
|
|||
if p < 0 { return tmpl }
|
||||
let b = sb_new()
|
||||
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)
|
||||
var j = p + slen("/*__SYMBOLS__*/{}")
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -953,10 +953,10 @@ function cmd_docs_gen() -> int {
|
|||
while i < arg_count() {
|
||||
let a = arg(i)
|
||||
if a == "--out" {
|
||||
i = i + 1
|
||||
i += 1
|
||||
if i < arg_count() { out = arg(i) }
|
||||
}
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
run(`mkdir -p {out}`)
|
||||
palette_init()
|
||||
|
|
@ -969,33 +969,33 @@ function cmd_docs_gen() -> int {
|
|||
var si = 0
|
||||
while si < len(sections) {
|
||||
let s = sections[si]
|
||||
si = si + 1
|
||||
si += 1
|
||||
if s.cat == "colors" { continue }
|
||||
var ei = 0
|
||||
while ei < len(s.entries) {
|
||||
let e = s.entries[ei]
|
||||
ei = ei + 1
|
||||
ei += 1
|
||||
let pb = sb_new()
|
||||
render_item(pb, e, s, cfg, syms.items)
|
||||
write_file(`{out}/{e.id}.html`, sb_str(pb))
|
||||
npages = npages + 1
|
||||
npages += 1
|
||||
}
|
||||
}
|
||||
si = 0
|
||||
while si < len(sections) {
|
||||
let s = sections[si]
|
||||
si = si + 1
|
||||
si += 1
|
||||
if slen(s.ns_name) > 0 {
|
||||
let pb = sb_new()
|
||||
render_ns(pb, s.ns_name, s, cfg)
|
||||
write_file(`{out}/ns-{lower_ascii(s.ns_name)}.html`, sb_str(pb))
|
||||
npages = npages + 1
|
||||
npages += 1
|
||||
}
|
||||
}
|
||||
let cb = sb_new()
|
||||
render_color(cb, cfg)
|
||||
write_file(`{out}/ns-color.html`, sb_str(cb))
|
||||
npages = npages + 1
|
||||
npages += 1
|
||||
let ab = sb_new()
|
||||
render_api(ab, sections, cfg)
|
||||
write_file(`{out}/api.html`, sb_str(ab))
|
||||
|
|
|
|||
93
tools/x/forgejo.ludic
Normal file
93
tools/x/forgejo.ludic
Normal 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
|
||||
}
|
||||
|
|
@ -26,18 +26,18 @@ var jpos: int = 0
|
|||
var jerr: bool = false
|
||||
|
||||
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 {
|
||||
if c >= 48 and c <= 57 { return c - 48 }
|
||||
if c >= 97 and c <= 102 { return c - 97 + 10 }
|
||||
if c >= 65 and c <= 70 { return c - 65 + 10 }
|
||||
if c >= '0' and c <= '9' { return c - 48 }
|
||||
if c >= 'a' and c <= 'f' { return c - 97 + 10 }
|
||||
if c >= 'A' and c <= 'F' { return c - 65 + 10 }
|
||||
return 0
|
||||
}
|
||||
function j_hex4() -> int {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
function j_string() -> pointer {
|
||||
jpos = jpos + 1 # opening quote
|
||||
jpos += 1 # opening quote
|
||||
# over-allocate: escapes only ever shrink the byte count
|
||||
let cap = slen(jbuf) - jpos + 1
|
||||
let out = bytes(cap + 4)
|
||||
var n = 0
|
||||
while jbuf[jpos] != 0 and jbuf[jpos] != 34 {
|
||||
if jbuf[jpos] == 92 {
|
||||
jpos = jpos + 1
|
||||
let c = jbuf[jpos]; jpos = jpos + 1
|
||||
if c == 110 { out[n] = 10; n = n + 1 }
|
||||
else { if c == 116 { out[n] = 9; n = n + 1 }
|
||||
else { if c == 114 { out[n] = 13; n = n + 1 }
|
||||
else { if c == 98 { out[n] = 8; n = n + 1 }
|
||||
else { if c == 102 { out[n] = 12; n = n + 1 }
|
||||
else { if c == 117 {
|
||||
while jbuf[jpos] != 0 and jbuf[jpos] != '"' {
|
||||
if jbuf[jpos] == '\\' {
|
||||
jpos += 1
|
||||
let c = jbuf[jpos]; jpos += 1
|
||||
if c == 'n' { out[n] = '\n'; n += 1 }
|
||||
else { if c == 't' { out[n] = '\t'; n += 1 }
|
||||
else { if c == 'r' { out[n] = '\r'; n += 1 }
|
||||
else { if c == 'b' { out[n] = 8; n += 1 }
|
||||
else { if c == 'f' { out[n] = 12; n += 1 }
|
||||
else { if c == 'u' {
|
||||
var cp = j_hex4()
|
||||
if cp >= 55296 and cp < 56320 and jbuf[jpos] == 92 and jbuf[jpos + 1] == 117 {
|
||||
jpos = jpos + 2
|
||||
if cp >= 55296 and cp < 56320 and jbuf[jpos] == '\\' and jbuf[jpos + 1] == 'u' {
|
||||
jpos += 2
|
||||
let lo = j_hex4()
|
||||
cp = 65536 + ((cp - 55296) << 10) + (lo - 56320)
|
||||
}
|
||||
n = utf8_put(out, n, cp)
|
||||
} else { out[n] = c; n = n + 1 } } } } } }
|
||||
} else { out[n] = jbuf[jpos]; n = n + 1; jpos = jpos + 1 }
|
||||
} else { out[n] = c; n += 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
|
||||
return out
|
||||
}
|
||||
|
|
@ -85,55 +85,55 @@ function j_string() -> pointer {
|
|||
function j_value() -> JVal {
|
||||
j_ws()
|
||||
let c = jbuf[jpos]
|
||||
if c == 123 { # '{'
|
||||
jpos = jpos + 1
|
||||
if c == '{' { # '{'
|
||||
jpos += 1
|
||||
let v = jval_new(JV_OBJ)
|
||||
j_ws()
|
||||
if jbuf[jpos] == 125 { jpos = jpos + 1; return v }
|
||||
if jbuf[jpos] == '}' { jpos += 1; return v }
|
||||
while true {
|
||||
j_ws()
|
||||
var key = ""
|
||||
if jbuf[jpos] == 34 { key = j_string() } else { jerr = true }
|
||||
j_ws(); if jbuf[jpos] == 58 { jpos = jpos + 1 } else { jerr = true }
|
||||
if jbuf[jpos] == '"' { key = j_string() } else { jerr = true }
|
||||
j_ws(); if jbuf[jpos] == ':' { jpos += 1 } else { jerr = true }
|
||||
let kid = j_value()
|
||||
push(v.keys, key); push(v.kids, kid)
|
||||
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
|
||||
}
|
||||
if c == 91 { # '['
|
||||
jpos = jpos + 1
|
||||
if c == '[' { # '['
|
||||
jpos += 1
|
||||
let v = jval_new(JV_ARR)
|
||||
j_ws()
|
||||
if jbuf[jpos] == 93 { jpos = jpos + 1; return v }
|
||||
if jbuf[jpos] == ']' { jpos += 1; return v }
|
||||
while true {
|
||||
let kid = j_value()
|
||||
push(v.kids, kid)
|
||||
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
|
||||
}
|
||||
if c == 34 { # '"'
|
||||
if c == '"' { # '"'
|
||||
let v = jval_new(JV_STR)
|
||||
v.s = j_string()
|
||||
return v
|
||||
}
|
||||
if c == 116 { # true
|
||||
jpos = jpos + 4; let v = jval_new(JV_BOOL); v.b = 1; return v
|
||||
if c == 't' { # true
|
||||
jpos += 4; let v = jval_new(JV_BOOL); v.b = 1; return v
|
||||
}
|
||||
if c == 102 { # false
|
||||
jpos = jpos + 5; let v = jval_new(JV_BOOL); v.b = 0; return v
|
||||
if c == 'f' { # false
|
||||
jpos += 5; let v = jval_new(JV_BOOL); v.b = 0; return v
|
||||
}
|
||||
if c == 110 { # null
|
||||
jpos = jpos + 4; return jval_new(JV_NULL)
|
||||
if c == 'n' { # null
|
||||
jpos += 4; return jval_new(JV_NULL)
|
||||
}
|
||||
# number
|
||||
let start = jpos
|
||||
if jbuf[jpos] == 45 { 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) }
|
||||
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 }
|
||||
if jbuf[jpos] == '-' { jpos += 1 }
|
||||
if jbuf[jpos] < '0' or jbuf[jpos] > '9' { jerr = true; if jbuf[jpos] != 0 { jpos += 1 }; return jval_new(JV_NULL) }
|
||||
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)
|
||||
v.s = sslice(jbuf, start, jpos)
|
||||
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)
|
||||
function str_to_int(s: pointer) -> int {
|
||||
var i = 0; var neg = false
|
||||
if s[0] == 45 { neg = true; i = 1 }
|
||||
if s[0] == '-' { neg = true; i = 1 }
|
||||
var v = 0
|
||||
while s[i] >= 48 and s[i] <= 57 { v = v * 10 + (s[i] - 48); i = i + 1 }
|
||||
if neg { return 0 - v }
|
||||
while s[i] >= '0' and s[i] <= '9' { v = v * 10 + (s[i] - 48); i += 1 }
|
||||
if neg { 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)
|
||||
function j_get(v: JVal, key: pointer) -> JVal {
|
||||
if v.t != JV_OBJ { return jval_new(JV_NULL) }
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
373
tools/x/lsp_test.ludic
Normal file
373
tools/x/lsp_test.ludic
Normal 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()
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ program X {
|
|||
import "selfhost_test.ludic"
|
||||
import "test.ludic"
|
||||
import "json.ludic"
|
||||
import "lsp_test.ludic"
|
||||
import "forgejo.ludic"
|
||||
import "checks.ludic"
|
||||
import "docgen.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 selfhost-test the self-hosting suite (correctness + bootstrap fixpoints)")
|
||||
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("")
|
||||
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 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-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("release:")
|
||||
print(" x version print the toolchain version (ludicc --version)")
|
||||
|
|
@ -83,53 +86,63 @@ program X {
|
|||
return dflt
|
||||
}
|
||||
|
||||
entry {
|
||||
if (arg_count() < 2) { usage(); exit(1) }
|
||||
let cmd = arg(1)
|
||||
|
||||
if (cmd == "build") { exit(cmd_build()) }
|
||||
if (cmd == "build-cli") { exit(cmd_build_cli()) }
|
||||
if (cmd == "app") { exit(cmd_app()) }
|
||||
if (cmd == "tools") { exit(cmd_tools()) }
|
||||
if (cmd == "clean") { exit(cmd_clean()) }
|
||||
# route one subcommand to its implementation and return its exit code; -1 when
|
||||
# the name is unknown. Returning (rather than exiting in place) lets entry tear
|
||||
# down the per-process scratch directory before the process ends.
|
||||
function dispatch(cmd: pointer) -> int {
|
||||
if (cmd == "build") { return cmd_build() }
|
||||
if (cmd == "build-cli") { return cmd_build_cli() }
|
||||
if (cmd == "app") { return cmd_app() }
|
||||
if (cmd == "tools") { return cmd_tools() }
|
||||
if (cmd == "clean") { return cmd_clean() }
|
||||
if (cmd == "test") {
|
||||
if (argn(2, "") == "--coverage") { exit(cmd_test_coverage()) }
|
||||
exit(cmd_test())
|
||||
if (argn(2, "") == "--coverage") { return cmd_test_coverage() }
|
||||
return cmd_test()
|
||||
}
|
||||
if (cmd == "selfhost-test") { exit(cmd_selfhost_test()) }
|
||||
if (cmd == "test-tools") { exit(cmd_test_tools()) }
|
||||
if (cmd == "check-docs") { exit(cmd_check_docs()) }
|
||||
if (cmd == "check-impl") { exit(cmd_check_impl()) }
|
||||
if (cmd == "check-vocabulary") { exit(cmd_check_vocab()) }
|
||||
if (cmd == "lint-asset") { exit(cmd_lint_asset()) }
|
||||
if (cmd == "docs-palette") { exit(cmd_docs_palette()) }
|
||||
if (cmd == "docs-gen") { exit(cmd_docs_gen()) }
|
||||
if (cmd == "docs-check") { exit(cmd_docs_check()) }
|
||||
if (cmd == "golden") { exit(cmd_golden()) }
|
||||
if (cmd == "bootstrap") { exit(cmd_bootstrap()) }
|
||||
if (cmd == "bootstrap-cfree") { exit(cmd_bootstrap_cfree()) }
|
||||
if (cmd == "reseed") { exit(cmd_reseed()) }
|
||||
if (cmd == "selfhost-build") { exit(cmd_selfhost_build(argn(2, "bin/ludicc"), argn(3, "build/selfhost"))) }
|
||||
if (cmd == "selfhost-test") { return cmd_selfhost_test() }
|
||||
if (cmd == "test-tools") { return cmd_test_tools() }
|
||||
if (cmd == "test-lsp") { return cmd_test_lsp() }
|
||||
if (cmd == "check-docs") { return cmd_check_docs() }
|
||||
if (cmd == "check-impl") { return cmd_check_impl() }
|
||||
if (cmd == "check-vocabulary") { return cmd_check_vocab() }
|
||||
if (cmd == "lint-asset") { return cmd_lint_asset() }
|
||||
if (cmd == "docs-palette") { return cmd_docs_palette() }
|
||||
if (cmd == "docs-gen") { return cmd_docs_gen() }
|
||||
if (cmd == "docs-check") { return cmd_docs_check() }
|
||||
if (cmd == "golden") { return cmd_golden() }
|
||||
if (cmd == "bootstrap") { return cmd_bootstrap() }
|
||||
if (cmd == "bootstrap-cfree") { return cmd_bootstrap_cfree() }
|
||||
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 (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 (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 == "get") { exit(cmd_pkg_get()) }
|
||||
if (cmd == "update") { exit(cmd_pkg_update()) }
|
||||
if (cmd == "verify") { exit(cmd_pkg_verify()) }
|
||||
if (cmd == "vendor") { exit(cmd_pkg_vendor()) }
|
||||
if (cmd == "build-lib") { exit(cmd_pkg_build_lib()) }
|
||||
if (cmd == "link-flags") { exit(cmd_pkg_link_flags()) }
|
||||
if (cmd == "test-pkg") { exit(cmd_test_pkg()) }
|
||||
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { exit(cmd_version()) }
|
||||
if (cmd == "release") { exit(cmd_release()) }
|
||||
if (cmd == "help") or (cmd == "--help") or (cmd == "-h") { usage(); exit(0) }
|
||||
if (cmd == "add") { return cmd_pkg_add() }
|
||||
if (cmd == "get") { return cmd_pkg_get() }
|
||||
if (cmd == "update") { return cmd_pkg_update() }
|
||||
if (cmd == "verify") { return cmd_pkg_verify() }
|
||||
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
||||
if (cmd == "build-lib") { return cmd_pkg_build_lib() }
|
||||
if (cmd == "link-flags") { return cmd_pkg_link_flags() }
|
||||
if (cmd == "test-pkg") { return cmd_test_pkg() }
|
||||
if (cmd == "version") or (cmd == "--version") or (cmd == "-v") { return cmd_version() }
|
||||
if (cmd == "release") { return cmd_release() }
|
||||
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`)
|
||||
usage()
|
||||
exit(1)
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ function manifest_new() -> Manifest {
|
|||
}
|
||||
|
||||
# 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) ------
|
||||
|
||||
# drop a leading 'v' (118) from a tag/version string
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ function ver_nums(s: pointer) -> []int {
|
|||
var i = 0
|
||||
while i < n {
|
||||
let c = v[i]
|
||||
if c >= 48 and c <= 57 { cur = cur * 10 + (c - 48) }
|
||||
else if c == 46 { push(parts, cur); cur = 0 }
|
||||
if c >= '0' and c <= '9' { cur = cur * 10 + (c - 48) }
|
||||
else if c == '.' { push(parts, cur); cur = 0 }
|
||||
else { i = n } # a '-'/'+' ends the numeric core
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
push(parts, cur)
|
||||
while len(parts) < 3 { push(parts, 0) }
|
||||
|
|
@ -82,9 +82,9 @@ function ver_cmp(a: pointer, b: pointer) -> int {
|
|||
let pb = ver_nums(b)
|
||||
var i = 0
|
||||
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 }
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
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 {
|
||||
let v = strip_v(s)
|
||||
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 ------------------------
|
||||
|
|
@ -107,17 +107,17 @@ function tok_line(line: pointer) -> []pointer {
|
|||
var i = 0
|
||||
while i < n {
|
||||
let c = line[i]
|
||||
if c == 35 { i = n } # '#' comment
|
||||
else if c == 32 or c == 9 { i = i + 1 } # whitespace
|
||||
else if c == 34 { # "quoted"
|
||||
i = i + 1
|
||||
if c == '#' { i = n } # '#' comment
|
||||
else if c == ' ' or c == '\t' { i += 1 } # whitespace
|
||||
else if c == '"' { # "quoted"
|
||||
i += 1
|
||||
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))
|
||||
if i < n { i = i + 1 }
|
||||
if i < n { i += 1 }
|
||||
} else { # bare word
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -153,8 +153,8 @@ function parse_manifest(text: pointer) -> Manifest {
|
|||
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 == "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 == "targets" { var k = 1; while k < len(ts) { push(m.targets, 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 += 1 } }
|
||||
else if head == "require" {
|
||||
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
|
||||
while k < len(ts) {
|
||||
let key = ts[k]
|
||||
if key == "version" and k + 1 < len(ts) { m.ver = ts[k + 1]; k = k + 2 }
|
||||
else if key == "hash" and k + 1 < len(ts) { m.hash = ts[k + 1]; k = k + 2 }
|
||||
else if key == "kind" and k + 1 < len(ts) { m.kind = ts[k + 1]; k = k + 2 }
|
||||
else if key == "provides" and k + 1 < len(ts) { push(m.provides, ts[k + 1]); k = k + 2 }
|
||||
else { k = k + 1 }
|
||||
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 += 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 += 2 }
|
||||
else { k += 1 }
|
||||
}
|
||||
push(out, m)
|
||||
}
|
||||
|
|
@ -201,7 +201,7 @@ function store_root() -> pointer {
|
|||
function ensure_dir_slash(d: pointer) -> pointer {
|
||||
let n = slen(d)
|
||||
if n == 0 { return d }
|
||||
if d[n - 1] == 47 { return d }
|
||||
if d[n - 1] == '/' { return d }
|
||||
return d + "/"
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ function latest_version(module: pointer) -> pointer {
|
|||
let cand = strip_v(tg)
|
||||
if best == "" or ver_gt(cand, best) { best = cand }
|
||||
}
|
||||
k = k + 1
|
||||
k += 1
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
|
@ -317,8 +317,8 @@ function link_module(module: pointer, hash: pointer) -> void {
|
|||
|
||||
function find_mod(mods: []pointer, m: pointer) -> int {
|
||||
var i = 0
|
||||
while i < len(mods) { if mods[i] == m { return i }; i = i + 1 }
|
||||
return 0 - 1
|
||||
while i < len(mods) { if mods[i] == m { return i }; i += 1 }
|
||||
return -1
|
||||
}
|
||||
|
||||
# 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 fv = new []pointer
|
||||
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 {
|
||||
var nm = new []pointer
|
||||
|
|
@ -346,9 +346,9 @@ function resolve(root: Manifest) -> []Manifest {
|
|||
if take {
|
||||
let man = fetch_manifest(m, v) # expand the chosen version's requires
|
||||
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
|
||||
fv = nv
|
||||
|
|
@ -361,11 +361,11 @@ function resolve(root: Manifest) -> []Manifest {
|
|||
var seen = new []pointer
|
||||
var q = new []pointer
|
||||
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
|
||||
while qi < len(q) {
|
||||
let m = q[qi]
|
||||
qi = qi + 1
|
||||
qi += 1
|
||||
if find_mod(seen, m) < 0 {
|
||||
push(seen, m)
|
||||
let idx = find_mod(mods, m)
|
||||
|
|
@ -374,7 +374,7 @@ function resolve(root: Manifest) -> []Manifest {
|
|||
let man = fetch_manifest(m, ver)
|
||||
push(out, man)
|
||||
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) {
|
||||
let x = a[i]
|
||||
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
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
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}` }
|
||||
push(ns, name)
|
||||
push(owner, sels[i].module)
|
||||
p = p + 1
|
||||
p += 1
|
||||
}
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -427,9 +427,9 @@ function write_lock(sels: []Manifest) -> bool {
|
|||
let m = sorted[i]
|
||||
var line = `module "{m.module}" version "{m.ver}" hash "{m.hash}" kind "{m.kind}"`
|
||||
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()
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return write_file("package.lock.ludic", body)
|
||||
}
|
||||
|
|
@ -462,7 +462,7 @@ function do_install(root: Manifest) -> int {
|
|||
m.hash = `sha256:{h}`
|
||||
link_module(m.module, h)
|
||||
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 }
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -498,9 +498,9 @@ function read_root_manifest() -> Manifest {
|
|||
function split_spec(spec: pointer) -> []pointer {
|
||||
var out = new []pointer
|
||||
let n = slen(spec)
|
||||
var at = 0 - 1
|
||||
var at = -1
|
||||
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, "") }
|
||||
else { push(out, sslice(spec, 0, at)); push(out, sslice(spec, at + 1, n)) }
|
||||
return out
|
||||
|
|
@ -575,7 +575,7 @@ function cmd_pkg_update() -> int {
|
|||
set_require(d.module, latest)
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
return do_install(read_root_manifest())
|
||||
}
|
||||
|
|
@ -596,19 +596,19 @@ function cmd_pkg_verify() -> int {
|
|||
let dest = `{store_root()}{raw}`
|
||||
if not file_exists(dest) {
|
||||
print(` MISSING {m.module}@{m.ver} (store entry {want} absent)`)
|
||||
bad_count = bad_count + 1
|
||||
bad_count += 1
|
||||
} else {
|
||||
let got = dir_hash(dest)
|
||||
if got == raw {
|
||||
let link = `ludic_modules/{m.module}`
|
||||
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 {
|
||||
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 }
|
||||
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(`cp -R {dest} {vdir}`)
|
||||
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`)
|
||||
return 0
|
||||
|
|
@ -656,14 +656,14 @@ function cmd_pkg_build_lib() -> int {
|
|||
let t = target_id()
|
||||
run(`mkdir -p lib/{t}`)
|
||||
let ludicc = getenv_or("LUDICC", "bin/ludicc")
|
||||
let ll = "/tmp/x_buildlib.ll"
|
||||
if not shq(`{ludicc} --emit-module {src} -o {ll} 2>/tmp/x_bl.err`) {
|
||||
err(`x: build-lib compile failed — {capture_line("tail -1 /tmp/x_bl.err")}\n`); return 1
|
||||
let ll = `{tmp_dir()}/buildlib.ll`
|
||||
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_dir()}/bl.err")}\n`); return 1
|
||||
}
|
||||
let out = `lib/{t}/lib{name}.dylib`
|
||||
# @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`) {
|
||||
err(`x: build-lib link failed — {capture_line("tail -1 /tmp/x_bl.err")}\n`); return 1
|
||||
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_dir()}/bl.err")}\n`); return 1
|
||||
}
|
||||
run(`rm -f {ll}`)
|
||||
print(`built {out} (target {t})`)
|
||||
|
|
@ -700,12 +700,12 @@ function prebuilt_link_flags() -> pointer {
|
|||
let dir = `{store_root()}{raw}/lib/{t}`
|
||||
let libs = split_lines(capture(`ls {dir}/*.dylib 2>/dev/null`))
|
||||
var k = 0
|
||||
while k < len(libs) { flags = flags + " " + libs[k]; any = true; k = k + 1 }
|
||||
if len(libs) > 0 { flags = flags + ` -Wl,-rpath,{dir}` }
|
||||
while k < len(libs) { flags = flags + " " + libs[k]; any = true; k += 1 }
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function cmd_test_pkg() -> int {
|
|||
}
|
||||
|
||||
let root = capture_line("pwd")
|
||||
let work = "/tmp/x_pkg_test"
|
||||
let work = `{tmp_dir()}/pkg_test`
|
||||
let proxy = `{work}/proxy`
|
||||
let store = `{work}/store`
|
||||
let proj = `{work}/proj`
|
||||
|
|
|
|||
|
|
@ -49,9 +49,37 @@ function sh(cmd: pointer) -> int { return exit_code(run(cmd)) }
|
|||
# run a command, true when it succeeded
|
||||
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.
|
||||
function capture(cmd: pointer) -> pointer {
|
||||
let tmp = "/tmp/x_capture.out"
|
||||
let tmp = tmp_path("capture.out")
|
||||
run(`{cmd} > {tmp} 2>/dev/null`)
|
||||
let s = read_file(tmp)
|
||||
if (s == null) { return "" }
|
||||
|
|
@ -83,7 +111,7 @@ function flat(p: pointer) -> pointer {
|
|||
let b = bytes(n + 1)
|
||||
for i in 0 .. n {
|
||||
var c = p[i]
|
||||
if (c == 47) { c = 95 } # '/' (47) -> '_' (95)
|
||||
if (c == '/') { c = 95 } # '/' (47) -> '_' (95)
|
||||
b[i] = c
|
||||
}
|
||||
b[n] = 0
|
||||
|
|
@ -108,11 +136,11 @@ var PASS: int = 0
|
|||
var FAIL: int = 0
|
||||
|
||||
function ok(msg: pointer) -> void {
|
||||
PASS = PASS + 1
|
||||
PASS += 1
|
||||
print(` {c_green()}PASS{c_reset()} {msg}`)
|
||||
}
|
||||
function bad(msg: pointer) -> void {
|
||||
FAIL = FAIL + 1
|
||||
FAIL += 1
|
||||
print(` {c_red()}FAIL{c_reset()} {msg}`)
|
||||
}
|
||||
function bad2(msg: pointer, detail: pointer) -> void {
|
||||
|
|
|
|||
|
|
@ -59,16 +59,16 @@ function cmd_version() -> int {
|
|||
}
|
||||
|
||||
# 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 {
|
||||
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)
|
||||
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("printf '\n' >> /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_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).
|
||||
function prepend_changelog() -> void {
|
||||
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")
|
||||
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 {
|
||||
run(`head -n $(expr {ln} - 1) CHANGELOG.md > /tmp/x_rel_new.md`)
|
||||
run("cat /tmp/x_rel_section.md >> /tmp/x_rel_new.md")
|
||||
run(`tail -n +{ln} CHANGELOG.md >> /tmp/x_rel_new.md`)
|
||||
run(`head -n $(expr {ln} - 1) CHANGELOG.md > {tmp_dir()}/rel_new.md`)
|
||||
run(`cat {tmp_dir()}/rel_section.md >> {tmp_dir()}/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.
|
||||
function publish_release(ver: pointer) -> int {
|
||||
let tok = getenv_or("FORGEJO_TOKEN", "")
|
||||
if (tok == "") { err("release --publish: set FORGEJO_TOKEN (a Forgejo access token)\n"); return 1 }
|
||||
if getenv_or("FORGEJO_TOKEN", "") == "" { 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 v{ver}`) { err("release: git push (tag) failed\n"); return 1 }
|
||||
# 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`)
|
||||
}
|
||||
# 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
|
||||
}
|
||||
return 0
|
||||
|
|
@ -116,7 +115,7 @@ function cmd_release() -> int {
|
|||
if (a == "--publish") { publish = true }
|
||||
else { if (a == "major") or (a == "minor") or (a == "patch") { level = a }
|
||||
else { err(`release: unknown argument {a}\n`); return 1 } }
|
||||
ai = ai + 1
|
||||
ai += 1
|
||||
}
|
||||
|
||||
if not has_changesets() {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function write_selfhost_src(outpath: pointer) -> bool {
|
|||
let s = read_file(frags[i])
|
||||
if (s != null) { file_write(f, s, len(s)) }
|
||||
file_write(f, "\n", 1)
|
||||
i = i + 1
|
||||
i += 1
|
||||
}
|
||||
let ftr = "}\n"
|
||||
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.
|
||||
|
||||
# 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 {
|
||||
let ll = `{outbin}.ll`
|
||||
if not shq(`{shbin} {game} > {ll} 2>/tmp/x_gb.err`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll} -o {outbin} 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_dir()}/gb.err`) { return false }
|
||||
run(`rm -f {ll}`)
|
||||
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 {
|
||||
if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 }
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
# compile selfhost/tests/<name>.ludic with bin/ludicc, assemble, run, compare
|
||||
function sh_case(name: pointer, exp: pointer) -> void {
|
||||
let ll = `/tmp/x_sh_{name}.ll`
|
||||
let er = `/tmp/x_sh_{name}.err`
|
||||
let bn = `/tmp/x_sh_{name}`
|
||||
let ll = `{tmp_dir()}/sh_{name}.ll`
|
||||
let er = `{tmp_dir()}/sh_{name}.err`
|
||||
let bn = `{tmp_dir()}/sh_{name}`
|
||||
if not shq(`bin/ludicc selfhost/tests/{name}.ludic > {ll} 2>{er}`) {
|
||||
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).
|
||||
function game_case(path: pointer, keys: pointer) -> void {
|
||||
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(`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
|
||||
# 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
|
||||
|
|
@ -41,11 +41,11 @@ function game_case(path: pointer, keys: pointer) -> void {
|
|||
# string on build failure). Shared by `bin/x golden` and nothing else.
|
||||
function bless_render(path: pointer, keys: pointer) -> pointer {
|
||||
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 ""
|
||||
}
|
||||
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")
|
||||
return `{path} {h}\n`
|
||||
}
|
||||
|
|
@ -58,10 +58,10 @@ function cmd_golden() -> int {
|
|||
ensure_ludicc()
|
||||
if not is_exec("bin/ludicc") { err("golden: bin/ludicc unavailable\n"); return 1 }
|
||||
var manifest = ""
|
||||
manifest = manifest + bless_render("games/snake", "ddssaawwddss")
|
||||
manifest = manifest + bless_render("games/menu", "ssss")
|
||||
manifest = manifest + bless_render("games/chronorift", "ddddwwwwaassK")
|
||||
manifest = manifest + bless_render("rendering/draw_prims", "")
|
||||
manifest += bless_render("games/snake", "ddssaawwddss")
|
||||
manifest += bless_render("games/menu", "ssss")
|
||||
manifest += bless_render("games/chronorift", "ddddwwwwaassK")
|
||||
manifest += bless_render("rendering/draw_prims", "")
|
||||
if not write_file("selfhost/golden/renders.sha256", manifest) {
|
||||
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", "")
|
||||
|
||||
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)") }
|
||||
else { bad("fixpoint not reached"); out(capture("cat /tmp/x_boot.out")) }
|
||||
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_dir()}/boot.out`)) }
|
||||
|
||||
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") }
|
||||
else { bad("C-free bootstrap failed (seed stale? run: x reseed)"); out(capture("cat /tmp/x_cfree.out")) }
|
||||
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_dir()}/cfree.out`)) }
|
||||
|
||||
return report()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@
|
|||
# its stdout against the expected space-joined output.
|
||||
function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `/tmp/x_e_{nm}`) {
|
||||
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return
|
||||
if not game_build_ok("bin/ludicc", `examples/{path}.ludic`, `{tmp_dir()}/e_{nm}`) {
|
||||
bad2(label, capture_line(`grep -i error {tmp_dir()}/gb.err | head -1`)); return
|
||||
}
|
||||
var got = ""
|
||||
if (keys == "") { got = capture_line(`/tmp/x_e_{nm} < /dev/null`) }
|
||||
else { got = capture_line(`printf '{keys}' | /tmp/x_e_{nm}`) }
|
||||
if (keys == "") { got = capture_line(`{tmp_dir()}/e_{nm} < /dev/null`) }
|
||||
else { got = capture_line(`printf '{keys}' | {tmp_dir()}/e_{nm}`) }
|
||||
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 {
|
||||
let nm = flat(path)
|
||||
let root = capture_line("pwd")
|
||||
let ll = `/tmp/x_c_{nm}.ll`
|
||||
if not shq(`LUDIC_HOME={root} LUDIC_MODULES={root}/packages bin/ludicc examples/{path}.ludic > {ll} 2>/tmp/x_gb.err`) {
|
||||
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return
|
||||
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_dir()}/gb.err`) {
|
||||
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`) {
|
||||
bad2(label, capture_line("grep -i error /tmp/x_gb.err | head -1")); return
|
||||
if not shq(`{cc()} -O2 {ll} -o {tmp_dir()}/c_{nm} 2>{tmp_dir()}/gb.err`) {
|
||||
bad2(label, capture_line(`grep -i error {tmp_dir()}/gb.err | head -1`)); return
|
||||
}
|
||||
var got = ""
|
||||
if (keys == "") { got = capture_line(`/tmp/x_c_{nm} < /dev/null`) }
|
||||
else { got = capture_line(`printf '{keys}' | /tmp/x_c_{nm}`) }
|
||||
if (keys == "") { got = capture_line(`{tmp_dir()}/c_{nm} < /dev/null`) }
|
||||
else { got = capture_line(`printf '{keys}' | {tmp_dir()}/c_{nm}`) }
|
||||
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.
|
||||
function runtime_home_case() -> void {
|
||||
let root = capture_line("pwd")
|
||||
let dir = "/tmp/x_ext75"
|
||||
let dir = `{tmp_dir()}/ext75`
|
||||
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")
|
||||
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
|
||||
function qsmoke(path: pointer) -> void {
|
||||
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)`)
|
||||
} 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
|
||||
# `entry` (so net_case can't drive it): prove it still parses, lowers and links.
|
||||
function smoke(path: pointer) -> void {
|
||||
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`) {
|
||||
bad2(path, capture_line(`tail -1 /tmp/x_sm_{nm}.err`)); return
|
||||
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_dir()}/sm_{nm}.err`)); return
|
||||
}
|
||||
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.
|
||||
function net_case(path: pointer, exp: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
let ll = `/tmp/x_n_{nm}.ll`
|
||||
let log = `/tmp/x_n_{nm}.out`
|
||||
let ll = `{tmp_dir()}/n_{nm}.ll`
|
||||
let log = `{tmp_dir()}/n_{nm}.out`
|
||||
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
|
||||
let t = capture_line(`tail -1 {log}`)
|
||||
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}`)
|
||||
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}]`) }
|
||||
}
|
||||
|
||||
|
|
@ -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.
|
||||
function spec_case(path: pointer, exp: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
let ll = `/tmp/x_spec_{nm}.ll`
|
||||
let er = `/tmp/x_spec_{nm}.err`
|
||||
let ll = `{tmp_dir()}/spec_{nm}.ll`
|
||||
let er = `{tmp_dir()}/spec_{nm}.err`
|
||||
if not shq(`bin/ludicc examples/{path}.ludic > {ll} 2>{er}`) {
|
||||
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
|
||||
}
|
||||
let rc = sh(`/tmp/x_spec_{nm} > /tmp/x_spec_{nm}.out 2>&1`)
|
||||
let summary = capture_line(`tail -1 /tmp/x_spec_{nm}.out`)
|
||||
let rc = sh(`{tmp_dir()}/spec_{nm} > {tmp_dir()}/spec_{nm}.out 2>&1`)
|
||||
let summary = capture_line(`tail -1 {tmp_dir()}/spec_{nm}.out`)
|
||||
if (rc == 0) and (summary == exp) { ok(`{path} ({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
|
||||
# `file:line: panic: <msg>` line on stderr (stdout is unaffected up to the panic).
|
||||
function panic_case() -> void {
|
||||
write_file("/tmp/x_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(`{cc()} -O2 /tmp/x_panic.ll -o /tmp/x_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 msg = capture_line("cat /tmp/x_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})`) }
|
||||
write_file(`{tmp_dir()}/panic.ludic`, "program P { entry { print(7); panic(\"boom\") } }\n")
|
||||
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_dir()}/panic.ll -o {tmp_dir()}/panic 2>/dev/null`) { bad("panic example did not link"); return }
|
||||
let rc = sh(`{tmp_dir()}/panic > {tmp_dir()}/panic.out 2>{tmp_dir()}/panic.err`)
|
||||
let msg = capture_line(`cat {tmp_dir()}/panic.err`)
|
||||
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}]`) }
|
||||
}
|
||||
|
||||
|
|
@ -132,14 +132,14 @@ function panic_case() -> void {
|
|||
# so this reuses the same seed-built bin/ludicc the rest of the suite does.
|
||||
function cov_one(path: pointer, dir: pointer) -> int {
|
||||
let nm = flat(path)
|
||||
let ll = `/tmp/x_cov_{nm}.ll`
|
||||
let bin = `/tmp/x_cov_{nm}`
|
||||
let ll = `{tmp_dir()}/cov_{nm}.ll`
|
||||
let bin = `{tmp_dir()}/cov_{nm}`
|
||||
let cov = `{dir}/{nm}.cov`
|
||||
if not shq(`bin/ludicc --coverage examples/{path}.ludic > {ll} 2>/tmp/x_cov.err`) {
|
||||
bad2(path, capture_line("tail -1 /tmp/x_cov.err")); return 1
|
||||
if not shq(`bin/ludicc --coverage examples/{path}.ludic > {ll} 2>{tmp_dir()}/cov.err`) {
|
||||
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`) {
|
||||
bad2(path, capture_line("tail -1 /tmp/x_cov.err")); return 1
|
||||
if not shq(`{cc()} -O2 {ll} -o {bin} 2>{tmp_dir()}/cov.err`) {
|
||||
bad2(path, capture_line(`tail -1 {tmp_dir()}/cov.err`)); return 1
|
||||
}
|
||||
run(`rm -f {ll}`)
|
||||
# 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)}%`
|
||||
if not (missed == "") { line = `{line} uncovered: {missed}` }
|
||||
print(line)
|
||||
COV_COVERED = COV_COVERED + covered
|
||||
COV_TOTAL = COV_TOTAL + total
|
||||
COV_COVERED += covered
|
||||
COV_TOTAL += total
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -170,15 +170,15 @@ var COV_TOTAL: int = 0
|
|||
function cmd_test_coverage() -> int {
|
||||
# the suite normally rebuilds bin/ludicc from the seed; do the same here so the
|
||||
# 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 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_TOTAL = 0
|
||||
print("== line coverage (bin/x test --coverage) ==")
|
||||
cov_one("library/coverage", "/tmp/x_cov_out")
|
||||
cov_one("library/testing", "/tmp/x_cov_out")
|
||||
cov_one("library/coverage", `{tmp_dir()}/cov_out`)
|
||||
cov_one("library/testing", `{tmp_dir()}/cov_out`)
|
||||
var pct = 100
|
||||
if COV_TOTAL > 0 { pct = (COV_COVERED * 100) / COV_TOTAL }
|
||||
print(` ----`)
|
||||
|
|
@ -209,10 +209,10 @@ function cmd_test() -> int {
|
|||
game_case("games/chronorift", "ddddwwwwaassK")
|
||||
|
||||
print("== self-hosting and the bootstrap fixpoints ==")
|
||||
run("bin/x selfhost-test > /tmp/x_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 'no C compiler' /tmp/x_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")
|
||||
run(`bin/x selfhost-test > {tmp_dir()}/sh.out 2>&1`)
|
||||
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_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_dir()}/sh.out`)
|
||||
ok(`self-host suite: {corr} checks passed (see: x selfhost-test)`)
|
||||
|
||||
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/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/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/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)")
|
||||
|
|
@ -289,7 +290,7 @@ function cmd_test() -> int {
|
|||
# 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).
|
||||
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)") }
|
||||
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() {
|
||||
|
|
@ -342,11 +343,11 @@ function cmd_test() -> int {
|
|||
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
|
||||
# run exercises the pure-Ludic response parser; no network is touched.
|
||||
if is_darwin() {
|
||||
if shq("LUDIC_HOME=. bin/ludicc examples/library/http.ludic -o /tmp/x_http_demo > /tmp/x_http.out 2>&1") {
|
||||
let got = capture_line("/tmp/x_http_demo < /dev/null")
|
||||
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_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)") }
|
||||
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)")
|
||||
|
|
@ -369,37 +370,37 @@ function cmd_test() -> int {
|
|||
print("== self-hosted front-end binaries (ludicc / ludic) ==")
|
||||
# the two commands are one multi-call native binary built from the seed with
|
||||
# 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)")
|
||||
} 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
|
||||
# (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
|
||||
# native binary" with a headless link, which needs no window backend.
|
||||
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")
|
||||
} 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
|
||||
# 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)")
|
||||
} else { bad("windowed binary is missing the GameController load command") }
|
||||
} 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)")
|
||||
} 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.
|
||||
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")
|
||||
} 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.
|
||||
write_file("/tmp/x_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")
|
||||
write_file(`{tmp_dir()}/cli_exit.ludic`, "entry { exit(42) }\n")
|
||||
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") }
|
||||
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
|
||||
# and this stage checks its footer).
|
||||
print("== package manager (x test-pkg) ==")
|
||||
run("bin/x test-pkg > /tmp/x_pkg.out 2>&1")
|
||||
if shq("grep -q '0 failed' /tmp/x_pkg.out") {
|
||||
let pc = capture_line("grep -c PASS /tmp/x_pkg.out")
|
||||
run(`bin/x test-pkg > {tmp_dir()}/pkg.out 2>&1`)
|
||||
if shq(`grep -q '0 failed' {tmp_dir()}/pkg.out`) {
|
||||
let pc = capture_line(`grep -c PASS {tmp_dir()}/pkg.out`)
|
||||
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
|
||||
# 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
|
||||
# + palette.json, so a clean git diff proves it stayed byte-identical).
|
||||
# the named-colour source of truth (its --check mode regenerates the tracked
|
||||
# emit_color.ludic + palette.json into scratch files and compares).
|
||||
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")
|
||||
} else { bad2("x docs-gen", capture_line("tail -1 /tmp/x_docs.out")) }
|
||||
if shq("bin/x docs-check /tmp/x_docs > /tmp/x_docs_chk.out 2>&1") {
|
||||
} else { bad2("x docs-gen", capture_line(`tail -1 {tmp_dir()}/docs.out`)) }
|
||||
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")
|
||||
} else { bad2("x docs-check", capture_line("tail -1 /tmp/x_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") {
|
||||
} else { bad2("x docs-check", capture_line(`tail -1 {tmp_dir()}/docs_chk.out`)) }
|
||||
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")
|
||||
} 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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ function sync_vscode_grammar() -> void {
|
|||
# is `flag` present anywhere in argv?
|
||||
function has_flag(flag: pointer) -> bool {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -64,12 +64,12 @@ function cmd_test_tools() -> int {
|
|||
|
||||
# --- idempotence: formatting twice equals formatting once ------------------
|
||||
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`)
|
||||
} else { bad("idempotent on the source tree") }
|
||||
|
||||
# --- 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))")
|
||||
} else { bad("formatting is not idempotent") }
|
||||
|
||||
|
|
@ -81,47 +81,47 @@ function cmd_test_tools() -> int {
|
|||
} else { bad("formatter altered non-whitespace bytes") }
|
||||
|
||||
# --- 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")
|
||||
if shq(`{fmt} /tmp/x_utf8.ludic > /tmp/x_utf8.out 2>/dev/null; cmp -s /tmp/x_utf8.ludic /tmp/x_utf8.out`) {
|
||||
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_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")
|
||||
} else { bad("UTF-8 mangled") }
|
||||
write_file("/tmp/x_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 ub = capture_line(`{fmt} /tmp/x_utf8b.ludic | tr -d ' \t\n' | shasum | cut -d' ' -f1`)
|
||||
write_file(`{tmp_dir()}/utf8b.ludic`, "program F {\nfn f() -> int { return … }\n}\n")
|
||||
let ua = capture_line(`tr -d ' \t\n' < {tmp_dir()}/utf8b.ludic | 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)
|
||||
|
||||
# --- 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")
|
||||
let ncmt = capture_line(`{fmt} /tmp/x_c.ludic | grep -c '#'`)
|
||||
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_dir()}/c.ludic | grep -c '#'`)
|
||||
check("comments preserved", ncmt, "2")
|
||||
|
||||
# --- 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")
|
||||
run(`{fmt} /tmp/x_m.ludic > /tmp/x_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 'a = -a' /tmp/x_m.out") { ok("unary minus not spaced") } else { bad("unary minus not spaced") }
|
||||
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_dir()}/m.ludic > {tmp_dir()}/m.out 2>/dev/null`)
|
||||
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_dir()}/m.out`) { ok("unary minus not spaced") } else { bad("unary minus not spaced") }
|
||||
|
||||
# --- 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")
|
||||
if shq(`{fmt} /tmp/x_u.ludic | grep -q 'id=Root'`) { ok("widget props stay tight (id=Root)") } else { bad("widget props stay tight") }
|
||||
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")
|
||||
if shq(`{fmt} /tmp/x_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")
|
||||
if shq(`{fmt} /tmp/x_al.ludic | grep -q 'R_A: int = 0 # one'`) { ok("hand alignment preserved") } else { bad("hand alignment preserved") }
|
||||
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_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_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_dir()}/q.ludic | grep -q '\\[Pos, {{Foe}}\\]'`) { ok("query tags stay tight ({Foe})") } else { bad("query tags stay tight") }
|
||||
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_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 ----------------------
|
||||
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")
|
||||
run(`{fmt} -w /tmp/x_m.ludic`)
|
||||
let chk2 = sh(`{fmt} --check -q /tmp/x_m.ludic`)
|
||||
run(`{fmt} -w {tmp_dir()}/m.ludic`)
|
||||
let chk2 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`)
|
||||
check("--check exits 0 after -w", string(chk2), "0")
|
||||
|
||||
# --- 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")
|
||||
run(`{fmt} /tmp/x_d.md > /tmp/x_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 'z = 1' /tmp/x_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") }
|
||||
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_dir()}/d.md > {tmp_dir()}/d.out 2>/dev/null`)
|
||||
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_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_dir()}/d.out`) { ok("ludic fences formatted") } else { bad("ludic fences formatted") }
|
||||
|
||||
print("language server")
|
||||
# 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
|
||||
# end-to-end protocol test on Darwin until the directory walk is portable.
|
||||
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)") }
|
||||
|
||||
print("")
|
||||
|
|
@ -145,7 +148,11 @@ function cmd_test_tools() -> int {
|
|||
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") }
|
||||
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 {
|
||||
print(" skip extension.js and grammar tests (node not installed)")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue