Examples

SVG-to-Turtle compiler

This example reads a small subset of SVG and compiles it into a Python module that reproduces the drawing with the standard-library turtle module. It demonstrates several fluent-codegen features:

  • Building a Module with multiple functions.

  • Using the E-object system (enames, .e) for natural-looking math and method calls.

  • Automatic name deduplication — the same local name (pos, heading) is used for each <use> element, and the scope manager appends suffixes automatically.

Supported SVG elements:

  • <line x1=… y1=… x2=… y2=…> — a straight line segment.

  • <defs> containing <line> elements with id attributes — compiled into helper functions.

  • <use href="#id" x="..." y="..."> — compiled into code that calls the helper.

The compiler script

docs/examples/svg_to_turtle/svg_to_turtle.py
#!/usr/bin/env python3
"""SVG-to-Turtle compiler.

Reads a subset of SVG (straight-line segments) and emits a Python module
that reproduces the drawing using the standard-library ``turtle`` module.

This is an example of using **fluent-codegen** to compile one language
into Python.

Supported SVG elements
----------------------
* ``<line x1=… y1=… x2=… y2=…>``  – a single line segment
* ``<defs>`` containing ``<line>`` elements with ``id`` attributes
* ``<use href="#id" x="tx" y="ty">``

Usage::

    python svg_to_turtle.py drawing.svg

Produces ``drawing.py`` with a ``draw(t)`` function that accepts a
``turtle.Turtle``.
"""

from __future__ import annotations

import sys
import xml.etree.ElementTree as ET
from pathlib import Path

from fluent_codegen import codegen

# ---------------------------------------------------------------------------
# SVG parsing helpers
# ---------------------------------------------------------------------------

SVG_NS = "http://www.w3.org/2000/svg"
XLINK_NS = "http://www.w3.org/1999/xlink"


def _float(element: ET.Element, attr: str) -> float:
    """Extract a float attribute from an SVG element."""
    return float(element.attrib[attr])


def _parse_x_y(element: ET.Element) -> tuple[float, float]:
    return (int(element.attrib.get("x", "0")), int(element.attrib.get("y", "0")))


# ---------------------------------------------------------------------------
# Code generation
# ---------------------------------------------------------------------------


def _emit_line(
    block: codegen.Block,
    t: codegen.E,
    start: codegen.E,
    x1: float,
    y1: float,
    x2: float,
    y2: float,
) -> None:
    """Emit turtle commands to draw a single line from (x1,y1) to (x2,y2)."""
    block.add_statement(t.penup())
    block.add_statement(t.goto(start[0] + x1, start[1] + y1))
    block.add_statement(t.pendown())
    block.add_statement(t.goto(start[0] + x2, start[1] + y2))


def compile_svg(svg_path: str | Path) -> str:
    """Compile an SVG file into Python source code."""
    tree = ET.parse(svg_path)
    root = tree.getroot()

    module = codegen.Module()
    module.add_comment(f"Generated from {Path(svg_path).name} by svg_to_turtle.py")
    module.add_comment("Do not edit — regenerate from the SVG source.")

    # Import turtle so the generated module is self-contained.
    _, turtle_mod = module.create_import("turtle")

    # Type annotation for the turtle parameter: turtle.Turtle
    turtle_type = turtle_mod.attr("Turtle")
    none_type = codegen.constants.None_
    pos_type = module.enames.tuple[module.enames.int, module.enames.int]

    # We'll use the E-object for the turtle parameter throughout.
    # First, collect <defs> definitions (Phase 2).
    defs: dict[str, ET.Element] = {}
    for defs_elem in root.iter(f"{{{SVG_NS}}}defs"):
        for child in defs_elem:
            tag = child.tag.removeprefix(f"{{{SVG_NS}}}")
            elem_id = child.get("id")
            if elem_id and tag == "line":
                defs[elem_id] = child

    # Create a helper function for each definition.
    def_func_names: dict[str, codegen.Name] = {}
    for def_id, elem in defs.items():
        func, func_name = module.create_function(
            f"_draw_{def_id}",
            args=[
                codegen.FunctionArg.standard("t", annotation=turtle_type),
                codegen.FunctionArg.standard("start", annotation=pos_type),
            ],
            return_type=none_type,
        )

        _emit_line(
            func.body,
            func.enames.t,
            func.enames.start,
            _float(elem, "x1"),
            _float(elem, "y1"),
            _float(elem, "x2"),
            _float(elem, "y2"),
        )
        def_func_names[def_id] = func_name

    # Main draw function.
    draw_func, draw_name = module.create_function(
        "draw",
        args=[codegen.FunctionArg.standard("t", annotation=turtle_type)],
        return_type=none_type,
    )
    t_e = draw_func.enames.t
    start = draw_func.body.assign("start", codegen.auto((0, 0)))

    for child in root:
        tag = child.tag.removeprefix(f"{{{SVG_NS}}}")

        if tag == "defs":
            continue  # already handled above

        if tag == "line":
            _emit_line(
                draw_func.body,
                t_e,
                start.e,
                _float(child, "x1"),
                _float(child, "y1"),
                _float(child, "x2"),
                _float(child, "y2"),
            )

        elif tag == "use":
            href = child.get("href") or child.get(f"{{{XLINK_NS}}}href") or ""
            ref_id = href.lstrip("#")
            if ref_id not in def_func_names:
                raise ValueError(f"<use> references unknown id {ref_id!r}")

            tx, ty = _parse_x_y(child)

            # Save turtle state
            pos = draw_func.body.assign("pos", t_e.position())
            heading = draw_func.body.assign("heading", t_e.heading())

            # Call the helper
            draw_func.body.add_statement(def_func_names[ref_id].e(t_e, (tx, ty)))

            # Restore
            draw_func.body.add_statement(t_e.penup())
            draw_func.body.add_statement(t_e.goto(pos.e))
            draw_func.body.add_statement(t_e.setheading(heading.e))

    # if __name__ == "__main__" block
    dunder_name = module.scope.name("__name__")
    if_main = module.create_if()
    main_block = if_main.create_if_branch(dunder_name.e == "__main__")
    t_var = main_block.assign("t", turtle_mod.e.Turtle())
    main_block.add_statement(draw_name.e(t_var.e))
    main_block.add_statement(turtle_mod.e.done())

    return module.as_python_source()


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------


