Day 29

A Compiler for the Forge Language

Try it!

You may need to wait or refresh to let it load



Source Code


def try_num(s):
    try:
        return float(s) if '.' in s else int(s)
    except ValueError:
        return s

def compile_forge(source):
    lines = [l for l in source.splitlines() if l.strip()]
    out = ["_mem = {}"]

    def emit(words, indent=0):
        pad = "    " * indent
        if not words:
            return
        cmd = words[0]
        if cmd == 'set':
            val = try_num(words[3])
            out.append(f"{pad}_mem['{words[1]}'] = {repr(val)}")
        elif cmd == 'print':
            out.append(f"{pad}print(_mem.get('{words[1]}', 'undefined'))")
        elif cmd == 'add':
            out.append(f"{pad}print(_mem['{words[1]}'] + _mem['{words[3]}'])")
        elif cmd == 'subtract':
            out.append(f"{pad}print(_mem['{words[1]}'] - _mem['{words[3]}'])")
        elif cmd == 'multiply':
            out.append(f"{pad}print(_mem['{words[1]}'] * _mem['{words[3]}'])")
        elif cmd == 'divide':
            out.append(f"{pad}if _mem['{words[3]}'] == 0:")
            out.append(f"{pad}    print('Error: division by zero')")
            out.append(f"{pad}else:")
            out.append(f"{pad}    print(_mem['{words[1]}'] / _mem['{words[3]}'])")
        elif cmd == 'ask':
            out.append(f"{pad}_raw = input('Enter {words[1]}: ')")
            out.append(f"{pad}try:")
            out.append(f"{pad}    _mem['{words[1]}'] = float(_raw) if '.' in _raw else int(_raw)")
            out.append(f"{pad}except ValueError:")
            out.append(f"{pad}    _mem['{words[1]}'] = _raw")
        elif cmd == 'loop':
            count = words[1]
            try:
                out.append(f"{pad}for _i in range({int(count)}):")
            except ValueError:
                out.append(f"{pad}for _i in range(int(_mem.get('{count}', 0))):")
            emit(words[2:], indent + 1)
        elif cmd == 'if':
            var, op, raw = words[1], words[2], words[3]
            b = try_num(raw)
            then_i = words.index('then')
            out.append(f"{pad}if _mem.get('{var}') {op} {repr(b)}:")
            emit(words[then_i + 1:], indent + 1)
        elif cmd == 'func':
            name = words[1]
            then_i = words.index('then')
            out.append(f"{pad}def _fn_{name}():")
            emit(words[then_i + 1:], indent + 1)
        elif cmd == 'call':
            out.append(f"{pad}_fn_{words[1]}()")

    for line in lines:
        emit(line.split())

    return '\n'.join(out)

while True:
    forge_lines = []
    while True:
        ln = input("forge> ")
        if ln.strip().lower() == 'exit':
            forge_lines = None
            break
        if ln.strip() == '':
            break
        forge_lines.append(ln)

    if forge_lines is None:
        break
    if not forge_lines:
        continue

    python_code = compile_forge('\n'.join(forge_lines))
    print(python_code)
    exec(python_code)

Description


Context: Days 27 and 28 built Forge as an interpreted language — each command was parsed and executed immediately. For Day 29 I wanted to understand the difference between an interpreter and a compiler by building the latter: instead of running Forge commands directly, the compiler reads the full program first, generates equivalent Python code, and then executes that output.

Challenges: The main shift was moving from executing one word-list at a time to emitting indented Python source. Nested constructs — loop, if...then, and func...then — required tracking indentation depth and recursing into emit() rather than run_cmd(). The compiler also had to handle the loop count being either a literal or a variable name, generating different Python for each case.

Result: A working Forge-to-Python transpiler. You enter a multi-line Forge program, hit a blank line, and the compiler prints the generated Python before running it — so you can see exactly what the compiler decided to produce. Example:

set x to 4
func double then multiply x and x
loop 2 call double
generates and runs the full Python equivalent, outputting 16 twice.

Previous Day
Next Day