Skip to content

Repository files navigation

TRACTOR CRISP

CRISP is a tool for migrating C code to safe Rust.

Features

The following features have been implemented in the CRISP transpiler loop:

  • Automatic translation of C to unsafe Rust using either c2rust-transpile or Hayroll. CRISP tries Hayroll first and falls back to ordinary c2rust-transpile only if it fails.
  • Agent-based safety refactoring. CRISP invokes the Codex agent to convert unsafe Rust to safe Rust. CRISP checks that the code produced by the agent builds, passes the tests, and does not introduce new unsafety before accepting it.
  • Automatic detection of unsafe code. The CRISP transpiler loop stops once there is no unsafe code left to make safe.
  • FFI function splitting. To preserve ABI compatibility when translating libraries, some function signatures must remain unsafe. To minimize the amount of unsafe code, CRISP splits each such function into a small unsafe wrapper and a separate implementation function that can then be made safe.
  • High-level planning. CRISP prompts the agent to develop a high-level safety plan for the codebase, then has the agent implement the plan step by step.

Running in Docker

This is the easiest way to get started using CRISP.

Building the Docker image

docker build --target tractor-crisp --tag tractor-crisp .

Configuring CRISP

First, copy the example crisp.toml config file into your project:

cp crisp.toml.example /path/to/my-project/crisp.toml

Replace /path/to/my-project with the path to the C project you want to translate. Afterwards, you may want to edit the new config file to customize it for your project.

Running CRISP

Run the Docker container:

docker run --rm -it -v /path/to/my-project:/root/project tractor-crisp

Then, within the container, run CRISP on the project:

cd /root/project

# Set up environment variables for accessing OpenAI models
export CRISP_API_BASE=https://api.openai.com/v1
export CRISP_API_KEY=sk-your-api-key-here

# Optional: override the `models.agent_plan`, `models.agent_loop`,
# `models.rewriter`, etc. selections # in crisp.toml for this run.
#export CRISP_API_MODEL=gpt-5.6-sol

# As an alternative, you can direct CRISP to connect to llama.cpp or another
# OpenAI-compatible provider running on the host machine:
#export CRISP_API_BASE=http://172.17.0.1:8080/v1

# Import the original files into CRISP and tag them as `c_code`.  This command
# collects all of the `*.c`, `*.h`, and `CMakeLists.txt` files under the
# current directory; if more files are needed to build your C project, edit the
# command accordingly.
find . -name \*.c -o -name \*.h -o -name CMakeLists.txt \
    | xargs crisp commit -t c_code

# Run the CRISP transpiler loop
crisp main --llm-mode agent_rand_target

# CRISP sets a default limit on the number of rewrite attempts to avoid
# excessive token usage.  If `crisp main` stops early, run this to keep going:
#crisp safety-loop --llm-mode agent_rand_target

# Export translated Rust from CRISP.  Rust code will be located in the
# `transpile.output_dir` specified in `crisp.toml`.
crisp checkout
ls rust/

Running outside Docker

CRISP can also run directly on the host machine, using Docker to sandbox building and testing of the project (to protect against erroneous LLM outputs). This is useful when developing CRISP.

Setting up the Python virtual environment

CRISP is built and run using the uv tool

# Install uv, if needed:
curl -LsSf https://astral.sh/uv/install.sh | sh

uv sync

Building the sandbox container

docker build --target tractor-crisp-user --tag tractor-crisp-user .

By default, CRISP uses the image named tractor-crisp-user. To use a different image, set CRISP_DOCKER_IMAGE when running CRISP:

docker build --target tractor-crisp-user --tag my-tractor-crisp-user .
CRISP_DOCKER_IMAGE=my-tractor-crisp-user crisp main

Configuring CRISP

Set up crisp.toml as above.

Running CRISP

Set up a crisp alias in your shell:

export CRISP_DIR=/path/to/tractor-crisp
alias crisp='uv run --project $CRISP_DIR crisp'

Replace /path/to/tractor-crisp with the path to your tractor-crisp checkout.

Set up environment variables (CRISP_API_BASE etc.) as above.

Follow the same sequence of crisp commit + crisp main + crisp checkout as above.

Reading the output

crisp main produces a large amount of debug output as it works, so it's often useful to redirect it to a file with a command like crisp main |& tee log.txt. Afterward, log.txt will contain many sections that look like this:

 ** llm_safety
orig_code = 7984945391f2bd68221a044e93b628bf5e9b9d4fc3a2b2bf4672fa7a992ca24c
<lots of LLM input and output...>
llm_safety result = 287272ddc60c242e0d90231703d9db4c6f3de23ad9ed48a5ba28eb88343ef237

This means it ran the llm_safety step, which uses an LLM to remove unsafe operations from the code, with MVIR node 7984945 as the input, and it produced node 287272d as the output. You can view the contents of these nodes using crisp show:

