Skip to content

Commit

Permalink
Add functions (#277)
Browse files Browse the repository at this point in the history
– Parse unary (no-argument) functions
– Add functions for detecting the current os, arch, and os family, according to rustc's cfg attributes
  • Loading branch information
casey authored Dec 2, 2017
1 parent 66391de commit afa4aeb
Show file tree
Hide file tree
Showing 15 changed files with 277 additions and 35 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
8 changes: 6 additions & 2 deletions GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
28 changes: 26 additions & 2 deletions README.asc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:

Expand Down
31 changes: 17 additions & 14 deletions src/assignment_evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,37 +82,40 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
expression: &Expression<'a>,
arguments: &Map<&str, Cow<str>>
) -> 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(
Expand Down
12 changes: 12 additions & 0 deletions src/assignment_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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"},
}

}
6 changes: 5 additions & 1 deletion src/compilation_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenKind>, found: TokenKind},
UnknownDependency{recipe: &'a str, unknown: &'a str},
UnknownFunction{function: &'a str},
UnknownStartOfToken,
UnterminatedString,
}
Expand Down Expand Up @@ -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:")?;
}
Expand Down
42 changes: 38 additions & 4 deletions src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expression<'a>>, rhs: Box<Expression<'a>>},
String{cooked_string: CookedString<'a>},
Variable{name: &'a str, token: Token<'a>},
}

impl<'a> Expression<'a> {
Expand All @@ -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)?,
Expand All @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions src/functions.rs
Original file line number Diff line number Diff line change
@@ -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()
}
16 changes: 15 additions & 1 deletion src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)$" );
Expand All @@ -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"^[{][{]" );
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -338,6 +344,8 @@ mod test {
InterpolationStart => "{",
Line{..} => "^",
Name => "N",
ParenL => "(",
ParenR => ")",
Plus => "+",
RawString => "'",
StringToken => "\"",
Expand Down Expand Up @@ -510,6 +518,12 @@ c: b
"$N:N$>^_$$<N:$>^_$^_$$<N:N$>^_$$<N:N$>^_<.",
}

summary_test! {
tokenize_parens,
r"((())) )abc(+",
"((())))N(+.",
}

error_test! {
name: tokenize_space_then_tab,
input: "a:
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -23,6 +24,7 @@ mod configuration;
mod cooked_string;
mod expression;
mod fragment;
mod functions;
mod justfile;
mod lexer;
mod misc;
Expand Down
Loading

0 comments on commit afa4aeb

Please sign in to comment.