diff --git a/src/build/compile_test.rs b/src/build/compile_test.rs index 92f34cc..54aacb2 100644 --- a/src/build/compile_test.rs +++ b/src/build/compile_test.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::cell::RefCell; +use std::collections::BTreeMap; use regex::Regex; @@ -24,7 +25,9 @@ fn assert_build(input: &str) { let i_paths = Vec::new(); let out_buffer: Vec = Vec::new(); let err_buffer: Vec = Vec::new(); - let env = RefCell::new(Environment::new(out_buffer, err_buffer)); + let mut env_vars = BTreeMap::new(); + env_vars.insert("FOO".to_owned(), "bar".to_owned()); + let env = RefCell::new(Environment::new_with_vars(out_buffer, err_buffer, env_vars)); let mut b = FileBuilder::new("", &i_paths, &env); b.enable_validate_mode(); b.eval_string(input).unwrap(); @@ -148,6 +151,47 @@ fn test_type_checks() { assert_build(include_str!("../../integration_tests/types_test.ucg")); } +#[test] +fn test_environment_variable_exists() { + assert_build("assert { ok = env.FOO == \"bar\", desc = \"env var $FOO is bar\"};"); +} +// TODO(jwall): that shadowing the env variable is not allowed? + +#[test] +fn test_env_as_field_name_works() { + assert_build( + "let tpl = { env = \"quux\" }; +assert { + ok = env.FOO == \"bar\", + desc = \"env var $FOO is bar\", +}; +assert { + ok = tpl.env == \"quux\", + desc = \"tpl.env is quux\", +}; + +let tpl2 = { env = { bar = 2 } }; +assert { + ok = tpl2.env.bar == 2, + desc = \"tpl2.env.bar is 2\", +}; +", + ); +} +// TODO(jwall): tests for missing values. + +#[test] +fn test_env_is_not_a_valid_binding() { + assert_build_failure( + "let env = 1;", + vec![ + Regex::new("Cannot use binding env").unwrap(), + Regex::new("It is a reserved word").unwrap(), + Regex::new("at line: 1 column: 5").unwrap(), + ], + ); +} + #[test] #[should_panic(expected = "UserDefined: I am a failure!")] fn test_declarative_failures_are_caused_by_msg() { diff --git a/src/build/opcode/environment.rs b/src/build/opcode/environment.rs index 5d12d5e..d4030db 100644 --- a/src/build/opcode/environment.rs +++ b/src/build/opcode/environment.rs @@ -17,16 +17,16 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::rc::Rc; -use super::cache; use super::pointer::OpPointer; use super::Error; -use super::Value; -use crate::build::stdlib; +use super::{cache, Primitive}; +use super::{Composite, Value}; use crate::build::AssertCollector; use crate::build::Val; use crate::convert::{ConverterRegistry, ImporterRegistry}; use crate::iter::OffsetStrIter; use crate::parse::parse; +use crate::{ast::Position, build::stdlib}; // Shared Environmental between VM's for runtime usage. pub struct Environment @@ -47,6 +47,7 @@ where impl Environment { pub fn new(out: Stdout, err: Stderr) -> Self { + // TODO(jwall): populate this with environment variables? Self::new_with_vars(out, err, BTreeMap::new()) } @@ -66,6 +67,16 @@ impl Environment { return me; } + pub fn get_env_vars_tuple(&self) -> Value { + let mut fields = Vec::new(); + let mut positions = Vec::new(); + for (key, val) in self.env_vars.iter() { + fields.push((key.clone(), Rc::new(Value::P(Primitive::Str(val.clone()))))); + positions.push((Position::new(0, 0, 0), Position::new(0, 0, 0))); + } + Value::C(Composite::Tuple(fields, positions)) + } + pub fn get_cached_path_val(&self, path: &String) -> Option> { self.val_cache.get(path).cloned() } diff --git a/src/build/opcode/vm.rs b/src/build/opcode/vm.rs index 0c01ae2..96e1c04 100644 --- a/src/build/opcode/vm.rs +++ b/src/build/opcode/vm.rs @@ -144,7 +144,7 @@ impl VM { 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::DeRef(s) => self.op_deref(s.clone(), env, &pos)?, Op::Add => self.op_add(pos)?, Op::Mod => self.op_mod(pos)?, Op::Sub => self.op_sub(pos)?, @@ -242,8 +242,17 @@ impl VM { Ok(()) } - fn op_deref(&mut self, name: String, pos: &Position) -> Result<(), Error> { - let (val, _) = self.get_binding(&name, pos)?.clone(); + fn op_deref( + &mut self, + name: String, + env: &RefCell>, + pos: &Position, + ) -> Result<(), Error> + where + O: std::io::Write + Clone, + E: std::io::Write + Clone, + { + let (val, _) = self.get_binding(&name, env, pos)?.clone(); self.push(val, pos.clone()) } @@ -1078,9 +1087,29 @@ impl VM { Ok(()) } - pub fn get_binding(&self, name: &str, pos: &Position) -> Result<(Rc, Position), Error> { + pub fn get_binding( + &self, + name: &str, + env: &RefCell>, + pos: &Position, + ) -> Result<(Rc, Position), Error> + where + O: std::io::Write + Clone, + E: std::io::Write + Clone, + { + // TODO(jwall): Handle environment variables here? let tpl = if name == "self" { self.self_stack.last().cloned() + } else if name == "env" { + let candidate = self.symbols.get(name); + if candidate.is_some() { + candidate + } else { + Some(( + Rc::new(env.borrow().get_env_vars_tuple()), + Position::new(0, 0, 0), + )) + } } else { self.symbols.get(name) }; diff --git a/src/main.rs b/src/main.rs index 5a2853d..c2f64db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -426,7 +426,11 @@ fn env_help() { ); } -fn do_repl(import_paths: &Vec, strict: bool) -> std::result::Result<(), Box> { +fn do_repl( + import_paths: &Vec, + strict: bool, + env: &RefCell>, +) -> std::result::Result<(), Box> { let config = rustyline::Config::builder(); let mut editor = rustyline::Editor::<()>::with_config( config @@ -458,10 +462,6 @@ fn do_repl(import_paths: &Vec, strict: bool) -> std::result::Result<(), } } } - let env = std::cell::RefCell::new(build::opcode::Environment::new( - StdoutWrapper::new(), - StderrWrapper::new(), - )); let mut builder = build::FileBuilder::new(std::env::current_dir()?, import_paths, &env); builder.set_strict(strict); @@ -469,8 +469,12 @@ fn do_repl(import_paths: &Vec, strict: bool) -> std::result::Result<(), Ok(()) } -fn repl(import_paths: &Vec, strict: bool) { - if let Err(e) = do_repl(import_paths, strict) { +fn repl( + import_paths: &Vec, + strict: bool, + env: &RefCell>, +) { + if let Err(e) = do_repl(import_paths, strict, env) { eprintln!("{}", e); process::exit(1); } @@ -482,7 +486,15 @@ fn main() { // FIXME(jwall): Do we want these to be shared or not? let registry = ConverterRegistry::make_registry(); let mut import_paths = Vec::new(); - let env = RefCell::new(Environment::new(StdoutWrapper::new(), StderrWrapper::new())); + let mut env_vars = BTreeMap::new(); + for (var, val) in std::env::vars() { + env_vars.insert(var, val); + } + let env = RefCell::new(Environment::new_with_vars( + StdoutWrapper::new(), + StderrWrapper::new(), + env_vars, + )); if let Some(mut p) = dirs::home_dir() { p.push(".ucg"); // Attempt to create directory if it doesn't exist. @@ -518,7 +530,7 @@ fn main() { } else if let Some(_) = app_matches.subcommand_matches("env") { env_help() } else if let Some(_) = app_matches.subcommand_matches("repl") { - repl(&import_paths, strict) + repl(&import_paths, strict, &env) } else if let Some(matches) = app_matches.subcommand_matches("fmt") { if let Err(e) = fmt_command(matches) { eprintln!("{}", e); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 5e2282c..087183d 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -805,11 +805,33 @@ make_fn!( ) ); +/// Verify binding is not a reserved word. +macro_rules! match_binding_name { + ($i:expr,) => {{ + use abortable_parser::{Error, Result}; + let mut _i = $i.clone(); + match match_type!(_i, BAREWORD) { + Result::Complete(i, t) => { + if t.fragment == "env" { + return Result::Abort(Error::new( + format!("Cannot use binding {}. It is a reserved word.", t.fragment), + Box::new($i.clone()), + )); + } + Result::Complete(i, t) + } + Result::Incomplete(i) => Result::Incomplete(i), + Result::Fail(e) => Result::Fail(e), + Result::Abort(e) => Result::Abort(e), + } + }}; +} + make_fn!( let_stmt_body, Statement>, do_each!( pos => pos, - name => wrap_err!(match_type!(BAREWORD), "Expected name for binding"), + name => wrap_err!(match_binding_name!(), "Expected name for binding"), _ => optional!(trace_parse!(shape_suffix)), _ => punct!("="), val => trace_parse!(wrap_err!(expression, "Expected Expression to bind")),