From 8fed9add665e43243c75231718521fc431fe7400 Mon Sep 17 00:00:00 2001
From: Orkuncakilkaya
Date: Sat, 5 Sep 2026 01:12:26 +0300
Subject: [PATCH] refactor(x): per-process scratch dirs, Ludic ports of the LSP
test and Forgejo release
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- every scratch file lives in `$TMPDIR/x_/` (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
---
tools/atlas.ludic | 77 +++-----
tools/test-grammar.js | 49 ++---
tools/x/build.ludic | 4 +-
tools/x/checks.ludic | 158 +++++++--------
tools/x/docgen.ludic | 148 +++++++-------
tools/x/docgen_check.ludic | 48 ++---
tools/x/docgen_gen.ludic | 178 ++++++++---------
tools/x/forgejo.ludic | 93 +++++++++
tools/x/json.ludic | 116 ++++++-----
tools/x/lsp_test.ludic | 373 ++++++++++++++++++++++++++++++++++++
tools/x/main.ludic | 91 +++++----
tools/x/pkg.ludic | 114 +++++------
tools/x/pkg_test.ludic | 2 +-
tools/x/prelude.ludic | 36 +++-
tools/x/release.ludic | 27 ++-
tools/x/selfhost.ludic | 10 +-
tools/x/selfhost_test.ludic | 30 +--
tools/x/test.ludic | 147 +++++++-------
tools/x/tools.ludic | 67 ++++---
19 files changed, 1154 insertions(+), 614 deletions(-)
create mode 100644 tools/x/forgejo.ludic
create mode 100644 tools/x/lsp_test.ludic
diff --git a/tools/atlas.ludic b/tools/atlas.ludic
index e12791c9..20006f40 100644
--- a/tools/atlas.ludic
+++ b/tools/atlas.ludic
@@ -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)
- }
- text_int(x + 1, y + 18, i, 0x8090b0, 1)
+ # "/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`
+ }
+
+ 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 "/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
- }
}
diff --git a/tools/test-grammar.js b/tools/test-grammar.js
index ad37af66..865f1dc5 100644
--- a/tools/test-grammar.js
+++ b/tools/test-grammar.js
@@ -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) => {
diff --git a/tools/x/build.ludic b/tools/x/build.ludic
index da4953d2..444aa7fe 100644
--- a/tools/x/build.ludic
+++ b/tools/x/build.ludic
@@ -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 [--headless] [--save-temps]\n"); return 1 }
let name = capture_line(`basename {src} .ludic`)
diff --git a/tools/x/checks.ludic b/tools/x/checks.ludic
index a776234a..28f1e7ce 100644
--- a/tools/x/checks.ludic
+++ b/tools/x/checks.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 (` 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 { #
+ if text[i + 1] == '!' { #
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
+ if text[i + 1] == '/' { # closing
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
}
diff --git a/tools/x/docgen.ludic b/tools/x/docgen.ludic
index f53b2063..55bcc166 100644
--- a/tools/x/docgen.ludic
+++ b/tools/x/docgen.ludic
@@ -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, "")
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, "")
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
}
diff --git a/tools/x/docgen_check.ludic b/tools/x/docgen_check.ludic
index 46096a59..913a613f 100644
--- a/tools/x/docgen_check.ludic
+++ b/tools/x/docgen_check.ludic
@@ -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`) }
}
@@ -160,11 +160,11 @@ function cmd_docs_check() -> int {
let tok2ids = new []Bucket # k1=kind, k2=tok -> ids
let ns2dirs = new []Bucket # k1="", k2=ns -> dirs
let id2dirs = new []Bucket # k1="", k2=section id -> dirs
- let title2dirs = new []Bucket # k1="", k2=lower(title) -> dirs
+ let title2dirs = new []Bucket # k1="", k2=lower(title) -> dirs
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) }
}
diff --git a/tools/x/docgen_gen.ludic b/tools/x/docgen_gen.ludic
index 63f429ce..4d0aeff8 100644
--- a/tools/x/docgen_gen.ludic
+++ b/tools/x/docgen_gen.ludic
@@ -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
, each codeified
@@ -205,7 +205,7 @@ function split_body(body: pointer) -> Split {
while pi < len(paras) {
if pi > 0 { sb_puts(ob, "
") }
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, `{hesc(label)}`) }
@@ -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, `
{hesc(p.name)}{p.desc}
`)
}
sb_puts(b, ``)
@@ -564,7 +564,7 @@ function render_examples(e: Entry) -> pointer {
var i = 0
while i < len(e.examples) {
sb_puts(b, `{hesc(e.examples[i])}`)
- i = i + 1
+ i += 1
}
sb_puts(b, ``)
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, `