feat(tools): L10 ludic-fmt enforces a project's style

lint lines in package.ludic - one_statement, max_file_lines, max_function_lines,
max_comment_lines, max_header_lines, paths, baseline - checked by ludic-fmt --check
<files> and ludic-fmt --lint (the project), at the line; a baseline ratchet lets a
rule arrive in a codebase that breaks it (--init-baseline), lowered as it is fixed.
check-impl reads the alias declarations too (it had been blind to every namespace
L6 moved out of the compiler), and eight methods get their pages; a test holds the
formatter to keeping type arguments together (L5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 14:38:35 +03:00
parent e543525eb7
commit 334469ef61
16 changed files with 635 additions and 2 deletions

View file

@ -1360,6 +1360,23 @@ checked. Diagnostics are limited to parse-level errors, reported as
`file:line: error: message`; richer static checks (unknown identifiers, `file:line: error: message`; richer static checks (unknown identifiers,
duplicate types, unknown fields, arity) are future work. duplicate types, unknown fields, arity) are future work.
`ludic-fmt --check` also enforces a project's style, stated in its `package.ludic`:
```
lint one_statement # two statements on one line
lint max_file_lines 100
lint max_function_lines 50
lint max_comment_lines 2 # a comment says why, in a line or two
lint max_header_lines 3 # the comment that opens a file
lint paths "src" "lab" # what `ludic-fmt --lint` walks
lint baseline "tests/lint-baseline.txt"
```
`ludic-fmt --lint` checks the project's paths; the baseline is a ratchet - the violations each file
had when a rule came in, which it may keep but not add to, lowered automatically as they are
fixed - so a rule can arrive in a codebase that breaks it today (`ludic-fmt --init-baseline`
writes it). A `;` or a `#` inside a string does not count.
### Editors ### Editors
```bash ```bash

7
changes/lint.md Normal file
View file

@ -0,0 +1,7 @@
bump: minor
type: feature
**`ludic-fmt` enforces a project's style.** `lint` lines in `package.ludic` state it - one statement
per line, file, function and comment-block limits (a file's opening comment its own) - and
`ludic-fmt --check <files>` and `ludic-fmt --lint` (the project's paths) fail what breaks them, at the
line. A baseline file is the ratchet that lets a rule arrive in an existing codebase: the counts a
file had may stay but not grow, and they are lowered as they are fixed.

View file

@ -0,0 +1,20 @@
---
id: app-monitor_count
name: App.monitor_count
category: app
kind: namespace-method
tokens: App.monitor_count
sig: App.monitor_count() -> int
tip: How many displays the machine has.
order: 49
ns: App
member: monitor_count
---
The number of displays attached, for a setting that chooses which one the game opens on; 1 on a platform that reports only the main one.
```ludic
program Screens {
entry { print(App.monitor_count()) }
}
```

View file

@ -0,0 +1,23 @@
---
id: app-window_fixed
name: App.window_fixed
category: app
kind: namespace-method
tokens: App.window_fixed
sig: App.window_fixed(fixed)
tip: A window that cannot be resized or maximised.
order: 51
ns: App
member: window_fixed
---
With `true`, the window has no resize grip and no maximise button - for a launcher or a dialog, which a player should not take for the game itself.
Parameters:
- `fixed` — `true` to fix the window's size, `false` to free it
```ludic
program Launcher {
entry { App.window_fixed(true) }
}
```

View file

@ -0,0 +1,23 @@
---
id: app-window_to_monitor
name: App.window_to_monitor
category: app
kind: namespace-method
tokens: App.window_to_monitor
sig: App.window_to_monitor(index)
tip: Moves the window to that display.
order: 50
ns: App
member: window_to_monitor
---
Moves the game's window onto display `index` (0 is the main one), keeping its size. A no-op where the platform leaves moving windows between screens to the player.
Parameters:
- `index` — which display, 0 to `App.monitor_count() - 1`
```ludic
program Move {
entry { App.window_to_monitor(0) }
}
```

View file

@ -0,0 +1,26 @@
---
id: fs-read_bytes
name: Fs.read_bytes
category: fs
kind: namespace-method
tokens: Fs.read_bytes
sig: Fs.read_bytes(path) -> []byte
tip: A whole file as bytes, or null.
order: 11
ns: Fs
member: read_bytes
---
Reads a whole file into a `[]byte` - bounds-checked, so no raw memory - or returns <code>null</code> when it cannot be read. It reads through the asset pack like every other load, so it finds a shipped game's files. Pair it with <code>text_of(b, n)</code> for text.
Parameters:
- `path` — the file to read
```ludic
program Header {
entry {
let b = Fs.read_bytes("sound.wav")
if b != null { print(b[24]) }
}
}
```

View file

@ -0,0 +1,30 @@
---
id: fs-write_bytes
name: Fs.write_bytes
category: fs
kind: namespace-method
tokens: Fs.write_bytes
sig: Fs.write_bytes(path, data, count) -> bool
tip: Writes the first count bytes of a []byte to a file.
order: 12
ns: Fs
member: write_bytes
---
Writes the first `count` bytes of `data` to `path`, replacing the file, and answers whether all of them were written. `count` is clamped to the slice's length.
Parameters:
- `path` — the file to write
- `data` — a `[]byte`, e.g. from <code>buffer(n)</code>
- `count` — how many bytes of it
```ludic
program Save {
entry {
let b = buffer(2)
b[0] = 72
b[1] = 105
print(Fs.write_bytes("hi.bin", b, 2))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-text
name: Input.text
category: input
kind: namespace-method
tokens: Input.text
sig: Input.text() -> string
tip: The text typed this frame.
order: 61
ns: Input
member: text
---
What the keyboard layout, the modifiers and any dead key produced this frame, as UTF-8 - the only right way to fill a name field, because it is the characters the player meant rather than the keys they pressed (a binding reads keys by position; text reads what they type). Empty in a headless run.
```ludic
program Name {
entry {
var name = ""
name = name + Input.text()
print(name)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: time-now_us
name: Time.now_us
category: time
kind: namespace-method
tokens: Time.now_us
sig: Time.now_us() -> long
tip: A microsecond clock, for measuring.
order: 7
ns: Time
member: now_us
---
Returns a steady microsecond count from the platform clock, for timing a piece of work - the difference between two readings is how long it took. It is not the game's time (that is <a href="time-now"><code>Time.now</code></a>) and it does not stop when the game is paused.
```ludic
program Timed {
entry {
let t0 = Time.now_us()
print(Time.now_us() - t0)
}
}
```

View file

@ -0,0 +1,26 @@
---
id: time-sleep_us
name: Time.sleep_us
category: time
kind: namespace-method
tokens: Time.sleep_us
sig: Time.sleep_us(us)
tip: Waits this many microseconds.
order: 8
ns: Time
member: sleep_us
---
Suspends the program for about `us` microseconds - a high-resolution wait, for pacing a loop to a frame rate. No operating system's sleep is exact, so a limiter sleeps most of the wait and spins the last of it.
Parameters:
- `us` — how long to wait, in microseconds (a `long`)
```ludic
program Paced {
entry {
Time.sleep_us(1000)
print(1)
}
}
```

View file

@ -239,6 +239,30 @@ function collect_impl_pairs(allsrc: pointer, pairs: []pointer) -> void {
} }
} }
# {ns}.{method} pairs a namespace declares with `alias` (L6) - the engine's in
# runtime/native/namespaces.ludic - which the compiler dispatches as surely as its own
function collect_alias_pairs(pairs: []pointer) -> void {
let src = read_file("runtime/native/namespaces.ludic")
if src == null { return }
var ns = ""
let n = slen(src)
var i = 0
while i < n {
let ln = s_trim(line_at(src, i))
i = i + slen(line_at(src, i)) + 1
if s_starts(ln, "namespace ") {
var e = 10
while e < slen(ln) and ln[e] != ' ' and ln[e] != '{' { e += 1 }
ns = sslice(ln, 10, e)
}
if s_starts(ln, "alias ") and slen(ns) > 0 {
var e = 6
while e < slen(ln) and ln[e] != '(' and ln[e] != ' ' and ln[e] != '=' { e += 1 }
set_add(pairs, ns + "." + sslice(ln, 6, e))
}
}
}
# gather the docs facts: token set, and the (ns.member) pairs of ns-method pages # gather the docs facts: token set, and the (ns.member) pairs of ns-method pages
function collect_docs(tokens: []pointer, docpairs: []pointer) -> void { function collect_docs(tokens: []pointer, docpairs: []pointer) -> void {
let list = capture("find docs/language -name '*.md' ! -name '_section.md' 2>/dev/null") let list = capture("find docs/language -name '*.md' ! -name '_section.md' 2>/dev/null")
@ -298,6 +322,7 @@ function cmd_check_impl() -> int {
let allsrc = read_all_selfhost() let allsrc = read_all_selfhost()
let impl = new []pointer let impl = new []pointer
collect_impl_pairs(allsrc, impl) collect_impl_pairs(allsrc, impl)
collect_alias_pairs(impl)
let tokens = new []pointer let tokens = new []pointer
let docpairs = new []pointer let docpairs = new []pointer
collect_docs(tokens, docpairs) collect_docs(tokens, docpairs)

View file

@ -108,6 +108,26 @@ function cmd_test_tools() -> int {
if shq(`{fmt} {tmp_dir()}/q.ludic | grep -q '\\[Pos, {{Foe}}\\]'`) { ok("query tags stay tight ({Foe})") } else { bad("query tags stay tight") } if shq(`{fmt} {tmp_dir()}/q.ludic | grep -q '\\[Pos, {{Foe}}\\]'`) { ok("query tags stay tight ({Foe})") } else { bad("query tags stay tight") }
write_file(`{tmp_dir()}/al.ludic`, "program A {\n const R_A: int = 0 # one\n const R_BBB: int = 1 # two\n}\n") write_file(`{tmp_dir()}/al.ludic`, "program A {\n const R_A: int = 0 # one\n const R_BBB: int = 1 # two\n}\n")
if shq(`{fmt} {tmp_dir()}/al.ludic | grep -q 'R_A: int = 0 # one'`) { ok("hand alignment preserved") } else { bad("hand alignment preserved") } if shq(`{fmt} {tmp_dir()}/al.ludic | grep -q 'R_A: int = 0 # one'`) { ok("hand alignment preserved") } else { bad("hand alignment preserved") }
write_file(`{tmp_dir()}/gen.ludic`, "program G {\n function f<T>(p: Pool<T>, m: Map<string, []int>) -> Pool<Pool<T>> {\n let ok = a < b and c > d\n return null\n }\n}\n")
if shq(`{fmt} {tmp_dir()}/gen.ludic | cmp -s - {tmp_dir()}/gen.ludic`) { ok("type arguments stay together, comparisons keep their spaces (L5)") } else { bad("type arguments stay together (L5)") }
# --- the style rules a project states (L10) --------------------------------
let lp = `{tmp_dir()}/lintproj`
let lfmt = `{capture_line("pwd")}/{fmt}` # the tests below run from inside the project
shell(`rm -rf {lp} && mkdir -p {lp}/src`)
write_file(`{lp}/package.ludic`, "package \"t\"\nlint one_statement\nlint max_file_lines 20\nlint max_function_lines 5\nlint max_comment_lines 2\nlint max_header_lines 3\nlint paths \"src\"\nlint baseline \"lint-baseline.txt\"\n")
write_file(`{lp}/src/a.ludic`, "# a.ludic - a file\nfunction f() -> int {\n # one\n # two\n # three\n let a = 1; return a\n}\n")
if not shq(`cd {lp} && {lfmt} --lint > out.txt 2>&1`) and shq(`grep -q 'src/a.ludic:3: max_comment_lines' {lp}/out.txt`) and shq(`grep -q 'src/a.ludic:6: one_statement' {lp}/out.txt`) {
ok("--lint fails a comment over the limit and two statements on a line, at their lines")
} else { bad("--lint fails a comment over the limit and two statements on a line") }
if shq(`cd {lp} && {lfmt} --init-baseline > /dev/null && {lfmt} --lint > /dev/null`) { ok("--init-baseline allows what is there") } else { bad("--init-baseline allows what is there") }
write_file(`{lp}/src/b.ludic`, "function g() -> int {\n let b = 2; return b\n}\n")
if not shq(`cd {lp} && {lfmt} --lint > /dev/null 2>&1`) { ok("a new violation fails, whatever the baseline allows elsewhere") } else { bad("a new violation fails") }
write_file(`{lp}/src/b.ludic`, "function g() -> int { return 2 }\n")
write_file(`{lp}/src/a.ludic`, "# a.ludic - a file\nfunction f() -> int {\n let a = 1; return a\n}\n")
if shq(`cd {lp} && {lfmt} --lint > /dev/null && ! grep -q max_comment_lines lint-baseline.txt`) { ok("fixing one lowers the baseline, and it stays lowered") } else { bad("fixing one lowers the baseline") }
write_file(`{lp}/src/c.ludic`, "function h() -> string { return \"a; b\" }\n")
if shq(`cd {lp} && {lfmt} --lint > /dev/null`) { ok("a ; inside a string is not a statement") } else { bad("a ; inside a string is not a statement") }
# --- --check exit status, for CI and pre-commit hooks ---------------------- # --- --check exit status, for CI and pre-commit hooks ----------------------
let chk1 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`) let chk1 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`)

View file

@ -10,6 +10,9 @@
# ludic-fmt a.md format the ```ludic fences in a document # ludic-fmt a.md format the ```ludic fences in a document
# cat a.ludic | ludic-fmt - filter mode (stdin -> stdout) # cat a.ludic | ludic-fmt - filter mode (stdin -> stdout)
program LudicFmt { program LudicFmt {
import "fmt_lint.ludic"
import "fmt_lint_rules.ludic"
import "fmt_lint_run.ludic"
# ---- token kinds (mirror ludic_syntax.h) ---- # ---- token kinds (mirror ludic_syntax.h) ----
const LT_EOF: int = 0 const LT_EOF: int = 0
const LT_NL: int = 1 const LT_NL: int = 1
@ -589,6 +592,8 @@ program LudicFmt {
var quiet = false var quiet = false
var changed = false var changed = false
var failed = false var failed = false
var lint_only = false
var lint_init = false
let files = new []pointer let files = new []pointer
var ai = 1 var ai = 1
while ai < arg_count() { while ai < arg_count() {
@ -597,11 +602,32 @@ program LudicFmt {
else { if (a == "--check") or (a == "-l") { check = true } else { if (a == "--check") or (a == "-l") { check = true }
else { if (a == "-q") or (a == "--quiet") { quiet = true } else { if (a == "-q") or (a == "--quiet") { quiet = true }
else { if (a == "--indent") { ai += 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di += 1 } } } else { if (a == "--indent") { ai += 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di += 1 } } }
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source"); return } else { if (a == "--lint") { lint_only = true }
else { push(files, a) } } } } } else { if (a == "--init-baseline") { lint_only = true; lint_init = true }
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source; --check also checks package.ludic's lint rules, --lint checks the project"); return }
else { push(files, a) } } } } } } }
ai += 1 ai += 1
} }
if indent < 1 or indent > 8 { indent = 2 } if indent < 1 or indent > 8 { indent = 2 }
# --lint: the project's rules over its paths, against the baseline (L10)
if lint_only {
if not lint_config() { print("ludic-fmt --lint: package.ludic states no `lint` rules"); exit(2) }
let all = new []string
var li = 0
while li < len(lt_paths) {
lint_walk(lt_paths[li], all)
li += 1
}
var lj = 0
while lj < len(all) {
let t = read_file(all[lj])
if t != null { lint_file(all[lj], string(t)) }
lj += 1
}
if not lint_judge(lint_init) { exit(1) }
return
}
let linting = check and lint_config()
# stdin filter mode # stdin filter mode
if len(files) == 0 or (len(files) == 1 and (files[0] == "-")) { if len(files) == 0 or (len(files) == 1 and (files[0] == "-")) {
@ -620,6 +646,7 @@ program LudicFmt {
file_write(file_stderr(), m, cstr_len(m)); failed = true file_write(file_stderr(), m, cstr_len(m)); failed = true
} else { } else {
let is_md = ends_with(path, ".md") or ends_with(path, ".markdown") let is_md = ends_with(path, ".md") or ends_with(path, ".markdown")
if linting and not is_md { lint_file(path, string(text)) }
let out = format_source(text, is_md, indent) let out = format_source(text, is_md, indent)
let same = (out == text) let same = (out == text)
if check { if check {
@ -638,6 +665,11 @@ program LudicFmt {
fi += 1 fi += 1
} }
if failed { exit(2) } if failed { exit(2) }
# the files named are part of a project: judge them, but never rewrite its baseline
if linting {
lt_partial = true
if not lint_judge(false) { exit(1) }
}
if check and changed { exit(1) } if check and changed { exit(1) }
} }
} }

View file

@ -0,0 +1,111 @@
# fmt_lint.ludic — L10: the style rules, checked by the formatter. A project states them in its
# package.ludic, and `ludic-fmt --check` (the files given) or `ludic-fmt --lint` (the project's
# paths) fails a file that breaks one:
# lint one_statement two statements on one line (`a(); b()`)
# lint max_file_lines 100 a file's length
# lint max_function_lines 50 a function's, from `function` to its closing brace
# lint max_comment_lines 2 a block of comment lines - a comment says why, in a line or two
# lint max_header_lines 6 the block that opens a file, which says what the file is
# lint paths "src" "lab" what --lint walks
# lint baseline "tests/lint-baseline.txt"
# The baseline is a ratchet: `<count> <rule> <file>` lines, the violations a file had when a rule
# came in. A file may have that many and no more; when it has fewer the baseline is lowered, and
# `--init-baseline` writes it from what is there now.
var lt_one: bool = false
var lt_max_file: int = 0
var lt_max_fn: int = 0
var lt_max_comment: int = 0
var lt_max_header: int = 0
var lt_baseline: string = ""
var lt_paths: []string = new []string
var lr_file: []string = new []string
var lr_rule: []string = new []string
var lr_line: []int = new []int
var lr_msg: []string = new []string
function lt_atoi(s: string) -> int {
var n = 0
var i = 0
while i < len(s) {
let c = s[i]
if c >= '0' and c <= '9' { n = n * 10 + (c - '0') }
i += 1
}
return n
}
function lt_unquote(s: string) -> string {
let n = len(s)
if n >= 2 and s[0] == '"' and s[n - 1] == '"' { return s[1 .. n - 1] }
return s
}
# the words of a line, a "quoted" one kept whole, a # ending it
function lt_words(line: string) -> []string {
let out = new []string
var i = 0
let n = len(line)
while i < n {
while i < n and (line[i] == ' ' or line[i] == 9) { i += 1 }
if i >= n or line[i] == '#' { break }
var j = i
if line[i] == '"' {
j = i + 1
while j < n and line[j] != '"' { j += 1 }
if j < n { j += 1 }
} else {
while j < n and line[j] != ' ' and line[j] != 9 { j += 1 }
}
push(out, lt_unquote(line[i .. j]))
i = j
}
return out
}
function lt_lines(text: string) -> []string {
let out = new []string
var a = 0
var i = 0
let n = len(text)
while i < n {
if text[i] == '\n' {
push(out, text[a .. i])
a = i + 1
}
i += 1
}
if a < n { push(out, text[a .. n]) }
return out
}
# the project's rules, from package.ludic in the directory ludic-fmt runs in
function lint_config() -> bool {
let man = read_file("package.ludic")
if man == null { return false }
var any = false
let ls = lt_lines(string(man))
var i = 0
while i < len(ls) {
let w = lt_words(ls[i])
if len(w) >= 2 and w[0] == "lint" {
any = true
if w[1] == "one_statement" { lt_one = true }
if w[1] == "max_file_lines" and len(w) > 2 { lt_max_file = lt_atoi(w[2]) }
if w[1] == "max_function_lines" and len(w) > 2 { lt_max_fn = lt_atoi(w[2]) }
if w[1] == "max_comment_lines" and len(w) > 2 { lt_max_comment = lt_atoi(w[2]) }
if w[1] == "max_header_lines" and len(w) > 2 { lt_max_header = lt_atoi(w[2]) }
if w[1] == "baseline" and len(w) > 2 { lt_baseline = w[2] }
if w[1] == "paths" {
var k = 2
while k < len(w) {
push(lt_paths, w[k])
k += 1
}
}
}
i += 1
}
return any
}
function lt_report(path: string, rule: string, line: int, msg: string) -> void {
push(lr_file, path)
push(lr_rule, rule)
push(lr_line, line)
push(lr_msg, msg)
}

View file

@ -0,0 +1,109 @@
# fmt_lint_rules.ludic — L10: the rules themselves, over one file's text and its tokens
function lint_file(path: string, text: string) -> void {
let ls = lt_lines(text)
if lt_max_file > 0 and len(ls) > lt_max_file { lt_report(path, "max_file_lines", len(ls), `{len(ls)} lines (the limit is {lt_max_file})`) }
lint_comments(path, ls)
lex(text)
if lt_one { lint_statements(path) }
if lt_max_fn > 0 { lint_functions(path) }
}
function lint_comments(path: string, ls: []string) -> void {
var run = 0
var start = 0
var code_seen = false
var i = 0
while i <= len(ls) {
var is_c = false
var blank = true
if i < len(ls) {
let w = lt_words_raw(ls[i])
is_c = w == 1
blank = w == 0
}
if is_c {
if run == 0 { start = i }
run += 1
} else {
if run > 0 {
let header = not code_seen and start == 0
if header and lt_max_header > 0 and run > lt_max_header { lt_report(path, "max_header_lines", start + 1, `the opening comment is {run} lines (the limit is {lt_max_header})`) }
if not header and lt_max_comment > 0 and run > lt_max_comment { lt_report(path, "max_comment_lines", start + 1, `a comment of {run} lines (the limit is {lt_max_comment}): say why in a line or two; the story goes in the commit or docs/`) }
run = 0
}
if not blank { code_seen = true }
}
i += 1
}
}
# 0 a blank line, 1 a comment line, 2 code
function lt_words_raw(line: string) -> int {
var i = 0
while i < len(line) and (line[i] == ' ' or line[i] == 9) { i += 1 }
if i >= len(line) { return 0 }
if line[i] == '#' { return 1 }
return 2
}
function lint_statements(path: string) -> void {
var i = 0
while i < ntok() {
if tk_kind[i] == LT_OP and tok_len(i) == 1 and src[tk_start[i]] == ';' {
let k = tk_kind[i + 1]
if k != LT_NL and k != LT_COMMENT and k != LT_EOF { lt_report(path, "one_statement", tk_line[i] + 1, "two statements on one line") }
}
i += 1
}
}
function lint_functions(path: string) -> void {
var i = 0
while i < ntok() {
if tk_kind[i] == LT_KW and (tok_text(i) == "function") {
let end = lt_fn_end(i)
if end >= 0 {
let n = tk_line[end] - tk_line[i] + 1
if n > lt_max_fn { lt_report(path, "max_function_lines", tk_line[i] + 1, `{lt_fn_name(i)} is {n} lines (the limit is {lt_max_fn})`) }
}
}
i += 1
}
}
function lt_fn_name(i: int) -> string {
if i + 1 < ntok() { return string(tok_text(i + 1)) }
return "a function"
}
# the token closing a function's body, or -1 for one with none (an extern)
function lt_fn_end(i: int) -> int {
var j = i + 1
var paren = 0
var closed = false
while j < ntok() {
let k = tk_kind[j]
if k == LT_OP and tok_len(j) == 1 {
let c = src[tk_start[j]]
if c == '(' { paren += 1 }
if c == ')' {
paren -= 1
if paren == 0 { closed = true }
}
if c == '{' and paren == 0 and closed { return lt_close(j) }
}
if (k == LT_NL or k == LT_EOF) and paren == 0 and closed { return -1 }
j += 1
}
return -1
}
function lt_close(open: int) -> int {
var depth = 0
var j = open
while j < ntok() {
if tk_kind[j] == LT_OP and tok_len(j) == 1 {
let c = src[tk_start[j]]
if c == '{' { depth += 1 }
if c == '}' {
depth -= 1
if depth == 0 { return j }
}
}
j += 1
}
return ntok() - 1
}

View file

@ -0,0 +1,117 @@
# fmt_lint_run.ludic — L10: walking a project, and the baseline that lets a rule arrive in a
# codebase that breaks it today without letting it get worse
var bl_file: []string = new []string
var bl_rule: []string = new []string
var bl_count: []int = new []int
var lt_partial: bool = false # --check of some files: the baseline is read, never written
function lint_walk(dir: string, out: []string) -> void {
let names = Fs.list(dir)
if names == null { return }
var i = 0
while i < len(names) {
let p = `{dir}/{names[i]}`
if Fs.is_dir(p) { lint_walk(p, out) }
else { if ends_with(p, ".ludic") { push(out, p) } }
i += 1
}
}
function lt_baseline_read() -> void {
if len(lt_baseline) == 0 { return }
let t = read_file(lt_baseline)
if t == null { return }
let ls = lt_lines(string(t))
var i = 0
while i < len(ls) {
let w = lt_words(ls[i])
if len(w) == 3 {
push(bl_count, lt_atoi(w[0]))
push(bl_rule, w[1])
push(bl_file, w[2])
}
i += 1
}
}
function lt_allowed(path: string, rule: string) -> int {
var i = 0
while i < len(bl_file) {
if bl_file[i] == path and bl_rule[i] == rule { return bl_count[i] }
i += 1
}
return 0
}
# how many violations of `rule` the file has now
function lt_count(path: string, rule: string) -> int {
var n = 0
var i = 0
while i < len(lr_file) {
if lr_file[i] == path and lr_rule[i] == rule { n += 1 }
i += 1
}
return n
}
function lt_seen(fs: []string, rs: []string, path: string, rule: string) -> bool {
var i = 0
while i < len(fs) {
if fs[i] == path and rs[i] == rule { return true }
i += 1
}
return false
}
# true when every file is within its allowance; prints what is not
function lint_judge(init: bool) -> bool {
lt_baseline_read()
let pf = new []string
let pr = new []string
var i = 0
while i < len(lr_file) {
if not lt_seen(pf, pr, lr_file[i], lr_rule[i]) {
push(pf, lr_file[i])
push(pr, lr_rule[i])
}
i += 1
}
var ok = true
var lowered = false
var total = 0
let out = buf_new()
buf_puts(out, "# lint baseline: the violations each file had when a rule came in (ludic-fmt --lint); only ever lowered\n")
var p = 0
while p < len(pf) {
let now = lt_count(pf[p], pr[p])
let allow = lt_allowed(pf[p], pr[p])
total += now
if not init and now > allow {
ok = false
lt_print_pair(pf[p], pr[p], now, allow)
}
var keep = now
if not init and now > allow { keep = allow }
if not init and now < allow { lowered = true }
if keep > 0 { buf_puts(out, `{keep} {pr[p]} {pf[p]}\n`) }
p += 1
}
var b = 0
while b < len(bl_file) {
if lt_count(bl_file[b], bl_rule[b]) == 0 and bl_count[b] > 0 and not init { lowered = true }
b += 1
}
if len(lt_baseline) > 0 and (init or lowered) and not lt_partial {
let f = file_open(lt_baseline, "wb")
if f != null {
let s = buf_str(out)
file_write(f, s, cstr_len(s))
file_close(f)
}
}
print(` lint: {total} violation(s) in the baseline, {len(pf)} file/rule pair(s)`)
return ok
}
function lt_print_pair(path: string, rule: string, now: int, allow: int) -> void {
var i = 0
while i < len(lr_file) {
if lr_file[i] == path and lr_rule[i] == rule { print(`{path}:{lr_line[i]}: {rule}: {lr_msg[i]}`) }
i += 1
}
if allow > 0 { print(` ({path} may have {allow} of {rule}, and has {now})`) }
}