13948f57a6
The El compiler self-host has been broken since `fn main()` landed in
compiler.el. Both bootstrap.py and codegen.el skipped emitting an
`el_val_t main()` (correct - it would collide with C's int main),
but neither folded the body anywhere. The C int main() got just
runtime init + return, so any El program that put its work inside
`fn main()` produced a binary that did nothing.
Fix in two places (bootstrap.py and codegen.el, kept symmetric):
1. Capture the body of `fn main()` during the FnDef pass.
2. Emit `int main(int _argc, char** _argv)` so El programs can
declare their own local `argv` / `argc` (compiler.el itself
does this) without colliding.
3. After top-level statements, fold the captured fn main body
into C main alongside them, then return 0.
Self-host fixed point reached: gen 2 and gen 3 of compiler.el's
output are byte-identical (md5 5b4eca2a...). The new elc compiles
products/web/src/main.el natively now - 24 imports resolved, 1,173
lines of C, every imported function (page_open, nav, pricing,
checkout_page, account_page, founding_badge…) emits its forward
decl + body without a concat preprocessor in sight.
Backup of the prior self-hosted binary is at
dist/platform/elc.preselfhost in case we need to fall back.
1490 lines
52 KiB
Python
1490 lines
52 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
bootstrap.py — Python bootstrap compiler for the El language.
|
|
|
|
Reads an El source file and emits C source code to stdout, following exactly
|
|
the same codegen patterns as elc (the self-hosting El compiler).
|
|
|
|
Usage:
|
|
python3 bootstrap.py <source.el> > output.c
|
|
|
|
This is a clean-room implementation of the El lexer, parser, and C codegen
|
|
in Python. It handles the full El grammar as specified in spec/language.md
|
|
and as observed from the elc-combined.el source and the soul.c output.
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
|
|
# ─── Lexer ────────────────────────────────────────────────────────────────────
|
|
|
|
KEYWORDS = {
|
|
'let', 'fn', 'type', 'enum', 'match', 'return',
|
|
'if', 'else', 'for', 'in', 'while', 'import', 'from', 'as',
|
|
'with', 'sealed', 'activate', 'where', 'test', 'seed', 'assert',
|
|
'protocol', 'impl', 'retry', 'times', 'fallback', 'reason',
|
|
'parallel', 'trace', 'requires', 'deploy', 'to', 'via', 'target',
|
|
'true', 'false', 'cgi', 'service', 'manager', 'engine', 'accessor',
|
|
'vessel',
|
|
}
|
|
|
|
KEYWORD_TOKEN = {
|
|
'let': 'Let', 'fn': 'Fn', 'type': 'Type', 'enum': 'Enum',
|
|
'match': 'Match', 'return': 'Return', 'if': 'If', 'else': 'Else',
|
|
'for': 'For', 'in': 'In', 'while': 'While', 'import': 'Import',
|
|
'from': 'From', 'as': 'As', 'with': 'With', 'sealed': 'Sealed',
|
|
'activate': 'Activate', 'where': 'Where', 'test': 'Test',
|
|
'seed': 'Seed', 'assert': 'Assert', 'protocol': 'Protocol',
|
|
'impl': 'Impl', 'retry': 'Retry', 'times': 'Times',
|
|
'fallback': 'Fallback', 'reason': 'Reason', 'parallel': 'Parallel',
|
|
'trace': 'Trace', 'requires': 'Requires', 'deploy': 'Deploy',
|
|
'to': 'To', 'via': 'Via', 'target': 'Target',
|
|
'true': 'Bool', 'false': 'Bool',
|
|
'cgi': 'Cgi', 'service': 'Service',
|
|
'manager': 'Manager', 'engine': 'Engine', 'accessor': 'Accessor',
|
|
'vessel': 'Vessel',
|
|
}
|
|
|
|
|
|
class Token:
|
|
def __init__(self, kind, value, line=0):
|
|
self.kind = kind
|
|
self.value = value
|
|
self.line = line
|
|
|
|
def __repr__(self):
|
|
return f'Token({self.kind!r}, {self.value!r})'
|
|
|
|
|
|
def lex(source):
|
|
tokens = []
|
|
i = 0
|
|
n = len(source)
|
|
line = 1
|
|
|
|
while i < n:
|
|
c = source[i]
|
|
|
|
# Whitespace
|
|
if c in ' \t\r\n':
|
|
if c == '\n':
|
|
line += 1
|
|
i += 1
|
|
continue
|
|
|
|
# Single-line comment
|
|
if c == '/' and i + 1 < n and source[i + 1] == '/':
|
|
while i < n and source[i] != '\n':
|
|
i += 1
|
|
continue
|
|
|
|
# String literal
|
|
if c == '"':
|
|
i += 1
|
|
s = []
|
|
while i < n and source[i] != '"':
|
|
if source[i] == '\\' and i + 1 < n:
|
|
e = source[i + 1]
|
|
if e == 'n':
|
|
s.append('\n')
|
|
elif e == 't':
|
|
s.append('\t')
|
|
elif e == 'r':
|
|
s.append('\r')
|
|
elif e == '"':
|
|
s.append('"')
|
|
elif e == '\\':
|
|
s.append('\\')
|
|
else:
|
|
s.append(e)
|
|
i += 2
|
|
else:
|
|
if source[i] == '\n':
|
|
line += 1
|
|
s.append(source[i])
|
|
i += 1
|
|
i += 1 # closing quote
|
|
tokens.append(Token('Str', ''.join(s), line))
|
|
continue
|
|
|
|
# Number
|
|
if c.isdigit():
|
|
j = i
|
|
while i < n and source[i].isdigit():
|
|
i += 1
|
|
if i < n and source[i] == '.' and i + 1 < n and source[i + 1].isdigit():
|
|
i += 1
|
|
while i < n and source[i].isdigit():
|
|
i += 1
|
|
tokens.append(Token('Float', source[j:i], line))
|
|
else:
|
|
tokens.append(Token('Int', source[j:i], line))
|
|
continue
|
|
|
|
# Identifier or keyword
|
|
if c.isalpha() or c == '_':
|
|
j = i
|
|
while i < n and (source[i].isalnum() or source[i] == '_'):
|
|
i += 1
|
|
word = source[j:i]
|
|
kind = KEYWORD_TOKEN.get(word)
|
|
if kind:
|
|
tokens.append(Token(kind, word, line))
|
|
else:
|
|
tokens.append(Token('Ident', word, line))
|
|
continue
|
|
|
|
# Two-char operators first
|
|
if i + 1 < n:
|
|
two = source[i:i+2]
|
|
if two == '==':
|
|
tokens.append(Token('EqEq', '==', line)); i += 2; continue
|
|
if two == '!=':
|
|
tokens.append(Token('NotEq', '!=', line)); i += 2; continue
|
|
if two == '<=':
|
|
tokens.append(Token('LtEq', '<=', line)); i += 2; continue
|
|
if two == '>=':
|
|
tokens.append(Token('GtEq', '>=', line)); i += 2; continue
|
|
if two == '&&':
|
|
tokens.append(Token('And', '&&', line)); i += 2; continue
|
|
if two == '||':
|
|
tokens.append(Token('Or', '||', line)); i += 2; continue
|
|
if two == '|>':
|
|
tokens.append(Token('PipeOp', '|>', line)); i += 2; continue
|
|
if two == '->':
|
|
tokens.append(Token('Arrow', '->', line)); i += 2; continue
|
|
if two == '=>':
|
|
tokens.append(Token('FatArrow', '=>', line)); i += 2; continue
|
|
if two == '::':
|
|
tokens.append(Token('ColonColon', '::', line)); i += 2; continue
|
|
# Single & — consumed and discarded
|
|
if two == '&&':
|
|
tokens.append(Token('And', '&&', line)); i += 2; continue
|
|
|
|
# Single-char operators
|
|
if c == '&':
|
|
i += 1; continue # silently consumed
|
|
if c == '|':
|
|
tokens.append(Token('Pipe', '|', line)); i += 1; continue
|
|
if c == '=':
|
|
tokens.append(Token('Eq', '=', line)); i += 1; continue
|
|
if c == '<':
|
|
tokens.append(Token('Lt', '<', line)); i += 1; continue
|
|
if c == '>':
|
|
tokens.append(Token('Gt', '>', line)); i += 1; continue
|
|
if c == '+':
|
|
tokens.append(Token('Plus', '+', line)); i += 1; continue
|
|
if c == '-':
|
|
tokens.append(Token('Minus', '-', line)); i += 1; continue
|
|
if c == '*':
|
|
tokens.append(Token('Star', '*', line)); i += 1; continue
|
|
if c == '/':
|
|
tokens.append(Token('Slash', '/', line)); i += 1; continue
|
|
if c == '%':
|
|
tokens.append(Token('Percent', '%', line)); i += 1; continue
|
|
if c == '!':
|
|
tokens.append(Token('Not', '!', line)); i += 1; continue
|
|
if c == '(':
|
|
tokens.append(Token('LParen', '(', line)); i += 1; continue
|
|
if c == ')':
|
|
tokens.append(Token('RParen', ')', line)); i += 1; continue
|
|
if c == '{':
|
|
tokens.append(Token('LBrace', '{', line)); i += 1; continue
|
|
if c == '}':
|
|
tokens.append(Token('RBrace', '}', line)); i += 1; continue
|
|
if c == '[':
|
|
tokens.append(Token('LBracket', '[', line)); i += 1; continue
|
|
if c == ']':
|
|
tokens.append(Token('RBracket', ']', line)); i += 1; continue
|
|
if c == ',':
|
|
tokens.append(Token('Comma', ',', line)); i += 1; continue
|
|
if c == '.':
|
|
tokens.append(Token('Dot', '.', line)); i += 1; continue
|
|
if c == ';':
|
|
tokens.append(Token('Semicolon', ';', line)); i += 1; continue
|
|
if c == ':':
|
|
tokens.append(Token('Colon', ':', line)); i += 1; continue
|
|
if c == '@':
|
|
tokens.append(Token('At', '@', line)); i += 1; continue
|
|
if c == '?':
|
|
tokens.append(Token('QuestionMark', '?', line)); i += 1; continue
|
|
|
|
# Unknown — skip
|
|
i += 1
|
|
|
|
tokens.append(Token('Eof', '', line))
|
|
return tokens
|
|
|
|
|
|
# ─── Parser ───────────────────────────────────────────────────────────────────
|
|
|
|
class ParseError(Exception):
|
|
pass
|
|
|
|
|
|
class Parser:
|
|
def __init__(self, tokens):
|
|
self.tokens = tokens
|
|
self.pos = 0
|
|
|
|
def peek(self):
|
|
return self.tokens[self.pos]
|
|
|
|
def peek_kind(self):
|
|
return self.tokens[self.pos].kind
|
|
|
|
def advance(self):
|
|
t = self.tokens[self.pos]
|
|
self.pos += 1
|
|
return t
|
|
|
|
def expect(self, kind):
|
|
t = self.advance()
|
|
if t.kind != kind:
|
|
raise ParseError(
|
|
f'Line {t.line}: expected {kind!r}, got {t.kind!r} ({t.value!r})'
|
|
)
|
|
return t
|
|
|
|
def eat(self, kind):
|
|
if self.peek_kind() == kind:
|
|
return self.advance()
|
|
return None
|
|
|
|
# ── Type expression (parse and discard) ───────────────────────────────────
|
|
|
|
def parse_type(self):
|
|
"""Parse a type expression, returning it (unused in codegen)."""
|
|
k = self.peek_kind()
|
|
name = ''
|
|
if k == 'LBracket':
|
|
self.advance()
|
|
self.parse_type()
|
|
self.expect('RBracket')
|
|
name = 'Array'
|
|
elif k == 'Ident' or k in KEYWORD_TOKEN.values():
|
|
t = self.advance()
|
|
name = t.value
|
|
# Generic: Map<K, V>
|
|
if self.peek_kind() == 'Lt':
|
|
self.advance()
|
|
self.parse_type()
|
|
while self.peek_kind() == 'Comma':
|
|
self.advance()
|
|
self.parse_type()
|
|
self.expect('Gt')
|
|
else:
|
|
t = self.advance()
|
|
name = t.value
|
|
# Optional suffix
|
|
while self.peek_kind() == 'QuestionMark':
|
|
self.advance()
|
|
name += '?'
|
|
return name
|
|
|
|
# ── Statement ─────────────────────────────────────────────────────────────
|
|
|
|
def parse_program(self):
|
|
stmts = []
|
|
while self.peek_kind() != 'Eof':
|
|
s = self.parse_stmt()
|
|
if s is not None:
|
|
stmts.append(s)
|
|
return stmts
|
|
|
|
def parse_stmt(self):
|
|
k = self.peek_kind()
|
|
|
|
if k == 'Let':
|
|
return self.parse_let()
|
|
if k == 'Fn':
|
|
return self.parse_fn(decorator=None)
|
|
if k == 'Return':
|
|
return self.parse_return()
|
|
if k == 'While':
|
|
return self.parse_while()
|
|
if k == 'For':
|
|
return self.parse_for()
|
|
if k == 'If':
|
|
expr = self.parse_if()
|
|
return {'stmt': 'Expr', 'value': expr}
|
|
if k == 'Type':
|
|
return self.parse_type_def()
|
|
if k == 'Enum':
|
|
return self.parse_enum_def()
|
|
if k == 'Import':
|
|
return self.parse_import()
|
|
if k == 'From':
|
|
return self.parse_from_import()
|
|
if k == 'At':
|
|
return self.parse_decorator_stmt()
|
|
if k == 'Cgi':
|
|
return self.parse_cgi_block()
|
|
if k == 'Service':
|
|
return self.parse_service_block()
|
|
if k == 'Vessel':
|
|
return self.parse_vessel_block()
|
|
|
|
# Bare assignment: Ident = expr (without `let`) — rebinding syntax
|
|
# Emit as plain C assignment (name already declared in scope).
|
|
if k == 'Ident' and self.pos + 1 < len(self.tokens) and self.tokens[self.pos + 1].kind == 'Eq':
|
|
name = self.advance().value # consume Ident
|
|
self.advance() # consume =
|
|
val = self.parse_expr()
|
|
self.eat('Semicolon')
|
|
return {'stmt': 'Assign', 'name': name, 'value': val}
|
|
|
|
# Expression statement
|
|
expr = self.parse_expr()
|
|
self.eat('Semicolon')
|
|
return {'stmt': 'Expr', 'value': expr}
|
|
|
|
def parse_let(self):
|
|
self.expect('Let')
|
|
name = self.expect('Ident').value
|
|
type_ann = None
|
|
if self.eat('Colon'):
|
|
type_ann = self.parse_type()
|
|
self.expect('Eq')
|
|
val = self.parse_expr()
|
|
self.eat('Semicolon')
|
|
return {'stmt': 'Let', 'name': name, 'type': type_ann or '', 'value': val}
|
|
|
|
def parse_return(self):
|
|
self.expect('Return')
|
|
# Bare return: next token is } or Eof
|
|
k = self.peek_kind()
|
|
if k in ('RBrace', 'Eof'):
|
|
return {'stmt': 'Return', 'value': {'expr': 'Nil'}}
|
|
val = self.parse_expr()
|
|
self.eat('Semicolon')
|
|
return {'stmt': 'Return', 'value': val}
|
|
|
|
def parse_fn(self, decorator=None):
|
|
self.expect('Fn')
|
|
name = self.expect('Ident').value
|
|
self.expect('LParen')
|
|
params = self.parse_param_list()
|
|
self.expect('RParen')
|
|
ret_type = 'Any'
|
|
if self.eat('Arrow'):
|
|
ret_type = self.parse_type()
|
|
self.expect('LBrace')
|
|
body = self.parse_block()
|
|
return {
|
|
'stmt': 'FnDef',
|
|
'name': name,
|
|
'params': params,
|
|
'ret_type': ret_type,
|
|
'body': body,
|
|
'decorator': decorator or '',
|
|
}
|
|
|
|
def parse_param_list(self):
|
|
params = []
|
|
if self.peek_kind() == 'RParen':
|
|
return params
|
|
while True:
|
|
pname = self.expect('Ident').value
|
|
ptype = 'Any'
|
|
if self.eat('Colon'):
|
|
ptype = self.parse_type()
|
|
params.append({'name': pname, 'type': ptype})
|
|
if not self.eat('Comma'):
|
|
break
|
|
return params
|
|
|
|
def parse_while(self):
|
|
self.expect('While')
|
|
cond = self.parse_expr()
|
|
self.expect('LBrace')
|
|
body = self.parse_block()
|
|
return {'stmt': 'While', 'cond': cond, 'body': body}
|
|
|
|
def parse_for(self):
|
|
self.expect('For')
|
|
item = self.expect('Ident').value
|
|
self.expect('In')
|
|
lst = self.parse_expr()
|
|
self.expect('LBrace')
|
|
body = self.parse_block()
|
|
return {'stmt': 'For', 'item': item, 'list': lst, 'body': body}
|
|
|
|
def parse_if(self):
|
|
"""Parse if/else if/else — returns an If expression node."""
|
|
self.expect('If')
|
|
cond = self.parse_expr()
|
|
self.expect('LBrace')
|
|
then_stmts = self.parse_block()
|
|
else_stmts = []
|
|
has_else = False
|
|
if self.peek_kind() == 'Else':
|
|
self.advance()
|
|
if self.peek_kind() == 'If':
|
|
# else if → nest as else body containing the inner if
|
|
inner = self.parse_if()
|
|
else_stmts = [{'stmt': 'Expr', 'value': inner}]
|
|
has_else = True
|
|
else:
|
|
self.expect('LBrace')
|
|
else_stmts = self.parse_block()
|
|
has_else = True
|
|
return {
|
|
'expr': 'If',
|
|
'cond': cond,
|
|
'then': then_stmts,
|
|
'else': else_stmts,
|
|
'has_else': has_else,
|
|
}
|
|
|
|
def parse_block(self):
|
|
stmts = []
|
|
while self.peek_kind() not in ('RBrace', 'Eof'):
|
|
s = self.parse_stmt()
|
|
if s is not None:
|
|
stmts.append(s)
|
|
self.expect('RBrace')
|
|
return stmts
|
|
|
|
def parse_type_def(self):
|
|
self.expect('Type')
|
|
name = self.expect('Ident').value
|
|
self.expect('LBrace')
|
|
fields = []
|
|
while self.peek_kind() != 'RBrace':
|
|
if self.peek_kind() == 'Eof':
|
|
break
|
|
fn = self.expect('Ident').value
|
|
if self.eat('Colon'):
|
|
self.parse_type()
|
|
self.eat('Comma')
|
|
fields.append(fn)
|
|
self.expect('RBrace')
|
|
return {'stmt': 'TypeDef', 'name': name, 'fields': fields}
|
|
|
|
def parse_enum_def(self):
|
|
self.expect('Enum')
|
|
name = self.expect('Ident').value
|
|
self.expect('LBrace')
|
|
variants = []
|
|
while self.peek_kind() != 'RBrace':
|
|
if self.peek_kind() == 'Eof':
|
|
break
|
|
vn = self.expect('Ident').value
|
|
if self.eat('LParen'):
|
|
self.parse_type()
|
|
self.expect('RParen')
|
|
self.eat('Comma')
|
|
variants.append(vn)
|
|
self.expect('RBrace')
|
|
return {'stmt': 'EnumDef', 'name': name, 'variants': variants}
|
|
|
|
def parse_import(self):
|
|
self.expect('Import')
|
|
path = self.expect('Str').value
|
|
return {'stmt': 'Import', 'path': path}
|
|
|
|
def parse_from_import(self):
|
|
self.expect('From')
|
|
module = self.expect('Ident').value
|
|
self.expect('Import')
|
|
self.expect('LBrace')
|
|
names = []
|
|
while self.peek_kind() != 'RBrace':
|
|
if self.peek_kind() == 'Eof':
|
|
break
|
|
names.append(self.expect('Ident').value)
|
|
self.eat('Comma')
|
|
self.expect('RBrace')
|
|
return {'stmt': 'Import', 'module': module, 'names': names}
|
|
|
|
def parse_decorator_stmt(self):
|
|
self.expect('At')
|
|
dec_name = self.peek().value
|
|
self.advance() # consume decorator name (Ident or keyword like Manager)
|
|
# Next must be fn
|
|
if self.peek_kind() == 'Fn':
|
|
return self.parse_fn(decorator=dec_name)
|
|
# Otherwise skip decorator and parse next statement
|
|
return self.parse_stmt()
|
|
|
|
def parse_cgi_block(self):
|
|
"""Parse: cgi "name" { field: "value" ... }"""
|
|
self.expect('Cgi')
|
|
name = ''
|
|
if self.peek_kind() == 'Str':
|
|
name = self.advance().value
|
|
self.expect('LBrace')
|
|
fields = {}
|
|
while self.peek_kind() != 'RBrace':
|
|
if self.peek_kind() == 'Eof':
|
|
break
|
|
fname = self.expect('Ident').value
|
|
self.expect('Colon')
|
|
fval = self.expect('Str').value
|
|
fields[fname] = fval
|
|
self.eat('Comma')
|
|
self.expect('RBrace')
|
|
return {
|
|
'stmt': 'CgiBlock',
|
|
'name': name,
|
|
'dharma_id': fields.get('dharma_id', ''),
|
|
'principal': fields.get('principal', ''),
|
|
'network': fields.get('network', ''),
|
|
'engram': fields.get('engram', ''),
|
|
'has_dharma_id': 'dharma_id' in fields,
|
|
'has_principal': 'principal' in fields,
|
|
'has_network': 'network' in fields,
|
|
'has_engram': 'engram' in fields,
|
|
}
|
|
|
|
def parse_service_block(self):
|
|
self.expect('Service')
|
|
name = ''
|
|
if self.peek_kind() == 'Str':
|
|
name = self.advance().value
|
|
self.expect('LBrace')
|
|
while self.peek_kind() not in ('RBrace', 'Eof'):
|
|
self.advance()
|
|
self.eat('RBrace')
|
|
return {'stmt': 'ServiceBlock', 'name': name}
|
|
|
|
def parse_vessel_block(self):
|
|
self.expect('Vessel')
|
|
if self.peek_kind() == 'Str':
|
|
self.advance()
|
|
if self.peek_kind() == 'LBrace':
|
|
self.expect('LBrace')
|
|
depth = 1
|
|
while depth > 0 and self.peek_kind() != 'Eof':
|
|
k = self.peek_kind()
|
|
if k == 'LBrace':
|
|
depth += 1
|
|
elif k == 'RBrace':
|
|
depth -= 1
|
|
self.advance()
|
|
return {'stmt': 'Import', 'path': ''} # no-op
|
|
|
|
# ── Expressions ───────────────────────────────────────────────────────────
|
|
|
|
PREC = {
|
|
'Or': 1, 'And': 2,
|
|
'EqEq': 3, 'NotEq': 3,
|
|
'Lt': 4, 'Gt': 4, 'LtEq': 4, 'GtEq': 4,
|
|
'Plus': 5, 'Minus': 5,
|
|
'Star': 6, 'Slash': 6, 'Percent': 6,
|
|
}
|
|
|
|
def parse_expr(self):
|
|
return self.parse_binop(0)
|
|
|
|
def parse_binop(self, min_prec):
|
|
left = self.parse_unary()
|
|
while True:
|
|
k = self.peek_kind()
|
|
prec = self.PREC.get(k, 0)
|
|
if prec <= min_prec:
|
|
break
|
|
op_tok = self.advance()
|
|
right = self.parse_binop(prec) # left-assoc: use prec, not prec+1
|
|
left = {'expr': 'BinOp', 'op': k, 'left': left, 'right': right}
|
|
return left
|
|
|
|
def parse_unary(self):
|
|
k = self.peek_kind()
|
|
if k == 'Not':
|
|
self.advance()
|
|
inner = self.parse_postfix()
|
|
return {'expr': 'Not', 'inner': inner}
|
|
if k == 'Minus':
|
|
self.advance()
|
|
inner = self.parse_postfix()
|
|
return {'expr': 'Neg', 'inner': inner}
|
|
return self.parse_postfix()
|
|
|
|
def parse_postfix(self):
|
|
node = self.parse_primary()
|
|
while True:
|
|
k = self.peek_kind()
|
|
if k == 'Dot':
|
|
self.advance()
|
|
field = self.peek().value
|
|
self.advance()
|
|
if self.peek_kind() == 'LParen':
|
|
self.advance()
|
|
args = self.parse_arg_list()
|
|
self.expect('RParen')
|
|
# method call: obj.method(args) → func=Field node
|
|
func_node = {'expr': 'Field', 'object': node, 'field': field}
|
|
node = {'expr': 'Call', 'func': func_node, 'args': args}
|
|
else:
|
|
node = {'expr': 'Field', 'object': node, 'field': field}
|
|
elif k == 'LParen':
|
|
self.advance()
|
|
args = self.parse_arg_list()
|
|
self.expect('RParen')
|
|
node = {'expr': 'Call', 'func': node, 'args': args}
|
|
elif k == 'LBracket':
|
|
self.advance()
|
|
idx = self.parse_expr()
|
|
self.expect('RBracket')
|
|
node = {'expr': 'Index', 'object': node, 'index': idx}
|
|
elif k == 'QuestionMark':
|
|
self.advance()
|
|
node = {'expr': 'Try', 'inner': node}
|
|
else:
|
|
break
|
|
return node
|
|
|
|
def parse_primary(self):
|
|
k = self.peek_kind()
|
|
|
|
if k == 'Int':
|
|
v = self.advance().value
|
|
return {'expr': 'Int', 'value': v}
|
|
|
|
if k == 'Float':
|
|
v = self.advance().value
|
|
return {'expr': 'Float', 'value': v}
|
|
|
|
if k == 'Str':
|
|
v = self.advance().value
|
|
return {'expr': 'Str', 'value': v}
|
|
|
|
if k == 'Bool':
|
|
v = self.advance().value
|
|
return {'expr': 'Bool', 'value': v}
|
|
|
|
if k == 'Ident':
|
|
name = self.advance().value
|
|
return {'expr': 'Ident', 'name': name}
|
|
|
|
if k == 'LParen':
|
|
self.advance()
|
|
e = self.parse_expr()
|
|
self.expect('RParen')
|
|
return e
|
|
|
|
if k == 'LBracket':
|
|
self.advance()
|
|
elems = []
|
|
if self.peek_kind() != 'RBracket':
|
|
elems = self.parse_arg_list()
|
|
self.expect('RBracket')
|
|
return {'expr': 'Array', 'elems': elems}
|
|
|
|
if k == 'LBrace':
|
|
return self.parse_map_literal()
|
|
|
|
if k == 'If':
|
|
return self.parse_if()
|
|
|
|
if k == 'For':
|
|
# for as expression
|
|
self.expect('For')
|
|
item = self.expect('Ident').value
|
|
self.expect('In')
|
|
lst = self.parse_expr()
|
|
self.expect('LBrace')
|
|
body = self.parse_block()
|
|
return {'expr': 'For', 'item': item, 'list': lst, 'body': body}
|
|
|
|
if k == 'Match':
|
|
return self.parse_match()
|
|
|
|
# keyword used as identifier (e.g. `to`, `via`, etc.)
|
|
t = self.advance()
|
|
return {'expr': 'Ident', 'name': t.value}
|
|
|
|
def parse_map_literal(self):
|
|
self.expect('LBrace')
|
|
pairs = []
|
|
while self.peek_kind() != 'RBrace':
|
|
if self.peek_kind() == 'Eof':
|
|
break
|
|
key = self.expect('Str').value
|
|
self.expect('Colon')
|
|
val = self.parse_expr()
|
|
pairs.append({'key': key, 'value': val})
|
|
self.eat('Comma')
|
|
self.expect('RBrace')
|
|
return {'expr': 'Map', 'pairs': pairs}
|
|
|
|
def parse_arg_list(self):
|
|
args = []
|
|
if self.peek_kind() in ('RParen', 'RBracket'):
|
|
return args
|
|
args.append(self.parse_expr())
|
|
while self.eat('Comma'):
|
|
if self.peek_kind() in ('RParen', 'RBracket'):
|
|
break
|
|
args.append(self.parse_expr())
|
|
return args
|
|
|
|
def parse_match(self):
|
|
self.expect('Match')
|
|
subject = self.parse_expr()
|
|
self.expect('LBrace')
|
|
arms = []
|
|
while self.peek_kind() != 'RBrace':
|
|
if self.peek_kind() == 'Eof':
|
|
break
|
|
pat = self.parse_pattern()
|
|
self.expect('FatArrow')
|
|
body = self.parse_expr()
|
|
self.eat('Comma')
|
|
arms.append({'pattern': pat, 'body': body})
|
|
self.expect('RBrace')
|
|
return {'expr': 'Match', 'subject': subject, 'arms': arms}
|
|
|
|
def parse_pattern(self):
|
|
k = self.peek_kind()
|
|
if k == 'Ident':
|
|
t = self.advance()
|
|
if t.value == '_':
|
|
return {'pattern': 'Wildcard'}
|
|
return {'pattern': 'Binding', 'name': t.value}
|
|
if k == 'Int':
|
|
v = self.advance().value
|
|
return {'pattern': 'LitInt', 'value': v}
|
|
if k == 'Str':
|
|
v = self.advance().value
|
|
return {'pattern': 'LitStr', 'value': v}
|
|
if k == 'Bool':
|
|
v = self.advance().value
|
|
return {'pattern': 'LitBool', 'value': v}
|
|
# fallback
|
|
self.advance()
|
|
return {'pattern': 'Wildcard'}
|
|
|
|
|
|
# ─── Codegen ──────────────────────────────────────────────────────────────────
|
|
|
|
def c_escape(s):
|
|
"""Escape a string for a C string literal."""
|
|
out = []
|
|
for ch in s:
|
|
if ch == '"':
|
|
out.append('\\"')
|
|
elif ch == '\\':
|
|
out.append('\\\\')
|
|
elif ch == '\n':
|
|
out.append('\\n')
|
|
elif ch == '\r':
|
|
out.append('\\r')
|
|
elif ch == '\t':
|
|
out.append('\\t')
|
|
else:
|
|
out.append(ch)
|
|
return ''.join(out)
|
|
|
|
|
|
def c_str_lit(s):
|
|
return '"' + c_escape(s) + '"'
|
|
|
|
|
|
BINOP_C = {
|
|
'Plus': '+', 'Minus': '-', 'Star': '*', 'Slash': '/',
|
|
'Percent': '%',
|
|
'EqEq': '==', 'NotEq': '!=',
|
|
'Lt': '<', 'Gt': '>', 'LtEq': '<=', 'GtEq': '>=',
|
|
'And': '&&', 'Or': '||',
|
|
}
|
|
|
|
# Known builtins that return Int
|
|
INT_CALL_NAMES = {
|
|
'str_len', 'str_index_of', 'str_to_int', 'str_char_code',
|
|
'native_list_len', 'el_list_len', 'len', 'json_get_int',
|
|
'json_array_len', 'engram_node_count', 'engram_edge_count',
|
|
'time_now', 'time_now_utc', 'time_diff', 'time_add', 'time_from_parts',
|
|
'el_abs', 'el_max', 'el_min', 'float_to_int',
|
|
}
|
|
|
|
|
|
class CodeGen:
|
|
def __init__(self):
|
|
self.lines = []
|
|
self._if_counter = 0
|
|
self._match_counter = 0
|
|
self._int_names = set() # per-function typed int names
|
|
self._global_int_names = set() # top-level
|
|
|
|
def emit(self, line):
|
|
self.lines.append(line)
|
|
|
|
def blank(self):
|
|
self.lines.append('')
|
|
|
|
def output(self):
|
|
return '\n'.join(self.lines)
|
|
|
|
# ── Int-name tracking ──────────────────────────────────────────────────────
|
|
|
|
def add_int_name(self, name):
|
|
self._int_names.add(name)
|
|
|
|
def is_int_name(self, name):
|
|
return name in self._int_names or name in self._global_int_names
|
|
|
|
def reset_int_names(self):
|
|
self._int_names = set()
|
|
|
|
def seed_int_names_from_params(self, params):
|
|
self.reset_int_names()
|
|
for p in params:
|
|
if p.get('type') == 'Int':
|
|
self.add_int_name(p['name'])
|
|
|
|
def is_int_expr(self, expr):
|
|
k = expr.get('expr', '')
|
|
if k == 'Int':
|
|
return True
|
|
if k == 'Ident':
|
|
return self.is_int_name(expr['name'])
|
|
if k == 'Call':
|
|
func = expr.get('func', {})
|
|
if func.get('expr') == 'Ident':
|
|
return func.get('name', '') in INT_CALL_NAMES
|
|
if k == 'Neg':
|
|
return self.is_int_expr(expr['inner'])
|
|
if k == 'Not':
|
|
return True
|
|
if k == 'BinOp':
|
|
op = expr.get('op', '')
|
|
if op in ('EqEq', 'NotEq', 'Lt', 'Gt', 'LtEq', 'GtEq', 'And', 'Or'):
|
|
return True
|
|
if op in ('Plus', 'Minus', 'Star', 'Slash', 'Percent'):
|
|
return self.is_int_expr(expr['left']) and self.is_int_expr(expr['right'])
|
|
return False
|
|
|
|
# ── Expression codegen ────────────────────────────────────────────────────
|
|
|
|
def cg_expr(self, expr):
|
|
k = expr.get('expr', '')
|
|
|
|
if k == 'Int':
|
|
return expr['value']
|
|
|
|
if k == 'Float':
|
|
return 'el_from_float(' + expr['value'] + ')'
|
|
|
|
if k == 'Str':
|
|
return 'EL_STR(' + c_str_lit(expr['value']) + ')'
|
|
|
|
if k == 'Bool':
|
|
return '1' if expr['value'] == 'true' else '0'
|
|
|
|
if k == 'Nil':
|
|
return 'EL_NULL'
|
|
|
|
if k == 'Ident':
|
|
return expr['name']
|
|
|
|
if k == 'Not':
|
|
inner_c = self.cg_expr(expr['inner'])
|
|
return '!' + inner_c
|
|
|
|
if k == 'Neg':
|
|
inner_c = self.cg_expr(expr['inner'])
|
|
return '(-' + inner_c + ')'
|
|
|
|
if k == 'BinOp':
|
|
return self.cg_binop(expr)
|
|
|
|
if k == 'Call':
|
|
return self.cg_call(expr)
|
|
|
|
if k == 'Field':
|
|
obj_c = self.cg_expr(expr['object'])
|
|
field = expr['field']
|
|
return 'el_get_field(' + obj_c + ', EL_STR(' + c_str_lit(field) + '))'
|
|
|
|
if k == 'Index':
|
|
obj_c = self.cg_expr(expr['object'])
|
|
idx = expr['index']
|
|
idx_c = self.cg_expr(idx)
|
|
if idx.get('expr') == 'Str':
|
|
return 'el_get_field(' + obj_c + ', ' + idx_c + ')'
|
|
return 'el_list_get(' + obj_c + ', ' + idx_c + ')'
|
|
|
|
if k == 'Array':
|
|
elems = expr.get('elems', [])
|
|
n = len(elems)
|
|
if n == 0:
|
|
return 'el_list_empty()'
|
|
items = ', '.join(self.cg_expr(e) for e in elems)
|
|
return 'el_list_new(' + str(n) + ', ' + items + ')'
|
|
|
|
if k == 'Map':
|
|
pairs = expr.get('pairs', [])
|
|
n = len(pairs)
|
|
if n == 0:
|
|
return 'el_map_new(0)'
|
|
parts = []
|
|
for p in pairs:
|
|
parts.append(c_str_lit(p['key']))
|
|
parts.append(self.cg_expr(p['value']))
|
|
items = ', '.join(parts)
|
|
return 'el_map_new(' + str(n) + ', ' + items + ')'
|
|
|
|
if k == 'Try':
|
|
return self.cg_expr(expr['inner'])
|
|
|
|
if k == 'If':
|
|
return self.cg_if_expr(expr)
|
|
|
|
if k == 'Match':
|
|
return self.cg_match(expr)
|
|
|
|
if k == 'For':
|
|
# For in expression position — emit as void, return 0
|
|
return '0'
|
|
|
|
return 'EL_NULL'
|
|
|
|
def cg_binop(self, expr):
|
|
op = expr['op']
|
|
left = expr['left']
|
|
right = expr['right']
|
|
left_c = self.cg_expr(left)
|
|
right_c = self.cg_expr(right)
|
|
lk = left.get('expr', '')
|
|
rk = right.get('expr', '')
|
|
|
|
if op == 'Plus':
|
|
# Str literal on either side → always concat
|
|
if lk == 'Str':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'Str':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
# Both provably Int → arithmetic
|
|
if self.is_int_expr(left) and self.is_int_expr(right):
|
|
return '(' + left_c + ' + ' + right_c + ')'
|
|
# Int literal on either side → arithmetic
|
|
if lk == 'Int':
|
|
return '(' + left_c + ' + ' + right_c + ')'
|
|
if rk == 'Int':
|
|
return '(' + left_c + ' + ' + right_c + ')'
|
|
# Call/Ident/BinOp(+) → string concat (historical default)
|
|
if lk == 'Call':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'Call':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
if lk == 'BinOp' and left.get('op') == 'Plus':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'BinOp' and right.get('op') == 'Plus':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
if lk == 'Ident':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'Ident':
|
|
return 'el_str_concat(' + left_c + ', ' + right_c + ')'
|
|
return '(' + left_c + ' + ' + right_c + ')'
|
|
|
|
if op == 'EqEq':
|
|
if lk == 'Int' or rk == 'Int':
|
|
return '(' + left_c + ' == ' + right_c + ')'
|
|
if lk == 'Bool' or rk == 'Bool':
|
|
return '(' + left_c + ' == ' + right_c + ')'
|
|
# Both Ident and both are int-typed → plain ==
|
|
if lk == 'Ident' and rk == 'Ident':
|
|
if self.is_int_name(left['name']) and self.is_int_name(right['name']):
|
|
return '(' + left_c + ' == ' + right_c + ')'
|
|
if lk == 'Str' or rk == 'Str':
|
|
return 'str_eq(' + left_c + ', ' + right_c + ')'
|
|
if lk == 'Ident':
|
|
return 'str_eq(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'Ident':
|
|
return 'str_eq(' + left_c + ', ' + right_c + ')'
|
|
if lk == 'Call':
|
|
return 'str_eq(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'Call':
|
|
return 'str_eq(' + left_c + ', ' + right_c + ')'
|
|
return '(' + left_c + ' == ' + right_c + ')'
|
|
|
|
if op == 'NotEq':
|
|
if lk == 'Int' or rk == 'Int':
|
|
return '(' + left_c + ' != ' + right_c + ')'
|
|
if lk == 'Bool' or rk == 'Bool':
|
|
return '(' + left_c + ' != ' + right_c + ')'
|
|
if lk == 'Ident' and rk == 'Ident':
|
|
if self.is_int_name(left['name']) and self.is_int_name(right['name']):
|
|
return '(' + left_c + ' != ' + right_c + ')'
|
|
if lk == 'Str' or rk == 'Str':
|
|
return '!str_eq(' + left_c + ', ' + right_c + ')'
|
|
if lk == 'Ident':
|
|
return '!str_eq(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'Ident':
|
|
return '!str_eq(' + left_c + ', ' + right_c + ')'
|
|
if lk == 'Call':
|
|
return '!str_eq(' + left_c + ', ' + right_c + ')'
|
|
if rk == 'Call':
|
|
return '!str_eq(' + left_c + ', ' + right_c + ')'
|
|
return '(' + left_c + ' != ' + right_c + ')'
|
|
|
|
op_c = BINOP_C.get(op, op)
|
|
return '(' + left_c + ' ' + op_c + ' ' + right_c + ')'
|
|
|
|
def cg_call(self, expr):
|
|
func = expr['func']
|
|
args = expr.get('args', [])
|
|
args_c = ', '.join(self.cg_expr(a) for a in args)
|
|
fk = func.get('expr', '')
|
|
|
|
if fk == 'Ident':
|
|
fn_name = func['name']
|
|
if args_c:
|
|
return fn_name + '(' + args_c + ')'
|
|
return fn_name + '()'
|
|
|
|
if fk == 'Field':
|
|
obj_c = self.cg_expr(func['object'])
|
|
field = func['field']
|
|
if args_c:
|
|
return field + '(' + obj_c + ', ' + args_c + ')'
|
|
return field + '(' + obj_c + ')'
|
|
|
|
fn_c = self.cg_expr(func)
|
|
if args_c:
|
|
return fn_c + '(' + args_c + ')'
|
|
return fn_c + '()'
|
|
|
|
def cg_if_expr(self, expr):
|
|
self._if_counter += 1
|
|
idx = str(self._if_counter)
|
|
result_var = '_if_result_' + idx
|
|
cond_c = self.cg_expr(expr['cond'])
|
|
then_c = self.cg_if_arm(expr.get('then', []), result_var)
|
|
else_c = ''
|
|
if expr.get('has_else'):
|
|
else_c = self.cg_if_arm(expr.get('else', []), result_var)
|
|
return (
|
|
'({ el_val_t ' + result_var + ' = 0; '
|
|
'if (' + cond_c + ') { ' + then_c + '} else { ' + else_c + '} '
|
|
+ result_var + '; })'
|
|
)
|
|
|
|
def cg_if_arm(self, stmts, result_var):
|
|
"""Render a list of statements as the body of an if-expression arm."""
|
|
out = []
|
|
n = len(stmts)
|
|
for i, s in enumerate(stmts):
|
|
sk = s.get('stmt', '')
|
|
is_last = (i == n - 1)
|
|
if sk == 'Assign':
|
|
val_c = self.cg_expr(s['value'])
|
|
out.append(s['name'] + ' = ' + val_c + '; ')
|
|
elif sk == 'Let':
|
|
val_c = self.cg_expr(s['value'])
|
|
out.append('el_val_t ' + s['name'] + ' = ' + val_c + '; ')
|
|
elif sk == 'Return':
|
|
val_c = self.cg_expr(s['value'])
|
|
out.append(result_var + ' = (' + val_c + '); ')
|
|
elif sk == 'Expr':
|
|
val_c = self.cg_expr(s['value'])
|
|
if is_last:
|
|
out.append(result_var + ' = (' + val_c + '); ')
|
|
else:
|
|
out.append('(void)(' + val_c + '); ')
|
|
# While/For inside if-arm: skip (uncommon)
|
|
return ''.join(out)
|
|
|
|
def cg_match(self, expr):
|
|
self._match_counter += 1
|
|
idx = str(self._match_counter)
|
|
subj_var = '_match_subj_' + idx
|
|
result_var = '_match_result_' + idx
|
|
done_label = '_match_done_' + idx
|
|
subj_c = self.cg_expr(expr['subject'])
|
|
parts = ['({ el_val_t ' + subj_var + ' = ' + subj_c + '; el_val_t ' + result_var + ' = 0; ']
|
|
for arm in expr.get('arms', []):
|
|
pat = arm['pattern']
|
|
body_c = self.cg_expr(arm['body'])
|
|
pk = pat.get('pattern', '')
|
|
if pk == 'Wildcard':
|
|
parts.append('{ ' + result_var + ' = (' + body_c + '); goto ' + done_label + '; } ')
|
|
elif pk == 'Binding':
|
|
bname = pat['name']
|
|
parts.append('{ el_val_t ' + bname + ' = ' + subj_var + '; ' +
|
|
result_var + ' = (' + body_c + '); goto ' + done_label + '; } ')
|
|
elif pk == 'LitInt':
|
|
v = pat['value']
|
|
parts.append('if (' + subj_var + ' == ' + v + ') { ' +
|
|
result_var + ' = (' + body_c + '); goto ' + done_label + '; } ')
|
|
elif pk == 'LitStr':
|
|
v = pat['value']
|
|
parts.append('if (str_eq(' + subj_var + ', EL_STR(' + c_str_lit(v) + '))) { ' +
|
|
result_var + ' = (' + body_c + '); goto ' + done_label + '; } ')
|
|
elif pk == 'LitBool':
|
|
v = '1' if pat['value'] == 'true' else '0'
|
|
parts.append('if (' + subj_var + ' == ' + v + ') { ' +
|
|
result_var + ' = (' + body_c + '); goto ' + done_label + '; } ')
|
|
else:
|
|
parts.append('{ ' + result_var + ' = (' + body_c + '); goto ' + done_label + '; } ')
|
|
parts.append(done_label + ':; ' + result_var + '; })')
|
|
return ''.join(parts)
|
|
|
|
# ── Statement codegen ─────────────────────────────────────────────────────
|
|
|
|
def cg_stmt(self, stmt, indent, declared):
|
|
"""
|
|
Emit C for a statement. `declared` is a set of names declared in the
|
|
current C scope. Returns updated declared set.
|
|
"""
|
|
sk = stmt.get('stmt', '')
|
|
|
|
if sk == 'Assign':
|
|
# Bare assignment without `let` — always plain assignment
|
|
name = stmt['name']
|
|
val = stmt['value']
|
|
val_c = self.cg_expr(val)
|
|
self.emit(indent + name + ' = ' + val_c + ';')
|
|
# Treat as if declared (so subsequent let-rebind works too)
|
|
declared = declared | {name}
|
|
return declared
|
|
|
|
if sk == 'Let':
|
|
name = stmt['name']
|
|
val = stmt['value']
|
|
val_c = self.cg_expr(val)
|
|
# Track int names
|
|
if stmt.get('type') == 'Int':
|
|
self.add_int_name(name)
|
|
if val.get('expr') == 'Int':
|
|
self.add_int_name(name)
|
|
if name in declared:
|
|
self.emit(indent + name + ' = ' + val_c + ';')
|
|
else:
|
|
self.emit(indent + 'el_val_t ' + name + ' = ' + val_c + ';')
|
|
declared = declared | {name}
|
|
return declared
|
|
|
|
if sk == 'Return':
|
|
val = stmt['value']
|
|
if val.get('expr') == 'Nil':
|
|
self.emit(indent + 'return 0;')
|
|
else:
|
|
val_c = self.cg_expr(val)
|
|
self.emit(indent + 'return ' + val_c + ';')
|
|
return declared
|
|
|
|
if sk == 'Expr':
|
|
val = stmt['value']
|
|
vk = val.get('expr', '')
|
|
if vk == 'If':
|
|
self.cg_if_stmt(val, indent, declared)
|
|
return declared
|
|
if vk == 'For':
|
|
self.cg_for_body(val['item'], val['list'], val['body'], indent, declared)
|
|
return declared
|
|
val_c = self.cg_expr(val)
|
|
self.emit(indent + val_c + ';')
|
|
return declared
|
|
|
|
if sk == 'While':
|
|
cond_c = self.cg_expr(stmt['cond'])
|
|
cond_c = self.strip_outer_parens(cond_c)
|
|
self.emit(indent + 'while (' + cond_c + ') {')
|
|
self.cg_stmts(stmt['body'], indent + ' ', set(declared))
|
|
self.emit(indent + '}')
|
|
return declared
|
|
|
|
if sk == 'For':
|
|
self.cg_for_body(stmt['item'], stmt['list'], stmt['body'], indent, declared)
|
|
return declared
|
|
|
|
# FnDef, TypeDef, EnumDef, Import, CgiBlock, ServiceBlock — skip inside fn body
|
|
return declared
|
|
|
|
def strip_outer_parens(self, s):
|
|
if len(s) < 2:
|
|
return s
|
|
if s[0] != '(' or s[-1] != ')':
|
|
return s
|
|
depth = 0
|
|
for i, c in enumerate(s[:-1]):
|
|
if c == '(':
|
|
depth += 1
|
|
elif c == ')':
|
|
depth -= 1
|
|
if depth == 0 and i < len(s) - 1:
|
|
return s
|
|
return s[1:-1]
|
|
|
|
def cg_if_stmt(self, expr, indent, declared):
|
|
cond_c = self.cg_expr(expr['cond'])
|
|
cond_c = self.strip_outer_parens(cond_c)
|
|
self.emit(indent + 'if (' + cond_c + ') {')
|
|
self.cg_stmts(expr.get('then', []), indent + ' ', set(declared))
|
|
if expr.get('has_else'):
|
|
self.emit(indent + '} else {')
|
|
self.cg_stmts(expr.get('else', []), indent + ' ', set(declared))
|
|
self.emit(indent + '}')
|
|
|
|
def cg_for_body(self, item, list_expr, body, indent, declared):
|
|
list_c = self.cg_expr(list_expr)
|
|
self.emit(indent + '{')
|
|
self.emit(indent + ' el_val_t _el_lst = ' + list_c + ';')
|
|
self.emit(indent + ' el_val_t _el_len = el_list_len(_el_lst);')
|
|
self.emit(indent + ' for (el_val_t _el_i = 0; _el_i < _el_len; _el_i++) {')
|
|
self.emit(indent + ' el_val_t ' + item + ' = el_list_get(_el_lst, _el_i);')
|
|
body_decl = set(declared) | {item}
|
|
self.cg_stmts(body, indent + ' ', body_decl)
|
|
self.emit(indent + ' }')
|
|
self.emit(indent + '}')
|
|
|
|
def cg_stmts(self, stmts, indent, declared):
|
|
decl = set(declared)
|
|
for s in stmts:
|
|
decl = self.cg_stmt(s, indent, decl)
|
|
return decl
|
|
|
|
# ── Function codegen ──────────────────────────────────────────────────────
|
|
|
|
def params_to_c(self, params):
|
|
if not params:
|
|
return 'void'
|
|
return ', '.join('el_val_t ' + p['name'] for p in params)
|
|
|
|
def transform_implicit_return(self, body, ret_type):
|
|
"""
|
|
If the last statement is a bare Expr (not If/For/While), convert it
|
|
to a Return. Skip for Void-returning functions.
|
|
"""
|
|
if ret_type == 'Void':
|
|
return body
|
|
if not body:
|
|
return body
|
|
last = body[-1]
|
|
if last.get('stmt') == 'Expr':
|
|
val = last['value']
|
|
vk = val.get('expr', '')
|
|
if vk not in ('If', 'For'):
|
|
new_body = list(body[:-1])
|
|
new_body.append({'stmt': 'Return', 'value': val})
|
|
return new_body
|
|
return body
|
|
|
|
def cg_fn(self, stmt):
|
|
fn_name = stmt['name']
|
|
if fn_name == 'main':
|
|
return # skip — C provides main()
|
|
params = stmt.get('params', [])
|
|
body = stmt.get('body', [])
|
|
ret_type = stmt.get('ret_type', 'Any')
|
|
params_c = self.params_to_c(params)
|
|
self.seed_int_names_from_params(params)
|
|
self.emit('el_val_t ' + fn_name + '(' + params_c + ') {')
|
|
decl = {p['name'] for p in params}
|
|
body_xformed = self.transform_implicit_return(body, ret_type)
|
|
self.cg_stmts(body_xformed, ' ', decl)
|
|
self.emit(' return 0;')
|
|
self.emit('}')
|
|
self.blank()
|
|
|
|
# ── Top-level codegen entry ───────────────────────────────────────────────
|
|
|
|
def codegen(self, stmts):
|
|
# Reset state
|
|
self._if_counter = 0
|
|
self._match_counter = 0
|
|
self._int_names = set()
|
|
self._global_int_names = set()
|
|
|
|
# Preamble
|
|
self.emit('#include <stdint.h>')
|
|
self.emit('#include <stdlib.h>')
|
|
self.emit('#include "el_runtime.h"')
|
|
self.blank()
|
|
|
|
# Forward declarations (skip main)
|
|
for s in stmts:
|
|
if s.get('stmt') == 'FnDef':
|
|
fn_name = s['name']
|
|
if fn_name == 'main':
|
|
continue
|
|
params = s.get('params', [])
|
|
params_c = self.params_to_c(params)
|
|
self.emit('el_val_t ' + fn_name + '(' + params_c + ');')
|
|
self.blank()
|
|
|
|
# Top-level Let → file-scope storage
|
|
has_toplevel_lets = False
|
|
for s in stmts:
|
|
if s.get('stmt') == 'Let':
|
|
name = s['name']
|
|
if s.get('type') == 'Int':
|
|
self._global_int_names.add(name)
|
|
if s.get('value', {}).get('expr') == 'Int':
|
|
self._global_int_names.add(name)
|
|
self.emit('el_val_t ' + name + ';')
|
|
has_toplevel_lets = True
|
|
if has_toplevel_lets:
|
|
self.blank()
|
|
|
|
# Function definitions. Skip El's `fn main()` for the same reason we
|
|
# skip its forward decl above: a duplicate `el_val_t main(void)` would
|
|
# collide with the `int main(int argc, char**)` we emit below. The
|
|
# body of `fn main()` is instead folded into C's main() alongside
|
|
# any top-level statements.
|
|
el_main_body = None
|
|
for s in stmts:
|
|
if s.get('stmt') == 'FnDef':
|
|
if s.get('name') == 'main':
|
|
el_main_body = s.get('body', [])
|
|
continue
|
|
self.cg_fn(s)
|
|
|
|
# main(). Use _argc/_argv as C parameter names so El programs are
|
|
# free to declare local `argv` / `argc` (and call args() / count_args())
|
|
# without colliding with the C-side parameters.
|
|
self.emit('int main(int _argc, char** _argv) {')
|
|
self.emit(' el_runtime_init_args(_argc, _argv);')
|
|
|
|
# cgi block init
|
|
for s in stmts:
|
|
if s.get('stmt') == 'CgiBlock':
|
|
cname = s.get('name', '')
|
|
cdid = s.get('dharma_id', '')
|
|
cprin = s.get('principal', '')
|
|
cnet = s.get('network', '')
|
|
ceng = s.get('engram', '')
|
|
arg_name = 'EL_STR(' + c_str_lit(cname) + ')'
|
|
arg_did = ('EL_STR(' + c_str_lit(cdid) + ')' if s.get('has_dharma_id') else 'EL_NULL')
|
|
arg_prin = ('EL_STR(' + c_str_lit(cprin) + ')' if s.get('has_principal') else 'EL_NULL')
|
|
arg_net = ('EL_STR(' + c_str_lit(cnet) + ')' if s.get('has_network') else 'EL_NULL')
|
|
arg_eng = ('EL_STR(' + c_str_lit(ceng) + ')' if s.get('has_engram') else 'EL_NULL')
|
|
self.emit(' el_cgi_init(' + arg_name + ', ' + arg_did + ', ' + arg_prin + ', ' + arg_net + ', ' + arg_eng + ');')
|
|
break
|
|
|
|
# Seed declared with top-level let names (they live at file scope)
|
|
main_decl = set()
|
|
for s in stmts:
|
|
if s.get('stmt') == 'Let':
|
|
main_decl.add(s['name'])
|
|
|
|
# Reset int names for main body (use global + top-level lets)
|
|
self._int_names = set(self._global_int_names)
|
|
|
|
# Top-level statements (not FnDef, not declarative)
|
|
SKIP_KINDS = {'FnDef', 'TypeDef', 'EnumDef', 'Import', 'CgiBlock', 'ServiceBlock'}
|
|
for s in stmts:
|
|
sk = s.get('stmt', '')
|
|
if sk in SKIP_KINDS:
|
|
continue
|
|
main_decl = self.cg_stmt(s, ' ', main_decl)
|
|
|
|
# If the source declared `fn main() -> Void { ... }`, fold its body
|
|
# in here. Mirrors codegen.el's behaviour and lets El programs
|
|
# written either way (top-level statements OR an explicit fn main)
|
|
# produce the same C main(). compiler.el itself uses this form.
|
|
if el_main_body:
|
|
for s in el_main_body:
|
|
main_decl = self.cg_stmt(s, ' ', main_decl)
|
|
|
|
self.emit(' return 0;')
|
|
self.emit('}')
|
|
self.blank()
|
|
|
|
|
|
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
|
|
_IMPORT_RE = re.compile(r'^\s*import\s+"([^"]+\.el)"\s*$')
|
|
_FROM_IMPORT_RE = re.compile(r'^\s*from\s+([A-Za-z_][A-Za-z0-9_]*)\s+import\s*\{')
|
|
|
|
|
|
def resolve_imports(entry_path):
|
|
"""Textually inline every imported .el file into a single source string,
|
|
deduplicating by absolute path. Mirrors the logic compiler.el's resolve_imports
|
|
will do once self-hosted. Two import forms supported:
|
|
|
|
import "path/to/file.el"
|
|
from <module> import { ... }
|
|
|
|
The first is a quoted relative path; the second resolves <module> to
|
|
<module>.el in the same directory as the importer. Any module already
|
|
visited is skipped (depth-first, prepended once).
|
|
|
|
Strict matching via regex avoids false positives like CSS keyframes
|
|
("from { opacity: 0 }") embedded in El string literals.
|
|
"""
|
|
import os
|
|
seen = set()
|
|
|
|
def load(path):
|
|
path = os.path.abspath(path)
|
|
if path in seen:
|
|
return ''
|
|
seen.add(path)
|
|
try:
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
source = f.read()
|
|
except IOError as e:
|
|
print(f'resolve_imports: cannot read {path}: {e}', file=sys.stderr)
|
|
return ''
|
|
directory = os.path.dirname(path)
|
|
prefix = ''
|
|
body = []
|
|
for line in source.split('\n'):
|
|
imp_path = None
|
|
m = _IMPORT_RE.match(line)
|
|
if m:
|
|
rel = m.group(1)
|
|
imp_path = rel if os.path.isabs(rel) else os.path.join(directory, rel)
|
|
else:
|
|
m = _FROM_IMPORT_RE.match(line)
|
|
if m:
|
|
imp_path = os.path.join(directory, m.group(1) + '.el')
|
|
if imp_path is not None:
|
|
prefix += load(imp_path)
|
|
# drop the import line itself; codegen treats Import as no-op anyway
|
|
else:
|
|
body.append(line)
|
|
return prefix + '\n'.join(body) + '\n'
|
|
|
|
return load(entry_path)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print('Usage: bootstrap.py <source.el>', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
path = sys.argv[1]
|
|
try:
|
|
source = resolve_imports(path)
|
|
except IOError as e:
|
|
print(f'Error reading {path}: {e}', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
tokens = lex(source)
|
|
except Exception as e:
|
|
print(f'Lexer error: {e}', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
parser = Parser(tokens)
|
|
stmts = parser.parse_program()
|
|
except ParseError as e:
|
|
print(f'Parse error: {e}', file=sys.stderr)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
import traceback
|
|
print(f'Parser error: {e}', file=sys.stderr)
|
|
traceback.print_exc(file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
cg = CodeGen()
|
|
cg.codegen(stmts)
|
|
print(cg.output())
|
|
except Exception as e:
|
|
import traceback
|
|
print(f'Codegen error: {e}', file=sys.stderr)
|
|
traceback.print_exc(file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|