From 326d0511e7e8f5c45a6bb41d541e61106772059f Mon Sep 17 00:00:00 2001 From: Alex Bethel Date: Wed, 2 Jun 2021 15:29:31 -0500 Subject: [PATCH] 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. --- src/error.rs | 3 ++ src/interpret.rs | 76 ++++++++++++++++++++++++++++++------- src/variables.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 160 insertions(+), 18 deletions(-) diff --git a/src/error.rs b/src/error.rs index 064079d2..2d2a3541 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ use std::ops::Range; +use crate::brian::InterpretError; + #[derive(Debug, Clone)] pub struct Error { pub kind: ErrorKind, @@ -16,4 +18,5 @@ pub enum ErrorKind { TypeError(String), TopLevelBreak, ArithmeticError, + BfInterpretError(InterpretError), } diff --git a/src/interpret.rs b/src/interpret.rs index caf4b3af..38887eaa 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -8,13 +8,16 @@ #[deny(missing_docs)] use std::collections::HashMap; -use std::convert::TryFrom; +use std::{ + convert::TryFrom, + io::{stdout, Write}, +}; use crate::{ base_55, error::{Error, ErrorKind}, parser::item::{Expr, Iden, Item, Stmt}, - variables::{Value, Variable}, + variables::{Functio, Value, Variable}, }; /// An environment for executing AbleScript code. @@ -178,29 +181,64 @@ impl ExecEnv { None => Value::Nul, }; - // There's always at least one stack frame on the - // 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, - }, - ); + self.decl_var(&iden.0, init); } Stmt::FunctionDeclaration { iden: _, args: _, body: _, } => 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 } => { if self.eval_expr(cond)?.into() { 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 = 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 { let res = self.eval_items_hs(body)?; 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)] diff --git a/src/variables.rs b/src/variables.rs index 496138a5..0ef0e14e 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -1,8 +1,11 @@ -use std::{convert::TryFrom, fmt::Display}; +use std::{convert::TryFrom, fmt::Display, io::Write}; use rand::Rng; -use crate::error::{Error, ErrorKind}; +use crate::{ + error::{Error, ErrorKind}, + parser::item::Item, +}; #[derive(Debug, Clone, PartialEq)] pub enum Abool { @@ -31,23 +34,45 @@ impl From for bool { } } +#[derive(Debug, Clone, PartialEq)] +pub enum Functio { + BfFunctio(Vec), + AbleFunctio(Vec), +} + #[derive(Debug, Clone, PartialEq)] pub enum Value { + Nul, Str(String), Int(i32), Bool(bool), Abool(Abool), - Nul, + Functio(Functio), } impl Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Value::Nul => write!(f, "nul"), Value::Str(v) => write!(f, "{}", v), Value::Int(v) => write!(f, "{}", v), Value::Bool(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 for bool { Value::Str(s) => s.len() != 0, // 0 is falsey, nonzero is truthy. Value::Int(x) => x != 0, + // Functios are always truthy. + Value::Functio(_) => true, // And nul is truthy as a symbol of the fact that the // deep, fundamental truth of this world is nothing but // the eternal void. @@ -92,6 +119,70 @@ impl From 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`, 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 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)] pub struct Variable { pub melo: bool,