FEATURE: UCG Parameterized Modules

closes #10

Squashed commit of the following:

commit 3101c2bb9a385ed9e84481d36906a3e3ce93e571
Author: Jeremy Wall <jeremy@marzhillstudios.com>
Date:   Wed Nov 21 20:10:31 2018 -0600

    FEATURE: Module evaluation

    * handle evaluating the module definition.
    * Handle performing a module instantiation via the copy syntax.

commit 4ca863896b416e39f0c8eacc53384b9c514f6f14
Author: Jeremy Wall <jeremy@marzhillstudios.com>
Date:   Tue Nov 20 18:38:19 2018 -0600

    FEATURE: Add module parsing expression parsing to ucg.

    changes toward issue #10
This commit is contained in:
Jeremy Wall 2018-11-23 12:50:47 -06:00
parent a9b374bf33
commit fa96c7c0ef
23 changed files with 433 additions and 90 deletions

View File

@ -26,6 +26,7 @@ Some words are reserved in ucg and can not be used as a named binding.
* as
* select
* macro
* module
* env
* map
* filter

View File

@ -0,0 +1,11 @@
let host_mod = module{
hostname="",
mem=2048,
cpu=2,
} => {
let config = {
hostname = mod.hostname,
memory_size = mod.mem,
cpu_count = mod.cpu,
};
};

View File

@ -0,0 +1,25 @@
let site_mod = module{
hostname="",
port=0,
db="localhost",
db_user="admin",
db_pass="password"
} => { // mods do not close over their environment.
// this binding is not visible outside of the module. should be at the time of definition?
// or path should be rewritten to be absolue before instantiation.
import "../../shared.ucg" as shared;
// processing should be delayed till module instantiation.
let base_config = shared.mk_site_config(mod.hostname, mod.port);
let config = base_config{ // processing should also be delayed.
dbs = [
{
database = mod.db,
user = mod.db_user,
pass = mod.db_pass,
},
],
};
};

View File

@ -0,0 +1,12 @@
let composed = module{} => {
import "host_module.ucg" as host_mod;
import "site_module.ucg" as site_mod;
let site_conf = site_mod.site_mod{hostname="example.com", port=80};
let host_conf = host_mod.host_mod{hostname="example.com"};
};
let unified = composed{};
let site_conf = unified.site_conf;
let host_conf = unified.host_conf;

View File

@ -0,0 +1,3 @@
import "modules/unified.ucg" as unified;
out yaml unified.host_conf;

View File

@ -0,0 +1,3 @@
import "modules/unified.ucg" as unified;
out json unified.site_conf;

View File

@ -1 +1,7 @@
let port = 3306;
let mk_site_config = macro(hostname, port) => {
base_url = "https://@/" % (hostname),
port = port,
dbs = [],
};

View File

@ -1 +1 @@
--port 8080 --listen '0.0.0.0' --verbose --dir 'some/dir' --dir 'some/other/dir' --log.debug true--log.format 'json'
--port 8080 --listen '0.0.0.0' --verbose --dir 'some/dir' --dir 'some/other/dir' --log.debug true --log.format 'json'

View File

@ -0,0 +1,45 @@
let test_empty_mod = module {
} => {
};
let empty_mod_instance = test_empty_mod{};
let test_simple_mod = module {
arg = "value",
} => {
let value = mod.arg;
};
let simple_mod_instance = test_simple_mod{};
assert |
simple_mod_instance.value == "value";
|;
let simple_mod_with_args = test_simple_mod{arg = "othervalue"};
assert |
simple_mod_with_args.value == "othervalue";
|;
let embedded_mod = module {
deep_value = "None",
} => {
let embedded_def = module {
deep_value = "None",
} => {
let value = mod.deep_value;
};
let embedded = embedded_def{deep_value = mod.deep_value};
};
let embedded_default_params = embedded_mod{};
assert |
embedded_default_params.embedded.value == "None";
|;
let embedded_with_params = embedded_mod{deep_value = "Some"};
assert |
embedded_with_params.embedded.value == "Some";
|;

View File

