Skip to content

Commit fe3f8ba

Browse files
committed
Implement Converge slice expansion
1 parent ddc23e3 commit fe3f8ba

19 files changed

Lines changed: 1113 additions & 25 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ version = "0.1.0"
77
edition = "2024"
88
license = "MIT"
99
rust-version = "1.92"
10-

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,17 @@ cargo test
5454
cargo run -p converge-cli -- check examples/hello.cv
5555
cargo run -p converge-cli -- ast examples/hello.cv
5656
cargo run -p converge-cli -- cvir examples/hello.cv
57+
cargo run -p converge-cli -- sim examples/poisson.cv
5758
```
5859

5960
## Docs
6061

6162
1. `docs/spec.md` current accepted grammar and validation rules
62-
2. `docs/references.md` curated anchors for hardware and interchange
63-
3. `docs/voice.md` writing rules for project docs
64-
4. `docs/brand.md` logo and asset guidance
63+
2. `docs/semantics.md` time model and determinism rules
64+
3. `docs/cvir.md` canonical IR schema and examples
65+
4. `docs/references.md` curated anchors for hardware and interchange
66+
5. `docs/voice.md` writing rules for project docs
67+
6. `docs/brand.md` logo and asset guidance
6568

6669
## Origin
6770

crates/converge-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ path = "src/main.rs"
1111

1212
[dependencies]
1313
converge-lang = { path = "../converge-lang" }
14-
14+
converge-sim = { path = "../converge-sim" }

crates/converge-cli/src/main.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::path::Path;
44

55
use converge_lang::parser::{format_diagnostic, parse_program};
66
use converge_lang::validate::validate;
7+
use converge_sim::simulate;
78

89
fn main() {
910
let mut args = std::env::args().skip(1);
@@ -13,6 +14,7 @@ fn main() {
1314
"check" => cmd_check(args),
1415
"ast" => cmd_ast(args),
1516
"cvir" => cmd_cvir(args),
17+
"sim" => cmd_sim(args),
1618
"help" | "-h" | "--help" => {
1719
print_usage();
1820
std::process::exit(0);
@@ -103,6 +105,66 @@ fn cmd_cvir(mut args: impl Iterator<Item = String>) {
103105
print!("{}", converge_lang::emit::cvir_json(&program));
104106
}
105107

108+
fn cmd_sim(mut args: impl Iterator<Item = String>) {
109+
let mut file = None;
110+
let mut out_path = None;
111+
112+
while let Some(arg) = args.next() {
113+
if arg == "--out" {
114+
out_path = args.next();
115+
} else if file.is_none() {
116+
file = Some(arg);
117+
} else {
118+
eprintln!("error: unexpected argument `{arg}`\n");
119+
print_usage();
120+
std::process::exit(2);
121+
}
122+
}
123+
124+
let path = match file {
125+
Some(p) => p,
126+
None => {
127+
eprintln!("error: expected a file path\n");
128+
print_usage();
129+
std::process::exit(2);
130+
}
131+
};
132+
133+
let src = read_file(&path);
134+
let program = match parse_program(&src) {
135+
Ok(p) => p,
136+
Err(diag) => {
137+
eprintln!("{}", format_diagnostic(&src, &diag));
138+
std::process::exit(1);
139+
}
140+
};
141+
142+
if let Err(diags) = validate(&program) {
143+
for diag in diags {
144+
eprintln!("{}", format_diagnostic(&src, &diag));
145+
}
146+
std::process::exit(1);
147+
}
148+
149+
let summary = match simulate(&program) {
150+
Ok(s) => s,
151+
Err(err) => {
152+
eprintln!("error: {err}");
153+
std::process::exit(1);
154+
}
155+
};
156+
let json = converge_sim::summary_json(&summary);
157+
158+
if let Some(out) = out_path {
159+
std::fs::write(&out, json).unwrap_or_else(|e| {
160+
eprintln!("error: failed to write `{out}`: {e}");
161+
std::process::exit(2);
162+
});
163+
} else {
164+
print!("{json}");
165+
}
166+
}
167+
106168
fn read_file(path: &str) -> String {
107169
std::fs::read_to_string(Path::new(path)).unwrap_or_else(|e| {
108170
eprintln!("error: failed to read `{path}`: {e}");
@@ -122,12 +184,14 @@ COMMANDS:
122184
check Parse + validate a Converge file
123185
ast Print parsed AST (debug)
124186
cvir Emit canonical JSON IR (debug)
187+
sim Run deterministic simulator
125188
help Show this help
126189
127190
EXAMPLES:
128191
cargo run -p converge-cli -- check examples/hello.cv
129192
cargo run -p converge-cli -- ast examples/hello.cv
130193
cargo run -p converge-cli -- cvir examples/hello.cv
194+
cargo run -p converge-cli -- sim examples/poisson.cv
131195
"
132196
);
133197
}

crates/converge-cli/tests/sim.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use std::process::Command;
2+
3+
#[test]
4+
fn sim_cli_runs() {
5+
let exe = std::env::var("CARGO_BIN_EXE_converge").unwrap_or_else(|_| {
6+
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
7+
manifest
8+
.join("../../target/debug/converge")
9+
.to_string_lossy()
10+
.to_string()
11+
});
12+
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
13+
let example = manifest.join("../../examples/poisson.cv");
14+
let output = Command::new(exe)
15+
.args(["sim", example.to_string_lossy().as_ref()])
16+
.output()
17+
.expect("run converge sim");
18+
assert!(output.status.success());
19+
}

crates/converge-lang/src/ast.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ pub enum Item {
1616
Neuron(NeuronDef),
1717
Layer(LayerDef),
1818
Connect(ConnectDef),
19+
Stimulus(StimulusDef),
1920
Run(RunStmt),
21+
Seed(SeedStmt),
2022
}
2123

2224
#[derive(Debug, Clone, PartialEq)]
@@ -42,6 +44,24 @@ pub struct ConnectDef {
4244
#[derive(Debug, Clone, PartialEq)]
4345
pub struct RunStmt {
4446
pub duration: Quantity,
47+
pub step: Option<Quantity>,
48+
}
49+
50+
#[derive(Debug, Clone, PartialEq)]
51+
pub struct SeedStmt {
52+
pub value: u64,
53+
pub span: Span,
54+
}
55+
56+
#[derive(Debug, Clone, PartialEq)]
57+
pub struct StimulusDef {
58+
pub layer: Ident,
59+
pub model: StimulusModel,
60+
}
61+
62+
#[derive(Debug, Clone, PartialEq)]
63+
pub enum StimulusModel {
64+
Poisson { rate: Quantity },
4565
}
4666

4767
#[derive(Debug, Clone, PartialEq)]

crates/converge-lang/src/emit.rs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,32 @@ pub fn cvir_json(program: &Program) -> String {
44
let mut w = JsonWriter::new();
55
w.obj_begin();
66

7+
w.kv_str("cvir_version", "0.2");
8+
w.comma_nl();
79
w.key("items");
810
w.array_begin();
9-
for (idx, item) in program.items.iter().enumerate() {
10-
if idx != 0 {
11+
let seed = program
12+
.items
13+
.iter()
14+
.find_map(|item| match item {
15+
Item::Seed(s) => Some(s.value),
16+
_ => None,
17+
})
18+
.unwrap_or(0);
19+
20+
let mut first = true;
21+
for item in program.items.iter() {
22+
if matches!(item, Item::Seed(_)) {
23+
continue;
24+
}
25+
if !first {
1126
w.comma();
1227
}
28+
first = false;
1329
w.nl();
14-
emit_item(&mut w, item);
30+
emit_item(&mut w, item, seed);
1531
}
16-
if !program.items.is_empty() {
32+
if !first {
1733
w.nl();
1834
}
1935
w.array_end();
@@ -24,7 +40,7 @@ pub fn cvir_json(program: &Program) -> String {
2440
w.finish()
2541
}
2642

27-
fn emit_item(w: &mut JsonWriter, item: &Item) {
43+
fn emit_item(w: &mut JsonWriter, item: &Item, seed: u64) {
2844
w.obj_begin();
2945
match item {
3046
Item::Neuron(d) => {
@@ -54,11 +70,42 @@ fn emit_item(w: &mut JsonWriter, item: &Item) {
5470
w.key("body");
5571
emit_assigns(w, &d.body);
5672
}
73+
Item::Stimulus(d) => {
74+
w.kv_str("kind", "stimulus");
75+
w.comma_nl();
76+
w.kv_str("layer", &d.layer.name);
77+
w.comma_nl();
78+
w.key("model");
79+
emit_stimulus_model(w, &d.model);
80+
}
5781
Item::Run(d) => {
5882
w.kv_str("kind", "run");
5983
w.comma_nl();
6084
w.key("duration");
6185
emit_quantity(w, &d.duration);
86+
w.comma_nl();
87+
w.key("step");
88+
if let Some(step) = &d.step {
89+
emit_quantity(w, step);
90+
} else {
91+
emit_quantity_value(w, 1.0, Some("ms"));
92+
}
93+
w.comma_nl();
94+
w.kv_u64("seed", seed);
95+
}
96+
Item::Seed(_) => {}
97+
}
98+
w.obj_end();
99+
}
100+
101+
fn emit_stimulus_model(w: &mut JsonWriter, model: &StimulusModel) {
102+
w.obj_begin();
103+
match model {
104+
StimulusModel::Poisson { rate } => {
105+
w.kv_str("type", "poisson");
106+
w.comma_nl();
107+
w.key("rate");
108+
emit_quantity(w, rate);
62109
}
63110
}
64111
w.obj_end();
@@ -126,11 +173,15 @@ fn emit_expr(w: &mut JsonWriter, e: &Expr) {
126173
}
127174

128175
fn emit_quantity(w: &mut JsonWriter, q: &Quantity) {
176+
emit_quantity_value(w, q.value, q.unit.as_ref().map(|u| u.name.as_str()));
177+
}
178+
179+
fn emit_quantity_value(w: &mut JsonWriter, value: f64, unit: Option<&str>) {
129180
w.obj_begin();
130-
w.kv_f64("value", q.value);
131-
if let Some(u) = &q.unit {
181+
w.kv_f64("value", value);
182+
if let Some(u) = unit {
132183
w.comma_nl();
133-
w.kv_str("unit", &u.name);
184+
w.kv_str("unit", u);
134185
}
135186
w.obj_end();
136187
}

crates/converge-lang/src/lexer.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ pub enum TokenKind {
1111
KwConnect,
1212
KwRun,
1313
KwFor,
14+
KwSeed,
15+
KwStep,
16+
KwStimulus,
17+
KwRate,
1418

1519
LBrace,
1620
RBrace,
@@ -206,6 +210,10 @@ impl<'a> Lexer<'a> {
206210
"connect" => TokenKind::KwConnect,
207211
"run" => TokenKind::KwRun,
208212
"for" => TokenKind::KwFor,
213+
"seed" => TokenKind::KwSeed,
214+
"step" => TokenKind::KwStep,
215+
"stimulus" => TokenKind::KwStimulus,
216+
"rate" => TokenKind::KwRate,
209217
_ => TokenKind::Ident(text.to_string()),
210218
};
211219
Ok(Token {

crates/converge-lang/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ pub mod diagnostic;
55
pub mod emit;
66
pub mod lexer;
77
pub mod parser;
8+
pub mod units;
89
pub mod validate;

0 commit comments

Comments
 (0)