FEATURE: make macros close over their environment.

The benefits are great enough to allow this and the benefits of
not allowing closures are not terribly useful.

We do not get the same benefits for modules though so we don't add
it to them.
This commit is contained in:
Jeremy Wall 2019-01-14 18:23:39 -06:00
parent 5c2d8662bf
commit 685ee7407e
7 changed files with 48 additions and 231 deletions

View File

@ -64,4 +64,15 @@ let arg_tpl = {
assert {
ok = macro_tpl_arg(arg_tpl).result == arg_tpl.field,
desc = "macro_tpl_arg(arg_tpl).result == arg_tpl.field",
};
let closed_over = "foo";
let closure = macro(arg1) => {
result = closed_over + arg1,
};
assert {
ok = closure("bar").result == "foobar",
desc = "we closed over closed_over and got @" % (closure("bar").result),
};

View File

@ -19,7 +19,6 @@ use std::cmp::Eq;
use std::cmp::Ordering;
use std::cmp::PartialEq;
use std::cmp::PartialOrd;
use std::collections::HashSet;
use std::convert::Into;
use std::fmt;
use std::hash::Hash;
@ -29,6 +28,7 @@ use std::rc::Rc;
use abortable_parser;
use crate::build::scope::Scope;
use crate::build::Val;
pub mod walk;
@ -377,104 +377,12 @@ impl<'a> From<&'a PositionedItem<String>> for PositionedItem<String> {
/// any values except what is defined in their arguments.
#[derive(PartialEq, Debug, Clone)]
pub struct MacroDef {
pub scope: Option<Scope>,
pub argdefs: Vec<PositionedItem<String>>,
pub fields: FieldList,
pub pos: Position,
}
impl MacroDef {
fn symbol_is_in_args(&self, sym: &String) -> bool {
for arg in self.argdefs.iter() {
if &arg.val == sym {
return true;
}
}
return false;
}
fn validate_value_symbols<'a>(
&self,
stack: &mut Vec<&'a Expression>,
val: &'a Value,
) -> HashSet<String> {
let mut bad_symbols = HashSet::new();
if let &Value::Symbol(ref name) = val {
if name.val != "self" && !self.symbol_is_in_args(&name.val) {
bad_symbols.insert(name.val.clone());
}
} else if let &Value::Tuple(ref tuple_node) = val {
let fields = &tuple_node.val;
for &(_, ref expr) in fields.iter() {
stack.push(expr);
}
} else if let &Value::List(ref def) = val {
for elem in def.elems.iter() {
stack.push(elem);
}
}
return bad_symbols;
}
/// Performs typechecking of a ucg macro's arguments to ensure
/// that they are valid for the expressions in the macro.
pub fn validate_symbols(&self) -> Result<(), HashSet<String>> {
let mut bad_symbols = HashSet::new();
for &(_, ref expr) in self.fields.iter() {
let mut stack = Vec::new();
stack.push(expr);
while stack.len() > 0 {
match stack.pop().unwrap() {
&Expression::Binary(ref bexpr) => {
stack.push(&bexpr.left);
if bexpr.kind != BinaryExprType::DOT {
stack.push(&bexpr.right);
}
}
&Expression::Grouped(ref expr) => {
stack.push(expr);
}
&Expression::Format(ref def) => {
let exprs = &def.args;
for arg_expr in exprs.iter() {
stack.push(arg_expr);
}
}
&Expression::Select(ref def) => {
stack.push(def.default.borrow());
stack.push(def.val.borrow());
for &(_, ref expr) in def.tuple.iter() {
stack.push(expr);
}
}
&Expression::Call(ref def) => {
for expr in def.arglist.iter() {
stack.push(expr);
}
}
&Expression::Simple(ref val) => {
let mut syms_set = self.validate_value_symbols(&mut stack, val);
bad_symbols.extend(syms_set.drain());
}
&Expression::Macro(_)
| &Expression::Copy(_)
| &Expression::Module(_)
| &Expression::Range(_)
| &Expression::FuncOp(_)
| &Expression::Import(_)
| &Expression::Include(_) => {
// noop
continue;
}
}
}
}
if bad_symbols.len() > 0 {
return Err(bad_symbols);
}
return Ok(());
}
}
/// Specifies the types of binary operations supported in
/// UCG expression.
#[derive(Debug, PartialEq, Clone)]
@ -604,6 +512,7 @@ impl FuncOpDef {
// TODO(jwall): this should probably be moved to a Val::Module IR type.
#[derive(Debug, PartialEq, Clone)]
pub struct ModuleDef {
pub scope: Option<Scope>,
pub pos: Position,
pub arg_set: FieldList,
pub arg_tuple: Option<Rc<Val>>,
@ -613,6 +522,7 @@ pub struct ModuleDef {
impl ModuleDef {
pub fn new<P: Into<Position>>(arg_set: FieldList, stmts: Vec<Statement>, pos: P) -> Self {
ModuleDef {
scope: None,
pos: pos.into(),
arg_set: arg_set,
arg_tuple: None,
@ -782,6 +692,3 @@ pub enum Statement {
// Identify an Expression for output.
Output(Token, Expression),
}
#[cfg(test)]
pub mod test;

View File

@ -1,115 +0,0 @@
// Copyright 2017 Jeremy Wall <jeremy@marzhillstudios.com>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
#[test]
pub fn test_macro_validation_happy_path() {
let def = MacroDef {
argdefs: vec![value_node!("foo".to_string(), Position::new(1, 0, 0))],
fields: vec![(
make_tok!("f1", Position::new(1, 1, 0)),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Add,
left: Box::new(Expression::Simple(Value::Symbol(value_node!(
"foo".to_string(),
Position::new(1, 1, 0)
)))),
right: Box::new(Expression::Simple(Value::Int(value_node!(
1,
Position::new(1, 1, 0)
)))),
pos: Position::new(1, 0, 0),
}),
)],
pos: Position::new(1, 0, 0),
};
assert!(def.validate_symbols().unwrap() == ());
}
#[test]
pub fn test_macro_validation_fail() {
let def = MacroDef {
argdefs: vec![value_node!("foo".to_string(), Position::new(1, 0, 0))],
fields: vec![(
make_tok!("f1", Position::new(1, 1, 0)),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Add,
left: Box::new(Expression::Simple(Value::Symbol(value_node!(
"bar".to_string(),
Position::new(1, 1, 0)
)))),
right: Box::new(Expression::Simple(Value::Int(value_node!(
1,
Position::new(1, 1, 0)
)))),
pos: Position::new(1, 0, 0),
}),
)],
pos: Position::new(1, 0, 0),
};
let mut expected = HashSet::new();
expected.insert("bar".to_string());
assert_eq!(def.validate_symbols().err().unwrap(), expected);
}
#[test]
pub fn test_macro_validation_selector_happy_path() {
let def = MacroDef {
argdefs: vec![value_node!("foo".to_string(), Position::new(1, 0, 0))],
fields: vec![(
make_tok!("f1", Position::new(1, 1, 0)),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Add,
left: Box::new(Expression::Simple(Value::Symbol(PositionedItem::new(
"foo".to_string(),
Position::new(1, 1, 0),
)))),
right: Box::new(Expression::Simple(Value::Int(value_node!(
1,
Position::new(1, 1, 0)
)))),
pos: Position::new(1, 0, 0),
}),
)],
pos: Position::new(1, 0, 0),
};
assert!(def.validate_symbols().unwrap() == ());
}
#[test]
pub fn test_macro_validation_selector_fail() {
let def = MacroDef {
argdefs: vec![value_node!("foo".to_string(), Position::new(1, 0, 0))],
fields: vec![(
make_tok!("f1", Position::new(1, 1, 0)),
Expression::Binary(BinaryOpDef {
kind: BinaryExprType::Add,
left: Box::new(Expression::Simple(Value::Symbol(PositionedItem::new(
"bar".to_string(),
Position::new(1, 1, 0),
)))),
right: Box::new(Expression::Simple(Value::Int(value_node!(
1,
Position::new(1, 1, 0)
)))),
pos: Position::new(1, 0, 0),
}),
)],
pos: Position::new(1, 0, 0),
};
let mut expected = HashSet::new();
expected.insert("bar".to_string());
assert_eq!(def.validate_symbols(), Err(expected));
}

View File

@ -72,12 +72,18 @@ impl MacroDef {
// If the expressions reference Symbols not defined in the MacroDef that is also an error.
// TODO(jwall): We should probably enforce that the Expression Symbols must be in argdefs rules
// at Macro definition time not evaluation time.
let mut scope = HashMap::<PositionedItem<String>, Rc<Val>>::new();
let mut build_output = HashMap::<PositionedItem<String>, Rc<Val>>::new();
for (i, arg) in args.drain(0..).enumerate() {
scope.entry(self.argdefs[i].clone()).or_insert(arg.clone());
build_output
.entry(self.argdefs[i].clone())
.or_insert(arg.clone());
}
let mut b = parent_builder.clone_builder(root);
b.set_build_output(scope);
if let Some(ref scope) = self.scope {
b.scope = scope.spawn_child();
}
// We clobber anything that used to be in the scope with the arguments.
b.merge_build_output(build_output, true);
let mut result: Vec<(PositionedItem<String>, Rc<Val>)> = Vec::new();
for &(ref key, ref expr) in self.fields.iter() {
// We clone the expressions here because this macro may be consumed
@ -216,6 +222,16 @@ impl<'a> FileBuilder<'a> {
self.scope.build_output = scope;
}
pub fn merge_build_output(&mut self, scope: ValueMap, clobber: bool) {
for (name, value) in scope.iter() {
if !clobber && !self.scope.build_output.contains_key(name) {
self.scope.build_output.insert(name.clone(), value.clone());
} else {
self.scope.build_output.insert(name.clone(), value.clone());
}
}
}
pub fn set_strict(&mut self, to: bool) {
self.strict = to;
}
@ -1104,19 +1120,9 @@ impl<'a> FileBuilder<'a> {
)))
}
fn eval_macro_def(&self, def: &MacroDef) -> Result<Rc<Val>, Box<dyn Error>> {
match def.validate_symbols() {
Ok(()) => Ok(Rc::new(Val::Macro(def.clone()))),
Err(set) => Err(Box::new(error::BuildError::new(
format!(
"Macro has the following \
undefined symbols: {:?}",
set
),
error::ErrorType::NoSuchSymbol,
def.pos.clone(),
))),
}
fn eval_macro_def(&self, def: &mut MacroDef, scope: &Scope) -> Result<Rc<Val>, Box<dyn Error>> {
def.scope = Some(scope.spawn_child());
Ok(Rc::new(Val::Macro(def.clone())))
}
fn file_dir(&self) -> PathBuf {
@ -1625,8 +1631,14 @@ impl<'a> FileBuilder<'a> {
&Expression::Grouped(ref expr) => self.eval_expr(expr, scope),
&Expression::Format(ref def) => self.eval_format(def, scope),
&Expression::Call(ref def) => self.eval_call(def, scope),
&Expression::Macro(ref def) => self.eval_macro_def(def),
&Expression::Module(ref def) => self.eval_module_def(def, scope),
&Expression::Macro(ref def) => {
let mut def_clone = def.clone();
self.eval_macro_def(&mut def_clone, scope)
}
&Expression::Module(ref def) => {
let mut def_clone = def.clone();
self.eval_module_def(&mut def_clone, scope)
}
&Expression::Select(ref def) => self.eval_select(def, scope),
&Expression::FuncOp(ref def) => self.eval_func_op(def, scope),
&Expression::Include(ref def) => self.eval_include(def),

View File

@ -33,7 +33,7 @@ pub type ValueMap = HashMap<PositionedItem<String>, Rc<Val>>;
///
/// UCG Scopes do not descend up into their parent scopes so we do not maintain a stack
/// for those.
#[derive(Clone)]
#[derive(Debug, PartialEq, Clone)]
pub struct Scope {
pub import_stack: Vec<String>,
pub env: Rc<Val>,

View File

@ -248,6 +248,7 @@ fn test_macro_hermetic() {
.build_output
.entry(value_node!("tstmac".to_string(), Position::new(1, 0, 0)))
.or_insert(Rc::new(Val::Macro(MacroDef {
scope: None,
argdefs: vec![value_node!("arg2".to_string(), Position::new(1, 0, 0))],
fields: vec![(
make_tok!("foo", Position::new(1, 1, 1)),

View File

@ -361,6 +361,7 @@ fn tuple_to_macro<'a>(
.collect();
match val {
Value::Tuple(v) => Ok(Expression::Macro(MacroDef {
scope: None,
argdefs: arglist,
fields: v.val,
pos: pos,