@ -24,9 +24,13 @@ use std::convert::Into;
use std::fmt;
use std::hash::Hash;
use std::hash::Hasher;
use std::path::PathBuf;
use std::rc::Rc;
use abortable_parser;
use build::Val;
macro_rules! enum_type_equality {
( $slf:ident, $r:expr, $( $l:pat ),* ) => {
match $slf {
@ -612,6 +616,10 @@ impl MacroDef {
// noop
continue;
}
&Expression::Module(_) => {
// noop
continue;
}
&Expression::ListOp(_) => {
// noop
continue;
@ -705,6 +713,43 @@ pub struct ListOpDef {
pub pos: Position,
}
#[derive(Debug, PartialEq, Clone)]
pub struct ModuleDef {
pub pos: Position,
pub arg_set: FieldList,
// FIXME(jwall): this should probably be moved to a Val::Module IR type.
pub arg_tuple: Option<Rc<Val>>,
pub statements: Vec<Statement>,
}
impl ModuleDef {
pub fn new<P: Into<Position>>(arg_set: FieldList, stmts: Vec<Statement>, pos: P) -> Self {
ModuleDef {
pos: pos.into(),
arg_set: arg_set,
// TODO(jwall): Should this get moved to a Val version of our ModuleDef?
arg_tuple: None,
statements: stmts,
}
}
pub fn imports_to_absolute(&mut self, base: PathBuf) {
for stmt in self.statements.iter_mut() {
if let &mut Statement::Import(ref mut def) = stmt {
let mut path = PathBuf::from(&def.path.fragment);
if path.is_relative() {
def.path.fragment = base
.join(path)
.canonicalize()
.unwrap()
.to_string_lossy()
.to_string();
}
}
}
}
}
/// Encodes a ucg expression. Expressions compute a value from.
#[derive(Debug, PartialEq, Clone)]
pub enum Expression {
@ -724,6 +769,7 @@ pub enum Expression {
Macro(MacroDef),
Select(SelectDef),
ListOp(ListOpDef),
Module(ModuleDef),
}
impl Expression {
@ -738,6 +784,7 @@ impl Expression {
&Expression::Format(ref def) => &def.pos,
&Expression::Call(ref def) => &def.pos,
&Expression::Macro(ref def) => &def.pos,
&Expression::Module(ref def) => &def.pos,
&Expression::Select(ref def) => &def.pos,
&Expression::ListOp(ref def) => &def.pos,
}
@ -774,6 +821,9 @@ impl fmt::Display for Expression {
&Expression::Macro(_) => {
try!(write!(w, "<Macro>"));
}
&Expression::Module(_) => {
try!(write!(w, "<Module>"));
}
&Expression::Select(_) => {
try!(write!(w, "<Select>"));
}
@ -783,21 +833,21 @@ impl fmt::Display for Expression {
}
/// Encodes a let statement in the UCG AST.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub struct LetDef {
pub name: Token,
pub value: Expression,
}
/// Encodes an import statement in the UCG AST.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub struct ImportDef {
pub path: Token,
pub name: Token,
}
/// Encodes a parsed statement in the UCG AST.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub enum Statement {
// simple expression
Expression(Expression),

View File

@ -48,6 +48,11 @@ fn test_macros() {
assert_build(include_str!("../../integration_tests/macros_test.ucg"));
}
#[test]
fn test_modules() {
assert_build(include_str!("../../integration_tests/modules_test.ucg"));
}
#[test]
fn test_selectors() {
assert_build(include_str!("../../integration_tests/selectors_test.ucg"));

View File

@ -20,6 +20,7 @@ pub enum Val {
List(Vec<Rc<Val>>),
Tuple(Vec<(PositionedItem<String>, Rc<Val>)>),
Macro(MacroDef),
Module(ModuleDef),
}
impl Val {
@ -34,6 +35,7 @@ impl Val {
&Val::List(_) => "List".to_string(),
&Val::Tuple(_) => "Tuple".to_string(),
&Val::Macro(_) => "Macro".to_string(),
&Val::Module(_) => "Module".to_string(),
}
}
@ -49,7 +51,8 @@ impl Val {
&Val::Str(_),
&Val::List(_),
&Val::Tuple(_),
&Val::Macro(_)
&Val::Macro(_),
&Val::Module(_)
)
}
@ -105,6 +108,11 @@ impl Val {
error::ErrorType::TypeFail,
pos,
)),
(&Val::Module(_), &Val::Module(_)) => Err(error::BuildError::new(
format!("Module are not comparable in file: {}", file_name),
error::ErrorType::TypeFail,
pos,
)),
(me, tgt) => Err(error::BuildError::new(
format!("Types differ for {}, {} in file: {}", me, tgt, file_name),
error::ErrorType::TypeFail,
@ -188,6 +196,7 @@ impl Display for Val {
write!(f, "]")
}
&Val::Macro(_) => write!(f, "Macro(..)"),
&Val::Module(_) => write!(f, "Module{{..}}"),
&Val::Tuple(ref def) => {
try!(write!(f, "Tuple(\n"));
for v in def.iter() {

View File

@ -110,11 +110,14 @@ pub struct Builder<'a> {
/// are keyed by the canonicalized import path. This acts as a cache
/// so multiple imports of the same file don't have to be parsed
/// multiple times.
// FIXME(jwall): This probably needs to be running in a separate thread
// with some sort of RPC mechanism instead.
assets: Rc<RefCell<assets::Cache>>,
/// build_output is our built output.
build_output: ValueMap,
/// last is the result of the last statement.
pub stack: Option<Vec<Rc<Val>>>,
pub is_module: bool,
pub last: Option<Rc<Val>>,
pub out_lock: Option<(String, Rc<Val>)>,
}
@ -223,6 +226,7 @@ impl<'a> Builder<'a> {
build_output: scope,
out_lock: None,
stack: None,
is_module: false,
last: None,
}
}
@ -301,20 +305,26 @@ impl<'a> Builder<'a> {
fn build_import(&mut self, def: &ImportDef) -> Result<Rc<Val>, Box<Error>> {
let sym = &def.name;
let mut normalized = self.root.to_path_buf();
let mut normalized = self.root.clone();
let import_path = PathBuf::from(&def.path.fragment);
if import_path.is_relative() {
normalized.push(&def.path.fragment);
} else {
normalized = import_path;
}
normalized = try!(normalized.canonicalize());
eprintln!("processing import for {}", normalized.to_string_lossy());
// Introduce a scope so the above borrow is dropped before we modify
// the cache below.
// Only parse the file once on import.
let mut shared_assets = self.assets.borrow_mut();
let result = match try!(shared_assets.get(&normalized)) {
let maybe_asset = try!(self.assets.borrow().get(&normalized));
let result = match maybe_asset {
Some(v) => v.clone(),
None => {
let mut b = Self::new(normalized.clone(), self.assets.clone());
let filepath = normalized.to_str().unwrap().clone();
try!(b.build_file(filepath));
let fields: Vec<(PositionedItem<String>, Rc<Val>)> =
b.build_output.drain().collect();
Rc::new(Val::Tuple(fields))
b.get_outputs_as_val()
}
};
let key = sym.into();
@ -326,7 +336,8 @@ impl<'a> Builder<'a> {
)));
}
self.build_output.insert(key, result.clone());
try!(shared_assets.stash(normalized.clone(), result.clone()));
let mut mut_assets_cache = self.assets.borrow_mut();
try!(mut_assets_cache.stash(normalized.clone(), result.clone()));
return Ok(result);
}
@ -383,7 +394,7 @@ impl<'a> Builder<'a> {
return Some(self.env.clone());
}
if &sym.val == "self" {
// TODO(jwall): we need to look at the current tuple in the stack.
eprintln!("XXX: In tuple self is {:?}", self.peek_val());
return self.peek_val();
}
if self.build_output.contains_key(sym) {
@ -823,10 +834,16 @@ impl<'a> Builder<'a> {
}
}
fn eval_copy(&mut self, def: &CopyDef) -> Result<Rc<Val>, Box<Error>> {
let v = try!(self.lookup_selector(&def.selector.sel));
if let &Val::Tuple(ref src_fields) = v.as_ref() {
self.push_val(v.clone());
fn get_outputs_as_val(&mut self) -> Rc<Val> {
let fields: Vec<(PositionedItem<String>, Rc<Val>)> = self.build_output.drain().collect();
Rc::new(Val::Tuple(fields))
}
fn copy_from_base(
&mut self,
src_fields: &Vec<(PositionedItem<String>, Rc<Val>)>,
overrides: &Vec<(Token, Expression)>,
) -> Result<Rc<Val>, Box<Error>> {
let mut m = HashMap::<PositionedItem<String>, (i32, Rc<Val>)>::new();
// loop through fields and build up a hashmap
let mut count = 0;
@ -848,8 +865,7 @@ impl<'a> Builder<'a> {
)));
}
}
for &(ref key, ref val) in def.fields.iter() {
// TODO(jwall): Allow the special value self to refer to the base tuple.
for &(ref key, ref val) in overrides.iter() {
let expr_result = try!(self.eval_expr(val));
match m.entry(key.into()) {
// brand new field here.
@ -898,8 +914,63 @@ impl<'a> Builder<'a> {
}).collect(),
)));
}
fn eval_copy(&mut self, def: &CopyDef) -> Result<Rc<Val>, Box<Error>> {
let v = try!(self.lookup_selector(&def.selector.sel));
if let &Val::Tuple(ref src_fields) = v.as_ref() {
self.push_val(v.clone());
return self.copy_from_base(&src_fields, &def.fields);
}
if let &Val::Module(ref mod_def) = v.as_ref() {
let maybe_tpl = mod_def.clone().arg_tuple.unwrap().clone();
if let &Val::Tuple(ref src_fields) = maybe_tpl.as_ref() {
// 1. First we create a builder.
let mut b = Self::new(self.root.clone(), self.assets.clone());
b.is_module = true;
// 2. We construct an argument tuple by copying from the defs
// argset.
// Push our base tuple on the stack so the copy can use
// self to reference it.
b.push_val(maybe_tpl.clone());
let mod_args = try!(self.copy_from_base(src_fields, &def.fields));
// put our copied parameters tuple in our builder under the mod key.
let mod_key =
PositionedItem::new_with_pos(String::from("mod"), Position::new(0, 0, 0));
match b.build_output.entry(mod_key) {
Entry::Occupied(e) => {
return Err(Box::new(error::BuildError::new(
format!(
"Binding \
for {:?} already \
exists in module",
e.key(),
),
error::ErrorType::DuplicateBinding,
mod_def.pos.clone(),
)));
}
Entry::Vacant(e) => {
e.insert(mod_args.clone());
}
}
// 4. Evaluate all the statements using the builder.
try!(b.build(&mod_def.statements));
// 5. Take all of the bindings in the module and construct a new
// tuple using them.
return Ok(b.get_outputs_as_val());
} else {
return Err(Box::new(error::BuildError::new(
format!(
"Weird value stored in our module parameters slot {:?}",
mod_def.arg_tuple
),
error::ErrorType::TypeFail,
def.selector.pos.clone(),
)));
}
}
Err(Box::new(error::BuildError::new(
format!("Expected Tuple got {}", v),
format!("Expected Tuple or Module got {}", v),
error::ErrorType::TypeFail,
def.selector.pos.clone(),
)))
@ -917,6 +988,7 @@ impl<'a> Builder<'a> {
Ok(Rc::new(Val::Str(try!(formatter.render(&def.pos)))))
}
// FIXME(jwall): Handle module calls as well?
fn eval_call(&mut self, def: &CallDef) -> Result<Rc<Val>, Box<Error>> {
let sel = &def.macroref;
let args = &def.arglist;
@ -958,6 +1030,24 @@ impl<'a> Builder<'a> {
}
}
fn eval_module_def(&mut self, def: &ModuleDef) -> Result<Rc<Val>, Box<Error>> {
// Always work on a copy. The original should not be modified.
let mut def = def.clone();
// First we rewrite the imports to be absolute paths.
let root = if self.root.is_file() {
// Only use the dirname portion if the root is a file.
self.root.parent().unwrap().to_path_buf()
} else {
// otherwise use clone of the root..
self.root.clone()
};
def.imports_to_absolute(root);
// Then we create our tuple default.
def.arg_tuple = Some(try!(self.tuple_to_val(&def.arg_set)));
// Then we construct a new Val::Module
Ok(Rc::new(Val::Module(def)))
}
fn eval_select(&mut self, def: &SelectDef) -> Result<Rc<Val>, Box<Error>> {
let target = &def.val;
let def_expr = &def.default;
@ -1105,6 +1195,7 @@ impl<'a> Builder<'a> {
&Expression::Format(ref def) => self.eval_format(def),
&Expression::Call(ref def) => self.eval_call(def),
&Expression::Macro(ref def) => self.eval_macro_def(def),
&Expression::Module(ref def) => self.eval_module_def(def),
&Expression::Select(ref def) => self.eval_select(def),
&Expression::ListOp(ref def) => self.eval_list_op(def),
}

View File

@ -161,7 +161,7 @@ fn test_expr_copy_no_such_tuple() {
}
#[test]
#[should_panic(expected = "Expected Tuple got Int(1)")]
#[should_panic(expected = "Expected Tuple or Module got Int(1)")]
fn test_expr_copy_not_a_tuple() {
let cache = Rc::new(RefCell::new(MemoryCache::new()));
let mut b = Builder::new(std::env::current_dir().unwrap(), cache);

View File

@ -82,6 +82,10 @@ impl EnvConverter {
// This is ignored
eprintln!("Skipping macro...");
}
&Val::Module(ref _def) => {
// This is ignored
eprintln!("Skipping module...");
}
}
Ok(())
}

View File

@ -98,6 +98,10 @@ impl FlagConverter {
// This is ignored
eprintln!("Skipping macro...");
}
&Val::Module(ref _def) => {
// This is ignored
eprintln!("Skipping module...");
}
}
Ok(())
}

View File

@ -71,6 +71,10 @@ impl JsonConverter {
eprintln!("Skipping macro encoding as null...");
serde_json::Value::Null
}
&Val::Module(_) => {
eprintln!("Skipping module encoding as null...");
serde_json::Value::Null
}
&Val::List(ref l) => try!(self.convert_list(l)),
&Val::Tuple(ref t) => try!(self.convert_tuple(t)),
};

