Skip to content

Instantly share code, notes, and snippets.

@dpiponi
Created February 19, 2026 21:25
Show Gist options
  • Select an option

  • Save dpiponi/3ccfd944dc4e2390fc87898f74d6355a to your computer and use it in GitHub Desktop.

Select an option

Save dpiponi/3ccfd944dc4e2390fc87898f74d6355a to your computer and use it in GitHub Desktop.
Fast interactive bc-alike

uCompile

A tiny, bc-like compiler that emits LLVM IR (.ll).

Features (minimal)

  • Numbers (floating point)
  • Variables (identifiers)
  • Arithmetic: + - * / %
  • Parentheses
  • Assignment: x = 1 + 2;
  • Print: print x;
  • Conditionals: if <expr> { ... } else { ... }
  • While loops: while <expr> { ... }
  • For loops: for (i = 0; i < 10; i = i + 1) { ... }
  • Functions: define name(a, b) { return a + b; }
  • Arrays (auto-expanding, default 0): a[0] = 1; print a[0];
  • Comments: # ..., // ..., /* ... */

Usage

./ucompile.py input.bc -o out.ll

Example

example.bc

x = 2 + 3 * 4;
print x;
print (x - 1) / 2;
if x > 10 {
  print x;
} else {
  print 0;
}
while x > 0 {
  print x;
  x = x - 1;
}
define add(a, b) {
  return a + b;
}
print add(2, 3);
a[0] = 1;
a[1] = 2;
print a[0] + a[1];

Compile to LLVM IR:

./ucompile.py example.bc -o example.ll

You can assemble and run with LLVM tools (if installed):

llc example.ll -o example.s
clang example.s -o example
./example

Notes

  • This is intentionally small and designed to be extended later with conditionals and functions.
  • The backend emits a single main function and uses printf for print.

Tests

These tests require llvm-as in your PATH.

Run:

python -m unittest -v

Example: 1D Poisson (Jacobi)

See examples/poisson1d.bc for a small 1D Poisson solve using Jacobi iteration.

UI (Terminal)

A minimal split-view TUI that edits on the left and shows output or LLVM IR on the right:

./ucompile_tui.py [path/to/file.bc]
  • The right panel updates only on successful compilation (and execution for output mode).
  • If the code is invalid, the right panel stays stale and the status bar shows the error.
  • Default view shows program output; toggle with Ctrl+O to show IR.
  • Quit with Ctrl+Q.
