Phase 7f: len(x) works on strings too (retire slen)

`len` is now polymorphic — a slice's element count OR a string's byte length —
so the C-style `slen` (strlen) is gone: `slen(name)` -> `len(name)`. emit_len
branches on the operand type (slice header vs @strlen). Two reseeds: add the
string branch, then migrate the 19 slen calls and delete slen.

Reseeded (22565 lines); C-free fixpoint holds; goldens identical; 18/18; vocab +
doc-fences clean. (String slicing s[a..b] to replace substr is deferred to its
own phase.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-28 01:25:57 +03:00
parent 058cac88ec
commit 839a6bf0c0
11 changed files with 3555 additions and 3542 deletions

View file

@ -628,8 +628,9 @@ literal; test any pointer/record/slice with `x == null` / `x != null` (an unset
# save save() load()->bool (binary snapshot of the whole ECS World)
# control quit() print(x) (a value + newline)
# convert str(x) -> str (int/bool/fixed -> text)
# length len(x) -> int (elements of a slice, or bytes of a string)
# process os_argc()->int os_arg(i)->str (the command line; argv[0] included)
# file_stderr()->ptr (a handle for file_write, off stdout)
# file_stderr()->ptr file_stdout()->ptr (handles for file_write)
```
## Tooling

View file

@ -57,7 +57,7 @@ fn llty(t: ptr) -> ptr {
}
fn is_slice_ty(t: ptr) -> bool { return peek8(t, 0) == 91 and peek8(t, 1) == 93 } # "[]"
fn slice_elem(t: ptr) -> ptr { return substr(t, 2, slen(t) - 2) }
fn slice_elem(t: ptr) -> ptr { return substr(t, 2, len(t) - 2) }
fn find_arch(name: ptr) -> Node {
var i = 0

View file

@ -96,14 +96,14 @@ fn ir_flush(path: ptr) -> bool {
let c = buf_str(code)
if (path == null) { # raw IR to stdout (no trailing newline)
let out = file_stdout()
file_write(out, h, slen(h))
file_write(out, c, slen(c))
file_write(out, h, len(h))
file_write(out, c, len(c))
return true
}
let f = file_open(path, "wb")
if (f == null) { return false }
file_write(f, h, slen(h))
file_write(f, c, slen(c))
file_write(f, h, len(h))
file_write(f, c, len(c))
file_close(f)
return true
}

View file

@ -7,7 +7,7 @@ fn hexdig(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } # 0-9
fn emit_str_const(s: ptr) -> ptr {
let name = ("@.str" + itoa(ll_str))
ll_str = ll_str + 1
let n = slen(s)
let n = len(s)
emith(name); emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)); emith(" x i8] c\"")
var i = 0
@ -75,7 +75,7 @@ fn emit_header() -> void {
emith("@L_argc = internal global i32 0\n")
emith("@L_argv = internal global ptr null\n")
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(slen(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
emith(itoa(len(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
emith("%LSlice = type { ptr, i32, i32 }\n")
# property layouts — a %Cmp_ record of named fields, emitted here so `new`
# works whether or not the program runs the ECS. The per-entity @S_/@H_ arrays

View file

@ -50,8 +50,12 @@ fn slice_field(h: ptr, i: int) -> ptr {
fn emit_len(e: Node) -> Val {
let s = emit_expr(e.kids[0])
let lp = slice_field(s.code, 1)
return val(emit_bind(("load i32, ptr " + lp)), "int")
if is_slice_ty(s.ty) { # a slice: read its header length
let lp = slice_field(s.code, 1)
return val(emit_bind(("load i32, ptr " + lp)), "int")
}
let r = emit_bind(("call i64 @strlen(ptr " + (s.code + ")"))) # a string: byte length
return val(emit_bind(("trunc i64 " + (r + " to i32"))), "int")
}
fn emit_push(e: Node) -> Val {

View file

@ -41,7 +41,7 @@ fn has_ui() -> bool {
# index of a UI_<name>: a ui block's root, or a widget's id=
fn ui_index_of(nm: ptr) -> int {
let s = substr(nm, 3, slen(nm) - 3) # strip "UI_"
let s = substr(nm, 3, len(nm) - 3) # strip "UI_"
let u = 0; var bi = 0
var i = 0
while i < len(prog) {
@ -57,7 +57,7 @@ fn ui_index_of(nm: ptr) -> int {
return 0
}
fn is_ui_ident(nm: ptr) -> bool {
return slen(nm) > 3 and peek8(nm, 0) == 85 and peek8(nm, 1) == 73 and peek8(nm, 2) == 95 # "UI_"
return len(nm) > 3 and peek8(nm, 0) == 85 and peek8(nm, 1) == 73 and peek8(nm, 2) == 95 # "UI_"
}
fn ll_ui_set(idx: int, key: int, val: ptr) -> void {

View file

@ -41,7 +41,7 @@ fn lex(src: ptr) -> void {
toks = new []Tok
var i = 0
var line = 1
let n = slen(src)
let n = len(src)
while i < n {
let c = peek8(src, i)
if c == 10 { tok_push(TK_NL, null, 0, line); line = line + 1; i = i + 1; continue }

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@ fn base_name(path: ptr) -> ptr {
# drop a trailing ".ludic" if present
fn strip_ludic(name: ptr) -> ptr {
let n = slen(name)
let n = len(name)
if n > 6 {
if (substr(name, n - 6, 6) == ".ludic") { return substr(name, 0, n - 6) }
}
@ -40,14 +40,14 @@ fn getenv_or(name: ptr, dflt: ptr) -> ptr {
# guarantee a directory string ends in '/' so path_join concatenates cleanly
fn ensure_slash(d: ptr) -> ptr {
let n = slen(d)
let n = len(d)
if n == 0 { return d }
if peek8(d, n - 1) == 47 { return d }
return (d + ("/"))
}
fn die(msg: ptr) -> void {
file_write(file_stderr(), msg, slen(msg))
file_write(file_stderr(), msg, len(msg))
os_exit(1)
}
@ -78,7 +78,7 @@ entry {
# input file — '-' is ASCII 45
if peek8(a, 0) == 45 {
let m = ("ludicc: ignoring unknown flag " + (a + "\n"))
file_write(file_stderr(), m, slen(m))
file_write(file_stderr(), m, len(m))
} else { path = a }
} } } } } } }
ai = ai + 1
@ -122,7 +122,7 @@ entry {
# make the output directory if the user asked for e.g. bin/app
let odir = dir_of(out)
if slen(odir) > 0 { os_system((("mkdir -p ") + odir)) }
if len(odir) > 0 { os_system((("mkdir -p ") + odir)) }
let ll = (out + ".ll")
if not ir_flush(ll) { die("ludicc: cannot write IR\n") }

View file

@ -15,7 +15,7 @@ fn is_kw(v: ptr) -> bool { return is_id(v) }
fn perr(msg: ptr) -> void {
let e = file_stderr()
file_write(e, "ludicc(self): parse error: ", 27)
file_write(e, msg, slen(msg))
file_write(e, msg, len(msg))
file_write(e, "\n", 1)
os_exit(1)
}
@ -35,7 +35,7 @@ fn ptype() -> ptr {
pi = pi + 1
eat_op("]")
let el = ptype()
let out = mem_alloc(slen(el) + 3)
let out = mem_alloc(len(el) + 3)
poke8(out, 0, 91); poke8(out, 1, 93) # "[]"
var i = 0
while peek8(el, i) != 0 { poke8(out, 2 + i, peek8(el, i)); i = i + 1 }
@ -73,7 +73,7 @@ fn parse_hole(inner: ptr) -> Node { # re-lex+parse an embedded exp
return e
}
fn parse_interp(raw: ptr) -> Node {
let n = slen(raw)
let n = len(raw)
var acc: Node = null
let lit = mem_alloc(n + 1)
var lj = 0

View file

@ -2,12 +2,6 @@
# slicing out substrings, and integer<->text. All over raw NUL-terminated
# byte buffers reached with peek8/poke8.
fn slen(s: ptr) -> int {
var n = 0
while peek8(s, n) != 0 { n = n + 1 }
return n
}
# a fresh NUL-terminated copy of src[start .. start+n]
fn substr(src: ptr, start: int, n: int) -> ptr {
let b = mem_alloc(n + 1)