fix(lang): a template inside another's hole, and integer literals past 2^31 - 1

A template literal nested in a {...} hole crashed the parser: the outer
literal ended at the inner backtick. The lexer (and ludic-fmt's) now
reads a hole as code, taking strings, chars and templates in it whole.
A decimal literal past 2147483647, or a hex one of more than eight
digits, was wrapped into a negative int; it is a long with its value
now, and giving one to an int is refused. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 08:10:46 +03:00
parent b401f7acc1
commit afa4d23a4f
17 changed files with 47107 additions and 46056 deletions

View file

@ -1764,13 +1764,19 @@ let msg = `hello {name}, you have {count + 1} messages`
# == "hello " + name + ", you have " + str(count + 1) + " messages" # == "hello " + name + ", you have " + str(count + 1) + " messages"
``` ```
`str(x)` is the same conversion on its own. Write a literal brace as `{{` / `}}`. `str(x)` is the same conversion on its own. Write a literal brace as `{{` / `}}`. A hole is code,
so a string, a char or another template literal inside it is taken whole - `` `a {wrap(`b {n}`)} c` ``
is one literal, and a brace or a backtick inside a string in a hole is text.
**Slicing.** `s[a..b]` is a fresh substring of the bytes `[a, b)`, and `len(s)` **Slicing.** `s[a..b]` is a fresh substring of the bytes `[a, b)`, and `len(s)`
is a string's byte length — so `path[0..len(path) - 6]` trims an extension and is a string's byte length — so `path[0..len(path) - 6]` trims an extension and
`s[i]` still indexes a single byte. `expr with { field: … }` `s[i]` still indexes a single byte. `expr with { field: … }`
is not implemented; records appear only in `spawn`. Char literals (`'w'`) are is not implemented; records appear only in `spawn`. Char literals (`'w'`) are
`int` code points; colors are hex ints (`0xff8800`). `null` is the null-pointer `int` code points; colors are hex ints (`0xff8800`). An integer literal past `2147483647` is a
`long` and keeps its value (`let mask: long = 4294967295`); given to an `int` it is refused (`n wants
an int and 4294967295 does not fit one; it is a long`). A hex literal of up to eight digits is a
32-bit pattern - `0xFFFFFFFF` is `-1`, and `0xEDB88320` given to a `long` is negative - and one of
more digits is a `long` (`0x10000000000`). `null` is the null-pointer
literal; test any pointer/record/slice with `x == null` / `x != null` (an unset literal; test any pointer/record/slice with `x == null` / `x != null` (an unset
`Node`/`ptr` field reads back as `null`). `Node`/`ptr` field reads back as `null`).

View file

@ -0,0 +1,8 @@
bump: patch
type: fix
**A template literal inside another's `{…}` hole, and integer literals past 2^31 - 1.** The first
crashed the compiler (the outer literal ended at the inner one's backtick); a hole is now read as
code, so strings, chars and templates inside it are taken whole. The second was read as a wrapped
negative int; a decimal literal past `2147483647`, or a hex one of more than eight digits, is now a
`long` with its value, and giving one to an `int` is refused. Eight hex digits or fewer are still a
32-bit pattern.

View file

@ -0,0 +1,16 @@
# long_literals.ludic — a decimal literal past 2^31 - 1 is a long and keeps its value; so is a hex
# literal of more than eight digits (past 2^63 it wraps, as the bits say). Eight hex digits or
# fewer stay a 32-bit pattern (0xFFFFFFFF is -1, as it always was).
#
# Running it prints: 4294967295 3988292384 -4294967296 1099511627776 -3750763034362895579 -1 1
program LongLiterals {
const MASK: long = 4294967295
var poly: long = 3988292384
function neg() -> long { return -4294967296 }
entry {
let big = 0x10000000000
let fnv = 0xcbf29ce484222325
let small = 0xFFFFFFFF
print(`{MASK} {poly} {neg()} {big} {fnv} {small} {MASK > long(2147483647)}`)
}
}

View file

@ -0,0 +1,12 @@
# nested_templates.ludic — a template literal inside another's hole is one expression: the outer
# literal ends at its own backtick, not at the inner one's. A brace or a backtick inside a string in
# a hole is text, and {{ }} are still literal braces.
#
# Running it prints: outer [inner 3 {x}] "`}" end
program NestedTemplates {
function wrap(s: string) -> string { return `[{s}]` }
entry {
let n = 3
print(`outer {wrap(`inner {n} {{x}}`)} {"\"`}\""} end`)
}
}

View file

@ -0,0 +1,7 @@
# a literal past 2^31 - 1 is a long, and a long does not narrow into an int
program LongIntoInt {
entry {
let n: int = 4294967295
print(n)
}
}

View file

@ -985,7 +985,10 @@ function emit_call(e: Node) -> Val {
function emit_expr(e: Node) -> Val { function emit_expr(e: Node) -> Val {
if (e == null) { return val("0", "int") } if (e == null) { return val("0", "int") }
if e.kind == E_INT { return val(itoa(e.ival), "int") } if e.kind == E_INT {
if e.s != null { return val(e.s, "long") } # a literal past 2^31 - 1
return val(itoa(e.ival), "int")
}
if e.kind == E_FLOAT and is_float_file(e.file) { if e.kind == E_FLOAT and is_float_file(e.file) {
let ff = val(fp_lit_code(e.s, "float"), "float") let ff = val(fp_lit_code(e.s, "float"), "float")
ff.lit = e ff.lit = e

View file

@ -360,7 +360,10 @@ function static_type(e: Node) -> pointer {
if (et != null) { return "[]" + et } if (et != null) { return "[]" + et }
} }
if e.kind == E_STR or e.kind == E_SLICE { return "string" } if e.kind == E_STR or e.kind == E_SLICE { return "string" }
if e.kind == E_INT { return "int" } if e.kind == E_INT {
if e.s != null { return "long" }
return "int"
}
if e.kind == E_FLOAT and is_float_file(e.file) { return "float" } if e.kind == E_FLOAT and is_float_file(e.file) { return "float" }
if e.kind == E_FLOAT { return "fixed" } if e.kind == E_FLOAT { return "fixed" }
if e.kind == E_BOOL { return "bool" } if e.kind == E_BOOL { return "bool" }

View file

@ -39,7 +39,7 @@ function fp_result(a: pointer, b: pointer) -> pointer {
# a literal-only expression (Val.lit) evaluated exactly in float type t # a literal-only expression (Val.lit) evaluated exactly in float type t
function fp_const(e: Node, t: pointer) -> pointer { function fp_const(e: Node, t: pointer) -> pointer {
if e.kind == E_FLOAT { return fp_lit_code(e.s, t) } if e.kind == E_FLOAT { return fp_lit_code(e.s, t) }
if e.kind == E_INT { return fp_lit_code(itoa(e.ival), t) } if e.kind == E_INT { return fp_lit_code(int_lit_code(e), t) }
if e.kind == E_UN { if e.kind == E_UN {
let x = fp_const(e.a, t) let x = fp_const(e.a, t)
return emit_bind(`fneg {t} {x}`) return emit_bind(`fneg {t} {x}`)

View file

@ -30,12 +30,18 @@ function emit_escaped(s: pointer) -> void {
} }
} }
# an int literal's LLVM constant: its value, or a long literal's own digits
function int_lit_code(e: Node) -> pointer {
if e.s != null { return e.s }
return itoa(e.ival)
}
# the constant initializer for a global var: a literal, or 0/null # the constant initializer for a global var: a literal, or 0/null
function global_init(d: Node) -> pointer { function global_init(d: Node) -> pointer {
if (d.a == null) or is_fp(d.ty) { return zero_of(llty(d.ty)) } # a float's value is set by L_init_globals if (d.a == null) or is_fp(d.ty) { return zero_of(llty(d.ty)) } # a float's value is set by L_init_globals
let e = d.a let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) } if e.kind == E_INT { return int_lit_code(e) }
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return (("-") + itoa(e.a.ival)) } if e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return (("-") + int_lit_code(e.a)) }
if e.kind == E_MEMBER and e.a.kind == E_ID { # `Enum.Variant` is a compile-time int if e.kind == E_MEMBER and e.a.kind == E_ID { # `Enum.Variant` is a compile-time int
let ord = enum_ordinal(e.a.s, e.s) let ord = enum_ordinal(e.a.s, e.s)
if ord >= 0 { return itoa(ord) } if ord >= 0 { return itoa(ord) }

View file

@ -51,6 +51,11 @@ function ck_err(cat: pointer, n: Node, msg: pointer) -> void {
} }
# a value given where a type is wanted: `what` says where ("argument 2 of f", "x") # a value given where a type is wanted: `what` says where ("argument 2 of f", "x")
function ck_give(to: pointer, from: pointer, e: Node, what: pointer) -> void { function ck_give(to: pointer, from: pointer, e: Node, what: pointer) -> void {
# a literal past 2^31 - 1 given to an int would lose its value: say so, rather than wrap it
if e != null and e.kind == E_INT and e.s != null and ((to == "int") or (to == "byte") or (to == "fixed")) {
ck_err("literal", e, `{what} wants {ck_a(to)} and {e.s} does not fit one; it is a long`)
return
}
let cat = ck_mismatch(to, from, e) let cat = ck_mismatch(to, from, e)
if cat == null { return } if cat == null { return }
if (cat == "slice-pointer") { if (cat == "slice-pointer") {

View file

@ -32,7 +32,10 @@ function ck_expr(e: Node) -> pointer {
let ex = ck_expect let ex = ck_expect
ck_expect = null ck_expect = null
let k = e.kind let k = e.kind
if k == E_INT { return "int" } if k == E_INT {
if e.s != null { return "long" } # a literal past 2^31 - 1
return "int"
}
if k == E_FLOAT { if k == E_FLOAT {
if is_float_file(e.file) { return "float" } if is_float_file(e.file) { return "float" }
return "fixed" return "fixed"

View file

@ -60,6 +60,41 @@ function lex_error(line: int, c: int) -> void {
lex_fail(line, `unexpected character '{ch}' (byte {c})`) lex_fail(line, `unexpected character '{ch}' (byte {c})`)
} }
# the closing backtick of the template literal that opens at s[i]: a hole's `{...}` is code, so a
# string, a char or another template inside it is skipped whole - `a {f(`b {c}`)} d` is one literal
function tmpl_end(s: pointer, i0: int, n: int) -> int {
var i = i0 + 1
while i < n and s[i] != '`' {
if s[i] == CH_BACKSLASH { i += 2; continue }
if s[i] == '{' and i + 1 < n and s[i + 1] == '{' { i += 2; continue }
if s[i] == '{' {
i = hole_end(s, i + 1, n)
}
i += 1
}
return i
}
# the `}` that closes a hole whose code starts at s[i]; n when there is none
function hole_end(s: pointer, i0: int, n: int) -> int {
var i = i0
var depth = 1
while i < n {
let d = s[i]
if d == '"' or d == CH_SQUOTE {
i += 1
while i < n and s[i] != d { if s[i] == CH_BACKSLASH { i += 1 }; i += 1 }
}
else if d == '`' { i = tmpl_end(s, i, n) }
else if d == '{' { depth += 1 }
else if d == '}' {
depth -= 1
if depth == 0 { return i }
}
i += 1
}
return n
}
# report a lexical error in the standard `file:line: error: msg` shape and stop # report a lexical error in the standard `file:line: error: msg` shape and stop
function lex_fail(line: int, msg: pointer) -> void { function lex_fail(line: int, msg: pointer) -> void {
let m = `{g_parse_file}:{line}: error: {msg}\n` let m = `{g_parse_file}:{line}: error: {msg}\n`
@ -100,11 +135,16 @@ function lex_at(src: pointer, first_line: int) -> void {
continue continue
} }
if c == '`' { # `interpolated string` — captured raw if c == '`' { # `interpolated string` — captured raw
i += 1 let e = tmpl_end(src, i, n)
let out = bytes(n) let out = bytes(n)
var j = 0 var j = 0
while i < n and src[i] != '`' { out[j] = src[i]; j += 1; i += 1 }
i += 1 i += 1
while i < e {
out[j] = src[i]
j += 1
i += 1
}
i = e + 1
out[j] = 0 out[j] = 0
tok_push(TK_INTERP, out, 0, line) tok_push(TK_INTERP, out, 0, line)
continue continue
@ -122,6 +162,8 @@ function lex_at(src: pointer, first_line: int) -> void {
if char_is_digit(c) { if char_is_digit(c) {
if c == '0' and src[i + 1] == 'x' { # 0x hex if c == '0' and src[i + 1] == 'x' { # 0x hex
var v = 0 var v = 0
var lv: long = 0
var digits = 0
i += 2 i += 2
while i < n { while i < n {
let h = src[i] let h = src[i]
@ -130,14 +172,24 @@ function lex_at(src: pointer, first_line: int) -> void {
else { if h >= 'a' and h <= 'f' { d = h - 87 } else { if h >= 'a' and h <= 'f' { d = h - 87 }
else { if h >= 'A' and h <= 'F' { d = h - 55 } else { break } } } else { if h >= 'A' and h <= 'F' { d = h - 55 } else { break } } }
v = v * 16 + d v = v * 16 + d
lv = lv * long(16) + long(d)
if digits > 0 or d > 0 { digits += 1 }
i += 1 i += 1
} }
tok_push(TK_INT, null, v, line) # up to 8 hex digits is a 32-bit pattern (0xFFFFFFFF is -1, as it always was); more is a long
var big: pointer = null
if digits > 8 { big = string(lv) }
tok_push(TK_INT, big, v, line)
continue continue
} }
var v = 0 var v = 0
var lv: long = 0
let nstart = i let nstart = i
while i < n and char_is_digit(src[i]) { v = v * 10 + (src[i] - 48); i += 1 } while i < n and char_is_digit(src[i]) {
v = v * 10 + (src[i] - 48)
lv = lv * long(10) + long(src[i] - 48)
i += 1
}
# a fractional part makes it a Q16.16 fixed literal (its text is kept: in a # a fractional part makes it a Q16.16 fixed literal (its text is kept: in a
# float context the literal is exactly that decimal instead) # float context the literal is exactly that decimal instead)
if i < n and src[i] == '.' and char_is_digit(src[i + 1]) { if i < n and src[i] == '.' and char_is_digit(src[i + 1]) {
@ -154,7 +206,10 @@ function lex_at(src: pointer, first_line: int) -> void {
tok_push(TK_FLOAT, src[nstart..i], bits, line) tok_push(TK_FLOAT, src[nstart..i], bits, line)
continue continue
} }
tok_push(TK_INT, null, v, line) # a decimal past 2^31 - 1 does not fit an int: it is a long, and keeps its value as text
var big: pointer = null
if lv > long(2147483647) { big = string(lv) }
tok_push(TK_INT, big, v, line)
continue continue
} }
if char_is_alpha(c) { if char_is_alpha(c) {

View file

@ -199,19 +199,9 @@ function parse_interp(raw: pointer) -> Node {
if lj > 0 { acc = interp_add(acc, interp_lit(lit, lj)); lj = 0 } if lj > 0 { acc = interp_add(acc, interp_lit(lit, lj)); lj = 0 }
i += 1 i += 1
let hs = i let hs = i
# find the hole's closing brace: braces nest, and a brace inside a # find the hole's closing brace: braces nest, and a brace inside a "string", a 'char' or a
# "string" or 'char' literal in the hole is text, not structure # `template` in the hole is text, not structure (lex.ludic, hole_end)
var depth = 1 i = hole_end(raw, i, n)
while i < n and depth > 0 {
let d = raw[i]
if d == '"' or d == CH_SQUOTE {
i += 1
while i < n and raw[i] != d { if raw[i] == CH_BACKSLASH { i += 1 }; i += 1 }
}
else if d == '{' { depth += 1 }
else if d == '}' { depth -= 1; if depth == 0 { break } }
i += 1
}
acc = interp_add(acc, interp_str(parse_hole(raw[hs..i]))) acc = interp_add(acc, interp_str(parse_hole(raw[hs..i])))
i += 1 # skip the closing '}' i += 1 # skip the closing '}'
} else { } else {
@ -259,7 +249,7 @@ function p_primary() -> Node {
if t.kind == TK_INTERP { pi += 1; g_interp_line = t.line; return parse_interp(t.text) } if t.kind == TK_INTERP { pi += 1; g_interp_line = t.line; return parse_interp(t.text) }
if is_op("[") { return parse_list() } if is_op("[") { return parse_list() }
if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() } if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() }
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; pi += 1; return n } if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; n.s = t.text; pi += 1; return n } # s: a long literal's digits
if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; n.s = t.text; pi += 1; return n } if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; n.s = t.text; pi += 1; return n }
if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi += 1; return n } if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi += 1; return n }
if t.kind == TK_ID { if t.kind == TK_ID {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -835,6 +835,9 @@ function cmd_dev_test() -> int {
reject_case("rejected/generic_mismatch", "words wants a Pool<string> and this is a Pool<int>", "two instances of one generic are two types") reject_case("rejected/generic_mismatch", "words wants a Pool<string> and this is a Pool<int>", "two instances of one generic are two types")
reject_case("rejected/generic_arity", "Pool takes 1 type argument(s) and Pool<int, string> gives 2", "a generic takes as many type arguments as it has parameters") reject_case("rejected/generic_arity", "Pool takes 1 type argument(s) and Pool<int, string> gives 2", "a generic takes as many type arguments as it has parameters")
reject_case("rejected/private_generic", "grow is private to module kit", "an instance of a private generic is private to its module") 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/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)") 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)")
reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target") reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target")
reject_case("rejected/alias_twice", "Trail.length is declared twice", "a namespace method is declared once") reject_case("rejected/alias_twice", "Trail.length is declared twice", "a namespace method is declared once")

View file

@ -136,6 +136,38 @@ program LudicFmt {
return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~ return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~
} }
# the backtick that closes the template opening at s[i0] (or the end): a `{...}` hole is code, so
# a string, a char or another template inside it is skipped whole - as the compiler reads it
function fmt_tmpl_end(s: pointer, i0: int) -> int {
var i = i0 + 1
while s[i] != 0 and s[i] != '`' {
if s[i] == '\\' and s[i + 1] != 0 { i += 2; continue }
if s[i] == '{' and s[i + 1] == '{' { i += 2; continue }
if s[i] == '{' { i = fmt_hole_end(s, i + 1) }
if s[i] != 0 { i += 1 }
}
return i
}
function fmt_hole_end(s: pointer, i0: int) -> int {
var i = i0
var depth = 1
while s[i] != 0 {
let d = s[i]
if d == '"' or d == '\'' {
i += 1
while s[i] != 0 and s[i] != d { if s[i] == '\\' and s[i + 1] != 0 { i += 1 }; i += 1 }
}
else if d == '`' { i = fmt_tmpl_end(s, i) }
else if d == '{' { depth += 1 }
else if d == '}' {
depth -= 1
if depth == 0 { return i }
}
if s[i] != 0 { i += 1 }
}
return i
}
function lex(s: pointer) -> void { function lex(s: pointer) -> void {
src = s src = s
tk_kind = new []int; tk_start = new []int; tk_end = new []int; tk_line = new []int tk_kind = new []int; tk_start = new []int; tk_end = new []int; tk_line = new []int
@ -155,9 +187,9 @@ program LudicFmt {
if s[i] == '"' { i += 1 } if s[i] == '"' { i += 1 }
push_tok(LT_STR, st, i, line); continue push_tok(LT_STR, st, i, line); continue
} }
if c == '`' { # `interpolated` if c == '`' { # `interpolated`, holes and all
let st = i; i += 1 let st = i
while s[i] != 0 and s[i] != '`' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } } i = fmt_tmpl_end(s, i)
if s[i] == '`' { i += 1 } if s[i] == '`' { i += 1 }
push_tok(LT_STR, st, i, line); continue push_tok(LT_STR, st, i, line); continue
} }