#!/usr/bin/env python3
"""A super-minimal bc-like compiler that emits LLVM IR (.ll)."""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass
from typing import List, Optional, Dict, Tuple
# ---------------------------
# Lexer
# ---------------------------
@dataclass
class Token:
kind: str
value: str
pos: int
KEYWORDS = {"print", "if", "else", "while", "for", "define", "return"}
SINGLE_CHAR = set("+-*/%()=;{},[]\n<>!")
def lex(src: str) -> List[Token]:
tokens: List[Token] = []
i = 0
n = len(src)
def add(kind: str, value: str, pos: int) -> None:
tokens.append(Token(kind, value, pos))
while i < n:
c = src[i]
# Comments: #..., //..., /* ... */
if c == "#":
i += 1
while i < n and src[i] != "\n":
i += 1
continue
if c == "/" and i + 1 < n and src[i + 1] == "/":
i += 2
while i < n and src[i] != "\n":
i += 1
continue
if c == "/" and i + 1 < n and src[i + 1] == "*":
i += 2
while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"):
i += 1
if i + 1 >= n:
raise SyntaxError("Unterminated block comment")
i += 2
continue
if c.isspace():
i += 1
continue
if c in SINGLE_CHAR:
# Multi-char operators.
if c in ("<", ">", "=", "!") and i + 1 < n and src[i + 1] == "=":
add(c + "=", c + "=", i)
i += 2
continue
add(c, c, i)
i += 1
continue
if c.isdigit() or (c == "." and i + 1 < n and src[i + 1].isdigit()):
start = i
saw_dot = False
while i < n and (src[i].isdigit() or src[i] == "."):
if src[i] == ".":
if saw_dot:
break
saw_dot = True
i += 1
add("NUMBER", src[start:i], start)
continue
if c.isalpha() or c == "_":
start = i
i += 1
while i < n and (src[i].isalnum() or src[i] == "_"):
i += 1
ident = src[start:i]
if ident in KEYWORDS:
add(ident, ident, start)
else:
add("IDENT", ident, start)
continue
raise SyntaxError(f"Unexpected character '{c}' at {i}")
add("EOF", "", n)
return tokens
# ---------------------------
# Parser
# ---------------------------
@dataclass
class Expr:
pass
@dataclass
class Number(Expr):
value: float
@dataclass
class Var(Expr):
name: str
@dataclass
class BinOp(Expr):
op: str
left: Expr
right: Expr
@dataclass
class Stmt:
pass
@dataclass
class Assign(Stmt):
target: Expr
expr: Expr
@dataclass
class Print(Stmt):
expr: Expr
@dataclass
class ExprStmt(Stmt):
expr: Expr
@dataclass
class If(Stmt):
cond: Expr
then_block: List[Stmt]
else_block: Optional[List[Stmt]]
@dataclass
class While(Stmt):
cond: Expr
body: List[Stmt]
@dataclass
class For(Stmt):
init: Optional[Stmt]
cond: Optional[Expr]
step: Optional[Stmt]
body: List[Stmt]
@dataclass
class Return(Stmt):
expr: Expr
@dataclass
class Call(Expr):
name: str
args: List[Expr]
@dataclass
class Index(Expr):
name: str
index: Expr
@dataclass
class FuncDef:
name: str
params: List[str]
body: List[Stmt]
@dataclass
class Program:
funcs: List[FuncDef]
main: List[Stmt]
class Parser:
def __init__(self, tokens: List[Token]):
self.tokens = tokens
self.i = 0
def peek(self) -> Token:
return self.tokens[self.i]
def next(self) -> Token:
tok = self.tokens[self.i]
self.i += 1
return tok
def expect(self, kind: str) -> Token:
tok = self.next()
if tok.kind != kind:
raise SyntaxError(f"Expected {kind}, got {tok.kind} at {tok.pos}")
return tok
def parse(self) -> Program:
funcs: List[FuncDef] = []
main: List[Stmt] = []
while self.peek().kind != "EOF":
if self.peek().kind == ";":
self.next()
continue
if self.peek().kind == "define":
funcs.append(self.parse_funcdef())
else:
main.append(self.parse_stmt())
return Program(funcs, main)
def parse_stmt(self) -> Stmt:
tok = self.peek()
if tok.kind == "if":
self.next()
cond = self.parse_expr()
then_block = self.parse_block_or_stmt()
else_block = None
if self.peek().kind == "else":
self.next()
else_block = self.parse_block_or_stmt()
return If(cond, then_block, else_block)
if tok.kind == "while":
self.next()
cond = self.parse_expr()
body = self.parse_block_or_stmt()
return While(cond, body)
if tok.kind == "for":
self.next()
self.expect("(")
init = None
if self.peek().kind != ";":
init = self.parse_for_part()
self.expect(";")
cond = None
if self.peek().kind != ";":
cond = self.parse_expr()
self.expect(";")
step = None
if self.peek().kind != ")":
step = self.parse_for_part()
self.expect(")")
body = self.parse_block_or_stmt()
return For(init, cond, step, body)
if tok.kind == "return":
self.next()
expr = self.parse_expr()
self.expect(";")
return Return(expr)
if tok.kind == "print":
self.next()
expr = self.parse_expr()
self.expect(";")
return Print(expr)
if tok.kind == "IDENT":
# Lookahead for assignment or index assignment
if self.tokens[self.i + 1].kind in ("=", "["):
name = self.next().value
if self.peek().kind == "[":
self.next()
idx = self.parse_expr()
self.expect("]")
self.expect("=")
expr = self.parse_expr()
self.expect(";")
return Assign(Index(name, idx), expr)
self.expect("=")
expr = self.parse_expr()
self.expect(";")
return Assign(Var(name), expr)
expr = self.parse_expr()
self.expect(";")
return ExprStmt(expr)
def parse_for_part(self) -> Stmt:
tok = self.peek()
if tok.kind == "IDENT" and self.tokens[self.i + 1].kind in ("=", "["):
name = self.next().value
if self.peek().kind == "[":
self.next()
idx = self.parse_expr()
self.expect("]")
self.expect("=")
expr = self.parse_expr()
return Assign(Index(name, idx), expr)
self.expect("=")
expr = self.parse_expr()
return Assign(Var(name), expr)
return ExprStmt(self.parse_expr())
def parse_funcdef(self) -> FuncDef:
self.expect("define")
name = self.expect("IDENT").value
self.expect("(")
params: List[str] = []
if self.peek().kind != ")":
params.append(self.expect("IDENT").value)
while self.peek().kind == ",":
self.next()
params.append(self.expect("IDENT").value)
self.expect(")")
body = self.parse_block()
return FuncDef(name, params, body)
def parse_block(self) -> List[Stmt]:
self.expect("{")
stmts: List[Stmt] = []
while self.peek().kind != "}":
stmts.append(self.parse_stmt())
self.expect("}")
return stmts
def parse_block_or_stmt(self) -> List[Stmt]:
if self.peek().kind == "{":
return self.parse_block()
return [self.parse_stmt()]
def parse_expr(self) -> Expr:
return self.parse_cmp()
def parse_cmp(self) -> Expr:
node = self.parse_add()
while self.peek().kind in ("==", "!=", "<", "<=", ">", ">="):
op = self.next().kind
right = self.parse_add()
node = BinOp(op, node, right)
return node
def parse_add(self) -> Expr:
node = self.parse_mul()
while self.peek().kind in ("+", "-"):
op = self.next().kind
right = self.parse_mul()
node = BinOp(op, node, right)
return node
def parse_mul(self) -> Expr:
node = self.parse_unary()
while self.peek().kind in ("*", "/", "%"):
op = self.next().kind
right = self.parse_unary()
node = BinOp(op, node, right)
return node
def parse_unary(self) -> Expr:
if self.peek().kind in ("+", "-"):
op = self.next().kind
right = self.parse_unary()
if op == "+":
return right
return BinOp("-", Number(0.0), right)
return self.parse_primary()
def parse_primary(self) -> Expr:
tok = self.peek()
if tok.kind == "NUMBER":
self.next()
return Number(float(tok.value))
if tok.kind == "IDENT":
name = self.next().value
if self.peek().kind == "(":
self.next()
args: List[Expr] = []
if self.peek().kind != ")":
args.append(self.parse_expr())
while self.peek().kind == ",":
self.next()
args.append(self.parse_expr())
self.expect(")")
return Call(name, args)
if self.peek().kind == "[":
self.next()
idx = self.parse_expr()
self.expect("]")
return Index(name, idx)
return Var(name)
if tok.kind == "(":
self.next()
expr = self.parse_expr()
self.expect(")")
return expr
raise SyntaxError(f"Unexpected token {tok.kind} at {tok.pos}")
# ---------------------------
# LLVM IR Emitter
# ---------------------------
class IRBuilder:
def __init__(self) -> None:
self.lines: List[str] = []
self.tmp = 0
self.lbl = 0
def emit(self, line: str) -> None:
self.lines.append(line)
def fresh(self) -> str:
self.tmp += 1
return f"%t{self.tmp}"
def fresh_label(self, base: str) -> str:
self.lbl += 1
return f"{base}{self.lbl}"
def render(self) -> str:
return "\n".join(self.lines) + "\n"
class Compiler:
def __init__(self) -> None:
self.builder = IRBuilder()
self.vars: Dict[str, str] = {}
self.arrays: Dict[str, str] = {}
self.entry_insert_idx: Optional[int] = None
def compile(self, program: Program) -> str:
b = self.builder
b.emit('; ModuleID = "ucompile"')
b.emit('%Array = type { double*, i64 }')
b.emit('declare i32 @printf(i8*, ...)')
b.emit('declare i8* @realloc(i8*, i64)')
b.emit('declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)')
b.emit('@.fmt = private constant [4 x i8] c"%g\\0A\\00"')
b.emit('')
self.emit_array_helper()
b.emit('')
for func in program.funcs:
self.emit_func(func)
b.emit('')
b.emit('define i32 @main() {')
b.emit('entry:')
self.vars = {}
self.arrays = {}
self.entry_insert_idx = len(b.lines)
for stmt in program.main:
self.emit_stmt(stmt)
b.emit(' ret i32 0')
b.emit('}')
self.entry_insert_idx = None
return b.render()
def ensure_var(self, name: str) -> str:
if name in self.arrays:
raise ValueError(f"'{name}' is an array, not a scalar")
if name in self.vars:
return self.vars[name]
slot = f"%{name}"
self.insert_entry_lines(
[
f" {slot} = alloca double",
f" store double 0.0, double* {slot}",
]
)
self.vars[name] = slot
return slot
def ensure_array(self, name: str) -> str:
if name in self.vars:
raise ValueError(f"'{name}' is a scalar, not an array")
if name in self.arrays:
return self.arrays[name]
slot = f"%{name}"
self.insert_entry_lines(
[
f" {slot} = alloca %Array",
f" {slot}.data = getelementptr %Array, %Array* {slot}, i32 0, i32 0",
f" {slot}.size = getelementptr %Array, %Array* {slot}, i32 0, i32 1",
f" store double* null, double** {slot}.data",
f" store i64 0, i64* {slot}.size",
]
)
self.arrays[name] = slot
return slot
def emit_stmt(self, stmt: Stmt) -> None:
if isinstance(stmt, Assign):
val = self.emit_expr(stmt.expr)
if isinstance(stmt.target, Var):
slot = self.ensure_var(stmt.target.name)
self.builder.emit(f" store double {val}, double* {slot}")
return
if isinstance(stmt.target, Index):
ptr = self.emit_index_ptr(stmt.target)
self.builder.emit(f" store double {val}, double* {ptr}")
return
raise TypeError(f"Invalid assignment target: {type(stmt.target)}")
return
if isinstance(stmt, If):
self.emit_if(stmt)
return
if isinstance(stmt, While):
self.emit_while(stmt)
return
if isinstance(stmt, For):
self.emit_for(stmt)
return
if isinstance(stmt, Return):
val = self.emit_expr(stmt.expr)
self.builder.emit(f" ret double {val}")
# Start a fresh label so following statements are in a new block.
cont = self.builder.fresh_label("after_ret")
self.builder.emit(f"{cont}:")
return
if isinstance(stmt, Print):
val = self.emit_expr(stmt.expr)
fmt = self.builder.fresh()
self.builder.emit(
f" {fmt} = getelementptr [4 x i8], [4 x i8]* @.fmt, i32 0, i32 0"
)
self.builder.emit(f" call i32 (i8*, ...) @printf(i8* {fmt}, double {val})")
return
if isinstance(stmt, ExprStmt):
self.emit_expr(stmt.expr)
return
raise TypeError(f"Unknown stmt type: {type(stmt)}")
def emit_func(self, func: FuncDef) -> None:
b = self.builder
params_sig = ", ".join(f"double %{p}" for p in func.params)
b.emit(f"define double @{func.name}({params_sig}) {{")
b.emit("entry:")
self.vars = {}
self.arrays = {}
self.entry_insert_idx = len(b.lines)
for p in func.params:
slot = f"%{p}"
alloca = f"%{p}.addr"
self.insert_entry_lines(
[
f" {alloca} = alloca double",
f" store double {slot}, double* {alloca}",
]
)
self.vars[p] = alloca
for s in func.body:
self.emit_stmt(s)
b.emit(" ret double 0.0")
b.emit("}")
self.entry_insert_idx = None
def insert_entry_lines(self, lines: List[str]) -> None:
if self.entry_insert_idx is None:
raise RuntimeError("Entry insert position not set")
for line in lines:
self.builder.lines.insert(self.entry_insert_idx, line)
self.entry_insert_idx += 1
def emit_if(self, stmt: If) -> None:
b = self.builder
cond = self.emit_cond(stmt.cond)
then_lbl = b.fresh_label("then")
else_lbl = b.fresh_label("else") if stmt.else_block is not None else None
end_lbl = b.fresh_label("endif")
if else_lbl is None:
b.emit(f" br i1 {cond}, label %{then_lbl}, label %{end_lbl}")
else:
b.emit(f" br i1 {cond}, label %{then_lbl}, label %{else_lbl}")
b.emit(f"{then_lbl}:")
for s in stmt.then_block:
self.emit_stmt(s)
b.emit(f" br label %{end_lbl}")
if else_lbl is not None:
b.emit(f"{else_lbl}:")
for s in stmt.else_block or []:
self.emit_stmt(s)
b.emit(f" br label %{end_lbl}")
b.emit(f"{end_lbl}:")
def emit_while(self, stmt: While) -> None:
b = self.builder
cond_lbl = b.fresh_label("while_cond")
body_lbl = b.fresh_label("while_body")
end_lbl = b.fresh_label("while_end")
b.emit(f" br label %{cond_lbl}")
b.emit(f"{cond_lbl}:")
cond = self.emit_cond(stmt.cond)
b.emit(f" br i1 {cond}, label %{body_lbl}, label %{end_lbl}")
b.emit(f"{body_lbl}:")
for s in stmt.body:
self.emit_stmt(s)
b.emit(f" br label %{cond_lbl}")
b.emit(f"{end_lbl}:")
def emit_for(self, stmt: For) -> None:
b = self.builder
if stmt.init is not None:
self.emit_stmt(stmt.init)
cond_lbl = b.fresh_label("for_cond")
body_lbl = b.fresh_label("for_body")
step_lbl = b.fresh_label("for_step")
end_lbl = b.fresh_label("for_end")
b.emit(f" br label %{cond_lbl}")
b.emit(f"{cond_lbl}:")
if stmt.cond is None:
b.emit(f" br label %{body_lbl}")
else:
cond = self.emit_cond(stmt.cond)
b.emit(f" br i1 {cond}, label %{body_lbl}, label %{end_lbl}")
b.emit(f"{body_lbl}:")
for s in stmt.body:
self.emit_stmt(s)
b.emit(f" br label %{step_lbl}")
b.emit(f"{step_lbl}:")
if stmt.step is not None:
self.emit_stmt(stmt.step)
b.emit(f" br label %{cond_lbl}")
b.emit(f"{end_lbl}:")
def emit_cond(self, expr: Expr) -> str:
b = self.builder
if isinstance(expr, BinOp) and expr.op in ("==", "!=", "<", "<=", ">", ">="):
left = self.emit_expr(expr.left)
right = self.emit_expr(expr.right)
tmp = b.fresh()
pred = {
"==": "oeq",
"!=": "one",
"<": "olt",
"<=": "ole",
">": "ogt",
">=": "oge",
}[expr.op]
b.emit(f" {tmp} = fcmp {pred} double {left}, {right}")
return tmp
val = self.emit_expr(expr)
tmp = b.fresh()
b.emit(f" {tmp} = fcmp one double {val}, 0.0")
return tmp
def emit_expr(self, expr: Expr) -> str:
b = self.builder
if isinstance(expr, Number):
# Emit literal directly.
return self.format_double(expr.value)
if isinstance(expr, Var):
slot = self.ensure_var(expr.name)
tmp = b.fresh()
b.emit(f" {tmp} = load double, double* {slot}")
return tmp
if isinstance(expr, Index):
ptr = self.emit_index_ptr(expr)
tmp = b.fresh()
b.emit(f" {tmp} = load double, double* {ptr}")
return tmp
if isinstance(expr, Call):
args = [self.emit_expr(a) for a in expr.args]
args_sig = ", ".join(f"double {a}" for a in args)
tmp = b.fresh()
b.emit(f" {tmp} = call double @{expr.name}({args_sig})")
return tmp
if isinstance(expr, BinOp):
left = self.emit_expr(expr.left)
right = self.emit_expr(expr.right)
tmp = b.fresh()
if expr.op == "+":
b.emit(f" {tmp} = fadd double {left}, {right}")
elif expr.op == "-":
b.emit(f" {tmp} = fsub double {left}, {right}")
elif expr.op == "*":
b.emit(f" {tmp} = fmul double {left}, {right}")
elif expr.op == "/":
b.emit(f" {tmp} = fdiv double {left}, {right}")
elif expr.op == "%":
# fmod via frem
b.emit(f" {tmp} = frem double {left}, {right}")
elif expr.op in ("==", "!=", "<", "<=", ">", ">="):
pred = {
"==": "oeq",
"!=": "one",
"<": "olt",
"<=": "ole",
">": "ogt",
">=": "oge",
}[expr.op]
b.emit(f" {tmp} = fcmp {pred} double {left}, {right}")
as_double = b.fresh()
b.emit(f" {as_double} = uitofp i1 {tmp} to double")
return as_double
else:
raise ValueError(f"Unknown operator: {expr.op}")
return tmp
raise TypeError(f"Unknown expr type: {type(expr)}")
def emit_index_ptr(self, expr: Index) -> str:
b = self.builder
arr = self.ensure_array(expr.name)
idx_val = self.emit_expr(expr.index)
idx_i64 = b.fresh()
b.emit(f" {idx_i64} = fptosi double {idx_val} to i64")
ptr = b.fresh()
b.emit(f" {ptr} = call double* @__uc_array_getptr(%Array* {arr}, i64 {idx_i64})")
return ptr
def emit_array_helper(self) -> None:
b = self.builder
b.emit("define double* @__uc_array_getptr(%Array* %arr, i64 %idx) {")
b.emit("entry:")
b.emit(" %data_ptr = getelementptr %Array, %Array* %arr, i32 0, i32 0")
b.emit(" %size_ptr = getelementptr %Array, %Array* %arr, i32 0, i32 1")
b.emit(" %size = load i64, i64* %size_ptr")
b.emit(" %data = load double*, double** %data_ptr")
b.emit(" %need = icmp uge i64 %idx, %size")
b.emit(" br i1 %need, label %grow, label %ok")
b.emit("grow:")
b.emit(" %new_size = add i64 %idx, 1")
b.emit(" %new_bytes = mul i64 %new_size, 8")
b.emit(" %data_i8 = bitcast double* %data to i8*")
b.emit(" %new_i8 = call i8* @realloc(i8* %data_i8, i64 %new_bytes)")
b.emit(" %new_data = bitcast i8* %new_i8 to double*")
b.emit(" %delta = sub i64 %new_size, %size")
b.emit(" %new_region = getelementptr double, double* %new_data, i64 %size")
b.emit(" %new_region_i8 = bitcast double* %new_region to i8*")
b.emit(" %delta_bytes = mul i64 %delta, 8")
b.emit(" call void @llvm.memset.p0.i64(ptr %new_region_i8, i8 0, i64 %delta_bytes, i1 false)")
b.emit(" store double* %new_data, double** %data_ptr")
b.emit(" store i64 %new_size, i64* %size_ptr")
b.emit(" br label %ok")
b.emit("ok:")
b.emit(" %data2 = load double*, double** %data_ptr")
b.emit(" %ptr = getelementptr double, double* %data2, i64 %idx")
b.emit(" ret double* %ptr")
b.emit("}")
@staticmethod
def format_double(val: float) -> str:
# LLVM accepts decimal literals like 1.23 or 1.0. Keep it simple.
if val == float("inf"):
return "0x7FF0000000000000"
if val == float("-inf"):
return "0xFFF0000000000000"
if val != val: # NaN
return "0x7FF8000000000000"
s = repr(val)
if "e" in s or "E" in s:
# LLVM accepts scientific notation, keep as is.
return s
if "." not in s:
s += ".0"
return s
# ---------------------------
# CLI
# ---------------------------
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(description="Compile a tiny bc-like language to LLVM IR (.ll).")
parser.add_argument("input", help="Input script file")
parser.add_argument("-o", "--output", required=True, help="Output .ll file")
args = parser.parse_args(argv)
try:
with open(args.input, "r", encoding="utf-8") as f:
src = f.read()
tokens = lex(src)
parser_ = Parser(tokens)
program = parser_.parse()
compiler = Compiler()
ir = compiler.compile(program)
with open(args.output, "w", encoding="utf-8") as f:
f.write(ir)
except (SyntaxError, ValueError, TypeError) as e:
print(f"error: {e}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Terminal UI for ucompile: edit left, IR on right (updates on valid code)."""
from __future__ import annotations
import curses
import os
import shutil
import subprocess
import sys
import tempfile
from typing import List, Tuple, Optional
ROOT = os.path.abspath(os.path.dirname(__file__))
COMPILER = os.path.join(ROOT, "ucompile.py")
LLI = shutil.which("lli")
def parse_error_line(src: str, err: str) -> Optional[int]:
marker = " at "
if marker not in err:
return None
try:
pos = int(err.rsplit(marker, 1)[1])
except ValueError:
return None
if pos < 0:
return None
line = src[:pos].count("\n")
return line
def compile_source(src: str) -> Tuple[bool, str, Optional[int]]:
with tempfile.TemporaryDirectory() as td:
in_path = os.path.join(td, "input.bc")
out_path = os.path.join(td, "out.ll")
with open(in_path, "w", encoding="utf-8") as f:
f.write(src)
proc = subprocess.run(
[sys.executable, COMPILER, in_path, "-o", out_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=ROOT,
)
if proc.returncode != 0:
err = proc.stderr.strip() or proc.stdout.strip() or "Compilation failed"
return False, err, parse_error_line(src, err)
with open(out_path, "r", encoding="utf-8") as f:
return True, f.read(), None
def run_ir(ir: str) -> Tuple[bool, str]:
if not LLI:
return False, "lli not found in PATH"
with tempfile.TemporaryDirectory() as td:
ll_path = os.path.join(td, "out.ll")
with open(ll_path, "w", encoding="utf-8") as f:
f.write(ir)
proc = subprocess.run(
[LLI, ll_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=ROOT,
)
if proc.returncode != 0:
err = proc.stderr.strip() or proc.stdout.strip() or "Execution failed"
return False, err
return True, proc.stdout
def split_lines(text: str) -> List[str]:
if not text:
return [""]
return text.split("\n")
def clamp(n: int, lo: int, hi: int) -> int:
return max(lo, min(hi, n))
def run(stdscr: "curses._CursesWindow", initial: str) -> None:
curses.curs_set(1)
stdscr.nodelay(False)
stdscr.keypad(True)
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_CYAN, -1) # divider
curses.init_pair(2, curses.COLOR_GREEN, -1) # ok status
curses.init_pair(3, curses.COLOR_RED, -1) # error status
curses.init_pair(4, curses.COLOR_YELLOW, -1) # mode/status info
curses.init_pair(5, curses.COLOR_MAGENTA, -1) # keywords
curses.init_pair(6, curses.COLOR_BLUE, -1) # numbers
curses.init_pair(7, curses.COLOR_GREEN, -1) # comments
curses.init_pair(8, curses.COLOR_CYAN, -1) # operators
buf = split_lines(initial)
cur_y = 0
cur_x = 0
top = 0
last_good_ir = ""
last_good_out = ""
last_error = ""
error_line: Optional[int] = None
stale = False
show_ir = False
def current_source() -> str:
return "\n".join(buf)
# Initial compile
ok, out, err_line = compile_source(current_source())
if ok:
last_good_ir = out
run_ok, run_out = run_ir(out)
if run_ok:
last_good_out = run_out
stale = False
error_line = None
else:
last_error = run_out
stale = True
error_line = None
else:
last_error = out
stale = True
error_line = err_line
def draw_line_with_syntax(y: int, x: int, text: str, max_w: int, in_block: bool) -> bool:
if max_w <= 0:
return in_block
i = 0
col = 0
n = len(text)
while i < n and col < max_w:
c = text[i]
nxt = text[i + 1] if i + 1 < n else ""
if in_block:
# Consume until end of block comment
start = i
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
i += 1
if i + 1 < n:
i += 2
in_block = False
else:
i = n
seg = text[start:i]
stdscr.addnstr(y, x + col, seg, max_w - col, curses.color_pair(7))
col += len(seg)
continue
# Line comment
if c == "#":
seg = text[i:]
stdscr.addnstr(y, x + col, seg, max_w - col, curses.color_pair(7))
break
if c == "/" and nxt == "/":
seg = text[i:]
stdscr.addnstr(y, x + col, seg, max_w - col, curses.color_pair(7))
break
if c == "/" and nxt == "*":
in_block = True
stdscr.addnstr(y, x + col, "/*", max_w - col, curses.color_pair(7))
i += 2
col += 2
continue
# Numbers
if c.isdigit() or (c == "." and i + 1 < n and text[i + 1].isdigit()):
start = i
saw_dot = False
while i < n and (text[i].isdigit() or text[i] == "."):
if text[i] == ".":
if saw_dot:
break
saw_dot = True
i += 1
seg = text[start:i]
stdscr.addnstr(y, x + col, seg, max_w - col, curses.color_pair(6))
col += len(seg)
continue
# Identifiers / keywords
if c.isalpha() or c == "_":
start = i
i += 1
while i < n and (text[i].isalnum() or text[i] == "_"):
i += 1
seg = text[start:i]
if seg in ("print", "if", "else", "while", "for", "define", "return"):
attr = curses.color_pair(5)
else:
attr = 0
stdscr.addnstr(y, x + col, seg, max_w - col, attr)
col += len(seg)
continue
# Operators / punctuation
if c in "+-*/%()=;{},[]<>!":
stdscr.addnstr(y, x + col, c, max_w - col, curses.color_pair(8))
i += 1
col += 1
continue
# Default
stdscr.addnstr(y, x + col, c, max_w - col)
i += 1
col += 1
return in_block
while True:
stdscr.erase()
rows, cols = stdscr.getmaxyx()
left_w = max(20, cols // 2)
right_w = cols - left_w - 1
edit_h = rows - 1
# Draw divider
for r in range(edit_h):
if left_w < cols:
stdscr.addch(r, left_w, ord("|"), curses.color_pair(1))
# Render left editor with syntax highlighting
in_block = False
for i in range(edit_h):
line_idx = top + i
if line_idx >= len(buf):
break
line = buf[line_idx]
in_block = draw_line_with_syntax(i, 0, line, left_w - 1, in_block)
if error_line is not None and line_idx == error_line:
stdscr.addnstr(i, max(0, left_w - 2), "!", 1, curses.color_pair(3))
# Render right output (IR or program output)
right_text = last_good_ir if show_ir else last_good_out
ir_lines = split_lines(right_text)
for i in range(edit_h):
if i >= len(ir_lines):
break
line = ir_lines[i]
if right_w > 0:
stdscr.addnstr(i, left_w + 1, line, right_w)
# Status bar
status = "CTRL+Q: quit | CTRL+O: toggle IR/output"
mode = "MODE: IR" if show_ir else "MODE: OUTPUT"
stdscr.addnstr(rows - 1, 0, status, cols - 1, curses.color_pair(4))
mode_x = min(len(status) + 3, cols - 1)
stdscr.addnstr(rows - 1, mode_x, mode, cols - mode_x - 1, curses.color_pair(4))
if stale:
err = "ERROR (stale): " + last_error
if error_line is not None:
err += f" (line {error_line + 1})"
err_x = min(mode_x + len(mode) + 3, cols - 1)
stdscr.addnstr(rows - 1, err_x, err, cols - err_x - 1, curses.color_pair(3))
else:
ok = "OK"
ok_x = min(mode_x + len(mode) + 3, cols - 1)
stdscr.addnstr(rows - 1, ok_x, ok, cols - ok_x - 1, curses.color_pair(2))
# Cursor position
screen_y = cur_y - top
if 0 <= screen_y < edit_h:
stdscr.move(screen_y, clamp(cur_x, 0, left_w - 2))
stdscr.refresh()
ch = stdscr.getch()
if ch in (ord("\x11"),): # Ctrl+Q
break
elif ch in (ord("\x0f"),): # Ctrl+O
show_ir = not show_ir
elif ch in (curses.KEY_LEFT,):
if cur_x > 0:
cur_x -= 1
elif cur_y > 0:
cur_y -= 1
cur_x = len(buf[cur_y])
elif ch in (curses.KEY_RIGHT,):
if cur_x < len(buf[cur_y]):
cur_x += 1
elif cur_y + 1 < len(buf):
cur_y += 1
cur_x = 0
elif ch in (curses.KEY_UP,):
if cur_y > 0:
cur_y -= 1
cur_x = min(cur_x, len(buf[cur_y]))
elif ch in (curses.KEY_DOWN,):
if cur_y + 1 < len(buf):
cur_y += 1
cur_x = min(cur_x, len(buf[cur_y]))
elif ch in (curses.KEY_BACKSPACE, 127, 8):
if cur_x > 0:
line = buf[cur_y]
buf[cur_y] = line[: cur_x - 1] + line[cur_x:]
cur_x -= 1
elif cur_y > 0:
prev_len = len(buf[cur_y - 1])
buf[cur_y - 1] += buf[cur_y]
del buf[cur_y]
cur_y -= 1
cur_x = prev_len
elif ch in (curses.KEY_DC,):
line = buf[cur_y]
if cur_x < len(line):
buf[cur_y] = line[:cur_x] + line[cur_x + 1 :]
elif cur_y + 1 < len(buf):
buf[cur_y] += buf[cur_y + 1]
del buf[cur_y + 1]
elif ch in (10, 13): # Enter
line = buf[cur_y]
left = line[:cur_x]
right = line[cur_x:]
buf[cur_y] = left
buf.insert(cur_y + 1, right)
cur_y += 1
cur_x = 0
elif ch == 9: # Tab
line = buf[cur_y]
buf[cur_y] = line[:cur_x] + " " + line[cur_x:]
cur_x += 2
elif 32 <= ch <= 126:
line = buf[cur_y]
buf[cur_y] = line[:cur_x] + chr(ch) + line[cur_x:]
cur_x += 1
# Keep cursor in view
if cur_y < top:
top = cur_y
elif cur_y >= top + edit_h:
top = cur_y - edit_h + 1
# Recompile on any keypress that could change buffer
if ch not in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT):
ok, out, err_line = compile_source(current_source())
if ok:
last_good_ir = out
run_ok, run_out = run_ir(out)
if run_ok:
last_good_out = run_out
stale = False
last_error = ""
error_line = None
else:
stale = True
last_error = run_out
error_line = None
else:
stale = True
last_error = out
error_line = err_line
def main() -> int:
initial = ""
if len(sys.argv) > 1:
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
initial = f.read()
curses.wrapper(run, initial)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment