Skip to content

Adding Backends

cipherat edited this page Apr 15, 2026 · 3 revisions

Adding Backends

A backend is a subclass of CodegenBackend that overrides ~10 methods. The base class handles all IR tree walking — you only define target-specific output.

Step 1: Create the backend file

Create scratch2c/codegen/arduino.py (or whatever your target is):

from __future__ import annotations
from .base import CodegenBackend, _escape_c


class ArduinoBackend(CodegenBackend):

    def file_header(self) -> list[str]:
        return [
            "/* Generated by scratch2c */",
            '#include "scratch_runtime.h"',
            "",
        ]

    def file_footer(self) -> list[str]:
        return []

    def emit_say(self, expr_code: str, is_string: bool) -> str:
        if is_string:
            return f'Serial.println({expr_code});'
        return f'Serial.println((long){expr_code});'

    def main_function_name(self) -> str:
        return "setup"

    def main_signature_open(self) -> str:
        return "void setup() {\n    Serial.begin(9600);"

    def main_signature_close(self) -> str:
        return ""

    def exit_function_name(self) -> str:
        return "loop"

    def exit_signature_open(self) -> str:
        return "void loop() {"

    def exit_signature_close(self) -> str:
        return ""

    def declare_long_variable(self, c_name: str, initial: str) -> str:
        return f"long {c_name} = {initial};"

    def declare_string_variable(self, c_name: str, initial: str) -> str:
        escaped = _escape_c(initial)
        return f'char {c_name}[256] = "{escaped}";'

    def wait_comment(self, duration_code: str) -> str:
        return f"delay({duration_code} * 1000);"

Step 2: Register it

In scratch2c/codegen/__init__.py:

from .arduino import ArduinoBackend

BACKENDS: dict[str, type[CodegenBackend]] = {
    "userspace": UserspaceBackend,
    "kernel": KernelBackend,
    "arduino": ArduinoBackend,
}

Step 3: Add to CLI

In scratch2c/cli.py, add "arduino" to the choices list:

parser.add_argument(
    "-b", "--backend",
    default="userspace",
    choices=["userspace", "kernel", "arduino"],
)

Step 4: Write tests

Create tests/test_codegen_arduino.py. Test the properties that matter for your target — headers, output function, variable declarations:

def _generate_arduino(project_json: dict) -> str:
    project = build_ir(project_json)
    infer_types(project)
    return ArduinoBackend().generate(project)

class TestArduinoCodegen:
    def test_serial_println(self, simple_say_json):
        code = _generate_arduino(simple_say_json)
        assert "Serial.println" in code

    def test_setup_function(self, simple_say_json):
        code = _generate_arduino(simple_say_json)
        assert "void setup()" in code

Optional overrides

def emit_stop(self) -> str:
    """Default is 'return;'. Override if main returns non-void."""
    return "return;"

def _needs_exit(self) -> bool:
    """Default is False. Override if target always needs an exit function."""
    return False

The kernel backend overrides both: emit_stop() returns "return 0;" (because scratch_init returns int) and _needs_exit() returns True (kbuild requires module_exit).

Runtime header considerations

The runtime header uses #ifdef __KERNEL__ to switch between userspace and kernel implementations. If your target has its own constraints (no snprintf, no strlen), you may need to add a third #ifdef branch or create a separate runtime header.

Clone this wiki locally