ludic/tools/migrate/ludic_ast.py
Orkuncakilkaya cc89fc37a4 feat(lang): strict numbers in float files; render3d on float
A numbers float file adapts decimal literals to a fixed operand or slot, and refuses to
promote a computed int to a float implicitly: there it is almost always float bits. Explicit
float(x) is always allowed.

render3d's numbers are float, converted by tools/migrate/floatbits.py - a whole-program
inference of which ints carried IEEE bits (union-find over flows, calls, returns, buffers,
nested buffers and lexical scopes) and a rewriter to operators, Math.* and float literals,
with float_bits / float_from_bits left only where bits really cross (runtime scratch
buffers, mixed buffers). Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 17:13:25 +03:00

242 lines
8.6 KiB
Python

"""A small Ludic parser for migration tools: tokens with source offsets, statements and
expression trees, enough of the grammar to rewrite expressions in place and know which
declaration each name refers to. It does not check anything; it tolerates what it does not
understand by keeping it as raw text."""
import re
KEYWORDS = {'let', 'var', 'const', 'if', 'else', 'while', 'for', 'in', 'return', 'break', 'continue',
'function', 'handler', 'property', 'model', 'program', 'import', 'event', 'emit', 'not',
'and', 'or', 'new', 'true', 'false', 'null', 'extern', 'spawn', 'enum', 'match', 'fn',
'scene', 'layer', 'ui', 'test', 'namespace', 'entry', 'become', 'numbers', 'on'}
class Tok:
__slots__ = ('k', 't', 's', 'e', 'holes')
def __init__(self, k, t, s, e):
self.k, self.t, self.s, self.e, self.holes = k, t, s, e, None
def __repr__(self):
return f'{self.k}:{self.t!r}'
OPS = ['..', '->', '==', '!=', '<=', '>=', '+=', '-=', '*=', '/=', '%=', '<<', '>>', '&&', '||',
'+', '-', '*', '/', '%', '<', '>', '=', '(', ')', '[', ']', '{', '}', ',', '.', ':', ';',
'@', '&', '|', '^', '!', '?', '~']
NUM_RE = re.compile(r'0x[0-9a-fA-F_]+|\d[\d_]*(\.\d+(?!\.))?')
ID_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_]*')
def tokenize(src, base=0):
"""Tokens of src; offsets are relative to the whole file (base added)."""
toks = []
i, n = 0, len(src)
while i < n:
c = src[i]
if c == '\n':
toks.append(Tok('NL', '\n', base + i, base + i + 1))
i += 1
elif c in ' \t\r':
i += 1
elif c == '#':
j = src.find('\n', i)
j = n if j < 0 else j
i = j
elif c == '"':
j = i + 1
while j < n and src[j] != '"':
j += 2 if src[j] == '\\' else 1
toks.append(Tok('STR', src[i:j + 1], base + i, base + j + 1))
i = j + 1
elif c == "'":
j = i + 1
while j < n and src[j] != "'":
j += 2 if src[j] == '\\' else 1
toks.append(Tok('CHR', src[i:j + 1], base + i, base + j + 1))
i = j + 1
elif c == '`':
j = i + 1
holes = []
while j < n and src[j] != '`':
if src[j] == '{':
d, k = 1, j + 1
while k < n and d:
if src[k] == '{': d += 1
elif src[k] == '}': d -= 1
k += 1
holes.append((j + 1, k - 1))
j = k
else:
j += 1
t = Tok('TMPL', src[i:j + 1], base + i, base + j + 1)
t.holes = [tokenize(src[a:b], base + a) for a, b in holes]
toks.append(t)
i = j + 1
elif c.isdigit():
m = NUM_RE.match(src, i)
toks.append(Tok('NUM', m.group(0), base + i, base + i + len(m.group(0))))
i += len(m.group(0))
elif c.isalpha() or c == '_':
m = ID_RE.match(src, i)
toks.append(Tok('ID', m.group(0), base + i, base + i + len(m.group(0))))
i += len(m.group(0))
else:
for op in OPS:
if src.startswith(op, i):
toks.append(Tok('OP', op, base + i, base + i + len(op)))
i += len(op)
break
else:
toks.append(Tok('OP', c, base + i, base + i + 1))
i += 1
return toks
class Node:
"""kind: num str chr tmpl id call member index slice unary bin paren list new fnref raw named"""
__slots__ = ('kind', 'val', 'kids', 's', 'e', 'fn', 'sym', 'tmpl_holes')
def __init__(self, kind, val, kids, s, e):
self.kind, self.val, self.kids, self.s, self.e = kind, val, kids, s, e
self.fn = None
self.sym = None
self.tmpl_holes = None
PREC = {'or': 1, '||': 1, 'and': 2, '&&': 2, '==': 3, '!=': 3, '<': 3, '<=': 3, '>': 3, '>=': 3,
'..': 4, '|': 5, '^': 6, '&': 7, '<<': 8, '>>': 8, '+': 9, '-': 9, '*': 10, '/': 10, '%': 10}
class ExprParser:
def __init__(self, toks, i=0):
self.t, self.i = toks, i
def peek(self, o=0):
j = self.i + o
return self.t[j] if j < len(self.t) else Tok('EOF', '', -1, -1)
def eat(self):
tk = self.peek()
self.i += 1
return tk
def is_op(self, v):
tk = self.peek()
return tk.k == 'OP' and tk.t == v
def binop(self):
tk = self.peek()
if tk.k == 'OP' and tk.t in PREC: return tk.t
if tk.k == 'ID' and tk.t in ('and', 'or'): return tk.t
return None
def expr(self, minp=0):
left = self.unary()
while True:
op = self.binop()
if op is None or PREC[op] < minp:
return left
self.eat()
while self.peek().k == 'NL': self.eat()
right = self.expr(PREC[op] + 1)
left = Node('bin', op, [left, right], left.s, right.e)
def unary(self):
tk = self.peek()
if tk.k == 'OP' and tk.t in ('-', '!', '~'):
self.eat()
x = self.unary()
return Node('unary', tk.t, [x], tk.s, x.e)
if tk.k == 'ID' and tk.t == 'not':
self.eat()
x = self.unary()
return Node('unary', 'not', [x], tk.s, x.e)
return self.postfix(self.primary())
def primary(self):
tk = self.eat()
if tk.k == 'NUM': return Node('num', tk.t, [], tk.s, tk.e)
if tk.k == 'STR': return Node('str', tk.t, [], tk.s, tk.e)
if tk.k == 'CHR': return Node('chr', tk.t, [], tk.s, tk.e)
if tk.k == 'TMPL':
n = Node('tmpl', tk.t, [], tk.s, tk.e)
for ht in tk.holes:
if ht:
p = ExprParser(ht)
try:
n.kids.append(p.expr())
except Exception:
pass
return n
if tk.k == 'OP' and tk.t == '(':
x = self.expr()
end = self.eat()
return Node('paren', None, [x], tk.s, end.e)
if tk.k == 'OP' and tk.t == '[':
kids = []
while not self.is_op(']') and self.peek().k != 'EOF':
if self.peek().k == 'NL':
self.eat()
continue
kids.append(self.expr())
if self.is_op(','): self.eat()
end = self.eat()
return Node('list', None, kids, tk.s, end.e)
if tk.k == 'ID' and tk.t == 'new':
s = tk.s
ty = ''
while self.is_op('[') or self.is_op(']'): ty += self.eat().t
nm = self.eat()
return Node('new', ty + nm.t, [], s, nm.e)
if tk.k == 'ID' and tk.t == 'fn' and self.peek().k == 'ID':
nm = self.eat()
return Node('fnref', nm.t, [], tk.s, nm.e)
if tk.k == 'ID': return Node('id', tk.t, [], tk.s, tk.e)
if tk.k == 'EOF': raise SyntaxError('end of input')
return Node('raw', tk.t, [], tk.s, tk.e)
def postfix(self, x):
while True:
if self.is_op('('):
self.eat()
args = []
while not self.is_op(')') and self.peek().k != 'EOF':
if self.peek().k == 'NL':
self.eat()
continue
if self.peek().k == 'ID' and self.peek(1).k == 'OP' and self.peek(1).t == ':':
nm = self.eat()
self.eat()
v = self.expr()
args.append(Node('named', nm.t, [v], nm.s, v.e))
else:
args.append(self.expr())
if self.is_op(','): self.eat()
end = self.eat()
n = Node('call', None, [x] + args, x.s, end.e)
x = n
elif self.is_op('.') and self.peek(1).k == 'ID':
self.eat()
nm = self.eat()
x = Node('member', nm.t, [x], x.s, nm.e)
elif self.is_op('['):
self.eat()
a = self.expr()
if self.is_op('..'):
self.eat()
b = self.expr()
end = self.eat()
x = Node('slice', None, [x, a, b], x.s, end.e)
else:
end = self.eat()
x = Node('index', None, [x, a], x.s, end.e)
else:
return x
def walk(n):
yield n
for k in n.kids:
yield from walk(k)