Skip to content

Commit 1fc8bc3

Browse files
author
Douglas Jones
committed
v2.0.0 — Rust interpreter, parser, parallel evaluator, benchmarks
Shape A complete: - Rust tree-walking interpreter (codifide-interpreter crate) - All 49 primitives, 8 typed errors, cost-based dispatch - 70 conformance tests passing - 6-25x faster than Python reference - Rust default runtime (codifide run delegates to Rust binary) - --runtime python falls back to reference - Rust parser (fully self-contained, no Python subprocess) - 3 parser conformance tests, byte-for-byte agreement - codifide-run parse subcommand - Criterion benchmark harness - Baseline: sort 23.6us, classify 5.9us, fizzbuzz 28.8us, balanced_brackets 98.3us, pipeline 17.4us - Graph-native parallel evaluator infrastructure - Static effect analysis, disjointness check, rayon::scope - Declaration-order trace merge (PE-1) - Per-branch interpreter with inherited depth (PE-3) - Correct but needs larger programs to show speedup - Three new example programs - batch_classify.cod: 8 independent model calls - recursive_sum.cod: recursive list sum with postcondition - text_stats.cod: multi-statistic text analysis - 289 Python tests, 28 Rust canonical tests, 0 skipped - Capability manifest unchanged: sha256:23fdde779...
1 parent d110410 commit 1fc8bc3

39 files changed

Lines changed: 6127 additions & 10 deletions

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[workspace]
22
resolver = "2"
3-
members = ["crates/codifide-canonical"]
3+
members = ["crates/codifide-canonical", "crates/codifide-interpreter"]

codifide/__main__.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,70 @@ def _read(path: str) -> str:
7373

7474

