Adding support for a new block is the most common contribution. Here's exactly what to touch.
- Statement = a block that does something (set variable, say, repeat, if)
- Expression (reporter) = a block that returns a value (add, join, variable read)
Edit scratch2c/ir_builder.py only:
- Add your opcode to
_get_statement_builders():
def _get_statement_builders() -> dict:
return {
# ... existing entries ...
"sensing_askandwait": _stmt_ask_and_wait, # ← add this
}- Write the builder function right below the others:
def _stmt_ask_and_wait(block: dict, blocks: dict) -> Say:
# For now, treat "ask" like "say" — we can refine later
message = _resolve_input(block, "QUESTION", blocks)
return Say(message=message)That's it. The IR, type inference, and code generation already know how to handle Say statements.
If your new opcode needs a new IR node (not just reusing an existing one):
- Add a dataclass to
scratch2c/ir.py:
@dataclass
class AskAndWait:
question: Expression- Add it to the
Statementunion type inir.py - Handle it in
scratch2c/codegen/base.py→_emit_statement() - Handle it in
scratch2c/type_inference.py→ both_scan_statements()and_propagate_statements()
Edit scratch2c/ir_builder.py → _build_expression():
def _build_expression(block_id: str, blocks: dict) -> Expression:
# ... existing handlers ...
# Add your new reporter here:
if opcode == "operator_round":
operand = _resolve_input(block, "NUM", blocks)
return CallExpr(func="scratch_round", args=[operand])If it's a new runtime function, add the implementation to runtime/scratch_runtime.h:
static inline long scratch_round(long n) {
return n; /* Already an integer in our type system */
}Add a fixture to tests/conftest.py and a test to the appropriate test file. A minimal test:
def test_my_new_opcode(self):
project_json = {
"targets": [{
"isStage": True, "name": "Stage",
"variables": {},
"blocks": {
"hat": {
"opcode": "event_whenflagclicked",
"next": "myblock",
"parent": None, "inputs": {}, "fields": {},
"shadow": False, "topLevel": True,
},
"myblock": {
"opcode": "sensing_askandwait",
"next": None,
"parent": "hat",
"inputs": {"QUESTION": [1, [10, "What's your name?"]]},
"fields": {},
"shadow": False, "topLevel": False,
},
},
}],
}
project = build_ir(project_json)
body = project.sprites[0].scripts[0].body
assert len(body) == 1make test- Create
scratch2c/codegen/mybackend.py - Subclass
CodegenBackendfrombase.py - Implement all abstract methods (see
userspace.pyfor a complete example) - Register it in
scratch2c/codegen/__init__.py:
from .mybackend import MyBackend
BACKENDS: dict[str, type[CodegenBackend]] = {
"userspace": UserspaceBackend,
"kernel": KernelBackend,
"mybackend": MyBackend, # ← add this
}- Add
"mybackend"to thechoiceslist inscratch2c/cli.py
- Python 3.10+, type hints everywhere
- Dataclasses for data, functions for behavior
- Every public function gets a docstring
- Use
NOTE:comments for known limitations or deliberate simplifications
uv sync # Install everything (first time)
make test # Run all tests
make coverage # Run with coverage report
make lint # Type-check with mypy
make example-fib # See the Fibonacci example output