diff --git a/integration_tests/types_test.ucg b/integration_tests/types_test.ucg index bf774fd..2574378 100644 --- a/integration_tests/types_test.ucg +++ b/integration_tests/types_test.ucg @@ -80,4 +80,34 @@ let foo_string = "foo"; assert t.ok{ test = foo_string in ["foo"], desc = "List presence checks work", +}; + +assert t.ok{ + test = int("1") == 1, + desc = "You can cast a string into an int", +}; + +assert t.ok{ + test = str(1) == "1", + desc = "You can cast an int into a string", +}; + +assert t.ok{ + test = bool("true") == true, + desc = "You can cast an string into true", +}; + +assert t.ok{ + test = bool("false") == false, + desc = "You can cast a string into false", +}; + +assert t.ok{ + test = str(true) == "true", + desc = "You can cast true into a string", +}; + +assert t.ok{ + test = str(false) == "false", + desc = "You can cast true into a string", }; \ No newline at end of file diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 32a2483..d55b8a8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -318,6 +318,38 @@ pub struct CallDef { pub pos: Position, } +/// The allowable types to which you can perform a primitive cast. +#[derive(PartialEq, Debug, Clone)] +pub enum CastType { + Int, + Float, + Str, + Bool, +} + +impl fmt::Display for CastType { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!( + w, + "{}", + match self { + CastType::Int => "int", + CastType::Float => "float", + CastType::Bool => "bool", + CastType::Str => "str", + } + ) + } +} + +/// Represents a cast of a target to a primitive type. +#[derive(PartialEq, Debug, Clone)] +pub struct CastDef { + pub cast_type: CastType, + pub target: Box, + pub pos: Position, +} + /// Encodes a select expression in the UCG AST. #[derive(PartialEq, Debug, Clone)] pub struct SelectDef { @@ -694,6 +726,7 @@ pub enum Expression { Include(IncludeDef), Import(ImportDef), Call(CallDef), + Cast(CastDef), Func(FuncDef), Select(SelectDef), FuncOp(FuncOpDef), @@ -716,6 +749,7 @@ impl Expression { &Expression::Grouped(_, ref pos) => pos, &Expression::Format(ref def) => &def.pos, &Expression::Call(ref def) => &def.pos, + &Expression::Cast(ref def) => &def.pos, &Expression::Func(ref def) => &def.pos, &Expression::Module(ref def) => &def.pos, &Expression::Select(ref def) => &def.pos, @@ -754,7 +788,10 @@ impl fmt::Display for Expression { write!(w, "")?; } &Expression::Call(_) => { - write!(w, "")?; + write!(w, "")?; + } + &Expression::Cast(_) => { + write!(w, "")?; } &Expression::Func(_) => { write!(w, "")?; diff --git a/src/ast/printer/mod.rs b/src/ast/printer/mod.rs index ccac6be..0018145 100644 --- a/src/ast/printer/mod.rs +++ b/src/ast/printer/mod.rs @@ -258,6 +258,13 @@ where } self.render_expr(&_def.right)?; } + Expression::Cast(def) => { + self.w.write(format!("{}", def.cast_type).as_bytes())?; + self.w.write("(".as_bytes())?; + self.render_comment_if_needed(def.target.pos().line)?; + self.render_expr(&def.target)?; + self.w.write(")".as_bytes())?; + } Expression::Call(_def) => { self.render_value(&_def.funcref)?; self.w.write("(".as_bytes())?; diff --git a/src/ast/walk.rs b/src/ast/walk.rs index bce4ad9..fea500b 100644 --- a/src/ast/walk.rs +++ b/src/ast/walk.rs @@ -42,6 +42,9 @@ pub trait Walker { self.walk_expression(expr); } } + Expression::Cast(ref mut def) => { + self.walk_expression(&mut def.target); + } Expression::Copy(ref mut def) => { self.walk_fieldset(&mut def.fields); } diff --git a/src/build/compile_test.rs b/src/build/compile_test.rs index 2cbfc25..c71d09d 100644 --- a/src/build/compile_test.rs +++ b/src/build/compile_test.rs @@ -575,4 +575,4 @@ fn test_bad_import_path_compile_failure() { Regex::new(r"line: 1 column: 18").unwrap(), ], ) -} \ No newline at end of file +} diff --git a/src/build/mod.rs b/src/build/mod.rs index 8836345..483c4ed 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -17,28 +17,28 @@ use std::cell::RefCell; use std::collections::HashMap; use std::convert::TryInto; use std::env; -use std::process; use std::error::Error; use std::fs::File; use std::io::Read; use std::path::PathBuf; +use std::process; use std::rc::Rc; -use rustyline; -use rustyline::error::ReadlineError; use atty; use atty::Stream; +use rustyline; +use rustyline::error::ReadlineError; use simple_error; use crate::ast::*; -use crate::error; -use crate::iter::OffsetStrIter; -use crate::parse::parse; use crate::build::opcode::pointer::OpPointer; use crate::build::opcode::translate; use crate::build::opcode::translate::PositionMap; use crate::build::opcode::Environment; use crate::build::opcode::VM; +use crate::error; +use crate::iter::OffsetStrIter; +use crate::parse::parse; pub mod format; pub mod ir; @@ -216,12 +216,12 @@ where pub fn repl(&mut self, mut editor: rustyline::Editor<()>, config_home: PathBuf) -> BuildResult { // loop - let mut lines = crate::io::StatementAccumulator::new(); - if atty::is(Stream::Stdin) { - println!("Welcome to the UCG repl. Ctrl-D to exit, Ctrl-C to abort expression."); - println!("Type '#help' for help."); - println!(""); - } + let mut lines = crate::io::StatementAccumulator::new(); + if atty::is(Stream::Stdin) { + println!("Welcome to the UCG repl. Ctrl-D to exit, Ctrl-C to abort expression."); + println!("Type '#help' for help."); + println!(""); + } // Initialize VM with an empty OpPointer let mut vm = VM::new( self.strict, diff --git a/src/build/opcode/convert.rs b/src/build/opcode/convert.rs new file mode 100644 index 0000000..39e10a6 --- /dev/null +++ b/src/build/opcode/convert.rs @@ -0,0 +1,93 @@ +use std::convert::TryFrom; + +use super::Primitive; +use crate::ast::CastType; + +pub struct Error { + val: Primitive, + cast_type: CastType, +} + +impl Error { + pub fn message(&self) -> String { + format!("No cast from {} to {}", self.val, self.cast_type) + } +} + +impl From<&Primitive> for String { + fn from(p: &Primitive) -> Self { + match p { + Primitive::Int(i) => format!("{}", i), + Primitive::Float(f) => format!("{}", f), + Primitive::Str(s) => format!("{}", s), + Primitive::Bool(b) => format!("{}", b), + Primitive::Empty => "NULL".to_owned(), + } + } +} + +impl TryFrom<&Primitive> for i64 { + type Error = Error; + + fn try_from(p: &Primitive) -> Result { + match p { + Primitive::Bool(_) | Primitive::Empty => Err(Error { + val: p.clone(), + cast_type: CastType::Int, + }), + Primitive::Str(s) => match s.parse::() { + Ok(i) => Ok(i), + Err(_) => Err(Error { + val: Primitive::Str(s.clone()), + cast_type: CastType::Int, + }), + }, + Primitive::Float(f) => Ok(*f as i64), + Primitive::Int(i) => Ok(i.clone()), + } + } +} + +impl TryFrom<&Primitive> for f64 { + type Error = Error; + + fn try_from(p: &Primitive) -> Result { + match p { + Primitive::Bool(_) | Primitive::Empty => Err(Error { + val: p.clone(), + cast_type: CastType::Int, + }), + Primitive::Str(s) => match s.parse::() { + Ok(f) => Ok(f), + Err(_) => Err(Error { + val: Primitive::Str(s.clone()), + cast_type: CastType::Int, + }), + }, + Primitive::Int(i) => Ok(*i as f64), + Primitive::Float(f) => Ok(f.clone()), + } + } +} + +impl TryFrom<&Primitive> for bool { + type Error = Error; + + fn try_from(p: &Primitive) -> Result { + match p { + Primitive::Empty | Primitive::Int(_) | Primitive::Float(_) => Err(Error { + val: p.clone(), + cast_type: CastType::Int, + }), + Primitive::Bool(b) => Ok(*b), + Primitive::Str(s) => match s.as_str() { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(Error { + val: Primitive::Str(s.clone()), + cast_type: CastType::Int, + }), + }, + } + } +} diff --git a/src/build/opcode/display.rs b/src/build/opcode/display.rs index 8406feb..a8872fe 100644 --- a/src/build/opcode/display.rs +++ b/src/build/opcode/display.rs @@ -24,11 +24,7 @@ use Value::{C, F, M, P, S, T}; impl fmt::Display for Value { fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { match self { - P(Bool(v)) => write!(w, "{}", v), - P(Int(v)) => write!(w, "{}", v), - P(Float(v)) => write!(w, "{}", v), - P(Str(v)) => write!(w, "\"{}\"", v.replace("\"", "\\\"")), - P(Empty) => write!(w, "NULL"), + P(p) => write!(w, "{}", p), C(List(ref els, _)) => { write!(w, "[")?; for e in els { @@ -50,3 +46,15 @@ impl fmt::Display for Value { } } } + +impl fmt::Display for Primitive { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + match self { + Bool(v) => write!(w, "{}", v), + Int(v) => write!(w, "{}", v), + Float(v) => write!(w, "{}", v), + Str(v) => write!(w, "\"{}\"", v.replace("\"", "\\\"")), + Empty => write!(w, "NULL"), + } + } +} diff --git a/src/build/opcode/error.rs b/src/build/opcode/error.rs index 30b6367..556b587 100644 --- a/src/build/opcode/error.rs +++ b/src/build/opcode/error.rs @@ -17,6 +17,7 @@ use std::fmt::Display; use std::io; use crate::ast::Position; +use crate::build::opcode::convert; #[derive(Debug)] pub struct Error { @@ -91,6 +92,16 @@ impl From for Error { } } +impl From for Error { + fn from(e: convert::Error) -> Self { + Error { + message: e.message(), + pos: None, + call_stack: Vec::new(), + } + } +} + impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ref pos) = self.pos { diff --git a/src/build/opcode/mod.rs b/src/build/opcode/mod.rs index a9ac0b3..42d46e6 100644 --- a/src/build/opcode/mod.rs +++ b/src/build/opcode/mod.rs @@ -19,6 +19,7 @@ mod display; pub mod environment; #[macro_use] mod error; +mod convert; pub mod pointer; mod runtime; pub mod scope; @@ -29,11 +30,10 @@ pub use environment::Environment; pub use error::Error; pub use vm::VM; +use crate::ast::{CastType, Position}; use pointer::OpPointer; use scope::Stack; -use crate::ast::Position; - #[derive(Debug, PartialEq, Clone)] pub enum Primitive { // Primitive Types @@ -64,18 +64,6 @@ impl Value { } } -impl From<&Primitive> for String { - fn from(p: &Primitive) -> Self { - match p { - Int(i) => format!("{}", i), - Float(f) => format!("{}", f), - Str(s) => format!("{}", s), - Bool(b) => format!("{}", b), - Empty => "NULL".to_owned(), - } - } -} - #[derive(Debug, PartialEq, Clone)] pub enum Composite { List(Vec>, Vec), @@ -198,6 +186,8 @@ pub enum Op { Not, // Primitive Types ops Val(Primitive), + // Primitive casts + Cast(CastType), // A bareword for use in bindings or lookups Sym(String), // Reference a binding on the heap @@ -274,6 +264,7 @@ impl PartialEq for Value { } } +// TODO(jwall): Move all of this into the convert module. impl From> for Val { fn from(val: Rc) -> Val { val.as_ref().into() diff --git a/src/build/opcode/translate.rs b/src/build/opcode/translate.rs index 2993f2d..9d1be26 100644 --- a/src/build/opcode/translate.rs +++ b/src/build/opcode/translate.rs @@ -530,6 +530,10 @@ impl AST { Self::translate_value(call_def.funcref, &mut ops, root); ops.push(Op::FCall, func_pos); } + Expression::Cast(cast_def) => { + Self::translate_expr(*cast_def.target, &mut ops, root); + ops.push(Op::Cast(cast_def.cast_type), cast_def.pos); + } Expression::Copy(def) => { Self::translate_value(def.selector, &mut ops, root); Self::translate_copy(ops, def.fields, def.pos, root); diff --git a/src/build/opcode/vm.rs b/src/build/opcode/vm.rs index b93c6fd..f5ada4e 100644 --- a/src/build/opcode/vm.rs +++ b/src/build/opcode/vm.rs @@ -13,10 +13,11 @@ // limitations under the License. use std::cell::RefCell; use std::collections::BTreeSet; +use std::convert::TryInto; use std::path::PathBuf; use std::rc::Rc; -use crate::ast::Position; +use crate::ast::{CastType, Position}; use super::environment::Environment; use super::pointer::OpPointer; @@ -158,6 +159,7 @@ where let idx = self.ops.idx()?; match op { Op::Val(p) => self.push(Rc::new(P(p.clone())), pos)?, + Op::Cast(t) => self.op_cast(t)?, Op::Sym(s) => self.push(Rc::new(S(s.clone())), pos)?, Op::DeRef(s) => self.op_deref(s.clone(), &pos)?, Op::Add => self.op_add(pos)?, @@ -219,6 +221,22 @@ where Ok(()) } + fn op_cast(&mut self, t: CastType) -> Result<(), Error> { + let (val, pos) = self.pop()?; + if let Value::P(ref p) = val.as_ref() { + self.push( + Rc::new(match t { + CastType::Str => Value::P(Primitive::Str(format!("{}", p))), + CastType::Int => Value::P(Primitive::Int(p.try_into()?)), + CastType::Float => Value::P(Primitive::Float(p.try_into()?)), + CastType::Bool => Value::P(Primitive::Bool(p.try_into()?)), + }), + pos, + )?; + } + Ok(()) + } + fn op_typ(&mut self) -> Result<(), Error> { let (val, pos) = self.pop()?; let typ_name = match val.as_ref() { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 662fec6..dc0a9a9 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -538,6 +538,32 @@ fn tuple_to_call<'a>( } } +make_fn!( + cast_expression, Expression>, + do_each!( + typ => either!( + word!("int"), + word!("float"), + word!("str"), + word!("bool") + ), + _ => punct!("("), + expr => expression, + _ => punct!(")"), + (Expression::Cast(CastDef{ + cast_type: match typ.fragment.as_str() { + "int" => CastType::Int, + "float" => CastType::Float, + "str" => CastType::Str, + "bool" => CastType::Bool, + _ => unreachable!(), + }, + target: Box::new(expr), + pos: typ.pos, + })) + ) +); + fn call_expression(input: SliceIter) -> Result, Expression> { let parsed = do_each!(input.clone(), callee_name => trace_parse!(symbol), @@ -717,6 +743,8 @@ fn unprefixed_expression(input: SliceIter) -> ParseResult { trace_parse!(format_expression), trace_parse!(range_expression), trace_parse!(simple_expression), + // cast parse attempts must happen before call parse attempts. + trace_parse!(cast_expression), trace_parse!(call_expression), trace_parse!(copy_expression) )