View File

@ -65,6 +65,10 @@ impl TomlConverter {
let err = SimpleError::new("Macros are not allowed in Toml Conversions!");
return Err(Box::new(err));
}
&Val::Module(_) => {
let err = SimpleError::new("Modules are not allowed in Toml Conversions!");
return Err(Box::new(err));
}
&Val::List(ref l) => try!(self.convert_list(l)),
&Val::Tuple(ref t) => try!(self.convert_tuple(t)),
};

View File

@ -54,6 +54,10 @@ impl YamlConverter {
eprintln!("Skipping macro encoding as null...");
serde_yaml::Value::Null
}
&Val::Module(_) => {
eprintln!("Skipping module encoding as null...");
serde_yaml::Value::Null
}
&Val::List(ref l) => try!(self.convert_list(l)),
&Val::Tuple(ref t) => try!(self.convert_tuple(t)),
};

View File

@ -76,7 +76,10 @@ fn build_file(
validate: bool,
cache: Rc<RefCell<Cache>>,
) -> Result<build::Builder, Box<Error>> {
let root = PathBuf::from(file);
let mut root = PathBuf::from(file);
if root.is_relative() {
root = std::env::current_dir().unwrap().join(root);
}
let mut builder = build::Builder::new(root.parent().unwrap(), cache);
if validate {
builder.enable_validate_mode();

View File

@ -557,6 +557,37 @@ make_fn!(
separated!(punct!(","), symbol)
);
fn module_expression(input: SliceIter<Token>) -> Result<SliceIter<Token>, Expression> {
let parsed = do_each!(input,
pos => pos,
_ => word!("module"),
_ => punct!("{"),
arglist => trace_nom!(optional!(field_list)),
_ => optional!(punct!(",")),
_ => punct!("}"),
_ => punct!("=>"),
_ => punct!("{"),
stmt_list => trace_nom!(repeat!(statement)),
_ => punct!("}"),
(pos, arglist, stmt_list)
);
match parsed {
Result::Abort(e) => Result::Abort(e),
Result::Fail(e) => Result::Fail(e),
Result::Incomplete(offset) => Result::Incomplete(offset),
Result::Complete(rest, (pos, arglist, stmt_list)) => {
let def = ModuleDef::new(arglist.unwrap_or_else(|| Vec::new()), stmt_list, pos);
//eprintln!(
// "module def at: {:?} arg_typle len {} stmts len {}",
// def.pos,
// def.arg_set.len(),
// def.statements.len()
//);
Result::Complete(rest, Expression::Module(def))
}
}
}
fn macro_expression(input: SliceIter<Token>) -> Result<SliceIter<Token>, Expression> {
let parsed = do_each!(input,
pos => pos,
@ -691,11 +722,11 @@ make_fn!(
fn call_expression(input: SliceIter<Token>) -> Result<SliceIter<Token>, Expression> {
let parsed = do_each!(input.clone(),
macroname => trace_nom!(selector_value),
callee_name => trace_nom!(selector_value),
_ => punct!("("),
args => optional!(separated!(punct!(","), trace_nom!(expression))),
_ => punct!(")"),
(macroname, args)
(callee_name, args)
);
match parsed {
Result::Abort(e) => Result::Abort(e),
@ -806,6 +837,7 @@ make_fn!(
alt_peek!(
either!(word!("map"), word!("filter")) => trace_nom!(list_op_expression) |
word!("macro") => trace_nom!(macro_expression) |
word!("module") => trace_nom!(module_expression) |
word!("select") => trace_nom!(select_expression) |
punct!("(") => trace_nom!(grouped_expression) |
trace_nom!(unprefixed_expression))
@ -846,7 +878,7 @@ make_fn!(
name => wrap_err!(match_type!(BAREWORD), "Expected name for binding"),
_ => punct!("="),
// TODO(jwall): Wrap this error with an appropriate abortable_parser::Error
val => with_err!(trace_nom!(expression), "Expected Expression"),
val => wrap_err!(trace_nom!(expression), "Expected Expression"),
_ => punct!(";"),
(tuple_to_let(name, val))
)
@ -904,7 +936,7 @@ make_fn!(
do_each!(
_ => word!("out"),
typ => wrap_err!(must!(match_type!(BAREWORD)), "Expected converter name"),
expr => with_err!(must!(expression), "Expected Expression to export"),
expr => wrap_err!(must!(expression), "Expected Expression to export"),
_ => must!(punct!(";")),
(Statement::Output(typ.clone(), expr.clone()))
)

View File

@ -928,6 +928,28 @@ fn test_select_parse() {
);
}
#[test]
fn test_module_expression_parsing() {
assert_fail!(module_expression("foo"));
assert_fail!(module_expression("module"));
assert_fail!(module_expression("module("));
assert_fail!(module_expression("module["));
assert_fail!(module_expression("module {"));
assert_fail!(module_expression("module {}"));
assert_fail!(module_expression("module {} =>"));
assert_fail!(module_expression("module {} => {"));
assert_parse!(
module_expression("module {} => {}"),
Expression::Module(ModuleDef {
pos: Position::new(1, 1, 0),
arg_set: Vec::new(),
arg_tuple: None,
statements: Vec::new(),
})
);
}
#[test]
fn test_macro_expression_parsing() {
assert_fail!(macro_expression("foo"));

View File

@ -268,6 +268,10 @@ make_fn!(macrotok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::BAREWORD, "macro", WS)
);
make_fn!(moduletok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::BAREWORD, "module", WS)
);
make_fn!(lettok<OffsetStrIter, Token>,
do_text_token_tok!(TokenType::BAREWORD, "let", WS)
);
@ -386,6 +390,7 @@ fn token<'a>(input: OffsetStrIter<'a>) -> Result<OffsetStrIter<'a>, Token> {
selecttok,
asserttok,
macrotok,
moduletok,
importtok,
astok,
maptok,