$ crisp show 287272d
287272ddc60c242e0d90231703d9db4c6f3de23ad9ed48a5ba28eb88343ef237
{'files': {'Public-Tests/B01_organic/colourblind_lib/translated_rust/Cargo.lock': NodeId(d9aa22603d5bef7c6585bce72f036224872cc0455a3c6d43057adb2a1073bd63),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/Cargo.toml': NodeId(08f9a3fcebfcfcd0b85dcbf8d61ab0f17db0a934dce0ae725bc5a1561f73bf3b),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/build.rs': NodeId(be6b68ae15adc9f70e5909edd21a938a5961dfff72e45d5362cd620667d3c2be),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/lib.rs': NodeId(a7bc0d0d19bc3fbc24f098267f848956664d61f59904dd739921bb4a961d032a),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/performance.json': NodeId(3b4bb0e3c61b1f7efc572e72e187cdf3ebedc632e33c7015b8d2310df1cba0d2),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/rust-toolchain.toml': NodeId(9cfc889a3fdc24455c6e1459b2dd72227ba6b8a85ba9bd0fb218b32029907f4f),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/src/lib.c': NodeId(23a388b2b0fbfb44569bba25bb982427a1591e134df7191d864d0417c6bcea49),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/src/lib.rs': NodeId(73ac54edd470b727852f1d0b3f5bdeacda8f58cf697ab30080352724e0d44bcb),
           'Public-Tests/B01_organic/colourblind_lib/translated_rust/statistics.json': NodeId(3669bed0888f19e4005742fbd4dc6d4a54cf0b2fd905756a06d20cee7e65a69d)},
 'kind': 'tree'}
---

This is a TreeNode ('kind': 'tree') containing a number of FileNodes. You can view all the file contents by running crisp show --files 287272d, or view a single file's contents by running crisp show on its NodeId. To copy out the contents of the TreeNode to actual files on disk (e.g. so you can try compiling the code ), use crisp checkout 287272d --path ./out; this will create ./out/Public-Tests/B01_organic/colourblind_lib/translated_rust/Cargo.lock and so on.

Testing

To run all of the CI tests on Test-Corpus, you'll need access to Test-Corpus-Mirror. Due to this restriction, the Test-Corpus submodule is set to update = none and has to be explicitly checked out with

git submodule update --init --checkout Test-Corpus

The helper script at scripts/test_eval.py will automatically generate a crisp.toml, import c_code, and run crisp main on any Test-Corpus test case. Run it like this:

uv run scripts/test_eval.py /path/to/Test-Corpus/foo/bar -- --llm-mode agent_rand_target

GEPA prompt optimization

GEPA is the Genetic Pareto prompt optimization technique (paper). GEPA can be used to optimize the system prompt on any dataset to achieve better performance of converting unsafe Rust to safe Rust via LLMs.

See gepa_artifacts/ for prompts found via GEPA, and the results of running them on various datasets.

To run GEPA:

  1. Whenever GEPA is to be run on a new dataset which is present at Test-Corpus/Public-Tests/<dataset_dir>, first run ./gepa_setup_initial.sh <dataset_dir> from the root of this repo. See the docstring of gepa_setup_initial.sh for more details.

  2. Then, run GEPA prompt optimization as follows. For an example, see scripts/run_gepa.py.

    do_gepa(
    
        # dataset_path is the dataset folder on which GEPA will run its optimization
        dataset_path = <full/path/to/dataset/>,
    
        # seed_prompt is used to start the optimization
        # for best results, provide the entire prompt without any {...} blocks to be filled in
        seed_prompt_path = <full/path/to/seed_prompt.txt>,
    
        # task_lm is the LLM inside the loop which runs the prompts on the task
        # (in this case, unsafe Rust to safe Rust conversion)
        task_lm = <LLM name>,
    
        # reflection_lm is the LLM outside the loop which reflects on feedback
        # from the task_lm's performance and suggests better prompts
        reflection_lm = <LLM name>
    
    )

    Note: The GEPA optimization process may crash with OSError: Too many open files. To prevent this, run the following in the terminal first, then run GEPA optimization.

    ulimit -Sn $(ulimit -Hn)
  3. The performance of any prompt, whether seed or GEPA-optimized, can be evaluated as follows. For an example, see scripts/run_gepa.py.

    run_gepa_eval_on_prompt(
    
        # dataset_path is the dataset folder on which the prompt will be evaluated
        dataset_path = <full/path/to/dataset/>,
    
        # optimized_prompt_folder is a folder (usually inside gepa_artifacts/)
        # which contains `prompt.txt` containing the actual prompt to be evaluated
        optimized_prompt_folder = <full/path/to/prompt_folder/>,
    
        # model is the LLM which will run the prompt on the dataset
        model = <LLM name>
    
    )

About

No description, website, or topics provided.

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages