From afa4aebd4aa0635108aea1b771ce21551e628b76 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 2 Dec 2017 14:37:10 +0100 Subject: [PATCH] Add functions (#277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – Parse unary (no-argument) functions – Add functions for detecting the current os, arch, and os family, according to rustc's cfg attributes --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + GRAMMAR.md | 8 +++++-- README.asc | 28 ++++++++++++++++++++++-- src/assignment_evaluator.rs | 31 ++++++++++++++------------ src/assignment_resolver.rs | 12 +++++++++++ src/compilation_error.rs | 6 +++++- src/expression.rs | 42 ++++++++++++++++++++++++++++++++---- src/functions.rs | 33 ++++++++++++++++++++++++++++ src/lexer.rs | 16 +++++++++++++- src/main.rs | 2 ++ src/parser.rs | 36 +++++++++++++++++++++++++++++-- src/recipe_resolver.rs | 43 +++++++++++++++++++++++++++++-------- src/token.rs | 4 ++++ tests/integration.rs | 43 +++++++++++++++++++++++++++++++++++++ 15 files changed, 277 insertions(+), 35 deletions(-) create mode 100644 src/functions.rs diff --git a/Cargo.lock b/Cargo.lock index b8ad901..097b86b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,7 @@ dependencies = [ "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -199,6 +200,11 @@ name = "strsim" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "target" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "tempdir" version = "0.3.5" @@ -299,6 +305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum regex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ac6ab4e9218ade5b423358bbd2567d1617418403c7a512603630181813316322" "checksum regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad890a5eef7953f55427c50575c680c42841653abd2b028b68cd223d157f62db" "checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" +"checksum target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "10000465bb0cc031c87a44668991b284fd84c0e6bd945f62d4af04e9e52a222a" "checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" diff --git a/Cargo.toml b/Cargo.toml index 18879af..c5030a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ itertools = "0.7" lazy_static = "1.0.0" libc = "0.2.21" regex = "0.2.2" +target = "1.0.0" tempdir = "0.3.5" unicode-width = "0.1.3" diff --git a/GRAMMAR.md b/GRAMMAR.md index e84cb18..89ebe71 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -55,10 +55,14 @@ export : 'export' assignment expression : value '+' expression | value -value : STRING +value : NAME '(' arguments? ')' + | STRING | RAW_STRING - | NAME | BACKTICK + | NAME + +arguments : expression ',' arguments + | expression ','? recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body? diff --git a/README.asc b/README.asc index 61a7ac9..2c27e37 100644 --- a/README.asc +++ b/README.asc @@ -249,7 +249,31 @@ string! " ``` -=== Command Evaluation using Backticks +=== Functions + +Just provides a few built-in functions that might be useful when writing recipes. + +==== System Information + +- `arch()` – Instruction set architecture. Possible values are: `"aarch64"`, `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, and `"xcore"`. + +- `os()` – Operating system. Possible values are: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`. + +- `os_family()` - Operating system family; possible values are: `"unix"` and `"windows"`. + +For example: + +```make +system-info: + @echo "This is an {{arch()}} machine". +``` + +``` +$ just system-info +This is an x86_64 machine +``` + +=== Command Evaluation Using Backticks Backticks can be used to store the result of commands: @@ -390,7 +414,7 @@ search QUERY: lynx 'https://www.google.com/?q={{QUERY}}' ``` -=== Write Recipes in other Languages +=== Writing Recipes in Other Languages Recipes that start with a `#!` are executed as scripts, so you can write recipes in other languages: diff --git a/src/assignment_evaluator.rs b/src/assignment_evaluator.rs index 9183465..963f8a1 100644 --- a/src/assignment_evaluator.rs +++ b/src/assignment_evaluator.rs @@ -82,37 +82,40 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> { expression: &Expression<'a>, arguments: &Map<&str, Cow> ) -> RunResult<'a, String> { - Ok(match *expression { + match *expression { Expression::Variable{name, ..} => { if self.evaluated.contains_key(name) { - self.evaluated[name].clone() + Ok(self.evaluated[name].clone()) } else if self.scope.contains_key(name) { - self.scope[name].clone() + Ok(self.scope[name].clone()) } else if self.assignments.contains_key(name) { self.evaluate_assignment(name)?; - self.evaluated[name].clone() + Ok(self.evaluated[name].clone()) } else if arguments.contains_key(name) { - arguments[name].to_string() + Ok(arguments[name].to_string()) } else { - return Err(RuntimeError::Internal { + Err(RuntimeError::Internal { message: format!("attempted to evaluate undefined variable `{}`", name) - }); + }) } } - Expression::String{ref cooked_string} => cooked_string.cooked.clone(), + Expression::Call{name, ..} => ::functions::evaluate_function(name), + Expression::String{ref cooked_string} => Ok(cooked_string.cooked.clone()), Expression::Backtick{raw, ref token} => { if self.dry_run { - format!("`{}`", raw) + Ok(format!("`{}`", raw)) } else { - self.run_backtick(raw, token)? + Ok(self.run_backtick(raw, token)?) } } Expression::Concatination{ref lhs, ref rhs} => { - self.evaluate_expression(lhs, arguments)? - + - &self.evaluate_expression(rhs, arguments)? + Ok( + self.evaluate_expression(lhs, arguments)? + + + &self.evaluate_expression(rhs, arguments)? + ) } - }) + } } fn run_backtick( diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 72c0a76..44935c3 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -75,6 +75,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> { return Err(token.error(UndefinedVariable{variable: name})); } } + Expression::Call{ref token, ..} => ::functions::resolve_function(token)?, Expression::Concatination{ref lhs, ref rhs} => { self.resolve_expression(lhs)?; self.resolve_expression(rhs)?; @@ -129,4 +130,15 @@ mod test { width: Some(2), kind: UndefinedVariable{variable: "yy"}, } + + compilation_error_test! { + name: unknown_function, + input: "a = foo()", + index: 4, + line: 0, + column: 4, + width: Some(3), + kind: UnknownFunction{function: "foo"}, + } + } diff --git a/src/compilation_error.rs b/src/compilation_error.rs index 097c339..fc682e5 100644 --- a/src/compilation_error.rs +++ b/src/compilation_error.rs @@ -29,12 +29,13 @@ pub enum CompilationErrorKind<'a> { InvalidEscapeSequence{character: char}, MixedLeadingWhitespace{whitespace: &'a str}, OuterShebang, + ParameterFollowsVariadicParameter{parameter: &'a str}, ParameterShadowsVariable{parameter: &'a str}, RequiredParameterFollowsDefaultParameter{parameter: &'a str}, - ParameterFollowsVariadicParameter{parameter: &'a str}, UndefinedVariable{variable: &'a str}, UnexpectedToken{expected: Vec, found: TokenKind}, UnknownDependency{recipe: &'a str, unknown: &'a str}, + UnknownFunction{function: &'a str}, UnknownStartOfToken, UnterminatedString, } @@ -123,6 +124,9 @@ impl<'a> Display for CompilationError<'a> { UndefinedVariable{variable} => { writeln!(f, "Variable `{}` not defined", variable)?; } + UnknownFunction{function} => { + writeln!(f, "Call to unknown function `{}`", function)?; + } UnknownStartOfToken => { writeln!(f, "Unknown start of token:")?; } diff --git a/src/expression.rs b/src/expression.rs index 1172eb0..3af6bdd 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -2,10 +2,11 @@ use common::*; #[derive(PartialEq, Debug)] pub enum Expression<'a> { - Variable{name: &'a str, token: Token<'a>}, - String{cooked_string: CookedString<'a>}, Backtick{raw: &'a str, token: Token<'a>}, + Call{name: &'a str, token: Token<'a>}, Concatination{lhs: Box>, rhs: Box>}, + String{cooked_string: CookedString<'a>}, + Variable{name: &'a str, token: Token<'a>}, } impl<'a> Expression<'a> { @@ -14,12 +15,19 @@ impl<'a> Expression<'a> { stack: vec![self], } } + + pub fn functions(&'a self) -> Functions<'a> { + Functions { + stack: vec![self], + } + } } impl<'a> Display for Expression<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?, + Expression::Call {name, .. } => write!(f, "{}()", name)?, Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?, Expression::String {ref cooked_string} => write!(f, "\"{}\"", cooked_string.raw)?, Expression::Variable {name, .. } => write!(f, "{}", name)?, @@ -37,8 +45,34 @@ impl<'a> Iterator for Variables<'a> { fn next(&mut self) -> Option<&'a Token<'a>> { match self.stack.pop() { - None | Some(&Expression::String{..}) | Some(&Expression::Backtick{..}) => None, - Some(&Expression::Variable{ref token,..}) => Some(token), + None + | Some(&Expression::String{..}) + | Some(&Expression::Backtick{..}) + | Some(&Expression::Call{..}) => None, + Some(&Expression::Variable{ref token,..}) => Some(token), + Some(&Expression::Concatination{ref lhs, ref rhs}) => { + self.stack.push(lhs); + self.stack.push(rhs); + self.next() + } + } + } +} + +pub struct Functions<'a> { + stack: Vec<&'a Expression<'a>>, +} + +impl<'a> Iterator for Functions<'a> { + type Item = &'a Token<'a>; + + fn next(&mut self) -> Option<&'a Token<'a>> { + match self.stack.pop() { + None + | Some(&Expression::String{..}) + | Some(&Expression::Backtick{..}) + | Some(&Expression::Variable{..}) => None, + Some(&Expression::Call{ref token, ..}) => Some(token), Some(&Expression::Concatination{ref lhs, ref rhs}) => { self.stack.push(lhs); self.stack.push(rhs); diff --git a/src/functions.rs b/src/functions.rs new file mode 100644 index 0000000..fcbb2cc --- /dev/null +++ b/src/functions.rs @@ -0,0 +1,33 @@ +use common::*; +use target; + +pub fn resolve_function<'a>(token: &Token<'a>) -> CompilationResult<'a, ()> { + if !&["arch", "os", "os_family"].contains(&token.lexeme) { + Err(token.error(CompilationErrorKind::UnknownFunction{function: token.lexeme})) + } else { + Ok(()) + } +} + +pub fn evaluate_function<'a>(name: &'a str) -> RunResult<'a, String> { + match name { + "arch" => Ok(arch().to_string()), + "os" => Ok(os().to_string()), + "os_family" => Ok(os_family().to_string()), + _ => Err(RuntimeError::Internal { + message: format!("attempted to evaluate unknown function: `{}`", name) + }) + } +} + +pub fn arch() -> &'static str { + target::arch() +} + +pub fn os() -> &'static str { + target::os() +} + +pub fn os_family() -> &'static str { + target::os_family() +} diff --git a/src/lexer.rs b/src/lexer.rs index d5206f8..769d1c2 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -131,6 +131,8 @@ impl<'a> Lexer<'a> { lazy_static! { static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" ); static ref COLON: Regex = token(r":" ); + static ref PAREN_L: Regex = token(r"[(]" ); + static ref PAREN_R: Regex = token(r"[)]" ); static ref AT: Regex = token(r"@" ); static ref COMMENT: Regex = token(r"#([^!\n\r].*)?$" ); static ref EOF: Regex = token(r"(?-m)$" ); @@ -140,7 +142,7 @@ impl<'a> Lexer<'a> { static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" ); static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)" ); static ref PLUS: Regex = token(r"[+]" ); - static ref STRING: Regex = token("\"" ); + static ref STRING: Regex = token(r#"["]"# ); static ref RAW_STRING: Regex = token(r#"'[^']*'"# ); static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# ); static ref INTERPOLATION_START: Regex = re(r"^[{][{]" ); @@ -209,6 +211,10 @@ impl<'a> Lexer<'a> { (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon) } else if let Some(captures) = AT.captures(self.rest) { (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At) + } else if let Some(captures) = PAREN_L.captures(self.rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenL) + } else if let Some(captures) = PAREN_R.captures(self.rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenR) } else if let Some(captures) = PLUS.captures(self.rest) { (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus) } else if let Some(captures) = EQUALS.captures(self.rest) { @@ -338,6 +344,8 @@ mod test { InterpolationStart => "{", Line{..} => "^", Name => "N", + ParenL => "(", + ParenR => ")", Plus => "+", RawString => "'", StringToken => "\"", @@ -510,6 +518,12 @@ c: b "$N:N$>^_$$^_$^_$$^_$$^_<.", } + summary_test! { + tokenize_parens, + r"((())) )abc(+", + "((())))N(+.", + } + error_test! { name: tokenize_space_then_tab, input: "a: diff --git a/src/main.rs b/src/main.rs index 612a6eb..28f3aa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ extern crate edit_distance; extern crate itertools; extern crate libc; extern crate regex; +extern crate target; extern crate tempdir; extern crate unicode_width; @@ -23,6 +24,7 @@ mod configuration; mod cooked_string; mod expression; mod fragment; +mod functions; mod justfile; mod lexer; mod misc; diff --git a/src/parser.rs b/src/parser.rs index 9ef5c10..1e94aac 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -251,8 +251,20 @@ impl<'a> Parser<'a> { fn expression(&mut self, interpolation: bool) -> CompilationResult<'a, Expression<'a>> { let first = self.tokens.next().unwrap(); let lhs = match first.kind { - Name => Expression::Variable {name: first.lexeme, token: first}, - Backtick => Expression::Backtick { + Name => { + if self.peek(ParenL) { + if let Some(token) = self.expect(ParenL) { + return Err(self.unexpected_token(&token, &[ParenL])); + } + if let Some(token) = self.expect(ParenR) { + return Err(self.unexpected_token(&token, &[ParenR])); + } + Expression::Call {name: first.lexeme, token: first} + } else { + Expression::Variable {name: first.lexeme, token: first} + } + } + Backtick => Expression::Backtick { raw: &first.lexeme[1..first.lexeme.len()-1], token: first }, @@ -764,6 +776,26 @@ c = a + b + a + b", kind: UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent}, } + compilation_error_test! { + name: unclosed_parenthesis_in_expression, + input: "x = foo(", + index: 8, + line: 0, + column: 8, + width: Some(0), + kind: UnexpectedToken{expected: vec![ParenR], found: Eof}, + } + + compilation_error_test! { + name: unclosed_parenthesis_in_interpolation, + input: "a:\n echo {{foo(}}", + index: 15, + line: 1, + column: 12, + width: Some(2), + kind: UnexpectedToken{expected: vec![ParenR], found: InterpolationEnd}, + } + compilation_error_test! { name: plus_following_parameter, input: "a b c+:", diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs index 5973833..5c21db2 100644 --- a/src/recipe_resolver.rs +++ b/src/recipe_resolver.rs @@ -27,24 +27,39 @@ impl<'a, 'b> RecipeResolver<'a, 'b> { resolver.seen = empty(); } + // There are borrow issues here that seems too difficult to solve. + // The errors derived from the variable token has too short a lifetime, + // so we create a new error from its contents, which do live long + // enough. + // + // I suspect the solution here is to give recipes, pieces, and expressions + // two lifetime parameters instead of one, with one being the lifetime + // of the struct, and the second being the lifetime of the tokens + // that it contains. + for recipe in recipes.values() { for line in &recipe.lines { for fragment in line { if let Fragment::Expression{ref expression, ..} = *fragment { + for function in expression.functions() { + if let Err(error) = ::functions::resolve_function(function) { + return Err(CompilationError { + text: text, + index: error.index, + line: error.line, + column: error.column, + width: error.width, + kind: UnknownFunction { + function: &text[error.index..error.index + error.width.unwrap()], + } + }); + } + } for variable in expression.variables() { let name = variable.lexeme; let undefined = !assignments.contains_key(name) && !recipe.parameters.iter().any(|p| p.name == name); if undefined { - // There's a borrow issue here that seems too difficult to solve. - // The error derived from the variable token has too short a lifetime, - // so we create a new error from its contents, which do live long - // enough. - // - // I suspect the solution here is to give recipes, pieces, and expressions - // two lifetime parameters instead of one, with one being the lifetime - // of the struct, and the second being the lifetime of the tokens - // that it contains let error = variable.error(UndefinedVariable{variable: name}); return Err(CompilationError { text: text, @@ -152,4 +167,14 @@ mod test { width: Some(3), kind: UndefinedVariable{variable: "lol"}, } + + compilation_error_test! { + name: unknown_function_in_interpolation, + input: "a:\n echo {{bar()}}", + index: 11, + line: 1, + column: 8, + width: Some(3), + kind: UnknownFunction{function: "bar"}, + } } diff --git a/src/token.rs b/src/token.rs index 3f713dd..861006e 100644 --- a/src/token.rs +++ b/src/token.rs @@ -39,6 +39,8 @@ pub enum TokenKind { InterpolationStart, Line, Name, + ParenL, + ParenR, Plus, RawString, StringToken, @@ -63,6 +65,8 @@ impl Display for TokenKind { Name => "name", Plus => "'+'", At => "'@'", + ParenL => "'('", + ParenR => "')'", StringToken => "string", RawString => "raw string", Text => "command text", diff --git a/tests/integration.rs b/tests/integration.rs index f6a683d..0bbcb7a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,6 +1,7 @@ extern crate brev; extern crate executable_path; extern crate libc; +extern crate target; extern crate tempdir; use executable_path::executable_path; @@ -1174,6 +1175,35 @@ foo: status: EXIT_SUCCESS, } +integration_test! { + name: test_os_arch_functions_in_interpolation, + justfile: r#" +foo: + echo {{arch()}} {{os()}} {{os_family()}} +"#, + args: (), + stdout: format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(), + stderr: format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(), + status: EXIT_SUCCESS, +} + +integration_test! { + name: test_os_arch_functions_in_expression, + justfile: r#" +a = arch() +o = os() +f = os_family() + +foo: + echo {{a}} {{o}} {{f}} +"#, + args: (), + stdout: format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(), + stderr: format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(), + status: EXIT_SUCCESS, +} + + integration_test! { name: quiet_recipe, justfile: r#" @@ -1260,6 +1290,19 @@ integration_test! { status: EXIT_FAILURE, } +integration_test! { + name: unknown_function_in_assignment, + justfile: r#"foo = foo() + "hello" +bar:"#, + args: ("bar"), + stdout: "", + stderr: r#"error: Call to unknown function `foo` + | +1 | foo = foo() + "hello" + | ^^^ +"#, + status: EXIT_FAILURE, +} integration_test! { name: dependency_takes_arguments,