From acd0fd1b25af60008fbbb9a844bbbdc4e121783d Mon Sep 17 00:00:00 2001 From: Nyeogmi Date: Fri, 23 Jun 2023 20:55:40 -0700 Subject: [PATCH] Include my code --- .gitignore | 3 + Cargo.lock | 25 ++ Cargo.toml | 9 + asm_notes.txt | 77 ++++++ src/audio.rs | 131 +++++++++ src/device.rs | 122 +++++++++ src/error.rs | 38 +++ src/main.rs | 98 +++++++ src/rack.rs | 56 ++++ src/standard_devices/mod.rs | 33 +++ src/vm.rs | 519 ++++++++++++++++++++++++++++++++++++ 11 files changed, 1111 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 asm_notes.txt create mode 100644 src/audio.rs create mode 100644 src/device.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/rack.rs create mode 100644 src/standard_devices/mod.rs create mode 100644 src/vm.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b37166b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +example.wav +.vscode/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d00bad9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "lansky" +version = "0.1.0" +dependencies = [ + "wav", +] + +[[package]] +name = "riff" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b1a3d5f46d53f4a3478e2be4a5a5ce5108ea58b100dcd139830eae7f79a3a1" + +[[package]] +name = "wav" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65e199c799848b4f997072aa4d673c034f80f40191f97fe2f0a23f410be1609" +dependencies = [ + "riff", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..770efb7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "lansky" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +wav = "1.0.0" diff --git a/asm_notes.txt b/asm_notes.txt new file mode 100644 index 0000000..6b48700 --- /dev/null +++ b/asm_notes.txt @@ -0,0 +1,77 @@ +NOTES: Slightly out of date -- I haven't added the Imm mov mode or the ix register yet + +Registers: + $a0..9: Corresponds to input 0 through 9 + $l0..9: "Locals" 0..9 + $z0..9: Corresponds to output 0 through 9 + $r0..255: Auxiliary uint64_ts + + $ix: Index base + +Channels: + A channel is a vector of samples with a known "formal range" -- + this formal range includes its start (relative to the whole song) + and its basic length. + + You can use outputs that aren't exposed as part of your device. + This can be a good place to hide wavetables and stuff. + They just won't be returned. + +Memory model: + $a0..9, $l0..9, and $z0..9 correspond to arrays + When you read $a0, it reads channel a0 at $ix+a0.formal_start+a0.loop_start + When you write $z0, it writes channel z0 at $ix+z0.formal_start+z0.loop_start + + Unknown positions are 0. + + A channel has an informal_start and an informal_length which initially match + formal_start and formal_length. + + Writing to a position outside of informal_start..informal_start+informal_length + extends the bounds. + + In looped mode, then the ultimate indices are wrapped to + (formal_start+loop_start..formal_start+loop_start+loop_length) + and all other behavior remains the same as above. + +Instructions: + mov|add|xor ($l0..9, $z0..9, $r0..255), ($a0..9, $l0..9, $z0..9, $r0..255) + Copy value, add value, or xor value. + + range ($a0..9, $l0..9, $z0..9), loop_start, loop_length + Start and length are immediate values. + If length is nonzero, the channel is in "looped" mode. + + inp ($a0..9, $l0..9, $z0..9) + Push a copy of the channel to the stack for "invoke". + The channel will be sliced: + - if loop_start is zero and loop_length is zero: + The formal range runs from formal_start to formal_start+formal_length. + The informal range runs from informal_start to informal_start+informal_length. + - if loop_start is nonzero and loop_length is zero: + The formal range runs from formal_start+loop_start to formal_start+formal_length + The informal range runs from formal_start+loop_start to informal_start+informal_length + - if loop_length is nonzero: + The formal range runs from formal_start+loop_start to formal_start+loop_start+loop_length + The informal range is the same as the formal range. + + dup + Duplicate an item from the invoke stack. + + oup (mov|add|xor) ($l0..9, $z0..9) + Pop an output from the invoke stack and write it to an output channel or local. + + All indexes from informal_start to informal_start+informal_length are written. + + The instruction used to write can be mov, add, or xor + + invoke device, n_inputs, n_outputs + Invoke the device by name (an immediate string) + Asserts that the device takes n_inputs inputs (an immediate integer) and n_outputs outputs (an immediate integer) + Asserts that there are actually n_inputs inputs on the stack. + After invoke, there will be n_outputs outputs on the stack. + They will be ordered such that the first call to oup produces the first output, the second produces the second, + and so on + + exit + Exit the current procedure. \ No newline at end of file diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..79ecbbd --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,131 @@ +use std::ops::{Range, Index, IndexMut}; + +// Fixed point: 0xFFFF_FFFF.FFFF_FFFF +#[derive(Clone, Copy)] +pub struct Sample(pub i64); + +impl Sample { + pub fn to_i16_sample(&self) -> i16 { + // interpret self as fixed-point + if self.0 > i32::MAX as i64 { return i16::MAX } + if self.0 < i32::MIN as i64 { return i16::MIN } + let low_part = self.0 as i32; + return (low_part >> 16) as i16 + } +} + +impl Default for Sample { + fn default() -> Self { BLANK_SAMPLE } +} + +const BLANK_SAMPLE: Sample = Sample(0); + +#[derive(Clone)] +pub struct Channel { + formal: Range, + + minus: Vec, + plus: Vec, +} + +pub struct Bounds { + pub formal: Range, + pub informal: Range, +} + +impl Channel { + pub fn new(formal: Range) -> Self { + assert!(formal.end >= formal.start); + Channel { + formal: formal, + minus: vec![], + plus: vec![], + } + } + + pub fn formal(&self) -> Range { + self.formal.clone() + } + + pub fn bounds(&self) -> Bounds { + let formal = self.formal.clone(); + let informal_start = formal.start - self.minus.len() as i32; + let informal_len_0 = self.minus.len() + self.plus.len(); + let informal_len_1 = (formal.end - informal_start) as usize; + let informal_len = informal_len_0.max(informal_len_1); + + return Bounds { + formal, + informal: informal_start..(informal_start + informal_len as i32) + } + } + + pub(crate) fn blank() -> Channel { + return Channel::new(0..0); + } + + pub fn slice(&self, start: i32, length: i32) -> Channel { + if start == 0 && length == 0 { + return self.clone() + } + if start != 0 && length == 0 { + let formal_start = self.formal.start + start; + let mut formal_end = self.formal.start + self.formal.end; + if formal_end < formal_start { formal_end = formal_start; } + + let mut chan = Channel::new(formal_start..formal_end); + for i in 0..self.plus.len() { + chan[self.formal.start + i as i32] = self[self.formal.start + i as i32] + } + return chan + } + + let formal_start = self.formal.start + start; + let formal_end = self.formal.start + start + length; + let mut chan = Channel::new(formal_start..formal_end); + for i in formal_start..formal_end { + chan[i] = self[i]; + } + return chan + } +} + +impl Index for Channel { + type Output = Sample; + + fn index(&self, index: i32) -> &Self::Output { + let adj_idx = index - self.formal.start; + if adj_idx < 0 { + let neg_adj_idx = (-adj_idx) as usize - 1; + self.minus.get(neg_adj_idx).unwrap_or(&BLANK_SAMPLE) + } else { + let pos_adj_idx = adj_idx as usize; + self.plus.get(pos_adj_idx).unwrap_or(&BLANK_SAMPLE) + } + } +} + +impl IndexMut for Channel { + fn index_mut(&mut self, index: i32) -> &mut Self::Output { + let adj_idx = index - self.formal.start; + if adj_idx < 0 { + let neg_adj_idx = (-adj_idx) as usize - 1; + if self.minus.len() < neg_adj_idx + 1 { + self.minus.reserve(neg_adj_idx + 1); + while self.minus.len() < neg_adj_idx + 1 { + self.minus.push(BLANK_SAMPLE); + } + } + return &mut self.minus[neg_adj_idx] + } else { + let pos_adj_idx = adj_idx as usize; + if self.plus.len() < pos_adj_idx + 1{ + self.plus.reserve(pos_adj_idx + 1); + while self.plus.len() < pos_adj_idx + 1 { + self.plus.push(BLANK_SAMPLE); + } + } + return &mut self.plus[pos_adj_idx] + } + } +} \ No newline at end of file diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..0e5a7ee --- /dev/null +++ b/src/device.rs @@ -0,0 +1,122 @@ +use std::{collections::HashMap, cell::Cell}; + +use crate::{audio::Channel, error::{Lansky, LanskyError}, vm::{Procedure, Block}, rack::Rack}; + +pub struct Device { + proto: Proto, + implementation: Implementation, +} + +#[derive(Clone)] +pub struct Proto { + pub name: String, + pub inputs: Vec, + pub outputs: Vec, +} + + +enum Implementation { + Native(fn(Vec) -> Lansky>), + Hosted(Procedure), +} + +impl Device { + pub fn native( + name: String, + inputs: Vec, + outputs: Vec, + native: fn(Vec) -> Lansky> + ) -> Lansky { + let proto = Proto { name, inputs, outputs }; + Self::new(proto, Implementation::Native(native)) + } + + pub fn hosted( + name: String, + inputs: Vec, + outputs: Vec, + blocks: Vec, + ) -> Lansky { + let proto = Proto { name, inputs, outputs }; + Self::new(proto.clone(), Implementation::Hosted(Procedure::new(proto, blocks))) + } + + fn new(proto: Proto, implementation: Implementation) -> Lansky { + for i0 in 0..proto.inputs.len() { + for i1 in i0+1..proto.inputs.len() { + if proto.inputs[i0] == proto.inputs[i1] { + return Err(LanskyError::DuplicateInput { name: proto.inputs[i0].clone(), i0, i1 }) + } + } + } + + for i0 in 0..proto.outputs.len() { + for i1 in i0+1..proto.outputs.len() { + if proto.outputs[i0] == proto.outputs[i1] { + return Err(LanskyError::DuplicateOutput { name: proto.outputs[i0].clone(), i0, i1 }) + } + } + } + + // TODO: Validate the code in some way? + // Not needed for native devices + + Ok(Device { proto, implementation }) + } + + pub(crate) fn analyze(&self, rack: &Rack) -> Lansky<()> { + match &self.implementation { + Implementation::Native(_) => {} + Implementation::Hosted(h) => { + h.analyze(rack)?; + } + } + Ok(()) + } + + pub fn get_proto(&self) -> &Proto { + &self.proto + } + + pub(crate) fn invoke_named(&self, rack: &Rack, mut inputs: HashMap) -> Lansky> { + let expected = self.proto.inputs.len(); + let got = inputs.len(); + if expected != got { return Err(LanskyError::WrongNInputs { expected, got }) } + + let mut inputs_2 = Vec::with_capacity(self.proto.inputs.len()); + for i in self.proto.inputs.iter() { + let Some(channel_in) = inputs.remove(i) else { + return Err(LanskyError::MissingInput { name: i.clone() }) + }; + inputs_2.push(channel_in) + } + for i in inputs.keys() { + if !self.proto.inputs.contains(i) { + return Err(LanskyError::UnknownInput { name: i.clone() }) + } + } + + let outputs = self.invoke_positional(rack, inputs_2)?; + + let mut outputs_2 = HashMap::new(); + for (k, v) in self.proto.outputs.iter().zip(outputs.into_iter()) { + outputs_2.insert(k.clone(), v); + } + Ok(outputs_2) + } + + pub(crate) fn invoke_positional(&self, rack: &Rack, inputs: Vec) -> Lansky> { + let expected = self.proto.inputs.len(); + let got = inputs.len(); + if expected != got { return Err(LanskyError::WrongNInputs { expected, got }) } + let outputs = match &self.implementation { + Implementation::Native(f) => f(inputs)?, + Implementation::Hosted(procedure) => procedure.invoke(rack, inputs)?, + }; + + let expected = self.proto.outputs.len(); + let got = outputs.len(); + if expected != got { return Err(LanskyError::WrongNOutputs { expected, got }) } + Ok(outputs) + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..fdceb67 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,38 @@ +pub type Lansky = Result; + +#[derive(Debug)] +pub enum LanskyError { + DuplicateDevice { name: String }, + UnknownDevice { name: String }, + + DuplicateInput { name: String, i0: usize, i1: usize }, + DuplicateOutput { name: String, i0: usize, i1: usize }, + + MissingInput { name: String }, + UnknownInput { name: String }, + + WrongNInputs { expected: usize, got: usize }, + WrongNOutputs { expected: usize, got: usize }, + + NoFirstBlock, + + WrongNStack { why: LanskyStackReason, expected: usize, got: usize }, + + MissingRegA { i: usize }, + MissingRegZ { i: usize }, + + WrongNChannels { expected: usize, got: usize }, + + IO(std::io::Error), +} + +#[derive(Debug)] +pub enum LanskyStackReason { + CannotDupEmptyStack, + CannotOupEmptyStack, + CannotPopEmptyStack, + + BlockMustEndWithEmptyStack, + + InvokeNInputs +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..feaf733 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,98 @@ +// TODOs: +// Generate at 88200Hz, downsample to 44100Hz +// Parser for assembly language +// More context in error messages +// Replace the sample type with something more Pico 8 fixedpoint-like (right now we are always falling down on the i64 interface) +// Use the _top_ part when coercing the sample type to an i32 +// jmp and similar instructions +// Use primarily the named interface and allow defaulting +// Channel-like objects that produce a constant value + +use std::{path::Path, fs::File}; + +use audio::{Sample, Channel}; +use device::Device; +use error::{Lansky, LanskyError}; +use rack::Rack; +use vm::{Block, BodyInstruction, ExitInstruction, MovSrc, OupDst, RegZ}; + +mod audio; +mod error; +mod device; +mod standard_devices; +mod rack; +mod vm; + +fn main() { + main_err().expect("crash:"); +} + + +fn main_err() -> Lansky<()> { + let mut rack = Rack::new(); + + rack.add_device(Device::hosted( + "main".to_string(), + vec![], + vec!["audio".to_string()], + vec![ + Block { + name: "entry".to_string(), + body: vec![ + BodyInstruction::New(MovSrc::Imm(Sample(0)), MovSrc::Imm(Sample(100000)), MovSrc::Imm(Sample(440))), + BodyInstruction::Invoke("gensin".to_string(), 1, 1), + BodyInstruction::Pop(OupDst::Z(RegZ(0))), + ], + exit: ExitInstruction::Exit, + } + ], + )?)?; + + rack.analyze_all()?; + + let result = + rack.invoke_positional("main", vec![])?; + + export(Path::new("example.wav"), result)?; + + Ok(()) +} + +fn export(path: &Path, audio: Vec) -> Lansky<()> { + match &audio[..] { + [mono] => { + let header = wav::Header::new( + wav::header::WAV_FORMAT_PCM, + 1, + 44100, + 16 + ); + + let mut out_file = File::create(path).map_err(|e| LanskyError::IO(e))?; + let data = channel_to_data(&mono); + wav::write(header, &data, &mut out_file).map_err(|e| LanskyError::IO(e))?; + + Ok(()) + }, + _ => { + return Err(LanskyError::WrongNChannels { expected: 1, got: audio.len() }) + } + } +} + +fn channel_to_data(channel: &Channel) -> wav::BitDepth { + let mut v: Vec = vec![]; + for i in channel.bounds().informal { + v.push(channel[i].to_i16_sample()) + } + wav::BitDepth::Sixteen(v) +} + +/* +fn main_err() -> std::io::Result<()> { + let mut out_file = File::create(Path::new("output.wav"))?; + wav::write(header, &data, &mut out_file)?; + + Ok(()) +} +*/ \ No newline at end of file diff --git a/src/rack.rs b/src/rack.rs new file mode 100644 index 0000000..7146492 --- /dev/null +++ b/src/rack.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; + +use crate::{device::{Device, Proto}, error::{Lansky, LanskyError}, standard_devices, audio::Channel}; + +pub struct Rack { + devices: HashMap +} + +impl Rack { + pub fn new() -> Self { + let mut r = Rack { + devices: HashMap::new() + }; + standard_devices::augment(&mut r); + r + } + + pub fn analyze_all(&self) -> Lansky<()> { + for d in self.devices.values() { + d.analyze(self)?; + } + Ok(()) + } + + pub fn add_device(&mut self, device: Device) -> Lansky<()> { + if self.devices.contains_key(&device.get_proto().name) { + return Err(LanskyError::DuplicateDevice { name: device.get_proto().name.clone() }) + } + + let name = device.get_proto().name.clone(); + self.devices.insert(name, device); + + Ok(()) + } + + fn get_device(&self, name: &str) -> Lansky<&Device> { + let Some(device) = self.devices.get(name) else { + return Err(LanskyError::UnknownDevice { name: name.to_string() }) + }; + + Ok(device) + } + + pub fn invoke_named(&self, name: &str, inputs: HashMap) -> Lansky> { + self.get_device(name)?.invoke_named(self, inputs) + } + + pub fn invoke_positional(&self, name: &str, inputs: Vec) -> Lansky> { + self.get_device(name)?.invoke_positional(self, inputs) + } + + pub fn get_proto(&self, name: &str) -> Lansky<&Proto> { + Ok(self.get_device(name)?.get_proto()) + } + +} \ No newline at end of file diff --git a/src/standard_devices/mod.rs b/src/standard_devices/mod.rs new file mode 100644 index 0000000..b56f858 --- /dev/null +++ b/src/standard_devices/mod.rs @@ -0,0 +1,33 @@ +use std::f32::consts::PI; + +use crate::{rack::Rack, device::Device, audio::{Channel, Sample}, error::Lansky}; + +pub fn augment(r: &mut Rack) -> Lansky<()> { + r.add_device(Device::native( + "gensin".to_string(), + vec!["frequency".to_string()], + vec!["signal".to_string()], + gensin + )?)?; + + Ok(()) +} + +fn gensin(mut chans: Vec) -> Lansky> { + assert!(chans.len() == 1); + let inp = chans.pop().expect("must be one channel"); + + let mut pos = 0.0; + let sr = 44100.0; + + let mut oup = Channel::new(inp.formal()); + + for i in inp.bounds().informal { + let freq = inp[i].0 as f32; + pos += freq/sr; + pos %= 1.0; + oup[i] = Sample(((pos * PI * 2.0).sin() * i32::MAX as f32) as i64); + } + + Ok(vec![oup]) +} \ No newline at end of file diff --git a/src/vm.rs b/src/vm.rs new file mode 100644 index 0000000..3fbbf46 --- /dev/null +++ b/src/vm.rs @@ -0,0 +1,519 @@ +use std::cell::Cell; + +use crate::{audio::{Channel, Sample}, error::{Lansky, LanskyError, LanskyStackReason}, device::Proto, rack::Rack}; + +pub(crate) struct Procedure { + proto: Proto, + blocks: Vec, + analysis: Cell>, +} + +#[derive(Clone, Copy)] +struct Analysis { + stack_depth: usize, + + proto_n_inputs: usize, + proto_n_outputs: usize, + + n_reg_ls: usize, +} + +pub struct Block { + pub name: String, + pub body: Vec, + pub exit: ExitInstruction +} + +pub enum BodyInstruction { + Mov(MovType, MovDst, MovSrc), + Range(RangeDst, i32, i32), + Inp(InpSrc), + Dup(), + Oup(MovType, OupDst), + Pop(OupDst), + New(MovSrc, MovSrc, MovSrc), + Invoke(String, usize, usize), +} + +pub enum ExitInstruction { + Exit, +} + +pub enum MovType { Mov, Add, Xor } + +#[derive(Clone, Copy)] pub enum MovDst { L(RegL), R(RegR), Z(RegZ), Ix } +#[derive(Clone, Copy)] pub enum MovSrc { A(RegA), L(RegL), R(RegR), Z(RegZ), Ix, Imm(Sample) } +#[derive(Clone, Copy)] pub enum RangeDst { A(RegA), L(RegL), Z(RegZ) } +#[derive(Clone, Copy)] pub enum InpSrc { A(RegA), L(RegL), Z(RegZ) } +#[derive(Clone, Copy)] pub enum OupDst { L(RegL), Z(RegZ) } + +impl OupDst { + fn to_mov_dst(self) -> MovDst { + match self { + Self::L(l) => MovDst::L(l), + Self::Z(z) => MovDst::Z(z), + } + } +} + +#[derive(Clone, Copy)] pub struct RegA(pub u8); +#[derive(Clone, Copy)] pub struct RegL(pub u8); +#[derive(Clone, Copy)] pub struct RegR(pub u8); +#[derive(Clone, Copy)] pub struct RegZ(pub u8); + +struct Scope { + ix: Sample, + + reg_as: Vec, + reg_ls: Vec, + reg_rs: [Sample; 256], + reg_zs: Vec, + + loop_as: [LoopRange; 256], + loop_ls: [LoopRange; 256], + loop_zs: [LoopRange; 256], +} + +#[derive(Clone, Copy)] +struct LoopRange { + start: i32, + length: i32, +} + +impl Default for LoopRange { + fn default() -> Self { + Self { start: 0, length: 0 } + } +} + +impl Scope { + fn new(analysis: Analysis, inputs: Vec) -> Scope { + assert_eq!(inputs.len(), analysis.proto_n_inputs); // analysis is done elsewhere + + let reg_as: Vec = inputs; + let mut reg_ls: Vec = vec![]; + let reg_rs: [Sample; 256] = [Sample::default(); 256]; + let mut reg_zs: Vec = vec![]; + + for _ in 0..analysis.n_reg_ls { reg_ls.push(Channel::blank()); } + for _ in 0..analysis.proto_n_outputs { reg_zs.push(Channel::blank()); } + + Scope { + ix: Sample(0), + + reg_as, + reg_ls, + reg_rs, + reg_zs, + + loop_as: [LoopRange::default(); 256], + loop_ls: [LoopRange::default(); 256], + loop_zs: [LoopRange::default(); 256], + } + } + + fn exit(self) -> Vec { + self.reg_zs + } + + fn read(&self, src: MovSrc) -> Sample { + let flat_ix = self.ix.0 as i32; + let (chan, loop_) = match src { + MovSrc::A(RegA(a)) => { + let a = a as usize; + (&self.reg_as[a], &self.loop_as[a]) + } + MovSrc::L(RegL(l)) => { + let l = l as usize; + (&self.reg_ls[l], &self.loop_ls[l]) + } + MovSrc::R(RegR(r)) => { + let r = r as usize; + return self.reg_rs[r] + } + MovSrc::Z(RegZ(z)) => { + let z = z as usize; + (&self.reg_zs[z], &self.loop_zs[z]) + } + MovSrc::Ix => return self.ix, + MovSrc::Imm(i) => return i, + }; + + let mut ix = flat_ix; + if loop_.length != 0 { ix %= loop_.length; } + ix += chan.formal().start + loop_.start; + + chan[ix] + } + + fn write(&mut self, ty: &MovType, dst: MovDst, value: Sample, offset: i32) { + let flat_ix = self.ix.0 as i32; + let (chan, loop_) = match dst { + MovDst::L(RegL(l)) => { + let l = l as usize; + (&mut self.reg_ls[l], &self.loop_ls[l]) + } + MovDst::R(RegR(r)) => { + let r = r as usize; + self.reg_rs[r] = value; return + } + MovDst::Z(RegZ(z)) => { + let z = z as usize; + (&mut self.reg_zs[z], &self.loop_zs[z]) + } + MovDst::Ix => { self.ix = value; return } + }; + + let mut ix = flat_ix + offset; + if loop_.length != 0 { ix %= loop_.length; } + ix += chan.formal().start + loop_.start; + + chan[ix] = value + } + + fn replace(&mut self, dst: OupDst, value: Channel) { + match dst { + OupDst::L(RegL(l)) => { + self.reg_ls[l as usize] = value; + self.loop_ls[l as usize] = LoopRange::default() + } + OupDst::Z(RegZ(z)) => { + self.reg_zs[z as usize] = value; + self.loop_zs[z as usize] = LoopRange::default() + } + } + } + + fn apply_range(&mut self, dst: RangeDst, start: i32, length: i32) { + let lr = LoopRange{start, length}; + match dst { + RangeDst::A(RegA(a)) => { + self.loop_as[a as usize] = lr + } + RangeDst::L(RegL(l)) => { + self.loop_ls[l as usize] = lr + } + RangeDst::Z(RegZ(z)) => { + self.loop_zs[z as usize] = lr + } + } + } + + fn inp(&self, src: InpSrc) -> Channel { + let (chan, loop_) = match src { + InpSrc::A(RegA(a)) => { + let a = a as usize; + (&self.reg_as[a], &self.loop_as[a]) + } + InpSrc::L(RegL(l)) => { + let l = l as usize; + (&self.reg_ls[l], &self.loop_ls[l]) + } + InpSrc::Z(RegZ(z)) => { + let z = z as usize; + (&self.reg_zs[z], &self.loop_zs[z]) + } + }; + chan.slice(loop_.start, loop_.length) + } +} + +impl Procedure { + pub(crate) fn new(proto: Proto, blocks: Vec) -> Procedure { + Procedure { + proto, + blocks, + analysis: Cell::new(None), + } + } + + pub fn invoke(&self, rack: &Rack, inputs: Vec) -> Lansky> { + let analysis = self.internal_analyze(rack)?; + + let mut scope = Scope::new(analysis, inputs); + let mut stack: Vec = vec![]; + + let block_ix = 0; + loop { + let block = &self.blocks[block_ix]; + for instruction in block.body.iter() { + match instruction { + BodyInstruction::Mov(ty, dst, src) => { + scope.write(ty, *dst, scope.read(*src), 0) + } + BodyInstruction::Range(dst, start, len) => { + scope.apply_range(*dst, *start, *len) + } + BodyInstruction::Inp(src) => { + let slice = scope.inp(*src); + stack.push(slice) + } + BodyInstruction::Dup() => { + let top = stack.pop().expect("stack can't be empty"); + stack.push(top.clone()) + } + BodyInstruction::Oup(ty, dst) => { + let top = stack.pop().expect("stack can't be empty"); + let bounds = top.bounds(); + for ix in bounds.informal { + let offset = ix - bounds.formal.start; + scope.write(ty, dst.to_mov_dst(), top[ix], offset) + } + } + BodyInstruction::Pop(dst) => { + let top = stack.pop().expect("stack can't be empty"); + scope.replace(*dst, top) + } + BodyInstruction::New(formal_start, formal_len, fill) => { + let start = scope.read(*formal_start); + let len = scope.read(*formal_len); + let fill = scope.read(*fill); + let mut chan = Channel::new((start.0 as i32)..((start.0+len.0) as i32)); + for i in chan.formal() { + chan[i] = fill + } + stack.push(chan) + } + BodyInstruction::Invoke(device, n_inputs, n_outputs) => { + let mut inputs: Vec = vec![]; + std::mem::swap(&mut stack, &mut inputs); + assert_eq!(inputs.len(), *n_inputs); + let outputs = rack.invoke_positional(device, inputs)?; + assert_eq!(outputs.len(), *n_outputs); + stack.extend(outputs); + stack.reverse() + } + } + } + + match block.exit { + ExitInstruction::Exit => return Ok(scope.exit()) + } + } + } + + pub(crate) fn analyze(&self, rack: &Rack) -> Lansky<()> { + self.internal_analyze(rack)?; + Ok(()) + } + + fn internal_analyze(&self, rack: &Rack) -> Lansky { + if let Some(x) = self.analysis.get() { return Ok(x) } + + let mut analysis = Analysis { + stack_depth: 0, + + proto_n_inputs: self.proto.inputs.len(), + proto_n_outputs: self.proto.outputs.len(), + + n_reg_ls: 0 + }; + + if self.blocks.len() == 0 { + return Err(LanskyError::NoFirstBlock); + } + + for block in self.blocks.iter() { + block.touch_analysis( + &mut analysis, &|d| rack.get_proto(d).cloned() + )?; + } + + self.analysis.set(Some(analysis)); + + return Ok(analysis) + } +} + + +impl Block { + fn touch_analysis(&self, analysis: &mut Analysis, get_proto: &impl Fn(&str) -> Lansky) -> Lansky<()> { + for instruction in self.body.iter() { + instruction.touch_analysis(analysis, get_proto)?; + } + + if analysis.stack_depth != 0 { + return Err(LanskyError::WrongNStack { + why: LanskyStackReason::BlockMustEndWithEmptyStack, + got: analysis.stack_depth, expected: 0 + }) + } + self.exit.touch_analysis(analysis)?; + + Ok(()) + } +} + +impl BodyInstruction { + fn touch_analysis(&self, analysis: &mut Analysis, get_proto: &impl Fn(&str) -> Lansky) -> Lansky<()> { + match self { + BodyInstruction::Mov(_, dst, src) => { + dst.touch_analysis(analysis)?; + src.touch_analysis(analysis)?; + } + BodyInstruction::Range(dst, _, _) => { + dst.touch_analysis(analysis)?; + } + BodyInstruction::Inp(src) => { + analysis.stack_depth += 1; + src.touch_analysis(analysis)?; + } + BodyInstruction::Dup() => { + if analysis.stack_depth == 0 { + return Err(LanskyError::WrongNStack { + why: LanskyStackReason::CannotDupEmptyStack, + expected: 1, got: 0 + }) + } + analysis.stack_depth += 1; + } + BodyInstruction::Oup(_, dst) => { + if analysis.stack_depth == 0 { + return Err(LanskyError::WrongNStack { + why: LanskyStackReason::CannotOupEmptyStack, + expected: 1, got: 0 + }) + } + analysis.stack_depth -= 1; + + dst.touch_analysis(analysis)?; + } + BodyInstruction::Pop(dst) => { + if analysis.stack_depth == 0 { + return Err(LanskyError::WrongNStack { + why: LanskyStackReason::CannotPopEmptyStack, + expected: 1, got: 0 + }) + } + analysis.stack_depth -= 1; + + dst.touch_analysis(analysis)?; + } + BodyInstruction::New(formal_start, formal_len, fill) => { + analysis.stack_depth += 1; + + formal_start.touch_analysis(analysis)?; + formal_len.touch_analysis(analysis)?; + fill.touch_analysis(analysis)?; + } + BodyInstruction::Invoke(device, n_inputs, n_outputs) => { + let proto = get_proto(device)?; + + let actual_n_inputs = proto.inputs.len(); + let actual_n_outputs = proto.outputs.len(); + + if *n_inputs != actual_n_inputs { + return Err(LanskyError::WrongNInputs { expected: actual_n_inputs, got: *n_inputs }) + } + + if *n_outputs != actual_n_outputs { + return Err(LanskyError::WrongNInputs { expected: actual_n_inputs, got: *n_outputs }) + } + + if analysis.stack_depth != *n_inputs { + return Err(LanskyError::WrongNStack { + why: LanskyStackReason::InvokeNInputs, + expected: *n_inputs, + got: analysis.stack_depth, + }) + } + analysis.stack_depth = *n_outputs; + } + } + Ok(()) + } +} + +impl ExitInstruction { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + match self { + ExitInstruction::Exit => {} + } + Ok(()) + } +} + +impl MovDst { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + match self { + Self::L(l) => l.touch_analysis(analysis), + Self::R(r) => r.touch_analysis(analysis), + Self::Z(z) => z.touch_analysis(analysis), + Self::Ix => Ok(()), + } + } +} + +impl MovSrc { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + match self { + Self::A(a) => a.touch_analysis(analysis), + Self::L(l) => l.touch_analysis(analysis), + Self::R(r) => r.touch_analysis(analysis), + Self::Z(z) => z.touch_analysis(analysis), + Self::Ix => Ok(()), + Self::Imm(_) => Ok(()), + } + } +} + +impl RangeDst { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + match self { + Self::A(a) => a.touch_analysis(analysis), + Self::L(l) => l.touch_analysis(analysis), + Self::Z(z) => z.touch_analysis(analysis), + } + } +} + +impl InpSrc { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + match self { + Self::A(a) => a.touch_analysis(analysis), + Self::L(l) => l.touch_analysis(analysis), + Self::Z(z) => z.touch_analysis(analysis), + } + } +} + +impl OupDst { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + match self { + Self::L(l) => l.touch_analysis(analysis), + Self::Z(z) => z.touch_analysis(analysis), + } + } +} + + +impl RegA { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + if self.0 as usize >= analysis.proto_n_inputs { + return Err(LanskyError::MissingRegA { i: self.0 as usize }); + } + Ok(()) + } +} + +impl RegL { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + analysis.n_reg_ls = analysis.n_reg_ls.max(self.0 as usize); + Ok(()) + } +} + +impl RegR { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + Ok(()) + } +} + +impl RegZ { + fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> { + if self.0 as usize >= analysis.proto_n_outputs { + return Err(LanskyError::MissingRegZ { i: self.0 as usize }); + } + Ok(()) + } +} \ No newline at end of file