def main() -> None:
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <input.svg>", file=sys.stderr)
        sys.exit(1)

    svg_path = Path(sys.argv[1])
    if not svg_path.exists():
        print(f"File not found: {svg_path}", file=sys.stderr)
        sys.exit(1)

    output = svg_path.with_suffix(".py")
    source = compile_svg(svg_path)
    output.write_text(source + "\n")
    print(f"Wrote {output}")


if __name__ == "__main__":
    main()

Sample input

A simple house shape built from <line> elements and <use> references to reusable beams/walls defined in <defs>:

_images/house.svg
house.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   width="200"
   height="200"
   version="1.1"
   id="svg17"
   sodipodi:docname="house.svg"
   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <sodipodi:namedview
     id="namedview19"
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0.0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     showgrid="false"
     inkscape:zoom="2.655"
     inkscape:cx="117.3258"
     inkscape:cy="67.231638"
     inkscape:window-width="1920"
     inkscape:window-height="1024"
     inkscape:window-x="0"
     inkscape:window-y="0"
     inkscape:window-maximized="1"
     inkscape:current-layer="svg17" />
  <!-- A simple house shape made of line segments -->
  <defs>
    <!-- A horizontal beam -->
    <line
       id="beam"
       x1="0"
       y1="0"
       x2="80"
       y2="0"
       style="fill:#000000;stroke:#000000;stroke-opacity:1" />
    <!-- A vertical wall -->
    <line
       id="wall"
       x1="0"
       y1="0"
       x2="0"
       y2="80"
       style="fill:#000000;stroke:#000000;stroke-opacity:1" />
  </defs>
  <!-- Walls -->
  <use
     href="#wall"
     x="40" y="60"
     id="use1" />

  <use
     href="#wall"
     x="120" y="60"
     id="use2" />
  <!-- Roof -->
  <line
     x1="30"
     y1="60"
     x2="80"
     y2="20"
     id="line9"
     style="fill:#000000;stroke:#000000;stroke-opacity:1" />
  <line
     x1="80"
     y1="20"
     x2="130"
     y2="60"
     id="line11"
     style="fill:#000000;stroke:#000000;stroke-opacity:1" />
  <!-- Floor and ceiling as reused beams -->
  <use
     href="#beam"
     x="40" y="60"
     id="use3" />
  <use
     href="#beam"
     x="40" y="140"
     id="use4" />

</svg>

Generated output

Running python svg_to_turtle.py house.svg produces:

house.py (generated)
# Generated from house.svg by svg_to_turtle.py
# Do not edit — regenerate from the SVG source.
import turtle


def _draw_beam(t: turtle.Turtle, start: tuple[int, int]) -> None:
    t.penup()
    t.goto(start[0] + 0.0, start[1] + 0.0)
    t.pendown()
    t.goto(start[0] + 80.0, start[1] + 0.0)


def _draw_wall(t: turtle.Turtle, start: tuple[int, int]) -> None:
    t.penup()
    t.goto(start[0] + 0.0, start[1] + 0.0)
    t.pendown()
    t.goto(start[0] + 0.0, start[1] + 80.0)


def draw(t: turtle.Turtle) -> None:
    start = (0, 0)
    pos = t.position()
    heading = t.heading()
    _draw_wall(t, (40, 60))
    t.penup()
    t.goto(pos)
    t.setheading(heading)
    pos_2 = t.position()
    heading_2 = t.heading()
    _draw_wall(t, (120, 60))
    t.penup()
    t.goto(pos_2)
    t.setheading(heading_2)
    t.penup()
    t.goto(start[0] + 30.0, start[1] + 60.0)
    t.pendown()
    t.goto(start[0] + 80.0, start[1] + 20.0)
    t.penup()
    t.goto(start[0] + 80.0, start[1] + 20.0)
    t.pendown()
    t.goto(start[0] + 130.0, start[1] + 60.0)
    pos_3 = t.position()
    heading_3 = t.heading()
    _draw_beam(t, (40, 60))
    t.penup()
    t.goto(pos_3)
    t.setheading(heading_3)
    pos_4 = t.position()
    heading_4 = t.heading()
    _draw_beam(t, (40, 140))
    t.penup()
    t.goto(pos_4)
    t.setheading(heading_4)


if __name__ == "__main__":
    t = turtle.Turtle()
    draw(t)
    turtle.done()

Key points to note in the generated code:

  • _draw_beam() and _draw_wall() are helper functions compiled from the <defs> element, with names based on the element names.

  • Each <use> saves the turtle position, calls the helper, and restores.

  • The local variables pos, heading are auto-suffixed to pos_2, heading_2 for the second <use>.