7575
def cmd_run(args: argparse.Namespace) -> int:
76+
# Runtime selection: Rust is the default in v2.0.0; Python is the
77+
# reference fallback via --runtime python.
78+
runtime = getattr(args, "runtime", None) or os.environ.get("CODIFIDE_RUNTIME", "rust")
79+
80+
if runtime == "rust":
81+
return _cmd_run_rust(args)
82+
return _cmd_run_python(args)
83+
84+
85+
def _cmd_run_rust(args: argparse.Namespace) -> int:
86+
"""Delegate execution to the Rust interpreter binary."""
87+
import shutil
88+
import subprocess
89+
90+
# Locate the Rust binary: prefer the release build next to this repo,
91+
# then fall back to PATH.
92+
repo_root = Path(__file__).resolve().parent.parent
93+
rust_bin = repo_root / "target" / "release" / "codifide-run"
94+
if not rust_bin.exists():
95+
rust_bin_path = shutil.which("codifide-run")
96+
if rust_bin_path is None:
97+
# Rust binary not available; fall back to Python silently.
98+
return _cmd_run_python(args)
99+
rust_bin = Path(rust_bin_path)
100+
101+
cmd = [str(rust_bin), "run", args.file]
102+
if args.entry:
103+
cmd += ["--entry", args.entry]
104+
105+
result = subprocess.run(cmd, capture_output=True, text=True)
106+
# Forward stderr directly.
107+
if result.stderr:
108+
print(result.stderr.rstrip(), file=sys.stderr)
109+
if result.returncode != 0:
110+
return result.returncode
111+
112+
# The binary prints io.say output on earlier lines and the JSON result
113+
# on the last line. Print io.say lines as-is; unwrap the JSON result
114+
# for display (matching Python's `print(result)` behavior).
115+
lines = result.stdout.splitlines()
116+
if not lines:
117+
return 0
118+
# All lines except the last are io.say output — print them directly.
119+
for line in lines[:-1]:
120+
print(line)
121+
# Last line is the JSON result — unwrap for display.
122+
last = lines[-1]
123+
try:
124+
val = json.loads(last)
125+
# Print like Python's print(): strings unquoted, everything else as repr.
126+
if isinstance(val, str):
127+
print(val)
128+
elif val is None:
129+
pass # None result: print nothing (matches Python behavior)
130+
else:
131+
print(val)
132+
except (json.JSONDecodeError, ValueError):
133+
print(last)
134+
return 0
135+
136+
137+
def _cmd_run_python(args: argparse.Namespace) -> int:
138+
"""Original Python tree-walking interpreter."""
76139
try:
77-
# Parse first without the store; if the source uses `from`
78-
# imports, the parser will return an error that names the
79-
# missing store. We open the store then and retry, so modules
80-
# that do not need a store do not pay for opening one.
81140
src = _read(args.file)
82141
store: Optional[SymbolStore] = None
83142
try:
@@ -86,12 +145,10 @@ def cmd_run(args: argparse.Namespace) -> int:
86145
store = SymbolStore(_store_root(args))
87146
module = parse(src, store=store)
88147
if store is None and module.imports:
89-
# Plain `import` at runtime still needs a store.
90148
store = SymbolStore(_store_root(args))
91149
entry = args.entry or _default_entry(module)
92150
result = run(module, entry, store=store)
93151
if result is not None:
94-
# Print the unwrapped result for scripting convenience.
95152
print(result)
96153
return 0
97154
except CodifideError as e:
@@ -523,6 +580,13 @@ def main(argv=None) -> int:
523580
p_run = sub.add_parser("run", help="execute a Codifide program")
524581
p_run.add_argument("file")
525582
p_run.add_argument("--entry", help="entry definition (default: main or sole definition)")
583+
p_run.add_argument(
584+
"--runtime",
585+
choices=["rust", "python"],
586+
default=None,
587+
help="interpreter runtime: rust (default) or python (reference). "
588+
"Override with CODIFIDE_RUNTIME env var.",
589+
)
526590
p_run.add_argument(
527591
"--store",
528592
help="store root for import resolution (default: $CODIFIDE_STORE or ~/.codifide/store)",

crates/codifide-canonical/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codifide-canonical"
3-
version = "1.0.0"
3+
version = "2.0.0"
44
edition = "2021"
55
description = "Codifide canonical form: typed hypergraph with JSON projection and content addressing."
66
license = "MIT"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "codifide-interpreter"
3+
version = "2.0.0"
4+
edition = "2021"
5+
description = "Codifide tree-walking interpreter — Rust production runtime."
6+
license = "MIT"
7+
publish = false
8+
9+
[lib]
10+
path = "src/lib.rs"
11+
12+
[[bin]]
13+
name = "codifide-run"
14+
path = "src/bin/codifide_run.rs"
15+
16+
[dependencies]
17+
codifide-canonical = { path = "../codifide-canonical" }
18+
serde_json = "1"
19+
rayon = "1.10"
20+
21+
[dev-dependencies]
22+
criterion = { version = "0.5", features = ["html_reports"] }
23+
24+
[[bench]]
25+
name = "interpreter"
26+
harness = false
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//! Criterion benchmarks for the Codifide Rust interpreter.
2+
//!
3+
//! Measures pure interpreter throughput — parse once, run many times.
4+
//! This is the baseline for the graph-native parallel evaluator.
5+
//!
6+
//! Programs chosen to cover different dispatch patterns:
7+
//! sort — multi-candidate dispatch, postconditions, recursion
8+
//! classify — belief dispatch, model primitive
9+
//! fizzbuzz — cost-based dispatch, 15 recursive calls
10+
//! balanced_brackets — deep recursion (indexed primitives)
11+
//! pipeline — multi-function composition, contracts, bind chains
12+
13+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
14+
use std::path::Path;
15+
16+
fn load(path: &str) -> codifide_canonical::ast::Module {
17+
let src = std::fs::read_to_string(path).expect("read source");
18+
let stem = Path::new(path).file_stem().and_then(|s| s.to_str()).unwrap_or("main");
19+
codifide_interpreter::parse(&src, stem).expect("parse")
20+
}
21+
22+
fn bench_run(c: &mut Criterion, name: &str, path: &str) {
23+
let module = load(path);
24+
c.bench_function(name, |b| {
25+
b.iter(|| {
26+
codifide_interpreter::run(black_box(&module), "main", vec![])
27+
.expect("run")
28+
})
29+
});
30+
}
31+
32+
fn benchmarks(c: &mut Criterion) {
33+
bench_run(c, "sort", "../../examples/sort.cod");
34+
bench_run(c, "classify", "../../examples/classify.cod");
35+
bench_run(c, "fizzbuzz", "../../examples/assessment/03_fizzbuzz.cod");
36+
bench_run(c, "balanced_brackets","../../examples/assessment/05_balanced_brackets.cod");
37+
bench_run(c, "pipeline", "../../examples/assessment/06_pipeline.cod");
38+
}
39+
40+
criterion_group!(benches, benchmarks);
41+
criterion_main!(benches);
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! `codifide-run` — CLI binary for the Rust interpreter.
2+
//!
3+
//! Subcommands:
4+
//! run <file.cod> [--entry <name>] [--args <json_array>]
5+
//! Parse and run a .cod source file; print result to stdout.
6+
//! parse <file.cod>
7+
//! Parse a .cod source file and print canonical JSON to stdout.
8+
//! Used by the Python conformance bridge and the Python CLI.
9+
10+
use std::process;
11+
12+
fn main() {
13+
let args: Vec<String> = std::env::args().collect();
14+
if args.len() < 3 {
15+
eprintln!("usage: codifide-run <run|parse> <file.cod> [options]");
16+
process::exit(1);
17+
}
18+
match args[1].as_str() {
19+
"run" => cmd_run(&args),
20+
"parse" => cmd_parse(&args),
21+
other => {
22+
eprintln!("unknown subcommand: {}", other);
23+
process::exit(1);
24+
}
25+
}
26+
}
27+
28+
fn read_source(path: &str) -> String {
29+
match std::fs::read_to_string(path) {
30+
Ok(s) => s,
31+
Err(e) => {
32+
eprintln!("error reading {:?}: {}", path, e);
33+
process::exit(1);
34+
}
35+
}
36+
}
37+
38+
fn parse_source(path: &str) -> codifide_canonical::ast::Module {
39+
let source = read_source(path);
40+
// Derive module name from filename stem.
41+
let stem = std::path::Path::new(path)
42+
.file_stem()
43+
.and_then(|s| s.to_str())
44+
.unwrap_or("main");
45+
match codifide_interpreter::parse(&source, stem) {
46+
Ok(m) => m,
47+
Err(e) => {
48+
eprintln!("parse error: {}", e);
49+
process::exit(1);
50+
}
51+
}
52+
}
53+
54+
fn cmd_parse(args: &[String]) {
55+
let path = &args[2];
56+
let module = parse_source(path);
57+
let json_val = codifide_canonical::to_canonical_json(&module);
58+
match serde_json::to_string(&json_val) {
59+
Ok(s) => println!("{}", s),
60+
Err(e) => {
61+
eprintln!("serialization error: {}", e);
62+
process::exit(1);
63+
}
64+
}
65+
}
66+
67+
fn cmd_run(args: &[String]) {
68+
let path = &args[2];
69+
70+
// Parse optional flags: --entry <name> and --args <json_array>
71+
let mut entry = "main".to_string();
72+
let mut call_args: Vec<codifide_interpreter::Value> = vec![];
73+
let mut i = 3;
74+
while i < args.len() {
75+
match args[i].as_str() {
76+
"--entry" => {
77+
i += 1;
78+
if i < args.len() { entry = args[i].clone(); }
79+
}
80+
"--args" => {
81+
i += 1;
82+
if i < args.len() {
83+
match serde_json::from_str::<serde_json::Value>(&args[i]) {
84+
Ok(serde_json::Value::Array(arr)) => {
85+
call_args = arr.iter().map(|v| {
86+
codifide_interpreter::Value::concrete(
87+
codifide_interpreter::value::payload_from_json(v)
88+
)
89+
}).collect();
90+
}
91+
_ => {
92+
eprintln!("--args must be a JSON array");
93+
process::exit(1);
94+
}
95+
}
96+
}
97+
}
98+
_ => {}
99+
}
100+
i += 1;
101+
}
102+
103+
let module = parse_source(path);
104+
105+
match codifide_interpreter::run(&module, &entry, call_args) {
106+
Ok(result) => {
107+
println!("{}", result.to_json());
108+
}
109+
Err(e) => {
110+
eprintln!("runtime error: {}", e);
111+
process::exit(1);
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)