runtime: Log, DateTime.format, Input.text, Path, Mime, Fs, Os and Text keep nothing per call
Found by reading every builtin (Os.platform's 8 KB per call started it). Log builds its line only at or above the threshold and frees it; DateTime.format folds through + so its pieces go; Input.text encodes into one buffer; Path/Mime/Fs/Os free their temporaries on every path; string results of Text/Path/Mime/DateTime/Os dirs are fresh and Text frees a fresh argument. Reseeded. runtime_temps.ludic: 19.8 MB -> 0 over 20,000 rounds, 64 KB -> 0 over 200 of file work; clean under MallocScribble. string_temps still 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
404ee2ee3b
commit
254097657e
11 changed files with 38168 additions and 37714 deletions
14
changes/runtime-temporaries.md
Normal file
14
changes/runtime-temporaries.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**The runtime's text builtins keep nothing a call does not hand back.**
|
||||
- `Log.*` builds nothing below the threshold (as documented) and frees the line it writes; a number
|
||||
field's text and a made message go too. A `Log.debug` in a frame loop left its line every frame.
|
||||
- `DateTime.format` frees each field's digits and each piece joined so far.
|
||||
- `Input.text()` writes the typed text into one buffer instead of joining a string per character.
|
||||
- `Path.stem` / `Path.normalize`, `Mime.of` / `Mime.sniff`, `Fs.write_text` / `copy` / `mkdir` and
|
||||
`Os.save_dir` / `config_dir` / `cache_dir` free their working copies, on every way out.
|
||||
- Every string `Text.*`, `Path.*`, `Mime.*`, `DateTime.format` and the `Os` directories return is
|
||||
fresh, so a comparison, a `+` or a `print` that reads it frees it; a string made for a `Text` call's
|
||||
argument (`Text.to_int(string(n))`) is freed once read.
|
||||
`examples/lang/runtime_temps.ludic`: 20,000 rounds grew 19.8 MB before and 0 after; 200 rounds of
|
||||
file work 64 KB before and 0 after.
|
||||
55
examples/lang/runtime_temps.ludic
Normal file
55
examples/lang/runtime_temps.ludic
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# runtime_temps.ludic - the runtime's text builtins keep nothing a call does not hand back: a Log line
|
||||
# filtered out is never built and one written is freed, DateTime.format's pieces go, Path and Mime
|
||||
# free their working copies, Text frees a made argument, and Fs frees its temp names and buffers. Each
|
||||
# answer is fresh, so comparing it frees it. Prints 1 2 3 4 5, `grew 0` and `files grew 0` (the heap
|
||||
# is read on the Mac; 0 elsewhere).
|
||||
program RuntimeTemps {
|
||||
function words_round(i: int) -> int {
|
||||
var bad = 0
|
||||
Log.debug(`frame {i}`, "n", i, "big", long(i) * 1000)
|
||||
if DateTime.format(DateTime.from(2026, 9, 28, 12, 34, 7), "YYYY-MM-DD HH:mm:ss") != "2026-09-28 12:34:07" { bad += 1 }
|
||||
if Path.stem("a/b/camp.tar.png") != "camp.tar" or Path.normalize("a//b/./c/../d") != "a/b/d" { bad += 1 }
|
||||
if Path.dir("a/b/c.txt") != "a/b" or Path.base("a/b/c.txt") != "c.txt" or Path.ext("a/b/c.txt") != ".txt" { bad += 1 }
|
||||
if Path.join("a", "b") != "a/b" or Mime.of("x.PNG") == "" { bad += 1 }
|
||||
if Text.to_int(string(i)) != i or Text.upper(`ab{i}`) != `AB{i}` or Text.trim(` x{i} `) != `x{i}` { bad += 1 }
|
||||
if not Text.contains(`k{i}`, "k") or Text.replace(`a-{i}`, "-", "+") != `a+{i}` or Text.length(`{i}`) < 1 { bad += 1 }
|
||||
if Os.save_dir("RuntimeTemps") == "" { bad += 1 }
|
||||
return bad
|
||||
}
|
||||
function files_round(i: int, dir: string, a: string, b: string) -> int {
|
||||
var bad = 0
|
||||
if not Fs.write_text(a, "hello") { bad += 1 }
|
||||
if not Fs.copy(a, b) { bad += 1 }
|
||||
if not Fs.mkdir(dir) { bad += 1 }
|
||||
if Mime.sniff(b) == "" { bad += 1 }
|
||||
return bad
|
||||
}
|
||||
entry {
|
||||
Log.set_level(3)
|
||||
Log.warn("runtime_temps: a line that is written", "round", 1)
|
||||
if DateTime.format(DateTime.from(2026, 1, 2, 3, 4, 5), "YY/MM/DD") == "26/01/02" { print(1) }
|
||||
if Path.stem("x.png") == "x" and Path.normalize("/a/../b") == "/b" { print(2) }
|
||||
let kept = Text.upper("camp")
|
||||
let also = kept + "!"
|
||||
if kept == "CAMP" and also == "CAMP!" { print(3) }
|
||||
var bad = 0
|
||||
for i in 0 .. 200 { bad += words_round(i) }
|
||||
if bad == 0 { print(4) }
|
||||
let before = Os.heap_bytes()
|
||||
for i in 0 .. 20000 { bad += words_round(i) }
|
||||
let grew = Os.heap_bytes() - before
|
||||
let dir = Os.temp_dir() + "/runtime_temps_" + string(Os.pid())
|
||||
let a = dir + "/a.txt"
|
||||
let b = dir + "/b.txt"
|
||||
Fs.mkdir(dir)
|
||||
for i in 0 .. 20 { bad += files_round(i, dir, a, b) }
|
||||
if bad == 0 { print(5) }
|
||||
let fbefore = Os.heap_bytes()
|
||||
for i in 0 .. 200 { bad += files_round(i, dir, a, b) }
|
||||
let fgrew = Os.heap_bytes() - fbefore
|
||||
Fs.remove(a)
|
||||
Fs.remove(b)
|
||||
if bad == 0 { print(`grew {grew}`) } else { print(`bad {bad}`) }
|
||||
print(`files grew {fgrew}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -451,7 +451,10 @@ function input_text(rt_input_st: mut RtInputState) -> string {
|
|||
if rt_input_st.in_text_buf == null { rt_input_st.in_text_buf = words(64) }
|
||||
let n = win_text(rt_input_st.in_text_buf, 64)
|
||||
if n <= 0 { return "" }
|
||||
var out = ""
|
||||
# one buffer for the whole text, four bytes a unit at most: a string joined a character at a time
|
||||
# kept every shorter one and each character's own buffer
|
||||
let out = bytes(n * 4 + 1)
|
||||
var at = 0
|
||||
var i = 0
|
||||
while i < n {
|
||||
var c = rt_input_st.in_text_buf[i]
|
||||
|
|
@ -460,10 +463,29 @@ function input_text(rt_input_st: mut RtInputState) -> string {
|
|||
let lo = rt_input_st.in_text_buf[i + 1]
|
||||
if lo >= 56320 and lo < 57344 { c = 65536 + ((c - 55296) * 1024) + (lo - 56320); i = i + 1 }
|
||||
}
|
||||
out = out + in_utf8(c)
|
||||
at = in_utf8_into(out, at, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
out[at] = 0
|
||||
let s: string = out
|
||||
return s
|
||||
}
|
||||
# one code point's UTF-8 bytes into out at `at`; where the next one goes
|
||||
function in_utf8_into(out: bytes, at: int, c: int) -> int {
|
||||
if c < 128 {
|
||||
out[at] = c
|
||||
return at + 1
|
||||
}
|
||||
if c < 2048 {
|
||||
out[at] = 192 | (c >> 6); out[at + 1] = 128 | (c & 63)
|
||||
return at + 2
|
||||
}
|
||||
if c < 65536 {
|
||||
out[at] = 224 | (c >> 12); out[at + 1] = 128 | ((c >> 6) & 63); out[at + 2] = 128 | (c & 63)
|
||||
return at + 3
|
||||
}
|
||||
out[at] = 240 | (c >> 18); out[at + 1] = 128 | ((c >> 12) & 63); out[at + 2] = 128 | ((c >> 6) & 63); out[at + 3] = 128 | (c & 63)
|
||||
return at + 4
|
||||
}
|
||||
# one code point as a UTF-8 string
|
||||
function in_utf8(c: int) -> string {
|
||||
|
|
|
|||
|
|
@ -130,10 +130,6 @@ function is_datetime_ns(meth: pointer) -> bool {
|
|||
return false
|
||||
}
|
||||
|
||||
# concat two runtime string codes -> a fresh string code
|
||||
function dt_concat(a: pointer, b: pointer) -> pointer {
|
||||
return emit_bind(`call ptr @lp_str_concat(ptr {a}, ptr {b})`)
|
||||
}
|
||||
|
||||
# does the pattern have token `tok` (length tlen) starting at index i?
|
||||
function dt_tok_at(pat: pointer, n: int, i: int, tok: pointer, tlen: int) -> bool {
|
||||
|
|
@ -167,7 +163,9 @@ function emit_datetime_format(e: Node) -> Val {
|
|||
let mi = emit_bind(`sdiv i32 {m3}, 60`)
|
||||
let ss = emit_bind(`srem i32 {sod}, 60`)
|
||||
let yy = emit_bind(`srem i32 {yv}, 100`)
|
||||
var acc = emit_str_const("")
|
||||
# joined as ordinary text: each field's digits and each piece so far are freed once the next join
|
||||
# has copied them, so only the answer is left (every one of them was once kept)
|
||||
var acc = val(emit_str_const(""), "string")
|
||||
let lit = buf_new()
|
||||
var i = 0
|
||||
while i < n {
|
||||
|
|
@ -182,16 +180,16 @@ function emit_datetime_format(e: Node) -> Val {
|
|||
if (field == null) { buf_putc(lit, pat[i]); i += 1 }
|
||||
else {
|
||||
let ls = buf_str(lit)
|
||||
if len(ls) > 0 { acc = dt_concat(acc, emit_str_const(ls)) }
|
||||
let piece = emit_bind(`call ptr @lp_dt_pad0(i32 {field}, i32 {width})`)
|
||||
acc = dt_concat(acc, piece)
|
||||
if len(ls) > 0 { acc = emit_str_op("+", acc, val(emit_str_const(ls), "string")) }
|
||||
let piece = fresh_val(emit_bind(`call ptr @lp_dt_pad0(i32 {field}, i32 {width})`), "string")
|
||||
acc = emit_str_op("+", acc, piece)
|
||||
lit.len = 0 # start a fresh literal run
|
||||
i += tlen
|
||||
}
|
||||
}
|
||||
let tail = buf_str(lit)
|
||||
if len(tail) > 0 { acc = dt_concat(acc, emit_str_const(tail)) }
|
||||
return val(acc, "string")
|
||||
if len(tail) > 0 { acc = emit_str_op("+", acc, val(emit_str_const(tail), "string")) }
|
||||
return acc
|
||||
}
|
||||
|
||||
# DateTime.parse(s, "pattern") -> DateTime. The pattern MUST be a string literal;
|
||||
|
|
|
|||
|
|
@ -68,24 +68,25 @@ function emit_fs_ns(meth: pointer, e: Node) -> Val {
|
|||
return val(emit_bind(`call i32 @lp_fs_copy(ptr {a.code}, ptr {b.code})`), "bool")
|
||||
}
|
||||
|
||||
# every Path and Mime answer is a copy of its own, so it is fresh: freed once a +, == or print reads it
|
||||
function emit_path_ns(meth: pointer, e: Node) -> Val {
|
||||
use_pak()
|
||||
let a = emit_expr(e.kids[0])
|
||||
if (meth == "dir") { return val(emit_bind(`call ptr @lp_path_dir(ptr {a.code})`), "string") }
|
||||
if (meth == "base") { return val(emit_bind(`call ptr @lp_path_base(ptr {a.code})`), "string") }
|
||||
if (meth == "ext") { return val(emit_bind(`call ptr @lp_path_ext(ptr {a.code})`), "string") }
|
||||
if (meth == "stem") { return val(emit_bind(`call ptr @lp_path_stem(ptr {a.code})`), "string") }
|
||||
if (meth == "normalize") { return val(emit_bind(`call ptr @lp_path_norm(ptr {a.code})`), "string") }
|
||||
if (meth == "dir") { return fresh_val(emit_bind(`call ptr @lp_path_dir(ptr {a.code})`), "string") }
|
||||
if (meth == "base") { return fresh_val(emit_bind(`call ptr @lp_path_base(ptr {a.code})`), "string") }
|
||||
if (meth == "ext") { return fresh_val(emit_bind(`call ptr @lp_path_ext(ptr {a.code})`), "string") }
|
||||
if (meth == "stem") { return fresh_val(emit_bind(`call ptr @lp_path_stem(ptr {a.code})`), "string") }
|
||||
if (meth == "normalize") { return fresh_val(emit_bind(`call ptr @lp_path_norm(ptr {a.code})`), "string") }
|
||||
# join
|
||||
let b = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call ptr @lp_path_join(ptr {a.code}, ptr {b.code})`), "string")
|
||||
return fresh_val(emit_bind(`call ptr @lp_path_join(ptr {a.code}, ptr {b.code})`), "string")
|
||||
}
|
||||
|
||||
function emit_mime_ns(meth: pointer, e: Node) -> Val {
|
||||
use_pak()
|
||||
let a = emit_expr(e.kids[0])
|
||||
if (meth == "sniff") { return val(emit_bind(`call ptr @lp_mime_sniff(ptr {a.code})`), "string") }
|
||||
return val(emit_bind(`call ptr @lp_mime_of(ptr {a.code})`), "string")
|
||||
if (meth == "sniff") { return fresh_val(emit_bind(`call ptr @lp_mime_sniff(ptr {a.code})`), "string") }
|
||||
return fresh_val(emit_bind(`call ptr @lp_mime_of(ptr {a.code})`), "string")
|
||||
}
|
||||
|
||||
# emit_fs_prelude — the Fs/Path/Mime runtime, emitted once per program that uses
|
||||
|
|
@ -183,7 +184,8 @@ function emit_fs_path() -> void {
|
|||
emith("define ptr @lp_path_stem(ptr %s) {\n")
|
||||
emith("entry:\n %base = call ptr @lp_path_base(ptr %s)\n %ext = call ptr @lp_path_ext(ptr %s)\n")
|
||||
emith(" %bl64 = call i64 @strlen(ptr %base)\n %bl = trunc i64 %bl64 to i32\n %el64 = call i64 @strlen(ptr %ext)\n %el = trunc i64 %el64 to i32\n")
|
||||
emith(" %keep = sub i32 %bl, %el\n %r = call ptr @lp_fs_dup(ptr %base, i32 %keep)\n ret ptr %r\n}\n")
|
||||
# the base and the extension are its own copies: freed once the answer is cut out of them
|
||||
emith(" %keep = sub i32 %bl, %el\n %r = call ptr @lp_fs_dup(ptr %base, i32 %keep)\n call void @free(ptr %base)\n call void @free(ptr %ext)\n ret ptr %r\n}\n")
|
||||
|
||||
emit_fs_normalize()
|
||||
}
|
||||
|
|
@ -236,8 +238,9 @@ function emit_fs_normalize() -> void {
|
|||
emith("trim:\n store i32 %lasto, ptr %op\n br label %term\n")
|
||||
emith("fdefault:\n br label %term\n")
|
||||
emith("term:\n %eo = load i32, ptr %op\n %ez = icmp eq i32 %eo, 0\n br i1 %ez, label %emptyout, label %putnul\n")
|
||||
emith("emptyout:\n store i8 46, ptr %out\n %e1 = getelementptr i8, ptr %out, i32 1\n store i8 0, ptr %e1\n ret ptr %out\n")
|
||||
emith("putnul:\n %ep = getelementptr i8, ptr %out, i32 %eo\n store i8 0, ptr %ep\n ret ptr %out\n}\n")
|
||||
# the segment table is this call's own: freed on both ways out
|
||||
emith("emptyout:\n store i8 46, ptr %out\n %e1 = getelementptr i8, ptr %out, i32 1\n store i8 0, ptr %e1\n call void @free(ptr %segst)\n ret ptr %out\n")
|
||||
emith("putnul:\n %ep = getelementptr i8, ptr %out, i32 %eo\n store i8 0, ptr %ep\n call void @free(ptr %segst)\n ret ptr %out\n}\n")
|
||||
}
|
||||
|
||||
# ---- Fs.* (libc) -----------------------------------------------------------
|
||||
|
|
@ -280,10 +283,11 @@ function emit_fs_io() -> void {
|
|||
# write_text: write to "<p>.tmp" then rename over %p, so a crash mid-write
|
||||
# never truncates the previous file. Returns 1 on success.
|
||||
emith("define i32 @lp_fs_write_text(ptr %p, ptr %s) {\n")
|
||||
# the "<p>.tmp" name is this call's own and goes on both ways out
|
||||
emith("entry:\n %tmp = call ptr @lp_path_join_ext(ptr %p, ptr @fn_str_dottmp)\n %f = call ptr @fopen(ptr %tmp, ptr @fn_str_wb)\n %nz = icmp ne ptr %f, null\n br i1 %nz, label %ok, label %bad\n")
|
||||
emith("bad:\n ret i32 0\n")
|
||||
emith("bad:\n call void @free(ptr %tmp)\n ret i32 0\n")
|
||||
emith("ok:\n %n = call i64 @strlen(ptr %s)\n %w = call i64 @fwrite(ptr %s, i64 1, i64 %n, ptr %f)\n call i32 @fclose(ptr %f)\n")
|
||||
emith(" %rr = call i32 @rename(ptr %tmp, ptr %p)\n %ok2 = icmp eq i32 %rr, 0\n %z = zext i1 %ok2 to i32\n ret i32 %z\n}\n")
|
||||
emith(" %rr = call i32 @rename(ptr %tmp, ptr %p)\n call void @free(ptr %tmp)\n %ok2 = icmp eq i32 %rr, 0\n %z = zext i1 %ok2 to i32\n ret i32 %z\n}\n")
|
||||
|
||||
# concat two strings (used to build "<p>.tmp"); local so write_text needs no
|
||||
# dependency on the Os prelude
|
||||
|
|
@ -303,8 +307,10 @@ function emit_fs_io() -> void {
|
|||
emith("define i32 @lp_fs_copy(ptr %src, ptr %dst) {\n")
|
||||
emith("entry:\n %lp = alloca i32\n %buf = call ptr @lp_fs_readall(ptr %src, ptr %lp)\n %nz = icmp ne ptr %buf, null\n br i1 %nz, label %ok, label %bad\n")
|
||||
emith("bad:\n ret i32 0\n")
|
||||
emith("ok:\n %tmp = call ptr @lp_path_join_ext(ptr %dst, ptr @fn_str_dottmp)\n %f = call ptr @fopen(ptr %tmp, ptr @fn_str_wb)\n %fnz = icmp ne ptr %f, null\n br i1 %fnz, label %w, label %bad\n")
|
||||
emith("w:\n %n = load i32, ptr %lp\n %nz64 = zext i32 %n to i64\n call i64 @fwrite(ptr %buf, i64 1, i64 %nz64, ptr %f)\n call i32 @fclose(ptr %f)\n %rr = call i32 @rename(ptr %tmp, ptr %dst)\n %ok2 = icmp eq i32 %rr, 0\n %z = zext i1 %ok2 to i32\n ret i32 %z\n}\n")
|
||||
# the whole file read and the "<dst>.tmp" name are this call's own: both go on every way out
|
||||
emith("ok:\n %tmp = call ptr @lp_path_join_ext(ptr %dst, ptr @fn_str_dottmp)\n %f = call ptr @fopen(ptr %tmp, ptr @fn_str_wb)\n %fnz = icmp ne ptr %f, null\n br i1 %fnz, label %w, label %badw\n")
|
||||
emith("badw:\n call void @free(ptr %buf)\n call void @free(ptr %tmp)\n ret i32 0\n")
|
||||
emith("w:\n %n = load i32, ptr %lp\n %nz64 = zext i32 %n to i64\n call i64 @fwrite(ptr %buf, i64 1, i64 %nz64, ptr %f)\n call i32 @fclose(ptr %f)\n %rr = call i32 @rename(ptr %tmp, ptr %dst)\n call void @free(ptr %buf)\n call void @free(ptr %tmp)\n %ok2 = icmp eq i32 %rr, 0\n %z = zext i1 %ok2 to i32\n ret i32 %z\n}\n")
|
||||
|
||||
# mkdir: create %p and any missing parents (mkdir -p). Returns 1 if the
|
||||
# directory exists afterwards. Intermediate EEXIST errors are ignored.
|
||||
|
|
@ -314,7 +320,7 @@ function emit_fs_io() -> void {
|
|||
emith("go:\n %sl = icmp eq i32 %ci, 47\n br i1 %sl, label %cut, label %nx\n")
|
||||
emith("cut:\n store i8 0, ptr %pp\n call i32 @mkdir(ptr %dup, i32 493)\n store i8 47, ptr %pp\n br label %nx\n")
|
||||
emith("nx:\n %i1 = add i32 %i, 1\n store i32 %i1, ptr %ip\n br label %lp\n")
|
||||
emith("fin:\n call i32 @mkdir(ptr %dup, i32 493)\n %r = call i32 @lp_fs_is_dir(ptr %p)\n ret i32 %r\n}\n")
|
||||
emith("fin:\n call i32 @mkdir(ptr %dup, i32 493)\n call void @free(ptr %dup)\n %r = call i32 @lp_fs_is_dir(ptr %p)\n ret i32 %r\n}\n")
|
||||
}
|
||||
|
||||
# ---- Fs.list (+ sort) ------------------------------------------------------
|
||||
|
|
@ -404,9 +410,9 @@ function emit_fs_mime() -> void {
|
|||
emith("entry:\n %ext = call ptr @lp_path_ext(ptr %path)\n %lc = call ptr @lp_mime_lc(ptr %ext)\n %ip = alloca i32\n store i32 0, ptr %ip\n br label %lp\n")
|
||||
emith(`lp:\n %i = load i32, ptr %ip\n %lt = icmp slt i32 %i, {itoa(n)}\n br i1 %lt, label %body, label %def\n`)
|
||||
emith(`body:\n %kp = getelementptr [{itoa(n)} x {{ ptr, ptr }}], ptr @mime_tbl, i32 0, i32 %i, i32 0\n %k = load ptr, ptr %kp\n %c = call i32 @strcmp(ptr %lc, ptr %k)\n %eq = icmp eq i32 %c, 0\n br i1 %eq, label %hit, label %nx\n`)
|
||||
emith(`hit:\n %vp = getelementptr [{itoa(n)} x {{ ptr, ptr }}], ptr @mime_tbl, i32 0, i32 %i, i32 1\n %v = load ptr, ptr %vp\n %r = call ptr @lp_fs_strdup(ptr %v)\n ret ptr %r\n`)
|
||||
emith(`hit:\n %vp = getelementptr [{itoa(n)} x {{ ptr, ptr }}], ptr @mime_tbl, i32 0, i32 %i, i32 1\n %v = load ptr, ptr %vp\n %r = call ptr @lp_fs_strdup(ptr %v)\n call void @free(ptr %ext)\n call void @free(ptr %lc)\n ret ptr %r\n`)
|
||||
emith("nx:\n %i1 = add i32 %i, 1\n store i32 %i1, ptr %ip\n br label %lp\n")
|
||||
emith(`def:\n %d = call ptr @lp_fs_strdup(ptr {k_octet})\n ret ptr %d\n}}\n`)
|
||||
emith(`def:\n %d = call ptr @lp_fs_strdup(ptr {k_octet})\n call void @free(ptr %ext)\n call void @free(ptr %lc)\n ret ptr %d\n}}\n`)
|
||||
|
||||
# lowercase an extension, dropping a leading '.' (ASCII only, for table lookup)
|
||||
emith("define ptr @lp_mime_lc(ptr %s) {\n")
|
||||
|
|
@ -425,20 +431,22 @@ function emit_fs_mime() -> void {
|
|||
function emit_fs_sniff() -> void {
|
||||
emith("define ptr @lp_mime_sniff(ptr %path) {\n")
|
||||
emith("entry:\n %f = call ptr @fopen(ptr %path, ptr @fn_str_rb)\n %nz = icmp ne ptr %f, null\n br i1 %nz, label %ok, label %fallback\n")
|
||||
emith("ok:\n %buf = call ptr @malloc(i64 16)\n %rd = call i64 @fread(ptr %buf, i64 1, i64 8, ptr %f)\n call i32 @fclose(ptr %f)\n %rdi = trunc i64 %rd to i32\n %has4 = icmp sge i32 %rdi, 4\n br i1 %has4, label %chk, label %fallback\n")
|
||||
emith("ok:\n %buf = call ptr @malloc(i64 16)\n %rd = call i64 @fread(ptr %buf, i64 1, i64 8, ptr %f)\n call i32 @fclose(ptr %f)\n %rdi = trunc i64 %rd to i32\n %has4 = icmp sge i32 %rdi, 4\n br i1 %has4, label %chk, label %nomatch\n")
|
||||
emith("chk:\n %b0p = getelementptr i8, ptr %buf, i32 0\n %b0 = load i8, ptr %b0p\n %b0i = zext i8 %b0 to i32\n %b1p = getelementptr i8, ptr %buf, i32 1\n %b1 = load i8, ptr %b1p\n %b1i = zext i8 %b1 to i32\n %b2p = getelementptr i8, ptr %buf, i32 2\n %b2 = load i8, ptr %b2p\n %b2i = zext i8 %b2 to i32\n %b3p = getelementptr i8, ptr %buf, i32 3\n %b3 = load i8, ptr %b3p\n %b3i = zext i8 %b3 to i32\n")
|
||||
# PNG: 89 50 4E 47
|
||||
emith(" %p0 = icmp eq i32 %b0i, 137\n %p1 = icmp eq i32 %b1i, 80\n %p2 = icmp eq i32 %b2i, 78\n %p3 = icmp eq i32 %b3i, 71\n %pa = and i1 %p0, %p1\n %pb = and i1 %pa, %p2\n %pc = and i1 %pb, %p3\n br i1 %pc, label %png, label %cj\n")
|
||||
emith("png:\n %rpng = call ptr @lp_fs_strdup(ptr @fn_sig_png)\n ret ptr %rpng\n")
|
||||
emith("png:\n %rpng = call ptr @lp_fs_strdup(ptr @fn_sig_png)\n call void @free(ptr %buf)\n ret ptr %rpng\n")
|
||||
# JPEG: FF D8 FF
|
||||
emith("cj:\n %j0 = icmp eq i32 %b0i, 255\n %j1 = icmp eq i32 %b1i, 216\n %j2 = icmp eq i32 %b2i, 255\n %ja = and i1 %j0, %j1\n %jb = and i1 %ja, %j2\n br i1 %jb, label %jpg, label %cg\n")
|
||||
emith("jpg:\n %rjpg = call ptr @lp_fs_strdup(ptr @fn_sig_jpg)\n ret ptr %rjpg\n")
|
||||
emith("jpg:\n %rjpg = call ptr @lp_fs_strdup(ptr @fn_sig_jpg)\n call void @free(ptr %buf)\n ret ptr %rjpg\n")
|
||||
# GIF: 47 49 46
|
||||
emith("cg:\n %g0 = icmp eq i32 %b0i, 71\n %g1 = icmp eq i32 %b1i, 73\n %g2 = icmp eq i32 %b2i, 70\n %ga = and i1 %g0, %g1\n %gb = and i1 %ga, %g2\n br i1 %gb, label %gif, label %cp\n")
|
||||
emith("gif:\n %rgif = call ptr @lp_fs_strdup(ptr @fn_sig_gif)\n ret ptr %rgif\n")
|
||||
emith("gif:\n %rgif = call ptr @lp_fs_strdup(ptr @fn_sig_gif)\n call void @free(ptr %buf)\n ret ptr %rgif\n")
|
||||
# PDF: 25 50 44 46
|
||||
emith("cp:\n %q0 = icmp eq i32 %b0i, 37\n %q1 = icmp eq i32 %b1i, 80\n %q2 = icmp eq i32 %b2i, 68\n %q3 = icmp eq i32 %b3i, 70\n %qa = and i1 %q0, %q1\n %qb = and i1 %qa, %q2\n %qc = and i1 %qb, %q3\n br i1 %qc, label %pdf, label %fallback\n")
|
||||
emith("pdf:\n %rpdf = call ptr @lp_fs_strdup(ptr @fn_sig_pdf)\n ret ptr %rpdf\n")
|
||||
emith("cp:\n %q0 = icmp eq i32 %b0i, 37\n %q1 = icmp eq i32 %b1i, 80\n %q2 = icmp eq i32 %b2i, 68\n %q3 = icmp eq i32 %b3i, 70\n %qa = and i1 %q0, %q1\n %qb = and i1 %qa, %q2\n %qc = and i1 %qb, %q3\n br i1 %qc, label %pdf, label %nomatch\n")
|
||||
emith("pdf:\n %rpdf = call ptr @lp_fs_strdup(ptr @fn_sig_pdf)\n call void @free(ptr %buf)\n ret ptr %rpdf\n")
|
||||
# the 16 bytes read are this call's own and go on every way out
|
||||
emith("nomatch:\n call void @free(ptr %buf)\n br label %fallback\n")
|
||||
emith("fallback:\n %r = call ptr @lp_mime_of(ptr %path)\n ret ptr %r\n}\n")
|
||||
|
||||
# the small string constants the Fs/Path/Mime runtime references
|
||||
|
|
|
|||
|
|
@ -29,13 +29,17 @@ function is_log_ns(meth: pointer) -> bool {
|
|||
return false
|
||||
}
|
||||
|
||||
# format any value as a string for a log field: a string passes through, a long
|
||||
# and an int are converted the same way the `string(...)` builtin does.
|
||||
function log_stringify(v: Val) -> pointer {
|
||||
if (llty(v.ty) == "ptr") { return v.code }
|
||||
if (llty(v.ty) == "i64") { g_uses_longstr = true; return emit_bind(`call ptr @lp_long_str(i64 {v.code})`) }
|
||||
# format any value as a string for a log field: a string passes through (fresh if it was), a long
|
||||
# and an int are converted the same way the `string(...)` builtin does, into text freed once joined
|
||||
function log_stringify(v: Val) -> Val {
|
||||
if (llty(v.ty) == "ptr") {
|
||||
let s = val(v.code, "string")
|
||||
s.fresh = v.fresh
|
||||
return s
|
||||
}
|
||||
if (llty(v.ty) == "i64") { g_uses_longstr = true; return fresh_val(emit_bind(`call ptr @lp_long_str(i64 {v.code})`), "string") }
|
||||
g_uses_intstr = true
|
||||
return emit_bind(`call ptr @lp_int_str(i32 {v.code})`)
|
||||
return fresh_val(emit_bind(`call ptr @lp_int_str(i32 {v.code})`), "string")
|
||||
}
|
||||
|
||||
function emit_log_ns(meth: pointer, e: Node) -> Val {
|
||||
|
|
@ -55,21 +59,32 @@ function emit_log_ns(meth: pointer, e: Node) -> Val {
|
|||
if (meth == "warn") { lvl = "3"; pfx = "[WARN] " }
|
||||
if (meth == "error") { lvl = "4"; pfx = "[ERROR] " }
|
||||
g_uses_str = true
|
||||
# below the threshold nothing is built: the message and its fields are made, written and freed
|
||||
# only for a line that is shown (every call once left its whole line behind, shown or not)
|
||||
let cur = emit_bind("load i32, ptr @L_log_level")
|
||||
let on = emit_bind(`icmp sge i32 {lvl}, {cur}`)
|
||||
let lon = lbl("logon")
|
||||
let lend = lbl("logend")
|
||||
emit(` br i1 {on}, label %{lon}, label %{lend}\n`)
|
||||
emit(`{lon}:\n`)
|
||||
# line = "[LEVEL] " + msg, then " key=value" for each trailing pair
|
||||
var line = val(emit_str_const(pfx), "string")
|
||||
let msg = emit_expr(e.kids[0])
|
||||
line = emit_str_op("+", line, val(log_stringify(msg), "string"))
|
||||
line = emit_str_op("+", line, log_stringify(msg))
|
||||
var i = 1
|
||||
while (i + 1) < len(e.kids) {
|
||||
let k = emit_expr(e.kids[i])
|
||||
let v = emit_expr(e.kids[i + 1])
|
||||
line = emit_str_op("+", line, val(emit_str_const(" "), "string"))
|
||||
line = emit_str_op("+", line, val(log_stringify(k), "string"))
|
||||
line = emit_str_op("+", line, log_stringify(k))
|
||||
line = emit_str_op("+", line, val(emit_str_const("="), "string"))
|
||||
line = emit_str_op("+", line, val(log_stringify(v), "string"))
|
||||
line = emit_str_op("+", line, log_stringify(v))
|
||||
i += 2
|
||||
}
|
||||
emit(` call void @lp_log_emit(i32 {lvl}, ptr {line.code})\n`)
|
||||
emit(` call void @free(ptr {line.code})\n`)
|
||||
emit(` br label %{lend}\n`)
|
||||
emit(`{lend}:\n`)
|
||||
return val("0", "void")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,17 +110,18 @@ function emit_os_ns(meth: pointer, e: Node) -> Val {
|
|||
if (meth == "arch") { return val(emit_bind("call ptr @lp_os_arch()"), "string") }
|
||||
if (meth == "save_dir") {
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @lp_os_save_dir(ptr {a.code})`), "string")
|
||||
return fresh_val(emit_bind(`call ptr @lp_os_save_dir(ptr {a.code})`), "string")
|
||||
}
|
||||
if (meth == "config_dir") {
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @lp_os_config_dir(ptr {a.code})`), "string")
|
||||
return fresh_val(emit_bind(`call ptr @lp_os_config_dir(ptr {a.code})`), "string")
|
||||
}
|
||||
if (meth == "cache_dir") {
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @lp_os_cache_dir(ptr {a.code})`), "string")
|
||||
return fresh_val(emit_bind(`call ptr @lp_os_cache_dir(ptr {a.code})`), "string")
|
||||
}
|
||||
# temp_dir
|
||||
# temp_dir: the environment's own text on POSIX, a copy of its own on Windows
|
||||
if g_target_win { return fresh_val(emit_bind("call ptr @lp_os_temp_dir()"), "string") }
|
||||
return val(emit_bind("call ptr @lp_os_temp_dir()"), "string")
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +170,7 @@ function emit_os_prelude() -> void {
|
|||
emith(" %end = getelementptr i8, ptr %m, i64 %sum\n store i8 0, ptr %end\n ret ptr %m\n}\n")
|
||||
|
||||
emith("define ptr @lp_os_join3(ptr %a, ptr %b, ptr %c) {\n")
|
||||
emith("entry:\n %ab = call ptr @lp_os_join2(ptr %a, ptr %b)\n %r = call ptr @lp_os_join2(ptr %ab, ptr %c)\n ret ptr %r\n}\n")
|
||||
emith("entry:\n %ab = call ptr @lp_os_join2(ptr %a, ptr %b)\n %r = call ptr @lp_os_join2(ptr %ab, ptr %c)\n call void @free(ptr %ab)\n ret ptr %r\n}\n")
|
||||
|
||||
# Windows has its own known folders and no uname; emit_win.ludic supplies them
|
||||
if g_target_win { emit_os_win_known(k_dot, k_slash); return }
|
||||
|
|
@ -218,7 +219,7 @@ function emit_os_prelude() -> void {
|
|||
emith("define ptr @lp_os_known(ptr %app, ptr %mac, ptr %var, ptr %rel) {\n")
|
||||
emith("entry:\n %d = call i32 @lp_os_is_darwin()\n %isd = icmp ne i32 %d, 0\n br i1 %isd, label %m, label %x\n")
|
||||
emith("m:\n %h = call ptr @lp_os_home()\n %r1 = call ptr @lp_os_join3(ptr %h, ptr %mac, ptr %app)\n ret ptr %r1\n")
|
||||
emith("x:\n %b = call ptr @lp_os_xdg(ptr %var, ptr %rel)\n %r2 = call ptr @lp_os_join2(ptr %b, ptr %app)\n ret ptr %r2\n}\n")
|
||||
emith("x:\n %b = call ptr @lp_os_xdg(ptr %var, ptr %rel)\n %r2 = call ptr @lp_os_join2(ptr %b, ptr %app)\n call void @free(ptr %b)\n ret ptr %r2\n}\n")
|
||||
|
||||
emith(`define ptr @lp_os_save_dir(ptr %app) {{\n %r = call ptr @lp_os_known(ptr %app, ptr {k_appsp}, ptr {k_xdg_data}, ptr {k_dot_share})\n ret ptr %r\n}}\n`)
|
||||
emith(`define ptr @lp_os_config_dir(ptr %app) {{\n %r = call ptr @lp_os_known(ptr %app, ptr {k_appsp}, ptr {k_xdg_cfg}, ptr {k_dot_config})\n ret ptr %r\n}}\n`)
|
||||
|
|
|
|||
|
|
@ -15,94 +15,111 @@ function is_text_ns(meth: pointer) -> bool {
|
|||
return false
|
||||
}
|
||||
|
||||
# A string argument made for the call (a template, string(n), a slice) is the call's to give back once
|
||||
# read: Text.to_int(string(n)) kept n's text. Every string a Text call returns is a copy of its own,
|
||||
# so it is fresh and goes once a +, == or print has read it.
|
||||
function text_gone(v: Val) -> void {
|
||||
if v.fresh { emit(` call void @free(ptr {v.code})\n`) }
|
||||
}
|
||||
|
||||
function emit_text_ns(meth: pointer, e: Node) -> Val {
|
||||
if (meth == "from_int") { # int -> string, same as string(n)
|
||||
let n = emit_expr(e.kids[0])
|
||||
g_uses_intstr = true
|
||||
return val(emit_bind(`call ptr @lp_int_str(i32 {n.code})`), "string")
|
||||
return fresh_val(emit_bind(`call ptr @lp_int_str(i32 {n.code})`), "string")
|
||||
}
|
||||
if (meth == "slice") { # s[a..b], same substring helper
|
||||
let s0 = emit_expr(e.kids[0]); let a = emit_expr(e.kids[1]); let b = emit_expr(e.kids[2])
|
||||
g_uses_strslice = true
|
||||
return val(emit_bind(`call ptr @lp_str_slice(ptr {s0.code}, i32 {a.code}, i32 {b.code})`), "string")
|
||||
let r = fresh_val(emit_bind(`call ptr @lp_str_slice(ptr {s0.code}, i32 {a.code}, i32 {b.code})`), "string")
|
||||
text_gone(s0)
|
||||
return r
|
||||
}
|
||||
if (meth == "equals") { # byte-wise equality, same as ==
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
g_uses_str = true
|
||||
return val(emit_bind(`call i32 @lp_str_eq(ptr {a.code}, ptr {b.code})`), "bool")
|
||||
return emit_str_op("==", a, b)
|
||||
}
|
||||
if (meth == "concat") { # a + b, same as the + operator
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
g_uses_str = true
|
||||
return val(emit_bind(`call ptr @lp_str_concat(ptr {a.code}, ptr {b.code})`), "string")
|
||||
return emit_str_op("+", a, b)
|
||||
}
|
||||
|
||||
let s = emit_expr(e.kids[0])
|
||||
if (meth == "length") { # byte length
|
||||
let r = emit_bind(`call i64 @strlen(ptr {s.code})`)
|
||||
text_gone(s)
|
||||
return val(emit_bind(`trunc i64 {r} to i32`), "int")
|
||||
}
|
||||
if (meth == "char_at") { # the byte at index i, 0..255
|
||||
let i = emit_expr(e.kids[1])
|
||||
let a = emit_bind(`getelementptr inbounds i8, ptr {s.code}, i32 {i.code}`)
|
||||
let c = emit_bind(`load i8, ptr {a}`)
|
||||
text_gone(s)
|
||||
return val(emit_bind(`zext i8 {c} to i32`), "int")
|
||||
}
|
||||
if (meth == "to_float") { # parse a leading decimal number, 0.0 if none
|
||||
fp_declare("declare double @strtod(ptr, ptr)\n")
|
||||
let d = emit_bind(`call double @strtod(ptr {s.code}, ptr null)`)
|
||||
text_gone(s)
|
||||
return val(emit_bind(`fptrunc double {d} to float`), "float")
|
||||
}
|
||||
if (meth == "to_int") { # parse a leading integer, 0 if none
|
||||
return val(emit_bind(`call i32 @atoi(ptr {s.code})`), "int")
|
||||
let r = emit_bind(`call i32 @atoi(ptr {s.code})`)
|
||||
text_gone(s)
|
||||
return val(r, "int")
|
||||
}
|
||||
if (meth == "upper") { # ASCII a-z -> A-Z, fresh string
|
||||
if (meth == "upper") or (meth == "lower") or (meth == "trim") { # a fresh copy, changed
|
||||
g_uses_textrt = true
|
||||
return val(emit_bind(`call ptr @lp_str_upper(ptr {s.code})`), "string")
|
||||
}
|
||||
if (meth == "lower") { # ASCII A-Z -> a-z, fresh string
|
||||
g_uses_textrt = true
|
||||
return val(emit_bind(`call ptr @lp_str_lower(ptr {s.code})`), "string")
|
||||
}
|
||||
if (meth == "trim") { # drop leading/trailing whitespace
|
||||
g_uses_textrt = true
|
||||
return val(emit_bind(`call ptr @lp_str_trim(ptr {s.code})`), "string")
|
||||
var f = "lp_str_trim"
|
||||
if (meth == "upper") { f = "lp_str_upper" }
|
||||
if (meth == "lower") { f = "lp_str_lower" }
|
||||
let r = fresh_val(emit_bind(`call ptr @{f}(ptr {s.code})`), "string")
|
||||
text_gone(s)
|
||||
return r
|
||||
}
|
||||
if (meth == "repeat") { # s repeated n times
|
||||
g_uses_textrt = true
|
||||
let n = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call ptr @lp_str_repeat(ptr {s.code}, i32 {n.code})`), "string")
|
||||
let r = fresh_val(emit_bind(`call ptr @lp_str_repeat(ptr {s.code}, i32 {n.code})`), "string")
|
||||
text_gone(s)
|
||||
return r
|
||||
}
|
||||
if (meth == "pad_left") { # pad with spaces to width, on the left
|
||||
if (meth == "pad_left") or (meth == "pad_right") { # pad with spaces to width
|
||||
g_uses_textrt = true
|
||||
let w = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call ptr @lp_str_pad(ptr {s.code}, i32 {w.code}, i1 1)`), "string")
|
||||
}
|
||||
if (meth == "pad_right") { # pad with spaces to width, on the right
|
||||
g_uses_textrt = true
|
||||
let w = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call ptr @lp_str_pad(ptr {s.code}, i32 {w.code}, i1 0)`), "string")
|
||||
var left = "1"
|
||||
if (meth == "pad_right") { left = "0" }
|
||||
let r = fresh_val(emit_bind(`call ptr @lp_str_pad(ptr {s.code}, i32 {w.code}, i1 {left})`), "string")
|
||||
text_gone(s)
|
||||
return r
|
||||
}
|
||||
if (meth == "replace") { # replace every `from` with `to`
|
||||
g_uses_textrt2 = true
|
||||
let from = emit_expr(e.kids[1]); let to = emit_expr(e.kids[2])
|
||||
return val(emit_bind(`call ptr @lp_str_replace(ptr {s.code}, ptr {from.code}, ptr {to.code})`), "string")
|
||||
let r = fresh_val(emit_bind(`call ptr @lp_str_replace(ptr {s.code}, ptr {from.code}, ptr {to.code})`), "string")
|
||||
text_gone(s); text_gone(from); text_gone(to)
|
||||
return r
|
||||
}
|
||||
if (meth == "split") { # split on a separator -> []string
|
||||
g_uses_textrt2 = true
|
||||
let sep = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call ptr @lp_str_split(ptr {s.code}, ptr {sep.code})`), "[]string")
|
||||
let r = val(emit_bind(`call ptr @lp_str_split(ptr {s.code}, ptr {sep.code})`), "[]string")
|
||||
text_gone(s); text_gone(sep)
|
||||
return r
|
||||
}
|
||||
if (meth == "join") { # join a []string with a separator (s is the slice)
|
||||
g_uses_textrt2 = true
|
||||
let sep = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call ptr @lp_str_join(ptr {s.code}, ptr {sep.code})`), "string")
|
||||
let r = fresh_val(emit_bind(`call ptr @lp_str_join(ptr {s.code}, ptr {sep.code})`), "string")
|
||||
text_gone(sep)
|
||||
return r
|
||||
}
|
||||
if (meth == "contains") or (meth == "index_of") { # substring search
|
||||
let sub = emit_expr(e.kids[1])
|
||||
let p = emit_bind(`call ptr @strstr(ptr {s.code}, ptr {sub.code})`)
|
||||
if (meth == "contains") {
|
||||
let nn = emit_bind(`icmp ne ptr {p}, null`)
|
||||
text_gone(s); text_gone(sub)
|
||||
return val(emit_bind(`zext i1 {nn} to i32`), "bool")
|
||||
}
|
||||
let isnull = emit_bind(`icmp eq ptr {p}, null`) # index_of -> byte offset or -1
|
||||
|
|
@ -110,6 +127,7 @@ function emit_text_ns(meth: pointer, e: Node) -> Val {
|
|||
let si = emit_bind(`ptrtoint ptr {s.code} to i64`)
|
||||
let d = emit_bind(`sub i64 {pi}, {si}`)
|
||||
let d32 = emit_bind(`trunc i64 {d} to i32`)
|
||||
text_gone(s); text_gone(sub)
|
||||
return val(emit_bind(`select i1 {isnull}, i32 -1, i32 {d32}`), "int")
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +137,7 @@ function emit_text_ns(meth: pointer, e: Node) -> Val {
|
|||
if (meth == "starts_with") { # strncmp of the head is null-safe
|
||||
let cmp = emit_bind(`call i32 @strncmp(ptr {s.code}, ptr {affix.code}, i64 {la})`)
|
||||
let eqz = emit_bind(`icmp eq i32 {cmp}, 0`)
|
||||
text_gone(s); text_gone(affix)
|
||||
return val(emit_bind(`zext i1 {eqz} to i32`), "bool")
|
||||
}
|
||||
# ends_with: compare the tail, but only when the affix fits (else a negative
|
||||
|
|
@ -138,5 +157,6 @@ function emit_text_ns(meth: pointer, e: Node) -> Val {
|
|||
store_at("i32", z, res)
|
||||
emit(" br label %"); emit(en); emit("\n")
|
||||
emit(en); emit(":\n")
|
||||
text_gone(s); text_gone(affix)
|
||||
return val(emit_bind(`load i32, ptr {res}`), "bool")
|
||||
}
|
||||
|
|
|
|||
37794
selfhost/ludicc.seed.ll
37794
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1079,6 +1079,7 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/private_generic", "grow is private to module kit", "an instance of a private generic is private to its module")
|
||||
feat_case("lang/long_literals", "", "4294967295 3988292384 -4294967296 1099511627776 -3750763034362895579 -1 1", "long_literals.ludic (a decimal literal past 2^31 - 1, and a hex literal of more than eight digits, is a long)")
|
||||
reject_case("rejected/long_into_int", "4294967295 does not fit one; it is a long", "a long literal given to an int is refused, not wrapped")
|
||||
feat_case("lang/runtime_temps", "", "1 2 3 4 5 grew 0 files grew 0", "runtime_temps.ludic (Log, DateTime.format, Path, Mime, Text, Os and Fs keep nothing a call does not hand back)")
|
||||
feat_case("lang/string_temps", "", "1 2 3 4 5 6 7 grew 0", "string_temps.ludic (a concatenation's pieces, a template's holes and a compared side are freed once used; a kept one is not)")
|
||||
feat_case("lang/nested_templates", "", "outer [inner 3 {x}] \"`}\" end", "nested_templates.ludic (a template literal inside another's hole)")
|
||||
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue