Phase 7d: string interpolation text {expr} text
The readable way to build strings, as you noted `{a} {b}` beats `a + " " + b`.
A backtick string embeds any expression in `{…}` and desugars to the Phase-7c
`+` chain, wrapping each hole in `str(...)`:
`hello {name}, n={count + 1}`
== "hello " + name + ", n=" + str(count + 1)
- Lexer: a backtick captures its content raw as TK_INTERP.
- Parser: parse_interp splits literal runs from `{…}` holes (brace-depth aware,
`{{`/`}}` escape to literal braces), re-lexes each hole as a full expression
(save/restore toks/pi like an import), and folds it all into E_BIN(+) nodes —
so no new AST or runtime beyond the existing concat.
- str(x): a string passes through; int/bool/fixed convert via a small emitted
@fn_int_str prelude (digits from the end of a buffer, '-' for negatives),
emitted once into any program that uses it.
examples/strings.ludic gains interpolation cases (now prints 1..7); the smoke
covers it. Grammar + ludic_syntax.h tokenize backtick strings (holes highlighted
as embedded code). LANGUAGE.md documents it as the preferred form.
Reseeded (22530 lines); C-free fixpoint holds; goldens identical; 18/18; vocab +
doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a997e34b2d
commit
ad63f09d53
13 changed files with 4748 additions and 3962 deletions
|
|
@ -19,6 +19,7 @@ var loc_ty: []ptr
|
|||
var loc_mut: []int # 1 = mutable (var / param / loop-var), 0 = immutable (let)
|
||||
var nloc: int = 0
|
||||
var g_uses_str: bool = false # a `str + str` / `str == str` was emitted -> emit the prelude
|
||||
var g_uses_intstr: bool = false # `str(int)` was emitted -> emit the int->string prelude
|
||||
|
||||
# loop targets for break/continue (innermost last)
|
||||
var brk_lbl: []ptr
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ fn emit_program() -> void {
|
|||
head = buf_new()
|
||||
code = buf_new()
|
||||
g_uses_str = false
|
||||
g_uses_intstr = false
|
||||
loc_name = new []ptr; loc_reg = new []ptr; loc_ty = new []ptr; loc_mut = new []int
|
||||
brk_lbl = new []ptr; cnt_lbl = new []ptr
|
||||
self_stk = new []ptr
|
||||
|
|
@ -84,6 +85,7 @@ fn emit_program() -> void {
|
|||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
||||
}
|
||||
if g_uses_str { emit_str_prelude() } # @fn_str_eq / @fn_str_concat, after all uses are seen
|
||||
if g_uses_intstr { emit_int_str() } # @fn_int_str, for str(int) in interpolation
|
||||
}
|
||||
|
||||
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell
|
||||
|
|
|
|||
|
|
@ -126,6 +126,12 @@ fn emit_call(e: Node) -> Val {
|
|||
if (name == "quit") { emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
||||
if (name == "len") { return emit_len(e) }
|
||||
if (name == "push") { return emit_push(e) }
|
||||
if (name == "str") { # str(x): int/bool/fixed -> text, a string passes through
|
||||
let a = emit_expr(e.kids[0])
|
||||
if (llty(a.ty) == "ptr") { return a }
|
||||
g_uses_intstr = true
|
||||
return val(emit_bind(("call ptr @fn_int_str(i32 " + (a.code + ")"))), "str")
|
||||
}
|
||||
if (name == "fx") { let a = emit_expr(e.kids[0]); return val(emit_bind(("shl i32 " + (a.code + ", 16"))), "fixed") }
|
||||
if (name == "flr") { let a = emit_expr(e.kids[0]); return val(emit_bind(("ashr i32 " + (a.code + ", 16"))), "int") }
|
||||
if is_intrinsic(name) { return emit_intrinsic(name, e) }
|
||||
|
|
|
|||
|
|
@ -151,3 +151,26 @@ fn emit_str_prelude() -> void {
|
|||
emith(" %j1 = add i32 %j, 1\n br label %c2\n")
|
||||
emith("fin:\n %pe = getelementptr inbounds i8, ptr %out, i32 %sum\n store i8 0, ptr %pe\n ret ptr %out\n}\n")
|
||||
}
|
||||
|
||||
# int -> decimal string, emitted (once) into any program that uses str(int)
|
||||
# (string interpolation of a number). Writes digits from the end of a 24-byte
|
||||
# buffer, prepends '-' for negatives, and returns a pointer into the buffer.
|
||||
fn emit_int_str() -> void {
|
||||
emith("define ptr @fn_int_str(i32 %n0) {\n")
|
||||
emith("entry:\n %buf = call ptr @malloc(i64 24)\n")
|
||||
emith(" %isneg = icmp slt i32 %n0, 0\n %neg = sub i32 0, %n0\n")
|
||||
emith(" %n = select i1 %isneg, i32 %neg, i32 %n0\n")
|
||||
emith(" %term = getelementptr inbounds i8, ptr %buf, i32 23\n store i8 0, ptr %term\n")
|
||||
emith(" %iszero = icmp eq i32 %n0, 0\n br i1 %iszero, label %zc, label %dl\n")
|
||||
emith("zc:\n %zp = getelementptr inbounds i8, ptr %buf, i32 22\n store i8 48, ptr %zp\n ret ptr %zp\n")
|
||||
emith("dl:\n br label %dloop\n")
|
||||
emith("dloop:\n %pos = phi i32 [ 22, %dl ], [ %pos2, %dbody ]\n %cur = phi i32 [ %n, %dl ], [ %cur2, %dbody ]\n")
|
||||
emith(" %done = icmp eq i32 %cur, 0\n br i1 %done, label %sign, label %dbody\n")
|
||||
emith("dbody:\n %d = urem i32 %cur, 10\n %ch = add i32 %d, 48\n %ch8 = trunc i32 %ch to i8\n")
|
||||
emith(" %pp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 %ch8, ptr %pp\n")
|
||||
emith(" %cur2 = udiv i32 %cur, 10\n %pos2 = sub i32 %pos, 1\n br label %dloop\n")
|
||||
emith("sign:\n br i1 %isneg, label %addneg, label %fin\n")
|
||||
emith("addneg:\n %sp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 45, ptr %sp\n %posn = sub i32 %pos, 1\n br label %fin\n")
|
||||
emith("fin:\n %fpos = phi i32 [ %pos, %sign ], [ %posn, %addneg ]\n")
|
||||
emith(" %rpos = add i32 %fpos, 1\n %res = getelementptr inbounds i8, ptr %buf, i32 %rpos\n ret ptr %res\n}\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const TK_OP: int = 3
|
|||
const TK_NL: int = 4
|
||||
const TK_EOF: int = 5
|
||||
const TK_FLOAT: int = 6
|
||||
const TK_INTERP: int = 7 # `text {expr} text` — raw content, split by the parser
|
||||
|
||||
property Tok { kind: int = 0, text: ptr = null, ival: int = 0, line: int = 0 }
|
||||
|
||||
|
|
@ -69,6 +70,16 @@ fn lex(src: ptr) -> void {
|
|||
tok_push(TK_STR, out, 0, line)
|
||||
continue
|
||||
}
|
||||
if c == 96 { # `interpolated string` — captured raw
|
||||
i = i + 1
|
||||
let out = mem_alloc(n)
|
||||
var j = 0
|
||||
while i < n and peek8(src, i) != 96 { poke8(out, j, peek8(src, i)); j = j + 1; i = i + 1 }
|
||||
i = i + 1
|
||||
poke8(out, j, 0)
|
||||
tok_push(TK_INTERP, out, 0, line)
|
||||
continue
|
||||
}
|
||||
if c == 39 { # 'c' char literal -> int
|
||||
i = i + 1
|
||||
var v = 0
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -54,8 +54,64 @@ fn args_call(call: Node) -> void {
|
|||
eat_op(")")
|
||||
}
|
||||
|
||||
# ---- string interpolation --------------------------------------------------
|
||||
# `text {expr} text` desugars to a `+` chain of string literals and `str(expr)`
|
||||
# holes, so it reuses the string-concat operator and needs no new runtime.
|
||||
fn interp_lit(buf: ptr, len: int) -> Node { let n = node(E_STR); n.s = substr(buf, 0, len); return n }
|
||||
fn interp_add(acc: Node, part: Node) -> Node {
|
||||
if acc == null { return part }
|
||||
return mkbin("+", acc, part)
|
||||
}
|
||||
fn interp_str(e: Node) -> Node { # wrap a hole in str(...)
|
||||
let c = node(E_CALL); let id = node(E_ID); id.s = "str"; c.a = id; push(c.kids, e); return c
|
||||
}
|
||||
fn parse_hole(inner: ptr) -> Node { # re-lex+parse an embedded expression
|
||||
let saved_toks = toks; let saved_pi = pi
|
||||
lex(inner); pi = 0; skipnl()
|
||||
let e = expr()
|
||||
toks = saved_toks; pi = saved_pi
|
||||
return e
|
||||
}
|
||||
fn parse_interp(raw: ptr) -> Node {
|
||||
let n = slen(raw)
|
||||
var acc: Node = null
|
||||
let lit = mem_alloc(n + 1)
|
||||
var lj = 0
|
||||
var i = 0
|
||||
while i < n {
|
||||
let c = peek8(raw, i)
|
||||
if c == 123 { # '{'
|
||||
if peek8(raw, i + 1) == 123 { poke8(lit, lj, 123); lj = lj + 1; i = i + 2; continue } # {{ -> {
|
||||
if lj > 0 { acc = interp_add(acc, interp_lit(lit, lj)); lj = 0 }
|
||||
i = i + 1
|
||||
let hs = i
|
||||
var depth = 1
|
||||
while i < n and depth > 0 {
|
||||
let d = peek8(raw, i)
|
||||
if d == 123 { depth = depth + 1 }
|
||||
else { if d == 125 { depth = depth - 1; if depth == 0 { break } } }
|
||||
i = i + 1
|
||||
}
|
||||
acc = interp_add(acc, interp_str(parse_hole(substr(raw, hs, i - hs))))
|
||||
i = i + 1 # skip the closing '}'
|
||||
} else {
|
||||
if c == 125 and peek8(raw, i + 1) == 125 { poke8(lit, lj, 125); lj = lj + 1; i = i + 2; continue } # }} -> }
|
||||
if c == 92 { # backslash escape in literal text
|
||||
let e = peek8(raw, i + 1); var r = e
|
||||
if e == 110 { r = 10 }
|
||||
if e == 116 { r = 9 }
|
||||
poke8(lit, lj, r); lj = lj + 1; i = i + 2; continue
|
||||
}
|
||||
poke8(lit, lj, c); lj = lj + 1; i = i + 1
|
||||
}
|
||||
}
|
||||
if lj > 0 or (acc == null) { acc = interp_add(acc, interp_lit(lit, lj)) }
|
||||
return acc
|
||||
}
|
||||
|
||||
fn p_primary() -> Node {
|
||||
let t = toks[pi]
|
||||
if t.kind == TK_INTERP { pi = pi + 1; return parse_interp(t.text) }
|
||||
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; pi = pi + 1; return n }
|
||||
if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; pi = pi + 1; return n }
|
||||
if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi = pi + 1; return n }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue