Tutorial 3: Transpiling to Python¶
Build a tiny source language that compiles to runnable Python, with if
and loop block constructs. Estimated time: 15 minutes.
Goal¶
Source:
Output (valid Python):
Step 1 — basic functions¶
extension py
context
imports []
end
function import
arg literal "import"
arg capture name ident
append context.imports name
end
function say
arg capture msg any
write `print(${msg})
`
end
function assign
arg capture name ident
arg literal "="
arg capture value any
write `${name} = ${value}
`
end
Try capy run so far — should handle import, say, and x = ....
Step 2 — add the if block¶
The if block emits Python if cond: + indented body. The body is the
already-rendered output of inner statements.
function if
arg literal "if"
arg capture cond any
block_closer end
write `if ${cond}:
${indent 4 body}
`
end
function end
end
Two key bits:
block_closer end— body is indent-delimited; after DEDENT, theendfunction must match.${indent 4 body}—bodyis the concatenated rendered output of the inner statements;indent 4 bodyprefixes every line with 4 spaces for Python's syntax.
Step 3 — add the loop block¶
function loop
arg literal "loop"
arg capture var ident
arg literal "in"
arg capture iter any
block_closer end
write `for ${var} in ${iter}:
${indent 4 body}
`
end
Same shape — emits Python for x in xs: + indented body.
Step 4 — file template¶
Assemble imports at the top, body below:
Each write emits exactly the bytes inside the backticks — no
whitespace-trimming sigils needed; you control the output directly.
Step 5 — run¶
You should see valid Python that runs:
What just happened¶
The transpiler model has three moving parts:
- Body — concatenated per-statement template output. Flows from inner statements outward.
- Context — state collected across all statements, regardless of nesting. Used for imports here.
- File template — final assembler that gets both.
Block functions reference ${body} (or ${indent N body} for
indented output) to get their inner output. The file template
references both ${context...} and ${body} / write body.
Try it¶
- Add an
elsecompanion by defining a second pattern:else_ifthat emitselse:inside the parent if's body. (Hint: harder than it looks; you might need to accumulate body parts into context.) - Make
sayuse${msg | toQuoted}so the source can besay hello(without quotes). - Add
import_as <name> as <alias>that emits Pythonimport name as alias.
Next¶
Tutorial 4: Custom Operators goes deeper into multi-token patterns and the auto-name-prepend rule.