Add Brainfuck functio interpretation

BF functios can now be declared and called from AbleScript code.
Function parameters supplied from AbleScript are serialized manually
into byte sequences using the `BfWriter` trait, and are available from
BF code using the input operator, `,`.

At the moment, BF functios simply write output to stdout, and are
therefore incapable of communicating results to the rest of the
program; this might change in the future if we can get something close
to pass-by-reference working.
This commit is contained in:
Alex Bethel 2021-06-02 15:29:31 -05:00
parent b3866eea9e
commit 2c35691ec4
3 changed files with 160 additions and 18 deletions

View file

@ -1,5 +1,7 @@
use std::ops::Range; use std::ops::Range;
use crate::brian::InterpretError;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Error { pub struct Error {
pub kind: ErrorKind, pub kind: ErrorKind,
@ -16,4 +18,5 @@ pub enum ErrorKind {
TypeError(String), TypeError(String),
TopLevelBreak, TopLevelBreak,
ArithmeticError, ArithmeticError,
BfInterpretError(InterpretError),
} }

View file

@ -8,13 +8,16 @@
#[deny(missing_docs)] #[deny(missing_docs)]
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom; use std::{
convert::TryFrom,
io::{stdout, Write},
};
use crate::{ use crate::{
base_55, base_55,
error::{Error, ErrorKind}, error::{Error, ErrorKind},
parser::item::{Expr, Iden, Item, Stmt}, parser::item::{Expr, Iden, Item, Stmt},
variables::{Value, Variable}, variables::{Functio, Value, Variable},
}; };
/// An environment for executing AbleScript code. /// An environment for executing AbleScript code.
@ -178,29 +181,64 @@ impl ExecEnv {
None => Value::Nul, None => Value::Nul,
}; };
// There's always at least one stack frame on the self.decl_var(&iden.0, init);
// stack if we're evaluating something, so we can
// `unwrap` here.
self.stack.iter_mut().last().unwrap().variables.insert(
iden.0.clone(),
Variable {
melo: false,
value: init,
},
);
} }
Stmt::FunctionDeclaration { Stmt::FunctionDeclaration {
iden: _, iden: _,
args: _, args: _,
body: _, body: _,
} => todo!(), } => todo!(),
Stmt::BfFDeclaration { iden: _, body: _ } => todo!(), Stmt::BfFDeclaration { iden, body } => {
self.decl_var(
&iden.0,
Value::Functio(Functio::BfFunctio(body.as_bytes().into())),
);
}
Stmt::If { cond, body } => { Stmt::If { cond, body } => {
if self.eval_expr(cond)?.into() { if self.eval_expr(cond)?.into() {
return self.eval_items_hs(body); return self.eval_items_hs(body);
} }
} }
Stmt::FunctionCall { iden: _, args: _ } => todo!(), Stmt::FunctionCall { iden, args } => {
let func = self.get_var(&iden.0)?;
match func {
Value::Functio(func) => {
match func {
Functio::BfFunctio(body) => {
use crate::variables::BfWriter;
let mut input: Vec<u8> = vec![];
for arg in args {
input.write_value(&self.eval_expr(arg)?);
}
println!("input = {:?}", input);
let mut output = vec![];
crate::brian::interpret_with_io(&body, &input as &[_], &mut output)
.map_err(|e| Error {
kind: ErrorKind::BfInterpretError(e),
position: 0..0,
})?;
// I guess Brainfuck functions write
// output to stdout? It's not quite
// clear to me what else to do. ~~Alex
stdout()
.write_all(&output)
.expect("Failed to write to stdout");
}
Functio::AbleFunctio(_) => {
todo!()
}
}
}
_ => {
return Err(Error {
kind: ErrorKind::TypeError(iden.0.to_owned()),
position: 0..0,
})
}
}
}
Stmt::Loop { body } => loop { Stmt::Loop { body } => loop {
let res = self.eval_items_hs(body)?; let res = self.eval_items_hs(body)?;
match res { match res {
@ -289,6 +327,16 @@ impl ExecEnv {
}), }),
} }
} }
/// Declares a new variable, with the given initial value.
fn decl_var(&mut self, name: &str, value: Value) {
self.stack
.iter_mut()
.last()
.expect("Declaring variable on empty stack")
.variables
.insert(name.to_owned(), Variable { melo: false, value });
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,8 +1,11 @@
use std::{convert::TryFrom, fmt::Display}; use std::{convert::TryFrom, fmt::Display, io::Write};
use rand::Rng; use rand::Rng;
use crate::error::{Error, ErrorKind}; use crate::{
error::{Error, ErrorKind},
parser::item::Item,
};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Abool { pub enum Abool {
@ -31,23 +34,45 @@ impl From<Abool> for bool {
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum Functio {
BfFunctio(Vec<u8>),
AbleFunctio(Vec<Item>),
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Value { pub enum Value {
Nul,
Str(String), Str(String),
Int(i32), Int(i32),
Bool(bool), Bool(bool),
Abool(Abool), Abool(Abool),
Nul, Functio(Functio),
} }
impl Display for Value { impl Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Value::Nul => write!(f, "nul"),
Value::Str(v) => write!(f, "{}", v), Value::Str(v) => write!(f, "{}", v),
Value::Int(v) => write!(f, "{}", v), Value::Int(v) => write!(f, "{}", v),
Value::Bool(v) => write!(f, "{}", v), Value::Bool(v) => write!(f, "{}", v),
Value::Abool(v) => write!(f, "{}", v), Value::Abool(v) => write!(f, "{}", v),
Value::Nul => write!(f, "nul"), Value::Functio(v) => match v {
Functio::BfFunctio(source) => {
write!(
f,
"{}",
String::from_utf8(source.to_owned())
.expect("Brainfuck functio source should be UTF-8")
)
}
Functio::AbleFunctio(source) => {
// TODO: what's the proper way to display an
// AbleScript functio?
write!(f, "{:?}", source)
}
},
} }
} }
} }
@ -84,6 +109,8 @@ impl From<Value> for bool {
Value::Str(s) => s.len() != 0, Value::Str(s) => s.len() != 0,
// 0 is falsey, nonzero is truthy. // 0 is falsey, nonzero is truthy.
Value::Int(x) => x != 0, Value::Int(x) => x != 0,
// Functios are always truthy.
Value::Functio(_) => true,
// And nul is truthy as a symbol of the fact that the // And nul is truthy as a symbol of the fact that the
// deep, fundamental truth of this world is nothing but // deep, fundamental truth of this world is nothing but
// the eternal void. // the eternal void.
@ -92,6 +119,70 @@ impl From<Value> for bool {
} }
} }
/// Allows writing AbleScript values to Brainfuck.
///
/// This trait is blanket implemented for all `Write`rs, but should
/// typically only be used for `Write`rs that cannot fail, e.g.,
/// `Vec<u8>`, because any IO errors will cause a panic.
///
/// The mapping from values to encodings is as follows, where all
/// multi-byte integers are little-endian:
///
/// | AbleScript representation | Brainfuck representation |
/// |---------------------------|-----------------------------------------------------------|
/// | Nul | 00 |
/// | Str | 01 [length, 4 bytes] [string, \[LENGTH\] bytes, as UTF-8] |
/// | Int | 02 [value, 4 bytes] |
/// | Bool | 03 00 false, 03 01 true. |
/// | Abool | 04 00 never, 04 01 always, 04 02 sometimes. |
/// | Brainfuck Functio | 05 00 [length, 4 bytes] [source code, \[LENGTH\] bytes] |
/// | AbleScript Functio | 05 01 (todo, not yet finalized or implemented) |
///
/// The existing mappings should never change, as they are directly
/// visible from Brainfuck code and modifying them would break a
/// significant amount of AbleScript code. If more types are added in
/// the future, they should be assigned the remaining discriminant
/// bytes from 06..FF.
pub trait BfWriter {
/// Write a value. Panic if writing fails for any reason.
fn write_value(&mut self, value: &Value);
}
impl<T: Write> BfWriter for T {
fn write_value(&mut self, value: &Value) {
match value {
Value::Nul => self.write_all(&[0]),
Value::Str(s) => self
.write_all(&[1])
.and_then(|_| self.write_all(&(s.len() as u32).to_le_bytes()))
.and_then(|_| self.write_all(&s.as_bytes())),
Value::Int(v) => self
.write_all(&[2])
.and_then(|_| self.write_all(&v.to_le_bytes())),
Value::Bool(b) => self
.write_all(&[3])
.and_then(|_| self.write_all(&[*b as _])),
Value::Abool(a) => self.write_all(&[4]).and_then(|_| {
self.write_all(&[match *a {
Abool::Never => 0,
Abool::Sometimes => 2,
Abool::Always => 1,
}])
}),
Value::Functio(f) => self.write_all(&[5]).and_then(|_| match f {
Functio::BfFunctio(f) => self
.write_all(&[0])
.and_then(|_| self.write_all(&(f.len() as u32).to_le_bytes()))
.and_then(|_| self.write_all(&f)),
Functio::AbleFunctio(_) => {
todo!()
}
}),
}
.expect("Failed to write to Brainfuck input");
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Variable { pub struct Variable {
pub melo: bool, pub melo: bool,