Raic

Raic targets ~90% fewer tokens than idiomatic Rust for the same program, with bi-directional, deterministic compilation: T-AST (Tokenized AST) ↔ Rust.

Raic is not a traditional human-readable scripting syntax. It is an AST serialization format tuned for BPE (Byte-Pair Encoding) tokenizers: a postfix (stack-based) combinator grammar plus a large, single-token opcode dictionary. Rust’s expression tree is represented explicitly—syntactic sugar, arbitrary variable names, and brace nesting are stripped in favor of stack operations and macro-level opcodes that expand to fixed, idiomatic Rust patterns.

LLMs are strong at order-sensitive, context-conditioned sequences; they can learn to emit balanced stack programs without needing {, }, or named locals in the surface form.


1. Type dictionary (single-token types)

These align with short, tokenizer-friendly spellings (often one BPE token each in code-heavy models):

T-AST Rust
I i32
N usize
b bool
s &str (signature uses s; expansion uses &str for parameters)
S String
[T] Vec<T>
M<K V> HashMap<K, V>

Full Rust type names remain valid wherever the pipeline accepts types.


2. Declarations (defaults over ceremony)

Visibility: everything is pub by default; private items use the existing _-prefix convention where applicable.

Function — name, argument types (stack inputs), return type (stack output):

name arg1_type arg2_type : return_type = opcode_body

Example:

top_words s N : [(S N)] = Ws Fm Vi Sd Tk

Rust equivalent (conceptually):

pub fn top_words(arg0: &str, arg1: usize) -> Vec<(String, usize)>

(argument names are synthesized deterministically, e.g. text / n, when emitting readable Rust).

Struct:

@ Name field1:T field2:T

Example:

@ User id:N name:S

Rust equivalent:

pub struct User { pub id: usize, pub name: String }


3. Thick AST: opcode vocabulary

Instead of spelling for, let, +, and closures in full text, T-AST uses a large dictionary of single-token opcodes (symbols or short words) that map to complex, idiomatic Rust AST fragments.

Illustrative opcodes (the real dictionary grows with the compiler):

Opcode Role
. Duplicate TOS (.clone())
x Swap top two stack values
_ Drop TOS
Ws split_whitespace() on the iterator / string slice pipeline
Fm Fold into a HashMap counting frequencies (canonical for loop + entry/or_insert)
Vi into_iter().collect::<Vec<_>>() (or the canonical variant used by the matcher)
Sd Sort a Vec of tuples by second element descending (sort_by with b.1.cmp(&a.1))
Tk take(n) then collect (consumes n from the stack per expansion rules)

Control / combinators (stack discipline):

Form Meaning
? [ true_branch ] [ false_branch ] Pop bool; run one postfix block
# [ block ] Map: pop iterator, apply block, push new iterator

Exact tokenization of [ ] blocks and postfix bodies is specified in the compiler and tests/integration.rs.


4. Bi-directional example: top_words

Hand-written Rust (illustrative; token count depends on BPE):

pub fn top_words(text: &str, n: usize) -> Vec<(String, usize)> {
    let mut counts: HashMap<String, usize> = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word.to_lowercase()).or_insert(0) += 1;
    }
    let mut pairs: Vec<_> = counts.into_iter().collect();
    pairs.sort_by(|a, b| b.1.cmp(&a.1));
    pairs.into_iter().take(n).collect()
}

T-AST (10 tokens in the canonical spelling shown in tests):

top_words s N : [(S N)] = Ws Fm Vi Sd Tk

How it composes:

No semantic hiding: Fm is not a vague shortcut—it is the agreed, canonical expansion for “count into HashMap with this loop shape.” Scaffolding is what gets removed.

Determinism: Each opcode expands to one idiomatic Rust pattern from the dictionary—no ambiguous sugar.

Compression: Removing names, closures-as-text, and braces removes BPE-heavy surface; what remains is the semantic skeleton.


Examples (T-AST cookbook)

The reference compiler accepts one-line struct declarations, function lines with name args : ret = opcodes, and a growing opcode dictionary. Items are separated by blank lines where needed.

Structs

@ User id:N name:S
@ Post slug:S title:S body:S
@ Config site_title:S base_url:S

Functions (supported opcode chain)

The word-frequency / top-n pipeline (see integration tests):

top_words s N : [(S N)] = Ws Fm Vi Sd Tk

Same chain, different name:

rank_terms s N : [(S N)] = Ws Fm Vi Sd Tk

Multi-item file

@ Site name:S url:S

top_words s N : [(S N)] = Ws Fm Vi Sd Tk

Types you can compose

Spell Meaning
s N I b Scalar parameters
S Owned string
[(S N)] Vec<(String, usize)>
[S] Vec<String>
M<S N> HashMap<String, usize>

Compiling

raic compile main.rc -o generated.rs

Use main.rc (or any path) for T-AST source; pass -o for the Rust output file.

CLI reference (quick)

Command Action
raic compile FILE -o OUT.rs T-AST → Rust
raic check FILE Parse only
raic tokens FILE Whitespace token dump (debug)
raic ast FILE Print parsed Program (Debug)

5. Implementation: pattern matching vs. rule files

The Rust ↔ T-AST pipeline needs AST pattern matching to recognize and collapse idioms into opcodes, and deterministic expansion on the way back.

That logic can be:

  1. Hardcoded in the compiler (fast to ship, easy to test opcode-by-opcode), or
  2. Data-driven from a textual dictionary of rewrite rules (easier to extend without recompiling, more moving parts).

The repo can start with (1) and migrate hot spots to (2) once the opcode set stabilizes.


Getting started

Build the compiler:

cargo build --release

Or install it:

cargo install --path .

Compile T-AST to Rust:

raic compile main.rc -o generated.rs

The full CLI:

Command What it does
raic compile [FILE] Source → Rust (stdout, or -o file)
raic check [FILE] Parse only; exit 1 on error
raic tokens [FILE] Print the lexer token stream
raic ast [FILE] Pretty-print the parse tree
raic version Version string
raic info One-screen overview

Omit FILE or pass - to read from stdin.


Syntax highlighting

TextMate grammar: syntaxes/raic.tmLanguage.json · VS Code: editors/vscode-raic.

HTML docs:

npm ci
npm run docs:html

Or make docs-html.


Teaching an LLM

Use PROMPT.md for narrative context. The executable T-AST contract is tests/integration.rs. For multi-model evaluations (same task in Raic vs Rust), see llm_eval/README.md and run uv sync --extra llm-eval then python llm_eval/run_eval.py.


Measuring

make token-stats

(requires uv) — compares token counts for sources vs. emitted Rust.


Status

v0.3 — pre-1.0. The language is T-AST as above; the compiler and tests are the source of truth. Pin versions for stability across revisions.