Describe Logic ⴲ Provide Examples ⴲ Run Reliably
Marsha is an LLM-based programming language. Describe what you want done with a simple syntax, provide examples of usage, and the Marsha compiler will guide an LLM to produce tested Python software.
The Marsha compiler can be used to compile the syntax using a uv-installed package via a terminal or Jupyter Notebook:
uv pip install git+https://github.com/alantech/marsha
python -m marsha compile data_mangling.mrshFrom a source checkout you can instead install a marsha command directly:
make install # Linux / macOS
install.bat # WindowsThis builds a virtualenv with Marsha in it and drops a small launcher script at ~/.local/bin/marsha (~/.local/bin/marsha.bat on Windows; override the location with make install PREFIX=/usr/local or install.bat C:\tools) that simply runs python -m marsha from that virtualenv, passing your arguments through. The venv location can be overridden per-invocation with the MARSHA_VENV environment variable, and make uninstall / uninstall.bat remove the launcher.
The Marsha syntax looks a lot like markdown and is a mixture of English and mathematical notation. It has its own file format .mrsh that houses function definition(s). The syntax is subject to change as Marsha is currently in an alpha state. If you have a legitimate use case for Marsha, please let us know.
Data types provide function type safety which helps improve the accuracy of the code generation. The data type format is almost identical to the CSV format.
# type EmployeeSkills
name, skill
Bob, math
Jake, spreadsheets
Lisa, coding
Sue, spreadsheetsIt is also possible for Marsha to infer the data type from CSV file
# type EmployeesByDepartment employees_by_department.csvFunctions are the bread and butter of Marsha and can easily define transformations between different data types. There are three sections to a Marsha function: the declaration, the description, and the examples.
The declaration is a Markdown heading section prefixed with func, then followed by a name, parenthesis containing the input type(s), and finally a colon followed by the output type. The name must be a single word, but the types don't need to be classic software types, or even the explicit data types defined above. They can themselves be simple descriptions of what the type is meant to be. Eg,
# func get_employee_skills(list of EmployeesByDepartment, list of DepartmentSkills): list of EmployeeSkillsThe next section is the description of the function. Here you explain what the function should do. Being more explicit here will reduce variability in the generated output and improve reliability in behavior, but it's up to you just how explicit you will be and how much you leave to the LLM to figure out. This is similar to declarative languages like SQL and HTML where there are defaults for things you do not specify, like the sort order of select statements or the default styling of a <div>. Eg,
This function receives a list of EmployeesByDepartment and a list of DepartmentSkills. The function should be able to create a response of EmployeeSkills merging the 2 list by department. Use the pandas library.The final section is the example section. Here you provide examples of calling the function and what its output should be. Marsha uses this to provide more information to the LLM to generate the logic you want, but also uses it to generate a test suite to validate that what it has generated actually does what you want it to. This feedback loop makes Marsha more reliable than directly using the LLM itself. In some ways, this is similar to Constraint-based programming languages where you validate and verify the behavior of your function in the definition of the function itself, but it is also less stringent than those, allowing incomplete constraints where constraint-based languages will fail to compile in the face of that ambiguity. Eg,
* get_employee_skills() = throws an error
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting')]) = throws an error
* get_employee_skills([], []) = []
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting')], []) = []
* get_employee_skills([], [DepartmentSkills('Accounting', 'math')]) = []
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting')], [DepartmentSkills('Accounting', 'math')]) = [EmployeeSkills('Joe', 'math')]
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting'), EmployeesByDepartment('Jake', 'Engineering')], [DepartmentSkills('Accounting', 'math')]) = [EmployeeSkills('Joe', 'math')]
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting'), EmployeesByDepartment('Jake', 'Engineering')], [DepartmentSkills('Accounting', 'math'), DepartmentSkills('Engineering', 'coding')]) = [EmployeeSkills('Joe', 'math'), EmployeeSkills('Jake', 'coding')]Altogether this produces:
# func get_employee_skills(list of EmployeesByDepartment, list of DepartmentSkills): list of EmployeeSkills
This function receives a list of EmployeesByDepartment and a list of DepartmentSkills. The function should be able to create a response of EmployeeSkills merging the 2 list by department. Use the pandas library.
* get_employee_skills() = throws an error
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting')]) = throws an error
* get_employee_skills([], []) = []
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting')], []) = []
* get_employee_skills([], [DepartmentSkills('Accounting', 'math')]) = []
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting')], [DepartmentSkills('Accounting', 'math')]) = [EmployeeSkills('Joe', 'math')]
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting'), EmployeesByDepartment('Jake', 'Engineering')], [DepartmentSkills('Accounting', 'math')]) = [EmployeeSkills('Joe', 'math')]
* get_employee_skills([EmployeesByDepartment('Joe', 'Accounting'), EmployeesByDepartment('Jake', 'Engineering')], [DepartmentSkills('Accounting', 'math'), DepartmentSkills('Engineering', 'coding')]) = [EmployeeSkills('Joe', 'math'), EmployeeSkills('Jake', 'coding')]The Marsha syntax is meant to be:
- minimal and "obvious", but also discourage lax or incomplete information that could lead to unpredictable behavior
- be mechanically parseable for syntax highlighting and quick feedback on correctness issues to the user
- make it easy to define examples to reduce the probability of generating faulty code and allow generating tests that the application code can be tested against
Marsha is compiled by an LLM into tested software that meets the requirements described, but implementation details can vary greatly across runs much like if different developers implemented it for you. There is typically more than one way to write software that fulfills a set of requirements. However, the compiler is best-effort and sometimes it will fail to generate the described program. We aim for 80%+ accuracy on our examples. In general, the more detailed the description and the more examples are provided the more likely the output will work.
Before generating any code, the compiler runs a sanity check that the definition is self-consistent, and prints warnings about significant ambiguities to stderr that could result in differently-behaving code between generation runs. Warnings are always shown while compiling, like a conventional compiler's; --no-warn suppresses them, but the check itself still runs and still fails the compile when the definition contradicts itself.
The compiler then generates a test suite for the definition — the oracle — anchored to the description and examples in the .mrsh file, and only then generates the implementation to satisfy it. The implementation is written against this fixed oracle rather than the two being generated independently and reconciled afterwards.
During the code and test generation stages — and, with --optimize, in the optimization and test-correction loops — the LLM has a simple fake terminal for looking up information it does not have. If it decides it needs more, it ends a response with a single line beginning with $; the compiler executes the command, feeds the result back (wrapped as explicitly-untrusted reference data), and repeats until the LLM produces its final output with no trailing command. The tools are grouped by category and scoped per phase. The language-agnostic set — the general web (web-search, view-web-page, with an SSRF guard) and sandboxed computation (calc, an isolated QuickJS subprocess) — is defined once in marsha/tools.py and shared by every target; the language-specific set — the package registry (for Python: search-dependencies, dependency-docs) and, only in the optimize/correction loops where a candidate virtualenv exists, installed-environment introspection (list-dependencies, show-dependency, list-symbols, show-symbol) — is layered on by the target's backend in marsha/backends/, so adding a target only adds its registry and environment tools. The MCP standard is deliberately out of scope. --no-tools disables the interface. When a generated implementation fails the oracle, the compiler diagnoses whether the fault lies in the implementation or in a test: a faulty implementation is fixed directly, while a test that over-specifies or contradicts the definition is corrected through a separate, spec-anchored pass that must justify every change against the definition and will not weaken a test that is actually correct. The implementation is never allowed to edit the tests, and the tests are only ever edited by that spec-anchored path, so a correct test is never bent to match a buggy implementation.
The TDD methodology above (sanity check, oracle-first generation, diagnose→fix routing, persona review loops) is target-language-agnostic: the language-specific steps — prompts, artifact naming and layout, output validation, linting, formatting, test execution, and the runnable-CLI step — are delegated to a language backend in marsha/backends/, selected with --target. Only the Python backend is wired today; the registry and dispatch are ready for additional targets.
An optional --optimize <level> flag spends additional LLM iterations refining the result, one inner loop per phase. A higher level runs more review iterations: the test suite is double-checked for coverage and fidelity against the definition (every stated behavior is tested and nothing is invented), the implementation is iterated for performance, safety, and code quality — with each change re-run against the oracle and reverted if it regresses — and any test correction is re-validated against the definition before it is applied. The default level is 0, which disables these loops and changes neither behavior nor cost.
Each review loop is driven by a panel of named review personas. In every iteration the loop's reviewers run independently and in parallel, each reporting MAJOR / MINOR / NIT findings; a per-phase implementor (the editor) then addresses those findings and returns a reasoning preamble plus the revised artifact. The built-in personas live in marsha/personas/ — one file per reviewer plus one editor per phase. The reviewer set for each loop is selectable with:
--test-personas— reviewers for the test-suite (oracle) loop--impl-personas— reviewers for the implementation loop--fix-personas— reviewers for the test-correction (oracle-fix) loop
Each flag takes a comma-separated list whose entries are either a built-in persona name (e.g. sage) or a path to a custom persona file (e.g. ./sharona.md); omit a flag to run that loop's default set. --optimize-severity major,minor,nit (default all three) chooses which finding severities the editor acts on.
In order to use the compiler, Marsha needs to know which LLM to send requests to. By default it uses the OpenAI API, which requires the following environment variables to be set:
OPENAI_ORGOPENAI_SECRET_KEY(orOPENAI_API_KEY)
Any OpenAI-compatible API can be used instead, such as the server that ships with llama.cpp for running models locally. The endpoint can be configured with the --api-base command line flag, the OPENAI_BASE_URL environment variable, or a config file, in that order of precedence.
Anthropic's Claude models are also supported. Select them with --provider anthropic (or the provider config file key); the API key comes from CLAUDE_API_KEY (or ANTHROPIC_API_KEY). The default code-generation model is claude-sonnet-5; the strong model (default claude-opus-5) is used for the test-fixing stage and as the escalation target when a prompt exceeds the model's context.
The config file is a JSON file read from the standard configuration location for your OS:
- Linux:
~/.config/marsha/config.json(or$XDG_CONFIG_HOME/marsha/config.jsonif that is set) - macOS:
~/Library/Application Support/marsha/config.json - Windows:
%LOCALAPPDATA%\marsha\config.json
It supports the keys provider, api_base, api_key, claude_api_key, model, and model_strong. The default model for code generation is gpt-5-mini (claude-sonnet-5 with the anthropic provider); model_strong (default gpt-5, claude-opus-5 with the anthropic provider) is used for the test-fixing stage and as the escalation target when a prompt exceeds the model's context.
Eg, to point Marsha at a llama.cpp server listening on localhost port 8080:
{
"api_base": "http://localhost:8080/v1",
"api_key": "sk-local"
}The key can be anything; local servers like llama.cpp do not validate it.
Against any OpenAI-compatible endpoint, Marsha probes the backend's /models list at startup and, when the configured model isn't served, remaps each model role to the closest match it does serve (logging the choice). The standard role lands on the cheapest, smallest model whose context is at least as large as the model it replaces (400k for the default gpt-5-mini); the strong role lands on the largest-context (most capable) one — so on a multi-model backend the two roles may resolve to different models. If no served model reaches the standard role's bar, the largest available is used. An explicitly chosen model (--model or the model/model_strong config keys) pins the role and is never remapped. If the endpoint can't be reached, the configured model is used as-is.
Marsha is organized around subcommands: marsha compile runs the compiler and marsha help explains them. (Bare marsha <source.mrsh> with no subcommand still works as a deprecated alias for marsha compile.)
$ marsha --help
usage: marsha [-h] {compile,help} ...
Marsha AI Compiler
commands:
compile Compile a .mrsh definition into generated code and a test suite.
help Show this overview, or detailed help for a subcommand.The compile subcommand takes these options (see marsha compile --help):
$ marsha compile --help
usage: marsha compile [-h] [-t TARGET] [--target-version TARGET_VERSION] [-d]
[--trace] [--trace-full] [-q] [-a ATTEMPTS]
[-n N_PARALLEL_EXECUTIONS] [--exclude-main-helper]
[--exclude-sanity-check] [--no-tools] [--no-warn]
[--optimize OPTIMIZE] [--test-personas TEST_PERSONAS]
[--impl-personas IMPL_PERSONAS]
[--fix-personas FIX_PERSONAS]
[--optimize-severity OPTIMIZE_SEVERITY]
[--context-window CONTEXT_WINDOW] [--context-cap CONTEXT_CAP]
[-s] [--api-base API_BASE] [--model MODEL]
[--provider {openai,anthropic}]
source
Marsha AI Compiler
positional arguments:
source
options:
-h, --help show this help message and exit
-t, --target TARGET Target language for the generated code, by backend id
or alias (default: python). Only `python` is wired
today; the registry is ready for more.
--target-version TARGET_VERSION
Version of the target language the generated code
should target, eg 3.12 for Python (where it becomes
the project requires-python). Default: the interpreter
running Marsha.
-d, --debug Turn on debug logging
--trace Also write a live, timestamped progress trace to
stderr (each phase and every LLM request, with its
label and duration). Implies -d. Useful for watching a
slow run in real time, e.g. against a local llama.cpp
server.
--trace-full As --trace, but also dump the full input prompt and
output of every LLM call to stderr. Implies --trace
(and -d). Use for debugging exact prompts and
responses.
-q, --quick-and-dirty
Code generation with no correction stages run
-a, --attempts ATTEMPTS
-n, --n-parallel-executions N_PARALLEL_EXECUTIONS
--exclude-main-helper
Skips addition of helper code for running as a script
--exclude-sanity-check
Skips an initial sanity check that the definition is
self-consistent
--no-tools Disable the LLM tool interface (the fake terminal
where the LLM can look up dependency APIs and the web
with $ commands: search-dependencies, dependency-docs,
web-search, view-web-page, calc, and — in the
optimize/correction loops — installed-environment
introspection). Enabled by default.
--no-warn Do not display warnings about ambiguous areas of the
definition from the sanity check
--optimize OPTIMIZE Optimization level: number of per-phase LLM review
iterations (test-suite coverage/fidelity,
implementation quality, and test-correction
validation). 0 (default) disables the optimization
loops.
--test-personas TEST_PERSONAS
Comma-separated reviewer personas for the test-suite
(oracle) loop. Each entry is a built-in name (e.g.
ada) or a path to a custom persona file (e.g.
./sharona.md). Default: all built-in oracle reviewers.
--impl-personas IMPL_PERSONAS
Comma-separated reviewer personas for the
implementation loop. Each entry is a built-in name
(e.g. sage) or a path to a custom persona file.
Default: all built-in impl reviewers.
--fix-personas FIX_PERSONAS
Comma-separated reviewer personas for the test-
correction (oracle-fix) loop. Each entry is a built-in
name (e.g. sol) or a path to a custom persona file.
Default: all built-in correction reviewers.
--optimize-severity OPTIMIZE_SEVERITY
Comma-separated finding severities to act on during
--optimize (major,minor,nit). Default: all three.
--context-window CONTEXT_WINDOW
Override the context window (in tokens) used to size
review/editor prompts. Auto-detected from the service
when possible, else documented defaults. Set it if
your backend mis-reports its window.
--context-cap CONTEXT_CAP
Fraction of the context window a single prompt may
occupy before its findings are compacted (default
0.5).
-s, --stats Save stats and write them to a file
--api-base API_BASE Base URL of an OpenAI-compatible API to use for LLM
requests, e.g. a local llama.cpp server. Overrides the
OPENAI_BASE_URL environment variable and the config
file (openai provider only)
--model MODEL Model to use for code generation, overriding the model
in the config file
--provider {openai,anthropic}
LLM provider to use: openai (default; any OpenAI-
compatible API) or anthropic (Claude)-dadds a significant amount of debug information to the screen. Probably not useful if you're not working on Marsha itself.--trace(implies-d) writes a live, timestamped progress trace tostderr: each phase transition and every LLM request with its label and duration. It is flushed immediately, so it stays visible in real time even whenstdoutis piped to a file and block-buffered — useful for watching a slow run, e.g. against a localllama.cppserver. Watch it withmarsha compile --trace your.mrsh 2>trace.log &thentail -f trace.log.--trace-full(implies--trace) is the same live trace, but it also dumps the full input prompt and output of every LLM call tostderr— bracketed by=== <label>: request ===/=== <label>: response ===markers. Use it to debug the exact prompts and responses, e.g.marsha compile --trace-full your.mrsh 2>trace.log &.-qruns only the initial code generation phase without any of the corrective feedback stages. This is significantly cheaper, but more likely to generate code that doesn't quite work. This could be useful if you're using Marsha like Github Copilot or directly asking for code from ChatGPT, but with the Marsha syntax providing some more structure to produce a better result than you might if simply given a blank screen to write into.-aThe number of times marsha should attempt to compile your program, defaulting to just once. If set to more than 1, on a failure it will try again. For some trickier programs this might improve the ability to get working code at the cost of more LLM calls.-nThe number of parallel LLM threads of "thought" to pursue per attempt. This defaults to 3. When a path succeeds, all of the other paths are cancelled.-sSave the stats that are printed by default to a file, instead. Probably not useful if you're not working on Marsha itself.--exclude-main-helperTurns off the automatically generated code to make using your compiled Marsha code from the CLI easier, which is included by default.--exclude-sanity-checkSkips the initial sanity check that the definition is self-consistent.--no-toolsDisables the LLM tool interface (the fake terminal: registry, web, sandboxedcalc, and — in the optimize/correction loops — installed-environment introspection). Enabled by default: the LLM only pays for it when it actually issues a command. The CItimebenchmark passes--no-toolsto stay deterministic and free of live-search flakiness.--no-warnSuppresses the warnings the sanity check prints about significant ambiguities in the definition. The check itself still runs, and still fails the compile when the definition contradicts itself.--api-baseOverrides the LLM endpoint with the base URL of any OpenAI-compatible API (eghttp://localhost:8080/v1for a llama.cpp server). Takes precedence over theOPENAI_BASE_URLenvironment variable and the config file.--modelOverrides the model used for code generation (defaultgpt-5-mini,claude-sonnet-5with the anthropic provider), eg to use a different model or the name of a locally served model.--context-windowOverrides the context window (in tokens) used to size review/editor prompts. It is auto-detected from the service when possible (eg a llama.cpp server'sn_ctx), else a documented default is used; set it if your backend mis-reports its window.--context-capThe fraction of the context window a single prompt may occupy before its findings are compacted (default0.5).--providerSelects the LLM provider:openai(default; any OpenAI-compatible API) oranthropic(Claude, keyed byCLAUDE_API_KEYorANTHROPIC_API_KEY).--targetSelects the target language for the generated code, by backend id or alias (python, default). The compiler's language-specific steps — prompts, artifact layout, validation, linting, formatting, test execution, and the runnable-CLI helper — are delegated to a language backend; onlypythonis wired today, and the registry is ready for more.--target-versionThe version of the target language the generated code should target (eg3.12for Python, where it becomes the project'srequires-python). Defaults to the interpreter running Marsha — the only one the generated code is verified against.
marsha review runs Marsha's review personas against a git change instead of a freshly generated one. By default it reviews the current branch against the repository's default branch and reports findings (MAJOR/MINOR/NIT, file:line, and a one-line note with a suggested fix):
$ marsha review # current branch vs. the default branch
$ marsha review --personas sage,sasha --severity majorReview is tool-driven. Each reviewer is given a read-only git tool (it can run diff, log, show, blame, grep, ls-files, … — but never a mutating command like commit/push/checkout) and a per-reviewer notes scratchpad (notes add … / notes show). Instead of being handed the full diff, each reviewer starts from git diff <base> --stat and probes the codebase itself; the notes survive context compaction. The default panel is the impl reviewers plus a git-history reviewer that grounds findings in the change's history and intent before flagging them.
A conventions gate then runs: it reads the repo's real conventions (AGENTS.md/CLAUDE.md/lint configs, via the git tool) and rebuts any finding that would push the code away from a convention the codebase actually follows. The panel re-runs with the rebuttal so each reviewer can drop its rebutted point. --review-rounds N bounds this (default 1; 0 disables the gate). Findings are finally de-duplicated across reviewers before being reported.
Optional context (both are treated as untrusted reference data, never as instructions):
--pr <num>(needs theghCLI) — checks the PR out (gh pr checkout), so the review runs against the PR's actual head, and seeds the review with the PR body and all comments so far. The working tree must be clean, or Marsha errors out and explains why.--linear <ticket>(needs thelinearCLI) — pulls the ticket's requirements and prepends them to the pull-request context.--post-review(requires--pr) — posts the findings back to the PR as inline comments at the correct file and line, folding any finding whose line isn't in the diff into the review body.
--target selects the target language for reviewer guidance; unlike compile, its toolchain does not need to be installed. The global flags (--model, --provider, --api-base, -d, --trace) work as they do for compile.
By default, Marsha appends logic to the generated Python code to make usage simpler, allowing you to invoke it from the CLI and potentially start a REST server.
$ python -m duckduckgo --help
usage: duckduckgo.py [-h] [-c {BeautifulSoup,duckduckgo}] [-j] [-t] [-i] [-f INFILE] [-o OUTFILE] [-s SERVE] [params ...]
Marsha-generated CLI options
positional arguments:
params Arguments to be provided to the function being run. Optimistically converted to simple python types by default, and left as strings if not possible
options:
-h, --help show this help message and exit
-c {BeautifulSoup,duckduckgo}, --func {BeautifulSoup,duckduckgo}
Specifies the function to call. Defaults to the last defined function
-j, --force-json Forces arguments, files, or stdin to be parsed as JSON
-t, --force-text Forces arguments, files, or stdin to be parsed as raw text
-i, --stdin Ignores CLI parameters in favor of stdin (as a single parameter)
-f INFILE, --infile INFILE
Ignores CLI parameters in favor of reading the specified file (as a single parameter)
-o OUTFILE, --outfile OUTFILE
Saves the result to a file instead of stdout
-s SERVE, --serve SERVE
Spins up a simple REST web server on the specified port. When used all other options are ignored-cLets you choose which function within the generated code you wish to invoke. By default it selects the last function defined, as that is usually a "main-like" function.paramsare all non-option arguments provided, in order, to the function you are invoking.-jand-tlet you choose if the param(s) provided will be parsed as JSON or kept as plain text. By default it will opportunistically parse the arguments but if it fails will keep it as text-i,-f, and-olet you choose how input and output is managed. By default inputs are theparamsarguments and the output is tostdout, but you can use-ito then ignore allparamsand treatstdinas the singular input param for your function. Similarly-fwill do the same, but for the file you specify, and-owill write the result to a file you specify instead of tostdout.-sIs a flag to instead run a simple REST server. Using this flag causes it to ignore all other flags. The various function names become/func_nameendpoints that you can POST to and get a response body back. If you set theContent-Typeheader toapplication/jsonthe input and output will be JSON, if not it will be plain text. If your function takes mutliple arguments, it must be called in JSON mode with the arguments each being an element of a top-level array.