rlx_core/milk/bytecode.rs
1//! The bytecode an EEL2 program compiles to, and its text encoding.
2//!
3//! **The seam between the two halves of Plan 0100.** `milkconv` compiles `.milk`
4//! text into an [`EelProgram`]; `core` executes one. Nothing here parses EEL2 —
5//! the parser is in the converter and never ships (ADR-0113).
6//!
7//! # Why the encoding is text
8//!
9//! A bundle carries a program as **assembly text**, not as packed bytes, and it
10//! is a deliberate trade of a few kilobytes for four properties:
11//!
12//! - **No serialization dependency.** `serde` would have to describe an enum with
13//! thirty-odd variants into a format `toml` can carry; hand-written `Display` +
14//! `FromStr` over one op per line costs nothing and adds no crate
15//! (`lightweight is a feature`).
16//! - **A bundle is diffable.** A converted preset's program shows up in review as
17//! lines, so a converter change that perturbs codegen is visible rather than a
18//! changed blob.
19//! - **Round-tripping is a property, not a hope.** [`EelProgram::to_assembly`] and
20//! [`EelProgram::from_assembly`] are inverses, which is assertable and asserted.
21//! - **A malformed program is a surfaced load error**, like every other preset
22//! boundary (ADR-0002 / NFR §10) — the decoder validates jump targets and
23//! register indices once, here, so the VM can trust them.
24//!
25//! # What the VM may assume after decoding
26//!
27//! [`EelProgram::from_assembly`] rejects a program whose jump target is out of
28//! range or whose register index is at or past `n_regs`. Everything downstream —
29//! [`vm::run`](super::vm::run) — indexes on that guarantee, which is what lets the
30//! interpreter loop stay free of bounds checks it would otherwise pay per op per
31//! vertex. **Nothing constructs an `EelProgram` except this decoder and the
32//! converter's codegen**, and the codegen runs its own output through
33//! `EelProgram::validate` before writing it.
34
35// Load-time code, but this module is named in the hygiene guard's scan set
36// (Plan 0100 Phase 2): the VM beside it runs per vertex per frame, and the file
37// convention for anything the guard scans is the panic-denial pragma.
38#![deny(
39 clippy::unwrap_used,
40 clippy::expect_used,
41 clippy::indexing_slicing,
42 clippy::panic,
43 clippy::unreachable
44)]
45
46use std::fmt;
47
48/// A one-argument builtin.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Unary {
51 /// EEL2's `sin` — radians.
52 Sin,
53 /// EEL2's `cos` — radians.
54 Cos,
55 /// EEL2's `tan` — radians.
56 Tan,
57 /// EEL2's `asin`, on an argument clamped into `-1..=1`.
58 Asin,
59 /// EEL2's `acos`, on an argument clamped into `-1..=1`.
60 Acos,
61 /// EEL2's `atan`.
62 Atan,
63 /// EEL2's `sqrt`, `0` on a negative argument.
64 Sqrt,
65 /// `1 / sqrt(x)`, EEL2's `invsqrt`.
66 InvSqrt,
67 /// EEL2's `exp`.
68 Exp,
69 /// Natural log, EEL2's `log`.
70 Log,
71 /// Base-10 logarithm, EEL2's `log10`.
72 Log10,
73 /// EEL2's `abs`.
74 Abs,
75 /// EEL2's three-way `sign`: `-1`, `0` or `1`.
76 Sign,
77 /// `x * x`, EEL2's `sqr`.
78 Sqr,
79 /// EEL2's `floor`.
80 Floor,
81 /// EEL2's `ceil`.
82 Ceil,
83 /// Truncation toward zero, EEL2's `int`.
84 Int,
85 /// EEL2's `bnot`: `1` when the argument is zero, else `0`.
86 BNot,
87 /// `rand(x)` — a salted deterministic draw in `[0, x)` (ADR-0051).
88 Rand,
89 /// `randint(x)` — `floor(rand(x))`.
90 RandInt,
91}
92
93impl Unary {
94 /// The name the assembly text uses, which is also the EEL2 spelling.
95 pub fn as_str(self) -> &'static str {
96 match self {
97 Unary::Sin => "sin",
98 Unary::Cos => "cos",
99 Unary::Tan => "tan",
100 Unary::Asin => "asin",
101 Unary::Acos => "acos",
102 Unary::Atan => "atan",
103 Unary::Sqrt => "sqrt",
104 Unary::InvSqrt => "invsqrt",
105 Unary::Exp => "exp",
106 Unary::Log => "log",
107 Unary::Log10 => "log10",
108 Unary::Abs => "abs",
109 Unary::Sign => "sign",
110 Unary::Sqr => "sqr",
111 Unary::Floor => "floor",
112 Unary::Ceil => "ceil",
113 Unary::Int => "int",
114 Unary::BNot => "bnot",
115 Unary::Rand => "rand",
116 Unary::RandInt => "randint",
117 }
118 }
119
120 /// Parse an assembly/EEL2 name.
121 pub fn from_name(name: &str) -> Option<Self> {
122 Some(match name {
123 "sin" => Unary::Sin,
124 "cos" => Unary::Cos,
125 "tan" => Unary::Tan,
126 "asin" => Unary::Asin,
127 "acos" => Unary::Acos,
128 "atan" => Unary::Atan,
129 "sqrt" => Unary::Sqrt,
130 "invsqrt" => Unary::InvSqrt,
131 "exp" => Unary::Exp,
132 "log" => Unary::Log,
133 "log10" => Unary::Log10,
134 "abs" => Unary::Abs,
135 "sign" => Unary::Sign,
136 "sqr" => Unary::Sqr,
137 "floor" => Unary::Floor,
138 "ceil" => Unary::Ceil,
139 "int" => Unary::Int,
140 "bnot" => Unary::BNot,
141 "rand" => Unary::Rand,
142 "randint" => Unary::RandInt,
143 _ => return None,
144 })
145 }
146
147 /// Whether this builtin reads the VM's RNG rather than only its argument.
148 /// The two that do are the reason a program's execution is a function of
149 /// `(inputs, salt, call sequence)` rather than of the inputs alone.
150 pub fn is_random(self) -> bool {
151 matches!(self, Unary::Rand | Unary::RandInt)
152 }
153}
154
155/// A two-argument builtin.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum Binary {
158 /// EEL2's `min`.
159 Min,
160 /// EEL2's `max`.
161 Max,
162 /// EEL2's `pow`.
163 Pow,
164 /// EEL2's `atan2(y, x)`.
165 Atan2,
166 /// EEL2's `sigmoid(x, c)`.
167 Sigmoid,
168 /// EEL2's `band` — non-lazy logical and. `&&` compiles to jumps instead.
169 BAnd,
170 /// EEL2's `bor` — non-lazy logical or.
171 BOr,
172 /// EEL2's `above(a, b)`.
173 Above,
174 /// EEL2's `below(a, b)`.
175 Below,
176 /// EEL2's `equal(a, b)`.
177 Equal,
178}
179
180impl Binary {
181 /// The name the assembly text uses.
182 pub fn as_str(self) -> &'static str {
183 match self {
184 Binary::Min => "min",
185 Binary::Max => "max",
186 Binary::Pow => "pow",
187 Binary::Atan2 => "atan2",
188 Binary::Sigmoid => "sigmoid",
189 Binary::BAnd => "band",
190 Binary::BOr => "bor",
191 Binary::Above => "above",
192 Binary::Below => "below",
193 Binary::Equal => "equal",
194 }
195 }
196
197 /// Parse an assembly/EEL2 name.
198 pub fn from_name(name: &str) -> Option<Self> {
199 Some(match name {
200 "min" => Binary::Min,
201 "max" => Binary::Max,
202 "pow" => Binary::Pow,
203 "atan2" => Binary::Atan2,
204 "sigmoid" => Binary::Sigmoid,
205 "band" => Binary::BAnd,
206 "bor" => Binary::BOr,
207 "above" => Binary::Above,
208 "below" => Binary::Below,
209 "equal" => Binary::Equal,
210 _ => return None,
211 })
212 }
213}
214
215/// Which scratch arena a memory op addresses.
216///
217/// `megabuf` is the preset's own; `gmegabuf` is shared across the three programs
218/// of one bundle. Both are **fixed arenas allocated once at preset load** —
219/// nothing here grows per frame, because this runs on the render thread
220/// (NFR §5).
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum Mem {
223 /// EEL2's `megabuf`.
224 Local,
225 /// EEL2's `gmegabuf`.
226 Global,
227}
228
229impl Mem {
230 /// The name the assembly text uses.
231 pub fn as_str(self) -> &'static str {
232 match self {
233 Mem::Local => "megabuf",
234 Mem::Global => "gmegabuf",
235 }
236 }
237
238 /// Parse an assembly/EEL2 name.
239 pub fn from_name(name: &str) -> Option<Self> {
240 Some(match name {
241 "megabuf" => Mem::Local,
242 "gmegabuf" => Mem::Global,
243 _ => return None,
244 })
245 }
246}
247
248/// One bytecode instruction.
249///
250/// A **stack** machine, not a register one, and deliberately: EEL2 is an
251/// expression language whose statements are expressions, so a stack matches its
252/// shape and the codegen is a post-order walk with no allocation. The named
253/// variables *are* registers ([`Op::Load`] / [`Op::Store`]); the stack is only
254/// the arithmetic's scratch.
255#[derive(Debug, Clone, Copy, PartialEq)]
256pub enum Op {
257 /// Push a literal.
258 Const(f32),
259 /// Push register `n`.
260 Load(u16),
261 /// Pop, store into register `n`, and **push the value back** — EEL2's
262 /// assignment is an expression that yields what it assigned.
263 Store(u16),
264 /// Discard the top of the stack. Emitted between the statements of a
265 /// sequence, whose value is its last statement's.
266 Pop,
267 /// Arithmetic negation.
268 Neg,
269 /// Addition.
270 Add,
271 /// Subtraction.
272 Sub,
273 /// Multiplication.
274 Mul,
275 /// Division. **Total**: a zero divisor yields `0`, which is EEL2's own
276 /// behaviour and what keeps the VM panic-free (Plan 0100 Phase 2).
277 Div,
278 /// Remainder, on the integer parts, EEL2's `%`. Total the same way.
279 Mod,
280 /// EEL2's `^`, which is exponentiation rather than xor.
281 /// EEL2's `pow`.
282 Pow,
283 /// `>` — pushes `1` or `0`.
284 Above,
285 /// `<`.
286 Below,
287 /// `>=`.
288 AboveEq,
289 /// `<=`.
290 BelowEq,
291 /// `==`, against EEL2's comparison epsilon.
292 Equal,
293 /// `!=`, against the same epsilon.
294 NotEqual,
295 /// `!` — `1` when the operand is zero, else `0`.
296 Not,
297 /// EEL2's `&` — **bitwise** and on the truncated integer parts, not a
298 /// logical one. `band` is the logical operator, and the two are different
299 /// functions: `3 & 4` is `0` where `band(3, 4)` is `1`.
300 BitAnd,
301 /// EEL2's `|` — bitwise or, the counterpart of [`Op::BitAnd`].
302 BitOr,
303 /// One-argument builtin.
304 Fn1(Unary),
305 /// Two-argument builtin.
306 Fn2(Binary),
307 /// Pop an index, push that slot.
308 MemLoad(Mem),
309 /// Pop a value and an index (value on top), store, and push the value back.
310 MemStore(Mem),
311 /// Unconditional jump to an absolute instruction index.
312 Jump(u32),
313 /// Pop; jump when the value is zero.
314 JumpIfZero(u32),
315 /// Pop; jump when the value is non-zero.
316 JumpIfNotZero(u32),
317 /// Pop a count, open a loop frame, and jump past [`Op::LoopEnd`] when the
318 /// count rounds to less than one. The count is clamped to
319 /// [`Budget::loops`](super::vm::Budget::loops).
320 LoopBegin(u32),
321 /// Close a loop iteration: discard the body's value, decrement, and jump back
322 /// to the operand while iterations remain. Pushes `0` when the loop ends,
323 /// because a loop is an expression.
324 LoopEnd(u32),
325 /// Open a `while` frame with [`Budget::loops`](super::vm::Budget::loops)
326 /// iterations, jumping past
327 /// its [`Op::WhileEnd`] never — the test is at the end, because EEL2's
328 /// `while(body)` runs the body first.
329 WhileBegin(u32),
330 /// Close a `while` iteration: pop the body's value, decrement, and jump back
331 /// to the operand while it is non-zero and iterations remain. Pushes `0`
332 /// when the loop ends.
333 WhileEnd(u32),
334}
335
336/// EEL2's comparison epsilon: `==` is true within this, and `!=` outside it.
337///
338/// Not a tolerance we chose — it is part of the language, and a preset written
339/// against it (`equal(x, 0)` after arithmetic that lands near zero) reads
340/// differently under an exact comparison.
341pub const COMPARE_EPSILON: f32 = 0.00001;
342
343/// One compiled EEL2 program: flat bytecode over a fixed register file.
344///
345/// Fixed-size at load; nothing here grows per frame.
346#[derive(Debug, Clone, PartialEq)]
347pub struct EelProgram {
348 /// The instructions, in execution order. Jump operands are indices into this.
349 code: Vec<Op>,
350 /// Register names, positionally — `names[i]` is register `i`. The host
351 /// resolves the roster it cares about (`zoom`, `q1`, `bass`, …) to indices
352 /// **once at load** through [`register`](Self::register), so the frame loop
353 /// never looks a name up.
354 names: Vec<String>,
355 /// The deepest the operand stack can get, computed at validation. The VM
356 /// sizes its stack from it, so a running program cannot outgrow one.
357 stack_depth: usize,
358 /// Every register this program can write, ascending and deduplicated.
359 ///
360 /// The per-vertex driver's whole optimization. MilkDrop runs a per-vertex
361 /// program against a **copy** of the per-frame register state, so writes
362 /// inside it do not leak from one vertex to the next; copying the whole file
363 /// per vertex is thousands of floats per frame for the sake of a handful that
364 /// actually move. Snapshotting only these is the same semantics at a fraction
365 /// of the memory traffic, and it is exact rather than an approximation —
366 /// a register no `Store` names cannot change.
367 written: Vec<u16>,
368}
369
370/// What is wrong with a program, as a surfaced load error.
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub enum ProgramError {
373 /// A jump operand addresses no instruction.
374 BadJump {
375 /// The instruction holding the jump.
376 at: usize,
377 /// The operand.
378 target: u32,
379 },
380 /// A register index is at or past the declared register count.
381 BadRegister {
382 /// The instruction holding it.
383 at: usize,
384 /// The index.
385 index: u16,
386 },
387 /// The program pops more than it pushes somewhere, so the stack would
388 /// underflow.
389 StackUnderflow {
390 /// The instruction that would underflow.
391 at: usize,
392 },
393 /// A `.regs` name appears twice, so [`register`](EelProgram::register) could
394 /// not say which one a host meant.
395 DuplicateRegister(String),
396 /// An assembly line the decoder does not recognize.
397 BadLine {
398 /// 1-based line number in the assembly text.
399 line: usize,
400 /// The offending text.
401 text: String,
402 },
403 /// A `.regs` or `.code` header is missing or out of order.
404 BadHeader(&'static str),
405}
406
407impl fmt::Display for ProgramError {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 match self {
410 ProgramError::BadJump { at, target } => {
411 write!(
412 f,
413 "instruction {at} jumps to {target}, which is not an instruction"
414 )
415 }
416 ProgramError::BadRegister { at, index } => {
417 write!(
418 f,
419 "instruction {at} addresses register {index}, which is not declared"
420 )
421 }
422 ProgramError::StackUnderflow { at } => {
423 write!(f, "instruction {at} pops from an empty stack")
424 }
425 ProgramError::DuplicateRegister(name) => {
426 write!(f, "register '{name}' is declared twice")
427 }
428 ProgramError::BadLine { line, text } => {
429 write!(f, "line {line}: unrecognized instruction '{text}'")
430 }
431 ProgramError::BadHeader(what) => write!(f, "{what}"),
432 }
433 }
434}
435
436impl std::error::Error for ProgramError {}
437
438impl EelProgram {
439 /// A program with no instructions and no registers — the identity, and what
440 /// an absent section in a bundle means.
441 pub fn empty() -> Self {
442 Self {
443 code: Vec::new(),
444 names: Vec::new(),
445 stack_depth: 0,
446 written: Vec::new(),
447 }
448 }
449
450 /// Assemble from code and register names, validating both.
451 ///
452 /// **The only constructor**, so the invariants `validate`
453 /// establishes hold of every `EelProgram` that exists.
454 pub fn new(code: Vec<Op>, names: Vec<String>) -> Result<Self, ProgramError> {
455 let mut program = Self {
456 code,
457 names,
458 stack_depth: 0,
459 written: Vec::new(),
460 };
461 program.stack_depth = program.validate()?;
462 program.written = program
463 .code
464 .iter()
465 .filter_map(|op| match op {
466 Op::Store(index) => Some(*index),
467 _ => None,
468 })
469 .collect();
470 program.written.sort_unstable();
471 program.written.dedup();
472 Ok(program)
473 }
474
475 /// The instructions.
476 pub fn code(&self) -> &[Op] {
477 &self.code
478 }
479
480 /// The register names, positionally.
481 pub fn names(&self) -> &[String] {
482 &self.names
483 }
484
485 /// How many registers the program declares.
486 pub fn register_count(&self) -> usize {
487 self.names.len()
488 }
489
490 /// The deepest the operand stack gets. The VM sizes its stack from this.
491 pub fn stack_depth(&self) -> usize {
492 self.stack_depth
493 }
494
495 /// Every register this program can write — see the field.
496 pub fn written_registers(&self) -> &[u16] {
497 &self.written
498 }
499
500 /// The index of the register called `name`, or `None`.
501 ///
502 /// **Called at load, never per frame.** The host resolves the whole roster it
503 /// cares about once and then addresses registers by index.
504 pub fn register(&self, name: &str) -> Option<u16> {
505 self.names
506 .iter()
507 .position(|n| n == name)
508 .and_then(|i| u16::try_from(i).ok())
509 }
510
511 /// Whether the program can draw from the RNG, i.e. whether its output depends
512 /// on anything but its inputs and its register file.
513 ///
514 /// Read by the capture harness's determinism argument: a program that says
515 /// `false` here is a pure function of its inputs, and one that says `true` is
516 /// a pure function of its inputs **and the VM's seeded RNG state**, which is
517 /// itself reset with the preset (ADR-0051).
518 pub fn uses_random(&self) -> bool {
519 self.code
520 .iter()
521 .any(|op| matches!(op, Op::Fn1(f) if f.is_random()))
522 }
523
524 /// Check every jump target, every register index, and that the stack never
525 /// underflows; return the deepest the stack gets.
526 ///
527 /// The stack walk is a **linear** pass rather than a flow-sensitive one: it
528 /// takes each instruction's net effect in program order. That is exact for
529 /// the codegen this crate's converter emits — every branch it generates is
530 /// stack-balanced at its join — and conservative nowhere, because an
531 /// unbalanced branch would show up as a mismatch at the join in a
532 /// flow-sensitive walk and as a wrong depth here. The VM does not rely on the
533 /// depth being tight: it is a capacity, and the interpreter guards its own
534 /// pops (returning `0` on an empty stack) so a hand-written program that
535 /// defeats this walk misbehaves rather than panicking.
536 fn validate(&self) -> Result<usize, ProgramError> {
537 let len = self.code.len();
538 for (at, op) in self.code.iter().enumerate() {
539 match *op {
540 Op::Jump(t)
541 | Op::JumpIfZero(t)
542 | Op::JumpIfNotZero(t)
543 | Op::LoopBegin(t)
544 | Op::LoopEnd(t)
545 | Op::WhileBegin(t)
546 | Op::WhileEnd(t) => {
547 // A jump to exactly `len` is the ordinary "fall off the end"
548 // target the codegen emits for a branch over the last
549 // instruction, so it is in range.
550 if t as usize > len {
551 return Err(ProgramError::BadJump { at, target: t });
552 }
553 }
554 Op::Load(index) | Op::Store(index) if index as usize >= self.names.len() => {
555 return Err(ProgramError::BadRegister { at, index });
556 }
557 _ => {}
558 }
559 }
560 for (i, name) in self.names.iter().enumerate() {
561 if self.names.iter().take(i).any(|n| n == name) {
562 return Err(ProgramError::DuplicateRegister(name.clone()));
563 }
564 }
565
566 let mut depth = 0i64;
567 let mut peak = 0i64;
568 for (at, op) in self.code.iter().enumerate() {
569 let (pops, pushes) = op.stack_effect();
570 depth -= pops as i64;
571 if depth < 0 {
572 return Err(ProgramError::StackUnderflow { at });
573 }
574 depth += pushes as i64;
575 peak = peak.max(depth);
576 }
577 // One slot of headroom, so an interpreter push never has to test the
578 // capacity it was sized from.
579 Ok(peak.max(0) as usize + 1)
580 }
581}
582
583impl Op {
584 /// How many operands this instruction pops and pushes.
585 ///
586 /// The loop ops are the interesting ones: `LoopBegin` pops its count and
587 /// pushes nothing (the counter lives on the VM's separate loop stack), and
588 /// `LoopEnd` pops the body's value and pushes the loop's own result — so a
589 /// loop is net neutral, which is what makes it usable as a sub-expression.
590 fn stack_effect(self) -> (u8, u8) {
591 match self {
592 Op::Const(_) | Op::Load(_) => (0, 1),
593 Op::Store(_) => (1, 1),
594 Op::Pop => (1, 0),
595 Op::Neg | Op::Not | Op::Fn1(_) | Op::MemLoad(_) => (1, 1),
596 Op::Add
597 | Op::Sub
598 | Op::Mul
599 | Op::Div
600 | Op::Mod
601 | Op::Pow
602 | Op::Above
603 | Op::Below
604 | Op::AboveEq
605 | Op::BelowEq
606 | Op::Equal
607 | Op::NotEqual
608 | Op::BitAnd
609 | Op::BitOr
610 | Op::Fn2(_) => (2, 1),
611 Op::MemStore(_) => (2, 1),
612 Op::Jump(_) => (0, 0),
613 Op::JumpIfZero(_) | Op::JumpIfNotZero(_) => (1, 0),
614 Op::LoopBegin(_) | Op::WhileBegin(_) => (1, 0),
615 Op::LoopEnd(_) | Op::WhileEnd(_) => (1, 1),
616 }
617 }
618}
619
620// ---------------------------------------------------------------------------
621// The text encoding
622// ---------------------------------------------------------------------------
623
624impl EelProgram {
625 /// This program as assembly text — the form a bundle carries.
626 ///
627 /// ```text
628 /// .regs zoom rot _t0
629 /// .code
630 /// const 1.5
631 /// load 0
632 /// mul
633 /// store 0
634 /// ```
635 ///
636 /// The inverse of [`from_assembly`](Self::from_assembly), which is asserted
637 /// rather than intended.
638 pub fn to_assembly(&self) -> String {
639 let mut out = String::new();
640 out.push_str(".regs");
641 for name in &self.names {
642 out.push(' ');
643 out.push_str(name);
644 }
645 out.push_str("\n.code\n");
646 for op in &self.code {
647 out.push_str(&op_to_text(*op));
648 out.push('\n');
649 }
650 out
651 }
652
653 /// Decode assembly text, validating it into a program the VM may trust.
654 ///
655 /// Blank lines and `#` comments are ignored, so a bundle stays legible.
656 pub fn from_assembly(text: &str) -> Result<Self, ProgramError> {
657 let mut names: Vec<String> = Vec::new();
658 let mut code: Vec<Op> = Vec::new();
659 let mut seen_regs = false;
660 let mut seen_code = false;
661 for (index, raw) in text.lines().enumerate() {
662 let line = raw.split('#').next().unwrap_or("").trim();
663 if line.is_empty() {
664 continue;
665 }
666 if let Some(rest) = line.strip_prefix(".regs") {
667 if seen_regs {
668 return Err(ProgramError::BadHeader("a program declares .regs twice"));
669 }
670 seen_regs = true;
671 names = rest.split_whitespace().map(str::to_string).collect();
672 continue;
673 }
674 if line == ".code" {
675 if !seen_regs {
676 return Err(ProgramError::BadHeader(".code appears before .regs"));
677 }
678 seen_code = true;
679 continue;
680 }
681 if !seen_code {
682 return Err(ProgramError::BadHeader(
683 "an instruction appears before .code",
684 ));
685 }
686 let op = op_from_text(line).ok_or_else(|| ProgramError::BadLine {
687 line: index + 1,
688 text: line.to_string(),
689 })?;
690 code.push(op);
691 }
692 if !seen_regs || !seen_code {
693 return Err(ProgramError::BadHeader(
694 "a program needs a .regs line and a .code line",
695 ));
696 }
697 Self::new(code, names)
698 }
699}
700
701/// One instruction as a line of assembly.
702fn op_to_text(op: Op) -> String {
703 match op {
704 // `{:?}` on an `f32` round-trips exactly (Rust prints the shortest
705 // representation that parses back to the same bits), which is what makes
706 // the encoding lossless.
707 Op::Const(v) => format!("const {v:?}"),
708 Op::Load(n) => format!("load {n}"),
709 Op::Store(n) => format!("store {n}"),
710 Op::Pop => "pop".into(),
711 Op::Neg => "neg".into(),
712 Op::Not => "not".into(),
713 Op::Add => "add".into(),
714 Op::Sub => "sub".into(),
715 Op::Mul => "mul".into(),
716 Op::Div => "div".into(),
717 Op::Mod => "mod".into(),
718 Op::Pow => "pow".into(),
719 Op::Above => "above".into(),
720 Op::Below => "below".into(),
721 Op::AboveEq => "aboveeq".into(),
722 Op::BelowEq => "beloweq".into(),
723 Op::Equal => "equal".into(),
724 Op::NotEqual => "notequal".into(),
725 Op::BitAnd => "bitand".into(),
726 Op::BitOr => "bitor".into(),
727 Op::Fn1(f) => format!("fn1 {}", f.as_str()),
728 Op::Fn2(f) => format!("fn2 {}", f.as_str()),
729 Op::MemLoad(m) => format!("memload {}", m.as_str()),
730 Op::MemStore(m) => format!("memstore {}", m.as_str()),
731 Op::Jump(t) => format!("jump {t}"),
732 Op::JumpIfZero(t) => format!("jz {t}"),
733 Op::JumpIfNotZero(t) => format!("jnz {t}"),
734 Op::LoopBegin(t) => format!("loopbegin {t}"),
735 Op::LoopEnd(t) => format!("loopend {t}"),
736 Op::WhileBegin(t) => format!("whilebegin {t}"),
737 Op::WhileEnd(t) => format!("whileend {t}"),
738 }
739}
740
741/// One line of assembly as an instruction, or `None` if it is not one.
742fn op_from_text(line: &str) -> Option<Op> {
743 let mut parts = line.split_whitespace();
744 let head = parts.next()?;
745 let arg = parts.next();
746 if parts.next().is_some() {
747 return None;
748 }
749 let index = || arg.and_then(|a| a.parse::<u16>().ok());
750 let target = || arg.and_then(|a| a.parse::<u32>().ok());
751 Some(match (head, arg) {
752 ("const", Some(v)) => Op::Const(v.parse::<f32>().ok()?),
753 ("load", _) => Op::Load(index()?),
754 ("store", _) => Op::Store(index()?),
755 ("pop", None) => Op::Pop,
756 ("neg", None) => Op::Neg,
757 ("not", None) => Op::Not,
758 ("add", None) => Op::Add,
759 ("sub", None) => Op::Sub,
760 ("mul", None) => Op::Mul,
761 ("div", None) => Op::Div,
762 ("mod", None) => Op::Mod,
763 ("pow", None) => Op::Pow,
764 ("above", None) => Op::Above,
765 ("below", None) => Op::Below,
766 ("aboveeq", None) => Op::AboveEq,
767 ("beloweq", None) => Op::BelowEq,
768 ("equal", None) => Op::Equal,
769 ("notequal", None) => Op::NotEqual,
770 ("bitand", None) => Op::BitAnd,
771 ("bitor", None) => Op::BitOr,
772 ("fn1", Some(name)) => Op::Fn1(Unary::from_name(name)?),
773 ("fn2", Some(name)) => Op::Fn2(Binary::from_name(name)?),
774 ("memload", Some(name)) => Op::MemLoad(Mem::from_name(name)?),
775 ("memstore", Some(name)) => Op::MemStore(Mem::from_name(name)?),
776 ("jump", _) => Op::Jump(target()?),
777 ("jz", _) => Op::JumpIfZero(target()?),
778 ("jnz", _) => Op::JumpIfNotZero(target()?),
779 ("loopbegin", _) => Op::LoopBegin(target()?),
780 ("loopend", _) => Op::LoopEnd(target()?),
781 ("whilebegin", _) => Op::WhileBegin(target()?),
782 ("whileend", _) => Op::WhileEnd(target()?),
783 _ => return None,
784 })
785}