Skip to content

Scratch to C++ transpiler experiment - #695

Draft
devin-ai-integration[bot] wants to merge 3 commits into
masterfrom
devin/1792234428-cpp-transpiler
Draft

devin-ai-integration[bot] wants to merge 3 commits into
masterfrom
devin/1792234428-cpp-transpiler

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Note: This is just an experiment to implement the entire C++ transpiler using LLM - it will never be actually merged.

Summary

Adds a second compiler backend (src/engine/internal/cpp/) that turns Scratch scripts into C++ source code instead of LLVM IR. It plugs in at the existing ICodeBuilderFactory boundary. The default backend is still LLVM, so nothing changes for current users.

Covers backlog items #662–#681, #683–#687 and #689–#691. Not included: #682 (inlining), #693 (type analyzer) and #692 (in progress elsewhere).

How it works

  • CppCodeBuilder implements all of ICodeBuilder. Each script, procedure, reporter or predicate becomes a heap Frame subclass with a switch-based step() state machine. This is C++17 (no C++20 coroutines needed). All registers live as frame members, so they survive yields.
    int step() override {
        switch (state) {
            case 0:;
            ...
            state = 1; return cpp::CPP_SUSPEND; case 1:;   // yield / end of loop iteration
            ...
        }
        return cpp::CPP_DONE;   // CPP_END_THREAD for "stop this script"
    }
  • Value representations: double, bool, cpp::Str (pooled StringPtr), cpp::Val (ValueData) and void *. Conversions use the public value_* / string_* functions. Variables and lists are resolved from varData/listData (or the stage) in resolve(), which runs again after invalidateTarget() and procedure calls.
  • cppprelude.h is the runtime header for generated code. It only uses public headers and is embedded into the library as a string at configure time (CppCompilerContext::prelude()).
  • CppCompilerContext::moduleSource() outputs a module whose static initializer calls cpp_register_function(moduleKey, name, fn). moduleKey is an FNV-1a hash of the generated code. CppExecutableCode looks the function up by (moduleKey, name). So code only runs if the matching generated source was compiled into the binary.
  • CppExecutableCode follows LLVMExecutableCode: it waits for unresolved promises, reset() destroys the suspended frame, and the (void *)0x1 end-thread sentinel is passed up through procedure calls.
  • Pointer constants that point to a target's list or variable (e.g. from listblocks) are emitted as resolved references instead of raw addresses. Other pointer constants are still only valid in the current process.

Tests (#687): test/cpp has a cpp_test_generator executable. At build time it runs the shared test-case builders (cases.cpp) through CppCodeBuilder and writes generated_cases.cpp, which is compiled into cpp_test. cpp_test then rebuilds the same cases and runs the resulting ExecutableCode, checking stdout, run counts and return values. It also covers promise/kill/reset and the codebase generator. 24 tests pass locally, with and without LIBSCRATCHCPP_ENABLE_SANITIZER.

Codebase generation (#683–#686): CppCodebaseGenerator::generate(sb3, outDir, name) loads the project with a recording C++ factory and writes:

  • project.cpp: the prelude plus all modules
  • project_data.cpp: the .sb3 as a byte array
  • main.cpp: extracts the .sb3 to a temp file, calls cpp_use_cpp_backend(), then runs Project::load() and run()
  • CMakeLists.txt: uses either LIBSCRATCHCPP_SOURCE_DIR or a prebuilt LIBSCRATCHCPP_INCLUDE_DIR/LIBSCRATCHCPP_LIBRARY

The new scratchcpp-transpiler <project.sb3> <outdir> [name] tool (option LIBSCRATCHCPP_BUILD_TRANSPILER, default ON) wraps the generator. I checked this locally on test/custom_blocks.sb3 and test/bubble_sort.sb3: the generated projects build and run against libscratchcpp. 3_threads.sb3 never finishes, but it doesn't finish with the LLVM backend either.

Known limitations / review focus

  • No type analysis yet: variables and list items are always ValueData.
  • Generated code calls block functions through extern "C" declarations built from the addFunctionCall arg types. These have to match the real signatures (same requirement as LLVM).
  • Generated codebases only load the project when they start; they don't embed assets separately (Embed project assets into generated C++ codebase #685 is covered by embedding the whole .sb3).

devin-ai-integration Bot and others added 3 commits October 17, 2026 11:04
Co-Authored-By: zemanadam893@gmail.com <zemanadam893@gmail.com>
Co-Authored-By: zemanadam893@gmail.com <zemanadam893@gmail.com>
Co-Authored-By: zemanadam893@gmail.com <zemanadam893@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@adazem009
adazem009 marked this pull request as draft October 17, 2026 14:08
@adazem009 adazem009 changed the title Add Scratch to C++ transpiler backend Scratch to C++ transpiler experiment Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant