├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── crates ├── librush │ ├── Cargo.toml │ ├── src │ │ ├── eval │ │ │ ├── api │ │ │ │ ├── base.rs │ │ │ │ ├── conv.rs │ │ │ │ ├── functools.rs │ │ │ │ ├── itertools.rs │ │ │ │ ├── math.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── random.rs │ │ │ │ └── strings │ │ │ │ │ ├── frag.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── subst.rs │ │ │ ├── atoms.rs │ │ │ ├── mod.rs │ │ │ ├── model │ │ │ │ ├── arity.rs │ │ │ │ ├── context │ │ │ │ │ ├── defines.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── error.rs │ │ │ │ ├── function.rs │ │ │ │ ├── mod.rs │ │ │ │ └── value │ │ │ │ │ ├── cmp.rs │ │ │ │ │ ├── conv.rs │ │ │ │ │ ├── json.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── output.rs │ │ │ │ │ └── types.rs │ │ │ ├── operators │ │ │ │ ├── binary.rs │ │ │ │ ├── conditional.rs │ │ │ │ ├── curried.rs │ │ │ │ ├── mod.rs │ │ │ │ └── unary.rs │ │ │ ├── trailers.rs │ │ │ └── util │ │ │ │ ├── cmp.rs │ │ │ │ ├── fmt.rs │ │ │ │ ├── macros.rs │ │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── parse │ │ │ ├── ast.rs │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ └── syntax │ │ │ │ ├── literals.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── ops.rs │ │ │ │ ├── structure.rs │ │ │ │ └── util.rs │ │ └── wrappers.rs │ └── tests │ │ ├── test.rs │ │ ├── tests │ │ ├── api │ │ │ ├── base.rs │ │ │ ├── conv.rs │ │ │ ├── mod.rs │ │ │ └── strings.rs │ │ ├── constants.rs │ │ ├── mod.rs │ │ ├── operators │ │ │ ├── binary │ │ │ │ ├── arith.rs │ │ │ │ ├── cmp.rs │ │ │ │ └── mod.rs │ │ │ ├── curried.rs │ │ │ ├── mod.rs │ │ │ └── unary.rs │ │ └── trailers.rs │ │ └── util │ │ ├── asserts.rs │ │ ├── literals.rs │ │ └── mod.rs └── rush │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ ├── args.rs │ ├── logging.rs │ ├── main.rs │ └── rcfile.rs ├── docs ├── .docsignore ├── api.md ├── index.md └── partials │ ├── function.md.jinja │ └── module.md.jinja ├── mkdocs.yml ├── requirements-dev.txt ├── setup.cfg ├── syntax.md ├── tasks ├── __init__.py ├── build.py ├── clean.py ├── lint.py ├── misc.py ├── release.py ├── run.py ├── test.py └── util │ ├── __init__.py │ └── docs.py └── tools └── release /.gitignore: -------------------------------------------------------------------------------- 1 | crates/librush/Cargo.lock 2 | release/ 3 | target/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - nightly 5 | - beta 6 | - stable 7 | os: 8 | - linux 9 | - osx 10 | 11 | # Test on nightly Rust, but failures there won't break the build. 12 | matrix: 13 | allow_failures: 14 | - rust: nightly 15 | 16 | # Run the test for just the binary crate. as it will build & test everything. 17 | # TODO: make Travis run Invoke tasks instead 18 | script: cargo test --no-fail-fast --manifest-path ./crates/rush/Cargo.toml 19 | sudo: false 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rush 2 | 3 | _Warning:_: Work in progress. 4 | 5 | [![Build Status](https://img.shields.io/travis/Xion/rush.svg)](https://travis-ci.org/Xion/rush) 6 | [![License](https://img.shields.io/github/license/Xion/rush.svg)](https://github.com/Xion/rush/blob/master/LICENSE) 7 | 8 | Succinct & readable processing language for a Unix shell. Written in Rust. 9 | 10 | $ echo "Hello " | rh '_ + world' 11 | Hello world 12 | 13 | ## Requirements 14 | 15 | Any Unix-like system should do. 16 | 17 | ## Usage 18 | 19 | rh [--input | --string | --lines | --words | --chars | --bytes | --files] 20 | [--before ] 21 | [--after ] 22 | [ ...] 23 | 24 | OPTIONS: 25 | -i, --input 26 | Defines how the input should be treated when processed by EXPRESSION [values: string, lines, words, chars, bytes, files] 27 | -s, --string Apply the expression once to the whole input as single string 28 | -l, --lines Apply the expression to each line of input as string. This is the default 29 | -w, --words Apply the expression to each word in the input as string. 30 | -c, --chars Apply the expression to each character of input (treated as 1-character string). 31 | -b, --bytes Apply the expression to input bytes. The expression must take byte value as integer and return integer output. 32 | -f, --files Apply the expression to the content of each file (as string) whose path is given as a line of input 33 | -B, --before 34 | Optional expression to evaluate before processing the input. The result of this expression is discarded but any side effects (assignments) will persist. 35 | -A, --after 36 | Optional expression to evaluate after processing the input. If provided, only the result of this expression will be printed to standard output. 37 | 38 | ARGS: 39 | ... 40 | Expression(s) to apply to input. When multiple expressions are given, the result of one is passed as input to the next one. 41 | 42 | ## Examples 43 | 44 | ### Strings 45 | 46 | $ echo 'Alice has a cat' | rh 'before("cat")' '_ + "dog"' 47 | Alice has a dog 48 | $ echo 'Alice has a cat' | rh 'after("Alice")' '"Bob" + _' 49 | Bob has a cat 50 | 51 | # ROT13 52 | 53 | $ echo -n 'flap' | rh -c 'ord' '(_ - ord(a) + 13) % 26' '_ + ord(a)' chr | rh -s 'sub(/\s+/, "")' 54 | sync 55 | $ echo -n 'flap' | rh -s rot13 56 | sync 57 | 58 | ### CSV 59 | 60 | $ echo '1,2,3' | rh 'csv & map(int & (*2)) & csv' 61 | 2,4,6 62 | 63 | $ rh 'csv' '{number: _[0], symbol: _[1], name: _[2], mass: _[3]}' <./elements.csv 64 | {"mass":"1","name":"Hydrogen","number":"1","symbol":"H"} 65 | {"mass":"4","name":"Helium","number":"2","symbol":"He"} 66 | # etc. 67 | 68 | ## Contributing 69 | 70 | You need a Rust toolchain (with Cargo) to build _rush_ itself. 71 | 72 | Additionally, the Python-based [Invoke](http://pyinvoke.org) task runner is used for automation. 73 | It is recommended you install it inside a Python virtualenv. e.g.: 74 | 75 | $ virtualenv ~/venv/rush && source ~/venv/rush/bin/activate 76 | $ pip install -r -requirements-dev.txt 77 | 78 | Then you can use: 79 | 80 | $ inv 81 | 82 | to build the binary & the library crate, and run their tests. 83 | 84 | ## License 85 | 86 | _rush_ is licensed under the [GNU GPL v3](https://github.com/Xion/rush/blob/master/LICENSE) license. 87 | 88 | Copyright © 2016, Karol Kuczmarski 89 | -------------------------------------------------------------------------------- /crates/librush/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rush" 3 | version = "0.1.0" 4 | authors = ["Karol Kuczmarski "] 5 | license = "GPL-3.0" 6 | repository = "https://github.com/Xion/rush" 7 | 8 | [dependencies] 9 | conv = "0.3.1" 10 | csv = "0.14.4" 11 | fnv = "1.0.2" 12 | lazy_static = "*" 13 | log = "0.3" 14 | mopa = "0.2.1" 15 | nom = { version = "1.2.4", features = ["regexp"] } 16 | rand = "0.3" 17 | regex = "0.1" 18 | rustc-serialize = "0.3" 19 | unicode_categories = "0.1.0" 20 | unicode-normalization = "0.1.2" 21 | unicode-segmentation = "0.1.0" 22 | unidecode = "0.2.0" 23 | 24 | [dev-dependencies] 25 | maplit = "0.1.3" 26 | 27 | 28 | [lib] 29 | doctest = false 30 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/conv.rs: -------------------------------------------------------------------------------- 1 | //! Conversion functions. 2 | 3 | use std::io::Write; 4 | 5 | use csv; 6 | use regex; 7 | use rustc_serialize::json::{Json, ToJson}; 8 | 9 | use eval::{self, Error, Value}; 10 | use eval::value::{ArrayRepr, BooleanRepr, IntegerRepr, FloatRepr, RegexRepr, StringRepr}; 11 | 12 | 13 | // Basic data types conversions 14 | 15 | /// Convert a value to an array. 16 | pub fn array(value: Value) -> eval::Result { 17 | if value.is_array() { 18 | return Ok(value); 19 | } 20 | if value.is_string() { 21 | return super::strings::chars(value); 22 | } 23 | if value.is_object() { 24 | return super::base::keys(value); 25 | } 26 | Err(Error::new(&format!("cannot convert {} to array", value.typename()))) 27 | } 28 | 29 | /// Convert a value to a boolean, based on its "truthy" value. 30 | /// 31 | /// NOTE: This conversion is used in numerous places to coerce values 32 | /// to boolean where necessary. Users include: 33 | /// * logical (!, &&, ||) and conditional (:?) operators 34 | /// * API functions such as compact() 35 | pub fn bool(value: Value) -> eval::Result { 36 | match value { 37 | Value::Boolean(_) => Ok(value), 38 | Value::Integer(i) => Ok(Value::Boolean(i != 0)), 39 | Value::Float(f) => Ok(Value::Boolean(f != 0.0)), 40 | Value::String(ref s) => s.parse::() 41 | .map_err(|_| Error::new(&format!("invalid bool value: {}", s))) 42 | .map(Value::Boolean), 43 | Value::Array(ref a) => Ok(Value::Boolean(!a.is_empty())), 44 | Value::Object(ref o) => Ok(Value::Boolean(!o.is_empty())), 45 | _ => Err(Error::new( 46 | &format!("cannot convert {} to bool", value.typename()) 47 | )), 48 | } 49 | } 50 | 51 | /// Convert a value to an integer. 52 | pub fn int(value: Value) -> eval::Result { 53 | match value { 54 | Value::Boolean(b) => Ok(Value::Integer(if b { 1 } else { 0 })), 55 | Value::Integer(_) => Ok(value), 56 | Value::Float(f) => Ok(Value::Integer(f as IntegerRepr)), 57 | Value::String(ref s) => s.parse::() 58 | .map_err(|_| Error::new(&format!("invalid integer value: {}", s))) 59 | .map(Value::Integer), 60 | _ => Err(Error::new( 61 | &format!("cannot convert {} to int", value.typename()) 62 | )), 63 | } 64 | } 65 | 66 | /// Convert a value to a float. 67 | pub fn float(value: Value) -> eval::Result { 68 | match value { 69 | Value::Boolean(b) => Ok(Value::Float(if b { 1.0 } else { 0.0 })), 70 | Value::Integer(i) => Ok(Value::Float(i as FloatRepr)), 71 | Value::Float(_) => Ok(value), 72 | Value::String(ref s) => s.parse::() 73 | .map_err(|_| Error::new(&format!("invalid float value: {}", s))) 74 | .map(Value::Float), 75 | _ => Err(Error::new( 76 | &format!("cannot convert {} to float", value.typename()) 77 | )), 78 | } 79 | } 80 | 81 | /// Convert a value to string. 82 | pub fn str_(value: Value) -> eval::Result { 83 | match value { 84 | Value::Boolean(b) => Ok(Value::String(( 85 | if b { "true" } else { "false" } 86 | ).to_owned())), 87 | Value::Integer(i) => Ok(Value::String(i.to_string())), 88 | Value::Float(f) => Ok(Value::String(f.to_string())), 89 | Value::String(_) => Ok(value), 90 | Value::Regex(ref r) => Ok(Value::String(r.as_str().to_owned())), 91 | _ => Err(Error::new( 92 | &format!("cannot convert {} to string", value.typename()) 93 | )), 94 | } 95 | } 96 | 97 | /// Convert a value to a regular expression. 98 | /// If not a string, the value will be stringified first. 99 | pub fn regex(value: Value) -> eval::Result { 100 | if value.is_regex() { 101 | return Ok(value); 102 | } 103 | 104 | // handle strings separately because we don't want to regex-escape them 105 | if value.is_string() { 106 | let value = value.unwrap_string(); 107 | return RegexRepr::new(&value) 108 | .map(Value::Regex) 109 | .map_err(|e| Error::new(&format!( 110 | "invalid regular expression: {}", e))); 111 | } 112 | 113 | let value_type = value.typename(); 114 | str_(value) 115 | .map(|v| regex::quote(&v.unwrap_string())) 116 | .and_then(|s| RegexRepr::new(&s).map_err(|e| { 117 | Error::new(&format!("cannot compile regular expression: {}", e)) 118 | })) 119 | .map(Value::Regex) 120 | .map_err(|e| Error::new(&format!( 121 | "cannot convert {} to regular expression: {}", value_type, e 122 | ))) 123 | } 124 | 125 | 126 | // Serialization to and from various formats 127 | 128 | /// Converts a value to or from CSV: 129 | /// * string input is converted from CSV into an array (of arrays) of strings 130 | /// * array input is converted to CSV string 131 | pub fn csv(value: Value) -> eval::Result { 132 | eval1!((value: &String) -> Array {{ 133 | let mut reader = csv::Reader::from_string(value as &str) 134 | .flexible(true) // allow rows to have variable number of fields 135 | .has_headers(false) 136 | .record_terminator(csv::RecordTerminator::CRLF); 137 | 138 | // if we have been given a single line of CSV without the terminating 139 | // newline, return it as a single row 140 | // TODO(xion): cross-platform line ending detection 141 | if value.find("\n").is_none() { 142 | let record = reader.records().next().unwrap(); 143 | let row = record.unwrap(); 144 | row.into_iter().map(Value::String).collect() 145 | } else { 146 | // otherwise, return the parsed CSV as array of array of strings 147 | let mut result: Vec = Vec::new(); 148 | for row in reader.records() { 149 | result.push(Value::Array( 150 | row.unwrap().into_iter().map(Value::String).collect() 151 | )); 152 | } 153 | result 154 | } 155 | }}); 156 | 157 | eval1!((value: &Array) -> String {{ 158 | let mut writer = csv::Writer::from_memory() 159 | .flexible(true) // allow rows to have variable number of fields 160 | .record_terminator(csv::RecordTerminator::CRLF); 161 | 162 | // if we have been given an array of just scalar values, 163 | // write it as a single CSV row 164 | let one_row = is_flat_array(&value); 165 | if one_row { 166 | try!(write_row(&mut writer, value.clone())); 167 | } else { 168 | // otherwise, treat each subarray as a row of elements to write 169 | for row in value { 170 | if !row.is_array() { 171 | return Err(eval::Error::new(&format!( 172 | "expected a CSV row to be an array, got {}", 173 | row.typename() 174 | ))); 175 | } 176 | let row = row.clone().unwrap_array(); 177 | try!(ensure_flat_array(&row)); 178 | try!(write_row(&mut writer, row)); 179 | } 180 | } 181 | 182 | let mut result = writer.into_string(); 183 | if one_row { 184 | result.pop(); // remove trailing newline character 185 | } 186 | result 187 | }}); 188 | fn is_flat_array(array: &ArrayRepr) -> bool { 189 | array.iter().all(Value::is_scalar) 190 | } 191 | fn ensure_flat_array(array: &ArrayRepr) -> Result<(), eval::Error> { 192 | if !is_flat_array(array) { 193 | return Err(eval::Error::new( 194 | "array passed to csv() cannot contain any more nested arrays" 195 | )); 196 | } 197 | Ok(()) 198 | } 199 | fn write_row(writer: &mut csv::Writer, row: ArrayRepr) -> Result<(), eval::Error> { 200 | let mut output: Vec = Vec::new(); 201 | for item in row.into_iter() { 202 | output.push(try!(str_(item)).unwrap_string()); 203 | } 204 | writer.write(output.into_iter()) 205 | .map_err(|_| eval::Error::new("error writing CSV output")) 206 | } 207 | 208 | Err(Error::new( 209 | &format!("csv() expects string or array, got {}", value.typename()) 210 | )) 211 | } 212 | 213 | /// Converts a value to or from JSON: 214 | /// * an array or object input is converted to JSON string 215 | /// * a string input is parsed as JSON 216 | pub fn json(value: Value) -> eval::Result { 217 | if let Value::String(ref json_string) = value { 218 | let json_obj = try!(Json::from_str(json_string) 219 | .map_err(|e| Error::new(&format!("invalid JSON string: {}", e)))); 220 | return Ok(Value::from(json_obj)); 221 | } 222 | 223 | eval1!((value: &Array) -> String { value.to_json().to_string() }); 224 | eval1!((value: &Object) -> String { value.to_json().to_string() }); 225 | 226 | Err(Error::new(&format!( 227 | "json() expects a JSON string, an object or array, got {}", value.typename() 228 | ))) 229 | } 230 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/functools.rs: -------------------------------------------------------------------------------- 1 | //! API related to functions and "functional" programming. 2 | 3 | use eval::{self, Context, Error, Function, Value}; 4 | use eval::model::{Args, ArgCount, Invoke}; 5 | use super::conv::bool; 6 | 7 | 8 | /// Identity function. 9 | pub fn identity(value: Value) -> eval::Result { 10 | Ok(value) 11 | } 12 | 13 | /// Create a function that invokes given one with arguments reversed. 14 | pub fn flip(value: Value) -> eval::Result { 15 | if value.is_function() { 16 | let func = value.unwrap_function(); 17 | let arity = func.arity(); 18 | let flipped = move |mut args: Args, context: &Context| { 19 | args.reverse(); 20 | func.invoke(args, context) 21 | }; 22 | return Ok(Value::Function(Function::from_native_ctx(arity, flipped))); 23 | } 24 | mismatch!("flip"; ("function") => (value)) 25 | } 26 | 27 | 28 | /// Map a function over an array. 29 | /// Returns the array created by applying the function to each element. 30 | pub fn map(func: Value, array: Value, context: &Context) -> eval::Result { 31 | let array_type = array.typename(); 32 | 33 | eval2!((func: &Function, array: Array) -> Array {{ 34 | try!(ensure_argcount(&func, 1, "map")); 35 | 36 | let mut result = Vec::new(); 37 | for item in array.into_iter() { 38 | let context = Context::with_parent(&context); 39 | let mapped = try!(func.invoke1(item, &context)); 40 | result.push(mapped); 41 | } 42 | result 43 | }}); 44 | 45 | Err(Error::new(&format!( 46 | "map() requires a function and an array, got {} and {}", 47 | func.typename(), array_type 48 | ))) 49 | } 50 | 51 | /// Filter an array through a predicate function. 52 | /// This is the opposite of reject(). 53 | /// 54 | /// Returns the array created by applying the function to each element 55 | /// and preserving only those for it returned a truthy value. 56 | pub fn filter(func: Value, array: Value, context: &Context) -> eval::Result { 57 | let array_type = array.typename(); 58 | 59 | eval2!((func: &Function, array: Array) -> Array {{ 60 | try!(ensure_argcount(&func, 1, "filter")); 61 | 62 | let mut result = Vec::new(); 63 | for item in array.into_iter() { 64 | let context = Context::with_parent(context); 65 | let keep = try!( 66 | func.invoke1(item.clone(), &context).and_then(bool) 67 | ).unwrap_bool(); 68 | if keep { 69 | result.push(item); 70 | } 71 | } 72 | result 73 | }}); 74 | 75 | Err(Error::new(&format!( 76 | "filter() requires a function and an array, got {} and {}", 77 | func.typename(), array_type 78 | ))) 79 | } 80 | 81 | /// Reject array elements that do not satisfy a predicate. 82 | /// This the opposite of filter(). 83 | /// 84 | /// Returns the array created by applying the function to each element 85 | /// and preserving only those for it returned a falsy value. 86 | pub fn reject(func: Value, array: Value, context: &Context) -> eval::Result { 87 | let array_type = array.typename(); 88 | 89 | eval2!((func: &Function, array: Array) -> Array {{ 90 | try!(ensure_argcount(&func, 1, "reject")); 91 | 92 | let mut result = Vec::new(); 93 | for item in array.into_iter() { 94 | let context = Context::with_parent(context); 95 | let discard = try!( 96 | func.invoke1(item.clone(), &context).and_then(bool) 97 | ).unwrap_bool(); 98 | if !discard { 99 | result.push(item); 100 | } 101 | } 102 | result 103 | }}); 104 | 105 | Err(Error::new(&format!( 106 | "reject() requires a function and an array, got {} and {}", 107 | func.typename(), array_type 108 | ))) 109 | } 110 | 111 | /// Apply a binary function cumulatively to array elements. 112 | /// Also known as the "fold" operation (left fold, to be precise). 113 | pub fn reduce(func: Value, array: Value, start: Value, context: &Context) -> eval::Result { 114 | let func_type = func.typename(); 115 | let array_type = array.typename(); 116 | 117 | if let (Value::Function(func), Value::Array(array)) = (func, array) { 118 | try!(ensure_argcount(&func, 2, "reduce")); 119 | 120 | let mut result = start; 121 | for item in array.into_iter() { 122 | let context = Context::with_parent(context); 123 | result = try!(func.invoke2(result, item, &context)); 124 | } 125 | return Ok(result); 126 | } 127 | 128 | Err(Error::new(&format!( 129 | "reduce() requires a function and an array, got {} and {}", 130 | func_type, array_type 131 | ))) 132 | } 133 | 134 | 135 | // Utility functions 136 | 137 | #[inline] 138 | fn ensure_argcount(func: &Function, argcount: ArgCount, api_call: &str) -> Result<(), Error> { 139 | let arity = func.arity(); 140 | if !arity.accepts(argcount) { 141 | return Err(Error::new(&format!( 142 | "{}() requires a {}-argument function, got one with {} arguments", 143 | api_call, argcount, arity 144 | ))); 145 | } 146 | Ok(()) 147 | } 148 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/itertools.rs: -------------------------------------------------------------------------------- 1 | //! Iteration-related API functions. 2 | 3 | use eval::{self, Context, Error, Value}; 4 | use parse::ast::BinaryOpNode; 5 | use super::conv::bool; 6 | 7 | 8 | /// Returns true if all elements of an array are truthy (as per bool() functions). 9 | /// Note that if the array is empty, it also returns true. 10 | pub fn all(value: Value) -> eval::Result { 11 | let value_type = value.typename(); 12 | 13 | eval1!((value: Array) -> Boolean {{ 14 | let mut result = true; 15 | for elem in value.into_iter() { 16 | let truthy = try!(bool(elem)).unwrap_bool(); 17 | if !truthy { 18 | result = false; 19 | break; 20 | } 21 | } 22 | result 23 | }}); 24 | 25 | Err(Error::new(&format!("all() requires an array, got {}", value_type))) 26 | } 27 | 28 | /// Returns true if at least one element of the array is truthy 29 | /// (as per the bool() function). 30 | pub fn any(value: Value) -> eval::Result { 31 | let value_type = value.typename(); 32 | 33 | eval1!((value: Array) -> Boolean {{ 34 | let mut result = false; 35 | for elem in value.into_iter() { 36 | let truthy = try!(bool(elem)).unwrap_bool(); 37 | if truthy { 38 | result = true; 39 | break; 40 | } 41 | } 42 | result 43 | }}); 44 | 45 | Err(Error::new(&format!("any() requires an array, got {}", value_type))) 46 | } 47 | 48 | 49 | // TODO(xion): make min(), max() and sum() accept arbitrary number of scalars 50 | 51 | /// Find a minimum value in the array. Returns nil for empty arrays. 52 | pub fn min(value: Value, context: &Context) -> eval::Result { 53 | let value_type = value.typename(); 54 | 55 | if let Value::Array(array) = value { 56 | if array.is_empty() { 57 | return Ok(Value::Empty); 58 | } 59 | 60 | let mut items = array.into_iter(); 61 | let mut result = items.next().unwrap(); 62 | for item in items { 63 | let is_less = try!( 64 | BinaryOpNode::eval_op("<", item.clone(), result.clone(), context) 65 | ); 66 | if is_less.unwrap_bool() { 67 | result = item; 68 | } 69 | } 70 | return Ok(result); 71 | } 72 | 73 | Err(Error::new(&format!("min() requires an array, got {}", value_type))) 74 | } 75 | 76 | /// Find a maximum value in the array. Returns nil for empty arrays. 77 | pub fn max(value: Value, context: &Context) -> eval::Result { 78 | let value_type = value.typename(); 79 | 80 | if let Value::Array(array) = value { 81 | if array.is_empty() { 82 | return Ok(Value::Empty); 83 | } 84 | 85 | let mut items = array.into_iter(); 86 | let mut result = items.next().unwrap(); 87 | for item in items { 88 | let is_greater = try!( 89 | BinaryOpNode::eval_op(">", item.clone(), result.clone(), context) 90 | ); 91 | if is_greater.unwrap_bool() { 92 | result = item; 93 | } 94 | } 95 | return Ok(result); 96 | } 97 | 98 | Err(Error::new(&format!("max() requires an array, got {}", value_type))) 99 | } 100 | 101 | /// Return a sum of all elements in an array. 102 | pub fn sum(value: Value, context: &Context) -> eval::Result { 103 | let value_type = value.typename(); 104 | 105 | if let Value::Array(array) = value { 106 | if array.is_empty() { 107 | return Ok(Value::Empty); 108 | } 109 | 110 | let mut items = array.into_iter(); 111 | let mut result = items.next().unwrap(); 112 | for item in items { 113 | result = try!(BinaryOpNode::eval_op("+", result, item, context)); 114 | } 115 | return Ok(result); 116 | } 117 | 118 | Err(Error::new(&format!("sum() requires an array, got {}", value_type))) 119 | } 120 | 121 | 122 | /// Returns the array with all falsy values removed. 123 | /// This is determined via the bool() conversion. 124 | pub fn compact(array: Value) -> eval::Result { 125 | let array_type = array.typename(); 126 | 127 | eval1!(array : Array {{ 128 | let mut result = Vec::new(); 129 | for item in array.into_iter() { 130 | let keep = try!(bool(item.clone())).unwrap_bool(); 131 | if keep { 132 | result.push(item); 133 | } 134 | } 135 | result 136 | }}); 137 | 138 | Err(Error::new(&format!( 139 | "compact() requires an array, got {}", array_type 140 | ))) 141 | } 142 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/math.rs: -------------------------------------------------------------------------------- 1 | //! Math functions. 2 | 3 | use std::fmt::Display; 4 | 5 | use eval::{self, Error, Value}; 6 | use eval::value::FloatRepr; 7 | 8 | 9 | /// Compute the absolute value of a number. 10 | pub fn abs(value: Value) -> eval::Result { 11 | eval1!(value : Integer { value.abs() }); 12 | eval1!(value : Float { value.abs() }); 13 | Err(Error::new(&format!( 14 | "abs() requires a number, got {}", value.typename() 15 | ))) 16 | } 17 | 18 | /// Compute the signum function. 19 | pub fn sgn(value : Value) -> eval::Result { 20 | eval1!(value : Integer { value.signum() }); 21 | eval1!(value : Float { value.signum() }); 22 | Err(Error::new(&format!( 23 | "sgn() requires a number, got {}", value.typename() 24 | ))) 25 | } 26 | 27 | /// Compute a square root of a number. 28 | pub fn sqrt(value : Value) -> eval::Result { 29 | fn ensure_nonnegative(x : T) -> Result 30 | where T: Default + Display + PartialOrd 31 | { 32 | // TODO(xion): use the Zero trait instead of Default 33 | // when it's available in stable Rust 34 | if x >= T::default() { 35 | Ok(x) 36 | } else { 37 | Err(Error::new(&format!( 38 | "sqrt() requires a non-negative number, got {}", x 39 | ))) 40 | } 41 | } 42 | 43 | eval1!((value: Integer) -> Float { 44 | (try!(ensure_nonnegative(value)) as FloatRepr).sqrt() 45 | }); 46 | eval1!(value : Float { 47 | try!(ensure_nonnegative(value)).sqrt() 48 | }); 49 | 50 | Err(Error::new(&format!( 51 | "sqrt() requires a number, got {}", value.typename() 52 | ))) 53 | } 54 | 55 | /// The exponential function. 56 | pub fn exp(value : Value) -> eval::Result { 57 | eval1!((value : Integer) -> Float { (value as FloatRepr).exp() }); 58 | eval1!(value : Float { value.exp() }); 59 | Err(Error::new(&format!( 60 | "exp() requires a number, got {}", value.typename() 61 | ))) 62 | } 63 | 64 | /// Natural logarithm (with respect to base 'e'). 65 | pub fn ln(value : Value) -> eval::Result { 66 | eval1!((value : Integer) -> Float { (value as FloatRepr).ln() }); 67 | eval1!(value : Float { value.ln() }); 68 | Err(Error::new(&format!( 69 | "ln() requires a number, got {}", value.typename() 70 | ))) 71 | } 72 | 73 | 74 | // Rounding 75 | 76 | /// Round a number down (towards negative infinity). 77 | pub fn floor(value: Value) -> eval::Result { 78 | eval1!(value : Integer { value }); 79 | eval1!(value : Float { value.floor() }); 80 | Err(Error::new(&format!( 81 | "floor() requires a number, got {}", value.typename() 82 | ))) 83 | } 84 | 85 | /// Round a number up (towards positive infinity). 86 | pub fn ceil(value: Value) -> eval::Result { 87 | eval1!(value : Integer { value }); 88 | eval1!(value : Float { value.ceil() }); 89 | Err(Error::new(&format!( 90 | "ceil() requires a number, got {}", value.typename() 91 | ))) 92 | } 93 | 94 | /// Round a number to the nearest integer. 95 | pub fn round(value : Value) -> eval::Result { 96 | eval1!(value : Integer { value }); 97 | eval1!(value : Float { value.round() }); 98 | Err(Error::new(&format!( 99 | "round() requires a number, got {}", value.typename() 100 | ))) 101 | } 102 | 103 | /// Return the integer part of the number (round towards zero). 104 | pub fn trunc(value : Value) -> eval::Result { 105 | eval1!(value : Integer { value }); 106 | eval1!(value : Float { value.trunc() }); 107 | Err(Error::new(&format!( 108 | "trunc() requires a number, got {}", value.typename() 109 | ))) 110 | } 111 | 112 | 113 | // Numeric bases 114 | 115 | /// Convert an integer to a binary string. 116 | pub fn bin(value: Value) -> eval::Result { 117 | eval1!((value : Integer) -> String { format!("{:b}", value) }); 118 | Err(Error::new(&format!( 119 | "bin() requires a number, got {}", value.typename() 120 | ))) 121 | } 122 | 123 | /// Convert an integer to an octal string. 124 | pub fn oct(value: Value) -> eval::Result { 125 | eval1!((value : Integer) -> String { format!("{:o}", value) }); 126 | Err(Error::new(&format!( 127 | "oct() requires a number, got {}", value.typename() 128 | ))) 129 | } 130 | 131 | /// Convert an integer to a hexidecimal string. 132 | pub fn hex(value: Value) -> eval::Result { 133 | eval1!((value : Integer) -> String { format!("{:x}", value) }); 134 | Err(Error::new(&format!( 135 | "hex() requires a number, got {}", value.typename() 136 | ))) 137 | } 138 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module with built-in API that's available to the expressions. 2 | //! This is basically the standard library of the language. 3 | 4 | // NOTE: All actual API functions should be defined in submodules. 5 | 6 | pub mod base; 7 | pub mod conv; 8 | pub mod functools; 9 | pub mod itertools; 10 | pub mod math; 11 | pub mod random; 12 | pub mod strings; 13 | 14 | 15 | use std::f64; 16 | 17 | use eval::{Context, Value}; 18 | use eval::value::FloatRepr; 19 | 20 | 21 | impl<'c> Context<'c> { 22 | /// Initialize symbols for the built-in functions and constants. 23 | /// This should be done only for the root Context (the one w/o a parent). 24 | pub fn init_builtins(&mut self) { 25 | assert!(self.is_root(), "Only root Context can have builtins!"); 26 | self.init_functions(); 27 | self.init_constants(); 28 | } 29 | 30 | fn init_functions(&mut self) { 31 | // 32 | // Keep the list sorted alphabetically by function names. 33 | // 34 | self.define_unary( "abs", math::abs ); 35 | self.define_binary( "after", strings::after ); 36 | self.define_unary( "all", itertools::all ); 37 | self.define_unary( "any", itertools::any ); 38 | self.define_unary( "array", conv::array ); 39 | self.define_binary( "before", strings::before ); 40 | self.define_unary( "bin", math::bin ); 41 | self.define_unary( "bool", conv::bool ); 42 | self.define_unary( "ceil", math::ceil ); 43 | self.define_unary( "char", strings::chr ); 44 | self.define_unary( "chars", strings::chars ); 45 | self.define_unary( "chr", strings::chr ); 46 | self.define_unary( "compact", itertools::compact ); 47 | self.define_unary( "csv", conv::csv ); 48 | self.define_unary( "deburr", strings::deburr ); 49 | self.define_unary( "exp", math::exp ); 50 | self.define_binary_ctx( "filter", functools::filter ); 51 | self.define_unary( "flip", functools::flip ); 52 | self.define_unary( "float", conv::float ); 53 | self.define_unary( "floor", math::floor ); 54 | self.define_ternary_ctx( "fold", functools::reduce ); 55 | self.define_ternary_ctx( "foldl", functools::reduce ); 56 | self.define_binary( "format", strings::format_ ); 57 | self.define_ternary_ctx( "gsub", strings::sub ); 58 | self.define_unary( "hex", math::hex ); 59 | self.define_unary( "id", functools::identity ); 60 | self.define_binary( "index", base::index ); 61 | self.define_unary( "int", conv::int ); 62 | self.define_binary( "join", strings::join ); 63 | self.define_unary( "json", conv::json ); 64 | self.define_unary( "keys", base::keys ); 65 | self.define_unary( "latin1", strings::latin1 ); 66 | self.define_unary( "len", base::len ); 67 | self.define_unary( "lines", strings::lines ); 68 | self.define_unary( "ln", math::ln ); 69 | self.define_binary_ctx( "map", functools::map ); 70 | self.define_unary_ctx( "max", itertools::max ); 71 | self.define_unary_ctx( "min", itertools::min ); 72 | self.define_unary( "oct", math::oct ); 73 | self.define_binary( "omit", base::omit ); 74 | self.define_unary( "ord", strings::ord ); 75 | self.define_binary( "pick", base::pick ); 76 | self.define_nullary( "rand", random::rand_ ); 77 | self.define_unary( "re", conv::regex ); 78 | self.define_ternary_ctx( "reduce", functools::reduce ); 79 | self.define_unary( "regex", conv::regex ); 80 | self.define_unary( "regexp", conv::regex ); 81 | self.define_binary_ctx( "reject", functools::reject ); 82 | self.define_unary( "rev", base::rev ); 83 | self.define_unary( "rot13", strings::rot13 ); 84 | self.define_unary( "round", math::round ); 85 | self.define_ternary( "rsub1", strings::rsub1 ); 86 | self.define_binary( "sample", random::sample ); 87 | self.define_unary( "sgn", math::sgn ); 88 | self.define_unary( "shuffle", random::shuffle ); 89 | self.define_unary( "sort", base::sort ); 90 | self.define_binary_ctx( "sortby", base::sort_by ); 91 | self.define_binary( "split", strings::split ); 92 | self.define_unary( "sqrt", math::sqrt ); 93 | self.define_unary( "str", conv::str_ ); 94 | self.define_unary( "string", conv::str_ ); 95 | self.define_ternary_ctx( "sub", strings::sub ); 96 | self.define_ternary_ctx( "sub1", strings::sub1 ); 97 | self.define_unary_ctx( "sum", itertools::sum ); 98 | self.define_unary( "trim", strings::trim ); 99 | self.define_unary( "trunc", math::trunc ); 100 | self.define_unary( "utf8", strings::utf8 ); 101 | self.define_unary( "values", base::values ); 102 | self.define_unary( "words", strings::words ); 103 | } 104 | 105 | fn init_constants(&mut self) { 106 | // 107 | // Keep the list sorted alphabetically by constant names (ignore case). 108 | // 109 | self.set("pi", Value::Float(f64::consts::PI as FloatRepr)); 110 | } 111 | } 112 | 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use eval::Context; 117 | 118 | #[test] 119 | fn no_bool_constants() { 120 | let ctx = Context::new(); 121 | for constant in &["true", "false"] { 122 | check_constant(&ctx, *constant); 123 | } 124 | } 125 | 126 | #[test] 127 | fn no_float_constants() { 128 | let ctx = Context::new(); 129 | for constant in &["NaN", "Inf"] { 130 | check_constant(&ctx, *constant) 131 | } 132 | } 133 | 134 | #[test] 135 | fn no_nil() { 136 | let ctx = Context::new(); 137 | check_constant(&ctx, "nil"); 138 | } 139 | 140 | fn check_constant(ctx: &Context, name: &'static str) { 141 | assert!(!ctx.is_defined(name), 142 | "`{}` is handled by parser and doesn't need to be in Context", name); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/random.rs: -------------------------------------------------------------------------------- 1 | //! Functions related to random number generation. 2 | 3 | use rand::{self, Rng}; 4 | 5 | use eval::{self, Value}; 6 | 7 | 8 | /// Generate a random floating point number from the 0..1 range. 9 | #[inline] 10 | pub fn rand_() -> eval::Result { 11 | Ok(Value::Float(rand::random())) 12 | } 13 | 14 | 15 | /// Shuffle the elements of an array or characters in a string. 16 | pub fn shuffle(value: Value) -> eval::Result { 17 | if value.is_array() { 18 | let mut array = value.unwrap_array(); 19 | rand::thread_rng().shuffle(&mut array); 20 | return Ok(Value::Array(array)); 21 | } 22 | 23 | if value.is_string() { 24 | let mut chars: Vec<_> = value.unwrap_string().chars().collect(); 25 | rand::thread_rng().shuffle(&mut chars); 26 | 27 | let mut result = String::with_capacity(chars.len()); 28 | for c in chars.into_iter() { 29 | result.push(c); 30 | } 31 | return Ok(Value::String(result)); 32 | } 33 | 34 | mismatch!("shuffle"; ("array") | ("string") => (value)) 35 | } 36 | 37 | 38 | /// Pick a sample of given size among the values from a collection. 39 | /// Returns the array of picked elements. 40 | pub fn sample(size: Value, source: Value) -> eval::Result { 41 | if size.is_integer() { 42 | let mut rng = rand::thread_rng(); 43 | 44 | if source.is_array() { 45 | let size = size.unwrap_integer() as usize; 46 | return Ok(Value::Array( 47 | rand::sample(&mut rng, source.unwrap_array().into_iter(), size) 48 | )); 49 | } 50 | if source.is_string() { 51 | let size = size.unwrap_integer() as usize; 52 | let mut result = String::with_capacity(size); 53 | for c in rand::sample(&mut rng, source.unwrap_string().chars(), size) { 54 | result.push(c); 55 | } 56 | return Ok(Value::String(result)); 57 | } 58 | if source.is_object() { 59 | let size = size.unwrap_integer() as usize; 60 | return Ok(Value::Array( 61 | rand::sample(&mut rng, 62 | source.unwrap_object().into_iter().map(|(_, v)| v), 63 | size) 64 | )); 65 | } 66 | } 67 | mismatch!("sample"; 68 | ("integer", "array") | ("integer", "string") | ("integer", "object") 69 | => (size, source)) 70 | } 71 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/strings/frag.rs: -------------------------------------------------------------------------------- 1 | //! API for blowing strings into fragments and putting them back together. 2 | 3 | use regex::Regex; 4 | 5 | use eval::{self, Error, Value}; 6 | use eval::api::conv::str_; 7 | use eval::value::{ArrayRepr, StringRepr}; 8 | 9 | 10 | lazy_static!{ 11 | static ref WORD_SEP: Regex = Regex::new(r"\s+").unwrap(); 12 | static ref LINE_SEP: Regex = Regex::new("\r?\n").unwrap(); 13 | } 14 | 15 | 16 | /// Join an array of values into a single delimited string. 17 | pub fn join(delim: Value, array: Value) -> eval::Result { 18 | let delim_type = delim.typename(); 19 | let array_type = array.typename(); 20 | 21 | if let (Value::String(d), Value::Array(a)) = (delim, array) { 22 | let elem_count = a.len(); 23 | let strings: Vec<_> = a.into_iter() 24 | .map(str_).filter(Result::is_ok) 25 | .map(Result::unwrap).map(Value::unwrap_string) 26 | .collect(); 27 | let error_count = strings.len() - elem_count; 28 | if error_count == 0 { 29 | return Ok(Value::String(strings.join(&d))); 30 | } else { 31 | // TODO: include the error message of the offending element's conversion 32 | return Err(Error::new(&format!( 33 | "join() failed to stringify {} element(s) of the input array", 34 | error_count))); 35 | } 36 | } 37 | 38 | Err(Error::new(&format!( 39 | "join() expects a string and an array, got: {}, {}", 40 | delim_type, array_type 41 | ))) 42 | } 43 | 44 | 45 | /// Split a string by given string delimiter. 46 | /// Returns an array of strings. 47 | // TODO(xion): introduce optional third parameter, maxsplit 48 | pub fn split(delim: Value, string: Value) -> eval::Result { 49 | eval2!((delim: &String, string: &String) -> Array { 50 | string.split(delim).map(StringRepr::from).map(Value::String).collect() 51 | }); 52 | eval2!((delim: &Regex, string: &String) -> Array { 53 | do_regex_split(delim, string) 54 | }); 55 | mismatch!("split"; ("string", "string") | ("regex", "string") => (delim, string)) 56 | } 57 | 58 | /// Split a string into array of words. 59 | pub fn words(string: Value) -> eval::Result { 60 | eval1!((string: &String) -> Array { do_regex_split(&WORD_SEP, string) }); 61 | mismatch!("words"; ("string") => (string)) 62 | } 63 | 64 | /// Split a string into array of lines. 65 | pub fn lines(string: Value) -> eval::Result { 66 | eval1!((string: &String) -> Array { do_regex_split(&LINE_SEP, string) }); 67 | mismatch!("lines"; ("string") => (string)) 68 | } 69 | 70 | 71 | // Utility functions 72 | 73 | #[inline] 74 | fn do_regex_split(delim: &Regex, string: &str) -> ArrayRepr { 75 | delim.split(string).map(StringRepr::from).map(Value::String).collect() 76 | } 77 | -------------------------------------------------------------------------------- /crates/librush/src/eval/api/strings/mod.rs: -------------------------------------------------------------------------------- 1 | //! String API available to expressions. 2 | 3 | mod frag; 4 | mod subst; 5 | 6 | pub use self::frag::*; 7 | pub use self::subst::*; 8 | 9 | 10 | use std::char; 11 | use std::error::Error as StdError; // just for its description() method 12 | use std::fmt::Display; 13 | use std::str::from_utf8; 14 | 15 | use eval::{self, Error, Value}; 16 | use eval::value::{IntegerRepr, StringRepr}; 17 | use eval::util::fmt::format; 18 | 19 | 20 | /// Returns a one-character string with the character of given ordinal value. 21 | pub fn chr(value: Value) -> eval::Result { 22 | eval1!((value: Integer) -> String where (value >= 0) {{ 23 | let ord = value as u32; 24 | let ch = try!(char::from_u32(ord) 25 | .ok_or_else(|| Error::new(&format!( 26 | "invalid character ordinal: {}", ord 27 | )))); 28 | let mut result = String::with_capacity(1); 29 | result.push(ch); 30 | result 31 | }}); 32 | Err(Error::new(&format!( 33 | "chr() expects a positive integer, got {}", value.typename() 34 | ))) 35 | } 36 | 37 | /// Returns the ordinal value for a single character in a string. 38 | pub fn ord(value: Value) -> eval::Result { 39 | eval1!((value: &String) -> Integer { 40 | match value.len() { 41 | 1 => value.chars().next().unwrap() as IntegerRepr, 42 | x => return Err(Error::new(&format!( 43 | "ord() requires string of length 1, got {}", x 44 | ))), 45 | } 46 | }); 47 | Err(Error::new(&format!( 48 | "ord() expects a string, got {}", value.typename() 49 | ))) 50 | } 51 | 52 | 53 | /// Converts a string into an array of characters. 54 | /// Each character is represented as a string of length 1. 55 | pub fn chars(value: Value) -> eval::Result { 56 | eval1!((value: &String) -> Array { 57 | value.chars() 58 | .map(|c| { let mut s = String::with_capacity(1); s.push(c); s }) 59 | .map(Value::String) 60 | .collect() 61 | }); 62 | mismatch!("chars"; ("string") => (value)) 63 | } 64 | 65 | /// Converts a string into an array of UTF8 bytes. 66 | /// Each byte is represented as its integer value. 67 | pub fn utf8(value: Value) -> eval::Result { 68 | eval1!((value: &String) -> Array { 69 | value.as_bytes().iter() 70 | .map(|b| *b as IntegerRepr).map(Value::Integer) 71 | .collect() 72 | }); 73 | mismatch!("utf8"; ("string") => (value)) 74 | } 75 | 76 | 77 | /// Peforms string formatting a'la Python str.format(). 78 | pub fn format_(fmt: Value, arg: Value) -> eval:: Result { 79 | if let Value::String(fmt) = fmt { 80 | let mut args: Vec<&Display> = Vec::new(); 81 | 82 | match arg { 83 | Value::Boolean(..) | 84 | Value::Integer(..) | 85 | Value::Float(..) | 86 | Value::String(..) => args.push(&arg), 87 | Value::Array(ref a) => { 88 | args = a.iter().map(|v| v as &Display).collect(); 89 | }, 90 | Value::Object(..) => { 91 | // TODO(xion): Object should be possible but the formatting code 92 | // doesn't support named placeholders yet :( 93 | return Err(Error::new( 94 | "objects are not supported as string formatting arguments" 95 | )); 96 | }, 97 | _ => return Err(Error::new(&format!( 98 | "invalid argument for string formatting: {}", arg.typename() 99 | ))), 100 | } 101 | 102 | return format(&fmt, &args) 103 | .map(Value::String) 104 | .map_err(|e| Error::new(&format!( 105 | "string formatting error: {}", e.description() 106 | ))); 107 | } 108 | 109 | Err(Error::new(&format!( 110 | "format() expects a format string, got {}", fmt.typename() 111 | ))) 112 | } 113 | 114 | 115 | /// Return part of a string ("haystack") before given one ("needle"), 116 | /// or empty string if not found. 117 | pub fn before(needle: Value, haystack: Value) -> eval::Result { 118 | eval2!((needle: &String, haystack: &String) -> String { 119 | match haystack.find(&needle as &str) { 120 | Some(index) => StringRepr::from( 121 | from_utf8(&haystack.as_bytes()[0..index]).unwrap() 122 | ), 123 | _ => String::new(), 124 | } 125 | }); 126 | eval2!((needle: &Regex, haystack: &String) -> String { 127 | match needle.find(&haystack) { 128 | Some((index, _)) => StringRepr::from( 129 | from_utf8(&haystack.as_bytes()[0..index]).unwrap() 130 | ), 131 | _ => String::new(), 132 | } 133 | }); 134 | 135 | Err(Error::new(&format!( 136 | "before() expects two strings, or regex and string, got {} and {}", 137 | needle.typename(), haystack.typename() 138 | ))) 139 | } 140 | 141 | /// Return part of a string ("haystack") after given one ("needle"), 142 | /// or empty string if not found. 143 | pub fn after(needle: Value, haystack: Value) -> eval::Result { 144 | eval2!((needle: &String, haystack: &String) -> String { 145 | match haystack.find(&needle as &str) { 146 | Some(index) => StringRepr::from( 147 | from_utf8(&haystack.as_bytes()[index + needle.len()..]).unwrap() 148 | ), 149 | _ => String::new(), 150 | } 151 | }); 152 | eval2!((needle: &Regex, haystack: &String) -> String { 153 | match needle.find(&haystack) { 154 | Some((_, index)) => StringRepr::from( 155 | from_utf8(&haystack.as_bytes()[index..]).unwrap() 156 | ), 157 | _ => String::new(), 158 | } 159 | }); 160 | 161 | Err(Error::new(&format!( 162 | "after() expects two strings, or regex and string, got {} and {}", 163 | needle.typename(), haystack.typename() 164 | ))) 165 | } 166 | 167 | /// Trim the string from whitespace characters at both ends. 168 | pub fn trim(string: Value) -> eval::Result { 169 | eval1!(string : &String { string.trim().to_owned() }); 170 | Err(Error::new(&format!( 171 | "trim() requires a string, got {}", string.typename() 172 | ))) 173 | } 174 | -------------------------------------------------------------------------------- /crates/librush/src/eval/atoms.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing evaluation of the "atomic" expressions, 2 | //! i.e. those that create the values that are then operated upon. 3 | 4 | use eval::{self, Context, Eval, Value}; 5 | use eval::model::value::{ArrayRepr, ObjectRepr}; 6 | use parse::ast::{ArrayNode, ObjectNode, ScalarNode}; 7 | 8 | 9 | /// Evaluate the AST node representing a scalar value. 10 | impl Eval for ScalarNode { 11 | #[inline] 12 | fn eval(&self, context: &mut Context) -> eval::Result { 13 | Ok(context.resolve(&self.value)) 14 | } 15 | } 16 | 17 | 18 | /// Evaluate the AST node representing an array value. 19 | impl Eval for ArrayNode { 20 | fn eval(&self, context: &mut Context) -> eval::Result { 21 | let mut elems = ArrayRepr::with_capacity(self.elements.len()); 22 | for ref x in &self.elements { 23 | let elem = try!(x.eval(context)); 24 | elems.push(elem); 25 | } 26 | Ok(Value::Array(elems)) 27 | } 28 | } 29 | 30 | 31 | /// Evaluate the AST node representing an object value. 32 | impl Eval for ObjectNode { 33 | fn eval(&self, context: &mut Context) -> eval::Result { 34 | let mut attrs = ObjectRepr::with_capacity(self.attributes.len()); 35 | for &(ref k, ref v) in &self.attributes { 36 | let key = try!(k.eval(context)); 37 | let value = try!(v.eval(context)); 38 | if let Value::String(attr) = key { 39 | attrs.insert(attr, value); 40 | } else { 41 | return Err(eval::Error::new(&format!( 42 | "object attribute name must be string, got {}", key.typename() 43 | ))); 44 | } 45 | } 46 | Ok(Value::Object(attrs)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/librush/src/eval/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing evaluation of parsed expressions. 2 | // 3 | //! Currently, the evaluator is a recursive descent over the AST. 4 | 5 | #![allow(doc_markdown)] 6 | #![allow(needless_borrow)] 7 | #![allow(option_map_unwrap_or)] 8 | #![allow(ptr_arg)] 9 | #![allow(transmute_ptr_to_ref)] 10 | 11 | #[macro_use] 12 | pub mod util; 13 | #[macro_use] 14 | pub mod model; 15 | 16 | mod api; 17 | mod atoms; 18 | mod operators; 19 | mod trailers; 20 | 21 | pub use self::model::{Context, Function, Invoke, Value}; 22 | pub use self::model::Error; 23 | pub use self::model::value; // for *Repr typedefs 24 | 25 | 26 | use std::fmt; 27 | use std::result; 28 | 29 | use mopa; 30 | 31 | 32 | /// Result of an evaluation attempt. 33 | pub type Result = result::Result; 34 | 35 | 36 | /// Trait for objects that can be evaluated within given Context. 37 | pub trait Eval : fmt::Debug + mopa::Any + 'static { 38 | fn eval(&self, context: &mut Context) -> Result; 39 | } 40 | mopafy!(Eval); 41 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/context/mod.rs: -------------------------------------------------------------------------------- 1 | //! Evaluation context. 2 | //! 3 | //! A context contains all the variable and function bindings that are used 4 | //! when evaluating an expression. 5 | //! 6 | //! This is roughly equivalent to a stack frame, 7 | //! or a block of code in languages with local scoping (like C++ or Rust). 8 | 9 | mod defines; 10 | 11 | 12 | use std::borrow::{Borrow, ToOwned}; 13 | use std::collections::HashMap; 14 | use std::fmt::Display; 15 | use std::hash::{BuildHasherDefault, Hash}; 16 | 17 | use fnv::FnvHasher; 18 | 19 | use eval; 20 | use super::{Args, Invoke, Value}; 21 | 22 | 23 | /// Type for names of variables present in the Context. 24 | pub type Name = String; 25 | 26 | /// Custom hasher for hashmap type that stores variables present in Context. 27 | /// Uses the Fowler-Noll-Vo hashing algorithm which is faster for short keys. 28 | type Hasher = BuildHasherDefault; 29 | 30 | 31 | /// Evaluation context for an expression. 32 | #[derive(Default)] 33 | pub struct Context<'c> { 34 | /// Optional parent Context, i.e. a lower "frame" on the "stack". 35 | parent: Option<&'c Context<'c>>, 36 | 37 | /// Names & values present in the context. 38 | scope: HashMap, 39 | } 40 | 41 | impl<'c> Context<'c> { 42 | /// Create a new root context. 43 | pub fn new() -> Context<'c> { 44 | let mut context = Context{parent: None, scope: HashMap::default()}; 45 | context.init_builtins(); 46 | context 47 | } 48 | 49 | /// Create a new Context that's a child of given parent. 50 | #[inline] 51 | pub fn with_parent(parent: &'c Context<'c>) -> Context<'c> { 52 | Context{parent: Some(parent), scope: HashMap::default()} 53 | } 54 | 55 | /// Whether this is a root context (one without a parent). 56 | #[inline] 57 | pub fn is_root(&self) -> bool { 58 | self.parent.is_none() 59 | } 60 | 61 | /// Whether this context is empty, i.e. has no own symbols defined in its scope. 62 | /// Note that because of builtins, the root context will never be empty! 63 | #[inline] 64 | pub fn is_empty(&self) -> bool { 65 | self.scope.is_empty() 66 | } 67 | 68 | /// Check if given name is defined within this Context 69 | /// or any of its ancestors. 70 | #[inline] 71 | pub fn is_defined(&self, name: &N) -> bool 72 | where Name: Borrow, N: Hash + Eq 73 | { 74 | self.scope.get(name) 75 | .or_else(|| self.parent.and_then(|ctx| ctx.get(name))) 76 | .is_some() 77 | } 78 | 79 | /// Check if given name is defined in this context. 80 | /// Does not look at parent Contexts. 81 | #[inline] 82 | pub fn is_defined_here(&self, name: &N) -> bool 83 | where Name: Borrow, N: Hash + Eq 84 | { 85 | self.scope.get(name).is_some() 86 | } 87 | 88 | /// Retrieves a value by name from the scope of the context 89 | /// or any of its parents. 90 | #[inline] 91 | pub fn get(&self, name: &N) -> Option<&Value> 92 | where Name: Borrow, N: Hash + Eq 93 | { 94 | self.scope.get(name) 95 | .or_else(|| self.parent.and_then(|ctx| ctx.get(name))) 96 | } 97 | 98 | /// Set a value for a variable inside the context's scope. 99 | /// If the name already exists in the parent scope (if any), 100 | /// it will be shadowed. 101 | #[inline] 102 | pub fn set(&mut self, name: &N, value: Value) 103 | where Name: Borrow, N: ToOwned 104 | { 105 | self.scope.insert(name.to_owned(), value); 106 | } 107 | 108 | /// "Unset" the value of a variable, making the symbol undefined 109 | /// in this context. 110 | /// 111 | /// Returns the value that the variable was previously set to 112 | /// (or None if it wasn't set before). 113 | /// 114 | /// Note how regardless of the return value, the variable won't be defined 115 | /// in this context after the call to this method. It may, however, 116 | /// still **be** defined in a parent Context, if any. 117 | pub fn unset_here(&mut self, name: &N) -> Option 118 | where Name: Borrow, N: Hash + Eq 119 | { 120 | self.scope.remove(name) 121 | } 122 | 123 | /// Reset the context, removing all variable bindings. 124 | /// Built-in functions and constants are preserved. 125 | pub fn reset(&mut self) { 126 | self.scope.clear(); 127 | if self.is_root() { 128 | self.init_builtins(); 129 | } 130 | } 131 | 132 | /// Resolve a possible variable reference. 133 | /// 134 | /// Returns the variable's Value (which may be just variable name as string), 135 | /// or a copy of the original Value if it wasn't a reference. 136 | pub fn resolve(&self, value: &Value) -> Value { 137 | let mut result = value; 138 | 139 | // follow the chain of references until it bottoms out 140 | while let Value::Symbol(ref sym) = *result { 141 | if let Some(target) = self.get(sym) { 142 | result = target; 143 | } else { 144 | return Value::String(sym.clone()) 145 | } 146 | } 147 | result.clone() 148 | } 149 | 150 | /// Call a function of given name with given arguments. 151 | pub fn call(&self, name: &N, args: Args) -> eval::Result 152 | where Name: Borrow, N: Hash + Eq + Display 153 | { 154 | match self.get(name) { 155 | Some(&Value::Function(ref f)) => f.invoke(args, &self), 156 | // Note that when both this & parent context have `name` in scope, 157 | // and in parent this is a function while in this context it's not, 158 | // the result is in error. 159 | // This is perfectly consistent with the way shadowing should work, 160 | // and would be VERY confusing otherwise. 161 | _ => Err(eval::Error::new(&format!("`{}` is not a function", name))), 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/error.rs: -------------------------------------------------------------------------------- 1 | //! Evaluation error. 2 | //! 3 | //! Several different error variants are defined. They are very analogous to 4 | //! basic exception types from languages such as Python or Java. 5 | 6 | use std::error::Error as StdError; 7 | use std::fmt; 8 | 9 | use super::Value; 10 | 11 | 12 | /// Macro to use at the beginning of operators' and API functions' implementations. 13 | /// Allows to check the actual types of arguments passed against 14 | /// one of expected function sigunatures. 15 | /// 16 | /// Example: 17 | /// 18 | /// argcheck!("rot13"; ("string") => (value)) 19 | /// 20 | /// Using this macro is typically more convenient than mismatch!, 21 | /// as it allows the arguments to be moved/consumed by the function later. 22 | macro_rules! argcheck ( 23 | ($op:expr; $(($($ex:expr),*))|+ => ($($ac:ident),*)) => ({ 24 | let specs: &[&[&'static str]] = &[ $( 25 | &[ $( $ex ),* ] 26 | ),* ]; 27 | let argtypes: &[&'static str] = &[ $($ac.typename()),* ]; 28 | 29 | let has_match = specs.len() == 0 || specs.iter().any(|spec| { 30 | spec.iter().zip(argtypes).all(|(ex, act)| *ex == *act) 31 | }); 32 | if !has_match { 33 | return mismatch!($op; $( ($( $ex ),*) )|+ => ($( $ac ),*)); 34 | } 35 | }); 36 | ); 37 | 38 | 39 | /// Macro for use within operators and API functions' implementations. 40 | /// Creates an erroneous eval::Result indicating a mismatch between expected argument types 41 | /// and values actually received. 42 | /// 43 | /// Example: 44 | /// 45 | /// mismatch!("rot13"; ("string") => (value)) 46 | /// 47 | /// Using argcheck! is typically preferable over mismatch!, 48 | /// as the latter requires argument not to have been moved/consumed by the function. 49 | macro_rules! mismatch ( 50 | ($op:expr; $(($($ex:expr),*))|+ => ($($ac:ident),*)) => ( 51 | Err($crate::eval::Error::mismatch($op, vec![$( 52 | vec![$( $ex ),*] 53 | ),+], vec![$( &$ac ),*])) 54 | ); 55 | ); 56 | 57 | 58 | /// Error that may have occurred during evaluation. 59 | #[derive(Clone,Debug,PartialEq)] 60 | pub enum Error { 61 | /// Invalid arguments. 62 | /// Indicates that actual arguments passed to an operation 63 | /// (like a function call) were invalid (for example, they had wrong types). 64 | Invalid(Mismatch), 65 | /// Other error with a custom message. 66 | Other(String), 67 | } 68 | 69 | impl Error { 70 | // TODO(xion): remove this legacy constructor after all usages of Error are fixed 71 | #[inline] 72 | pub fn new(msg: &str) -> Error { 73 | Error::other(msg) 74 | } 75 | 76 | /// Create an Error that indicates an operation has received invalid arguments. 77 | /// It will not specify a valid argument types, however. 78 | #[inline] 79 | pub fn invalid(operation: &str, args: Vec<&Value>) -> Error { 80 | Error::Invalid(Mismatch::new(operation, args)) 81 | } 82 | 83 | /// Create an Error that indicates an operation has received invalid arguments. 84 | /// The list of expected argument signatures is also required. 85 | #[inline] 86 | pub fn mismatch(operation: &str, expected: Vec>, actual: Vec<&Value>) -> Error 87 | where Type: From 88 | { 89 | assert!(expected.len() > 0, "No expected argument signatures"); 90 | Error::Invalid(Mismatch::against_many( 91 | operation, 92 | expected.into_iter() 93 | .map(|sig| sig.into_iter().map(Type::from).collect()) 94 | .collect(), 95 | actual 96 | )) 97 | } 98 | 99 | #[inline] 100 | pub fn other(msg: &str) -> Error { 101 | Error::Other(msg.to_owned()) 102 | } 103 | } 104 | 105 | impl fmt::Display for Error { 106 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 107 | match *self { 108 | Error::Invalid(ref m) => write!(f, "Invalid arguments: {}", m), 109 | Error::Other(ref msg) => write!(f, "Eval error: {}", msg), 110 | } 111 | } 112 | } 113 | 114 | impl StdError for Error { 115 | fn description(&self) -> &str { 116 | match *self { 117 | Error::Invalid(..) => "invalid arguments", 118 | Error::Other(..) => "evaluation error", 119 | } 120 | } 121 | 122 | #[inline] 123 | fn cause(&self) -> Option<&StdError> { 124 | None 125 | } 126 | } 127 | 128 | 129 | // Structures for various error variants 130 | 131 | /// Representation of a type. 132 | /// For now, this is merely a type name. 133 | pub type Type = String; 134 | 135 | /// Type of a function or operator signature. 136 | /// This is basically a list of argument type names it accepts. 137 | pub type Signature = Vec; 138 | 139 | /// Representation of a value. 140 | /// For now, this is merely a Debug formatting of the Value. 141 | pub type ValueRepr = String; 142 | 143 | /// Mismatch error. 144 | /// Indicates that values passed did not meet expectations. 145 | #[derive(Clone,Debug,Eq,PartialEq,Hash)] 146 | pub struct Mismatch { 147 | /// Name of the operation that caused the type mismatch. 148 | operation: String, 149 | /// List of expected signatures for the operation. 150 | expected: Vec, 151 | /// Actual arguments passed. 152 | actual: Vec<(Type, ValueRepr)>, 153 | } 154 | impl Mismatch { 155 | #[inline] 156 | pub fn new(operation: &str, args: Vec<&Value>) -> Mismatch { 157 | Mismatch::against_many(operation, Vec::new(), args) 158 | } 159 | 160 | #[inline] 161 | pub fn against_one(operation: &str, expected: Signature, actual: Vec<&Value>) -> Mismatch { 162 | Mismatch::against_many(operation, vec![expected], actual) 163 | } 164 | 165 | #[inline] 166 | pub fn against_many(operation: &str, 167 | expected: Vec, actual: Vec<&Value>) -> Mismatch { 168 | // Note that we don't assert that `expected` is non empty because this error 169 | // may be used also for indicating that arguments are invalid for non-type related reasons. 170 | assert!(operation.len() > 0, "Empty operation"); 171 | assert!(actual.len() > 0, "No actual arguments"); 172 | 173 | Mismatch{ 174 | operation: operation.to_owned(), 175 | expected: expected, 176 | actual: actual.into_iter() 177 | .map(|v| (Type::from(v.typename()), format!("{:?}", v))).collect(), 178 | } 179 | } 180 | } 181 | impl fmt::Display for Mismatch { 182 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 183 | // format the operation identifier as either a function or an operator 184 | let mut operation = self.operation.clone(); 185 | if operation.chars().all(|c| c == '_' || c.is_alphanumeric()) { 186 | if !operation.ends_with("()") { 187 | operation.push_str("()"); 188 | } 189 | } else { 190 | operation = format!("`{}`", operation); 191 | } 192 | 193 | // if present, format the expected type signatures as separate lines 194 | let expected = match self.expected.len() { 195 | 0 => "".to_owned(), 196 | 1 => format!("{} ", self.expected[0].join(", ")), 197 | _ => format!( 198 | "one of:\n{}\n", self.expected.iter() 199 | .map(|sig| format!("\t{}", sig.join(", "))) 200 | .collect::>().join("\n") 201 | ), 202 | }; 203 | 204 | // represent the actual values passed 205 | let actual_sep = if self.actual.len() > 2 { ", " } else { " and " }; 206 | let actual = self.actual.iter() 207 | .map(|&(ref t, ref v)| format!("`{}` ({})", v, t)) 208 | .collect::>().join(actual_sep); 209 | 210 | if expected != "" { 211 | write!(f, "{} expected {}but got: {}", operation, expected, actual) 212 | } else { 213 | write!(f, "{} got invalid arguments: {}", operation, actual) 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/function.rs: -------------------------------------------------------------------------------- 1 | //! Function type. 2 | //! 3 | //! This type represents both built-in functions (that are implemented directly 4 | //! in native code), as well as user defined functions that are basically 5 | //! pieces of AST. 6 | 7 | use std::cmp::PartialEq; 8 | use std::fmt; 9 | use std::rc::Rc; 10 | 11 | use eval::{self, Context, Eval}; 12 | use super::arity::{Args, Arity}; 13 | use super::value::Value; 14 | 15 | 16 | /// Denotes an object that works as a callable function within an expression. 17 | /// 18 | /// (This isn't named Call because call() function would conflict with 19 | /// the quasi-intrinsic method on Fn types in Rust). 20 | pub trait Invoke { 21 | /// Returns the arity of the invokable object, 22 | /// i.e. how many arguments it can accept. 23 | fn arity(&self) -> Arity; 24 | 25 | /// Invokes the object. 26 | /// 27 | /// The Context passed here should be the one where the invocation 28 | /// has been found. It is the object itself which can decide whether or not 29 | /// it wants to create its own Context ("stack frame") for the invocation. 30 | fn invoke(&self, args: Args, context: &Context) -> eval::Result; 31 | 32 | // 33 | // Convenience shortcuts for invocations with different number of arguments. 34 | // 35 | #[inline] 36 | fn invoke0(&self, context: &Context) -> eval::Result { 37 | self.invoke(vec![], context) 38 | } 39 | #[inline] 40 | fn invoke1(&self, arg: Value, context: &Context) -> eval::Result { 41 | self.invoke(vec![arg], context) 42 | } 43 | #[inline] 44 | fn invoke2(&self, arg1: Value, arg2: Value, context: &Context) -> eval::Result { 45 | self.invoke(vec![arg1, arg2], context) 46 | } 47 | #[inline] 48 | fn invoke3(&self, arg1: Value, arg2: Value, arg3: Value, context: &Context) -> eval::Result { 49 | self.invoke(vec![arg1, arg2, arg3], context) 50 | } 51 | } 52 | 53 | 54 | /// Function that can be invoked when evaluating an expression. 55 | #[derive(Clone)] 56 | pub enum Function { 57 | /// An unspecified "invokable" object. 58 | Raw(Rc>), 59 | 60 | /// Native function that's implemented in the interpreter. 61 | Native(Arity, Rc), 62 | 63 | /// Native function that's implemented in the interpreter 64 | /// and takes Context as an explicit parameter. 65 | NativeCtx(Arity, Rc), 66 | 67 | /// Custom function that's been defined as part of the expression itself. 68 | Custom(CustomFunction), 69 | } 70 | 71 | impl Function { 72 | #[inline] 73 | pub fn from_raw(invoke: Box) -> Function { 74 | Function::Raw(Rc::new(invoke)) 75 | } 76 | 77 | /// Create the Function struct from a simple native Rust function. 78 | /// 79 | /// Note that if the Rust function is a closure, you'll may need to 80 | /// use Function::from_native_ctx() -- even if you don't need the Context -- 81 | /// to resolve lifetime issues. 82 | #[inline] 83 | pub fn from_native(arity: Arity, f: F) -> Function 84 | where F: Fn(Args) -> eval::Result + 'static 85 | { 86 | Function::Native(arity, Rc::new(f)) 87 | } 88 | 89 | /// Create the Function struct from a native Rust function 90 | /// that receives a reference Context as an explicit parameter. 91 | /// 92 | /// This is useful is the function itself needs to call other Invoke objects. 93 | #[inline] 94 | pub fn from_native_ctx(arity: Arity, f: F) -> Function 95 | where F: Fn(Args, &Context) -> eval::Result + 'static 96 | { 97 | Function::NativeCtx(arity, Rc::new(f)) 98 | } 99 | 100 | /// Create the Function struct from a lambda expression. 101 | #[inline] 102 | pub fn from_lambda(argnames: Vec, body: Box) -> Function { 103 | Function::Custom(CustomFunction::new(argnames, body)) 104 | } 105 | 106 | /// Function composition: 107 | /// self.compose_with(other)(x) === self(other(x)) 108 | #[inline] 109 | pub fn compose_with(self, other: Function) -> Option { 110 | if self.arity() == 1 { 111 | let arity = other.arity(); 112 | let result = move |args, context: &Context| { 113 | let intermediate = try!(other.invoke(args, &context)); 114 | self.invoke1(intermediate, &context) 115 | }; 116 | return Some(Function::from_native_ctx(arity, result)); 117 | } 118 | None 119 | } 120 | 121 | /// Function currying (partial application): 122 | /// self.curry(arg)(x) === self(arg, x) 123 | #[inline] 124 | pub fn curry(self, arg: Value) -> Option { 125 | if self.arity() >= 1 { 126 | let arity = self.arity() - 1; 127 | let result = move |mut args: Args, context: &Context| { 128 | args.insert(0, arg.clone()); 129 | self.invoke(args, &context) 130 | }; 131 | return Some(Function::from_native_ctx(arity, result)); 132 | } 133 | None 134 | } 135 | } 136 | 137 | impl PartialEq for Function { 138 | #[inline] 139 | fn eq(&self, _: &Self) -> bool { 140 | // for simplicity, functions are never equal to one another 141 | false 142 | } 143 | } 144 | 145 | impl fmt::Debug for Function { 146 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 147 | match *self { 148 | Function::Raw(ref f) => write!(fmt, "", f.arity()), 149 | Function::Native(a, _) => write!(fmt, "", a), 150 | Function::NativeCtx(a, _) => write!(fmt, "", a), 151 | Function::Custom(ref f) => write!(fmt, "{:?}", f), 152 | } 153 | } 154 | } 155 | 156 | impl Invoke for Function { 157 | fn arity(&self) -> Arity { 158 | match *self { 159 | Function::Raw(ref f) => f.arity(), 160 | Function::Native(a, _) | Function::NativeCtx(a, _) => a, 161 | Function::Custom(ref f) => f.arity(), 162 | } 163 | } 164 | 165 | #[allow(match_same_arms)] 166 | fn invoke(&self, args: Args, context: &Context) -> eval::Result { 167 | match *self { 168 | Function::Raw(ref f) => f.invoke(args, &context), 169 | Function::Native(_, ref f) => f(args), 170 | Function::NativeCtx(_, ref f) => { 171 | let context = Context::with_parent(context); 172 | f(args, &context) 173 | }, 174 | Function::Custom(ref f) => f.invoke(args, &context), 175 | } 176 | } 177 | } 178 | 179 | 180 | // Function types 181 | 182 | /// Native function type, 183 | /// i.e. one that's implemented in the interpreter itself. 184 | pub type NativeFunction = Fn(Args) -> eval::Result; 185 | 186 | 187 | /// Native function that directly operates on its Context. 188 | pub type NativeCtxFunction = Fn(Args, &Context) -> eval::Result; 189 | 190 | 191 | /// Custom function type, 192 | /// i.e. one that has been defined using the expression syntax. 193 | #[derive(Clone)] 194 | pub struct CustomFunction { 195 | argnames: Vec, 196 | expr: Rc>, 197 | } 198 | 199 | impl CustomFunction { 200 | #[inline] 201 | pub fn new(argnames: Vec, expr: Box) -> CustomFunction { 202 | CustomFunction{ 203 | argnames: argnames, 204 | expr: Rc::new(expr), 205 | } 206 | } 207 | } 208 | 209 | impl fmt::Debug for CustomFunction { 210 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 211 | write!(fmt, "|{}| {:?}", self.argnames.join(","), self.expr) 212 | } 213 | } 214 | 215 | impl Invoke for CustomFunction { 216 | #[inline] 217 | fn arity(&self) -> Arity { 218 | Arity::Exact(self.argnames.len()) 219 | } 220 | 221 | fn invoke(&self, args: Args, context: &Context) -> eval::Result { 222 | let expected_count = self.argnames.len(); 223 | let actual_count = args.len(); 224 | if actual_count != expected_count { 225 | return Err(eval::Error::new(&format!( 226 | "function expects {} argument(s), got {}", 227 | expected_count, actual_count 228 | ))); 229 | } 230 | 231 | let mut context = Context::with_parent(context); 232 | for (name, value) in self.argnames.iter().zip(args.into_iter()) { 233 | context.set(name, value); 234 | } 235 | self.expr.eval(&mut context) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module defining the data structures that hold the necessary state 2 | //! that's used while evaluating expressions. 3 | 4 | mod arity; 5 | mod context; 6 | #[macro_use] 7 | mod error; 8 | pub mod function; 9 | pub mod value; 10 | 11 | pub use self::arity::{Args, ArgCount, Arity}; 12 | pub use self::context::{Context, Name}; 13 | pub use self::error::Error; 14 | pub use self::function::{Function, Invoke}; 15 | pub use self::value::Value; 16 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/value/cmp.rs: -------------------------------------------------------------------------------- 1 | //! Comparison and ordering of Value types. 2 | 3 | use std::cmp::{Ordering, PartialOrd}; 4 | 5 | use eval; 6 | use eval::util::cmp::{TryEq, TryOrd}; 7 | use super::Value; 8 | use super::types::FloatRepr; 9 | 10 | 11 | impl TryOrd for Value { 12 | type Err = eval::Error; 13 | 14 | fn try_cmp(&self, other: &Value) -> Result { 15 | match (self, other) { 16 | (&Value::Integer(a), &Value::Integer(b)) => a.partial_cmp(&b), 17 | (&Value::Integer(a), &Value::Float(b)) => (a as FloatRepr).partial_cmp(&b), 18 | (&Value::Float(a), &Value::Integer(b)) => a.partial_cmp(&(b as FloatRepr)), 19 | (&Value::Float(a), &Value::Float(b)) => a.partial_cmp(&b), 20 | 21 | (&Value::String(ref a), &Value::String(ref b)) => a.partial_cmp(b), 22 | // TODO(xion): consider implementing ordering of arrays, too 23 | 24 | _ => None, 25 | }.ok_or_else(|| eval::Error::new(&format!( 26 | "cannot compare {} with {}", self.typename(), other.typename() 27 | ))) 28 | } 29 | } 30 | impl_partialord_for_tryord!(Value); 31 | 32 | 33 | impl TryEq for Value { 34 | type Err = eval::Error; 35 | 36 | fn try_eq(&self, other: &Value) -> Result { 37 | match (self, other) { 38 | // numeric types 39 | (&Value::Integer(a), &Value::Integer(b)) => Ok(a == b), 40 | (&Value::Integer(a), &Value::Float(b)) => Ok((a as FloatRepr) == b), 41 | (&Value::Float(a), &Value::Integer(b)) => Ok(a == (b as FloatRepr)), 42 | (&Value::Float(a), &Value::Float(b)) => Ok(a == b), 43 | 44 | // others 45 | (&Value::Boolean(a), &Value::Boolean(b)) => Ok(a == b), 46 | (&Value::String(ref a), &Value::String(ref b)) => Ok(a == b), 47 | (&Value::Array(ref a), &Value::Array(ref b)) => Ok(a == b), 48 | (&Value::Object(ref a), &Value::Object(ref b)) => Ok(a == b), 49 | 50 | _ => Err(eval::Error::new(&format!( 51 | "cannot compare {} with {}", self.typename(), other.typename() 52 | ))), 53 | } 54 | } 55 | } 56 | impl_partialeq_for_tryeq!(Value); 57 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/value/conv.rs: -------------------------------------------------------------------------------- 1 | //! Module handling conversions of Value to & from Rust types. 2 | 3 | use std::convert::From; 4 | use std::str::FromStr; 5 | 6 | use conv::errors::NoError; 7 | 8 | use super::Value; 9 | use super::types::*; 10 | 11 | 12 | /// Macro to create a straighforward From -> Value::Foo implementation. 13 | macro_rules! value_from ( 14 | ($input:ty => $output:ident) => { 15 | impl From<$input> for Value { 16 | #[inline] 17 | fn from(input: $input) -> Self { 18 | Value::$output(input) 19 | } 20 | } 21 | } 22 | ); 23 | 24 | // Note how string input is deliberately omitted, for it is ambiguous. 25 | // (It could result in either Value::String or Value::Symbol). 26 | value_from!(BooleanRepr => Boolean); 27 | value_from!(IntegerRepr => Integer); 28 | value_from!(FloatRepr => Float); 29 | value_from!(RegexRepr => Regex); 30 | value_from!(ArrayRepr => Array); 31 | value_from!(ObjectRepr => Object); 32 | value_from!(FunctionRepr => Function); 33 | 34 | 35 | // Convert from characters to 1-character string values. 36 | impl From for Value { 37 | fn from(input: char) -> Self { 38 | let mut string = String::new(); 39 | string.push(input); 40 | Value::String(string) 41 | } 42 | } 43 | 44 | // Convert from bytes into integer values of those bytes. 45 | impl From for Value { 46 | fn from(input: u8) -> Self { 47 | Value::Integer(input as IntegerRepr) 48 | } 49 | } 50 | 51 | 52 | // This is a somewhat special "default" and "intelligent" conversion that's 53 | // by default applied to the expresion's input (the _ symbol). 54 | impl FromStr for Value { 55 | type Err = NoError; 56 | 57 | /// Create a Value from string, reinterpreting input as number 58 | /// if we find out it's in numeric form. 59 | fn from_str(s: &str) -> Result { 60 | if let Ok(int) = s.parse::() { 61 | return Ok(Value::Integer(int)); 62 | } 63 | if let Ok(float) = s.parse::() { 64 | return Ok(Value::Float(float)); 65 | } 66 | if let Ok(boolean) = s.parse::() { 67 | return Ok(Value::Boolean(boolean)); 68 | } 69 | Ok(Value::String(s.to_owned())) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/value/json.rs: -------------------------------------------------------------------------------- 1 | //! JSON conversions for Value. 2 | 3 | use std::convert::From; 4 | 5 | use conv::TryFrom; 6 | use conv::errors::GeneralError; 7 | use rustc_serialize::json::{Json, ToJson}; 8 | 9 | use super::types::IntegerRepr; 10 | use super::Value; 11 | 12 | 13 | impl TryFrom for Value { 14 | type Err = GeneralError; 15 | 16 | /// Parse a JSON object into a value. 17 | fn try_from(src: Json) -> Result { 18 | match src { 19 | Json::Null => Ok(Value::Empty), 20 | Json::Boolean(b) => Ok(Value::Boolean(b)), 21 | Json::I64(i) => Ok(Value::Integer(i)), 22 | Json::U64(u) => { 23 | if u > (IntegerRepr::max_value() as u64) { 24 | return Err(GeneralError::PosOverflow( 25 | format!("JSON integer too large: {}", u) 26 | )); 27 | } 28 | Ok(Value::Integer(u as IntegerRepr)) 29 | }, 30 | Json::F64(f) => Ok(Value::Float(f)), 31 | Json::String(s) => Ok(Value::String(s)), 32 | Json::Array(a) => Ok(Value::Array( 33 | a.into_iter().map(Value::from).collect() 34 | )), 35 | Json::Object(o) => Ok(Value::Object( 36 | o.into_iter().map(|(k, v)| (k, Value::from(v))).collect() 37 | )), 38 | } 39 | } 40 | } 41 | 42 | impl From for Value { 43 | fn from(input: Json) -> Self { 44 | Value::try_from(input).unwrap() 45 | } 46 | } 47 | 48 | 49 | impl ToJson for Value { 50 | /// Format the value as JSON. 51 | /// 52 | /// This is used for a variety of things, including the json() function 53 | /// and printing of Object values as final output. 54 | fn to_json(&self) -> Json { 55 | match *self { 56 | Value::Empty => Json::Null, 57 | Value::Symbol(ref t) => Json::String(t.clone()), 58 | Value::Boolean(b) => Json::Boolean(b), 59 | Value::Integer(i) => Json::I64(i), 60 | Value::Float(f) => Json::F64(f), 61 | Value::String(ref s) => Json::String(s.clone()), 62 | Value::Regex(ref r) => Json::String(r.as_str().to_owned()), 63 | Value::Array(ref a) => Json::Array( 64 | a.iter().map(|v| v.to_json()).collect() 65 | ), 66 | Value::Object(ref o) => Json::Object( 67 | o.iter().map(|(k, v)| (k.clone(), v.to_json())).collect() 68 | ), 69 | Value::Function(..) => panic!("function cannot be serialized as JSON"), 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/value/mod.rs: -------------------------------------------------------------------------------- 1 | //! Value type. 2 | //! 3 | //! Everything that expressions can operate on is encapsulated into values, 4 | //! which are tagged unions (Rust enums) with the basic types as variants. 5 | //! 6 | //! This module implements the Value type itself, as well as all the various 7 | //! conversions to and from Rust types, and serialization formats like JSON. 8 | 9 | mod cmp; 10 | mod conv; 11 | mod json; 12 | mod output; 13 | mod types; 14 | 15 | 16 | use std::fmt; 17 | use std::num::FpCategory; 18 | 19 | use conv::misc::InvalidSentinel; 20 | 21 | pub use self::types::*; 22 | 23 | 24 | // TODO: make Value an alias like: type Value = Rc; 25 | // and Data should contain actual, well, data. 26 | // this is because currently, we are cloning the values way too much 27 | // and we won't really be able to fix that 28 | 29 | 30 | /// Typed value that's operated upon. 31 | #[derive(Clone)] 32 | pub enum Value { 33 | /// No value at all. 34 | Empty, 35 | 36 | /// Symbol is a string that can be interpreted as a variable name. 37 | /// 38 | /// `Symbol("x")` shall evaluate to the value of variable `x` if one is in scope. 39 | /// Otherwise, it is an equivalent to String("x"). 40 | Symbol(SymbolRepr), 41 | 42 | // Various data types. 43 | Boolean(BooleanRepr), 44 | Integer(IntegerRepr), 45 | Float(FloatRepr), 46 | String(StringRepr), 47 | Regex(RegexRepr), 48 | Array(ArrayRepr), 49 | Object(ObjectRepr), 50 | Function(FunctionRepr), 51 | } 52 | 53 | impl Value { 54 | /// Return the type of this value as string. 55 | /// These names are user-facing, e.g. they can occur inside error messages. 56 | pub fn typename(&self) -> &'static str { 57 | match *self { 58 | Value::Empty => "empty", 59 | Value::Symbol(..) => "symbol", 60 | Value::Boolean(..) => "bool", 61 | Value::Integer(..) => "int", 62 | Value::Float(..) => "float", 63 | Value::String(..) => "string", 64 | Value::Regex(..) => "regex", 65 | Value::Array(..) => "array", 66 | Value::Object(..) => "object", 67 | Value::Function(..) => "function", 68 | } 69 | } 70 | } 71 | 72 | impl InvalidSentinel for Value { 73 | #[inline] 74 | fn invalid_sentinel() -> Self { Value::Empty } 75 | } 76 | 77 | impl fmt::Debug for Value { 78 | /// Format a Value for debugging purposes. 79 | /// This representation is not meant for consumption by end users. 80 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 81 | match *self { 82 | Value::Empty => write!(fmt, "{}", "nil"), 83 | Value::Symbol(ref t) => write!(fmt, "'{}", t), 84 | Value::Boolean(ref b) => write!(fmt, "{}", b.to_string()), 85 | Value::Integer(ref i) => write!(fmt, "{}i", i), 86 | Value::Float(ref f) => { 87 | match f.classify() { 88 | FpCategory::Nan => write!(fmt, "NaN"), 89 | FpCategory::Infinite => write!(fmt, "Inf"), 90 | _ => write!(fmt, "{}f", f), 91 | } 92 | }, 93 | Value::String(ref s) => write!(fmt, "\"{}\"", s), 94 | Value::Regex(ref r) => write!(fmt, "/{}/", r.as_str()), 95 | Value::Array(ref a) => { 96 | write!(fmt, "[{}]", a.iter() 97 | .map(|v| format!("{:?}", v)).collect::>() 98 | .join(",")) 99 | }, 100 | Value::Object(ref o) => { 101 | write!(fmt, "{{{}}}", o.iter() 102 | .map(|(k, v)| format!("\"{}\": {:?}", k, v)) 103 | .collect::>().join(",")) 104 | }, 105 | Value::Function(ref f) => write!(fmt, "{:?}", f), 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/value/output.rs: -------------------------------------------------------------------------------- 1 | //! Producing output from Value. 2 | //! 3 | //! This module defines how a Value is serialized as an output of the expression. 4 | //! See also the `json` module. 5 | 6 | #![allow(useless_format)] 7 | 8 | use std::fmt; 9 | 10 | use conv::TryFrom; 11 | use conv::errors::GeneralError; 12 | use rustc_serialize::json::ToJson; 13 | 14 | use super::Value; 15 | 16 | 17 | impl TryFrom for String { 18 | type Err = GeneralError<&'static str>; 19 | 20 | #[inline] 21 | fn try_from(src: Value) -> Result { 22 | String::try_from(&src) 23 | } 24 | } 25 | 26 | impl<'a> TryFrom<&'a Value> for String { 27 | type Err = >::Err; 28 | 29 | /// Try to convert a Value to string that can be emitted 30 | /// as a final result of a computation. 31 | fn try_from(src: &'a Value) -> Result { 32 | match *src { 33 | Value::Empty => Err(GeneralError::Unrepresentable( 34 | "cannot serialize an empty value" 35 | )), 36 | Value::Symbol(ref t) => Ok(format!("{}", t)), 37 | Value::Boolean(ref b) => Ok(format!("{}", b)), 38 | Value::Integer(ref i) => Ok(format!("{}", i)), 39 | Value::Float(ref f) => { 40 | // always include decimal point and zero, even if the float 41 | // is actually an integer 42 | let mut res = f.to_string(); 43 | if !res.contains('.') { 44 | res.push_str(".0"); 45 | } 46 | Ok(res) 47 | }, 48 | Value::String(ref s) => Ok(s.clone()), 49 | Value::Regex(..) => Err(GeneralError::Unrepresentable( 50 | "cannot serialize a regex" 51 | )), 52 | Value::Array(ref a) => { 53 | // for final display, an array is assumed to contain lines of output 54 | Ok(format!("{}", a.iter() 55 | .map(|v| format!("{}", v)).collect::>() 56 | .join("\n"))) 57 | }, 58 | Value::Object(..) => Ok(src.to_json().to_string()), 59 | Value::Function(..) => Err(GeneralError::Unrepresentable( 60 | "cannot serialize a function" 61 | )), 62 | } 63 | } 64 | } 65 | 66 | 67 | impl fmt::Display for Value { 68 | /// Format a Value for outputing it as a result of the computation. 69 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 70 | String::try_from(self) 71 | .map(|s| write!(fmt, "{}", s)) 72 | // TODO(xion): return an Err(fmt::Error) rather than panicking 73 | // when formatting constructs actually react to it constructively 74 | .expect(&format!("can't display a value of type `{}`", self.typename())) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/librush/src/eval/model/value/types.rs: -------------------------------------------------------------------------------- 1 | //! Value types and related logic. 2 | //! 3 | //! Note that "type" is not a first-class concept in the language. 4 | //! The various Value types are just a set of predefined enum variants. 5 | 6 | use std::collections::HashMap; 7 | 8 | use regex::Regex; 9 | 10 | use eval::model::Function; 11 | use super::Value; 12 | 13 | 14 | // Representations of various possible types of Value. 15 | pub type SymbolRepr = String; 16 | pub type BooleanRepr = bool; 17 | pub type IntegerRepr = i64; 18 | pub type FloatRepr = f64; 19 | pub type StringRepr = String; 20 | pub type RegexRepr = Regex; 21 | pub type ArrayRepr = Vec; 22 | pub type ObjectRepr = HashMap; 23 | pub type FunctionRepr = Function; 24 | 25 | 26 | /// Macro to implement methods on Value that deal with its various types. 27 | /// For a type X, those methods are is_X(), unwrap_X(), as_X(), and as_mut_X(). 28 | macro_rules! impl_value_type { 29 | // Unfortunately, due to Rust's strict macro hygiene requirements 30 | // and the general uselessness of concat_idents!, all the method names have to be given 31 | // as macro arguments. 32 | // More details: https://github.com/rust-lang/rust/issues/12249 33 | ($variant:ident($t:ty) => ($is:ident, $unwrap:ident, $as_:ident, $as_mut:ident)) => ( 34 | impl Value { 35 | /// Check whether Value is of type $t. 36 | #[inline] 37 | pub fn $is(&self) -> bool { 38 | match *self { Value::$variant(..) => true, _ => false, } 39 | } 40 | 41 | /// Consumes the Value, returning the underlying $t. 42 | /// Panics if the Value is not a $t. 43 | #[inline] 44 | pub fn $unwrap(self) -> $t { 45 | match self { 46 | Value::$variant(x) => x, 47 | _ => panic!(concat!(stringify!($unwrap), "() on {} value"), self.typename()), 48 | } 49 | } 50 | 51 | /// Returns a reference to the underlying $t. 52 | /// Panics if the Value is not a $t. 53 | #[inline] 54 | pub fn $as_(&self) -> &$t { 55 | match *self { 56 | Value::$variant(ref x) => x, 57 | _ => panic!(concat!(stringify!($as_), "() on {} value"), self.typename()), 58 | } 59 | } 60 | 61 | /// Returns a mutable reference to the underlying $t. 62 | /// Panics if the Value is not a $t. 63 | #[inline] 64 | pub fn $as_mut(&mut self) -> &mut $t { 65 | match *self { 66 | Value::$variant(ref mut x) => x, 67 | _ => panic!(concat!(stringify!($as_mut), "() on {} value"), self.typename()) 68 | } 69 | } 70 | } 71 | ); 72 | } 73 | 74 | impl_value_type!(Boolean(BooleanRepr) => (is_bool, unwrap_bool, as_bool, as_mut_bool)); 75 | impl_value_type!(Integer(IntegerRepr) => (is_integer, unwrap_integer, as_integer, as_mut_integer)); 76 | impl_value_type!(Float(FloatRepr) => (is_float, unwrap_float, as_float, as_mut_float)); 77 | impl_value_type!(String(StringRepr) => (is_string, unwrap_string, as_string, as_mut_string)); 78 | impl_value_type!(Regex(RegexRepr) => (is_regex, unwrap_regex, as_regex, as_mut_regex)); 79 | impl_value_type!(Array(ArrayRepr) => (is_array, unwrap_array, as_array, as_mut_array)); 80 | impl_value_type!(Object(ObjectRepr) => (is_object, unwrap_object, as_object, as_mut_object)); 81 | impl_value_type!(Function(FunctionRepr) => (is_function, unwrap_function, as_function, as_mut_function)); 82 | 83 | impl_value_type!(Integer(IntegerRepr) => (is_int, unwrap_int, as_int, as_mut_int)); // alias 84 | impl_value_type!(String(StringRepr) => (is_str, unwrap_str, as_str, as_mut_str)); // alias 85 | 86 | 87 | /// Additional methods that deal with more than one type at once. 88 | impl Value { 89 | #[inline] 90 | pub fn is_collection(&self) -> bool { 91 | self.is_array() || self.is_object() 92 | } 93 | 94 | #[inline] 95 | pub fn is_scalar(&self) -> bool { 96 | self.is_bool() || self.is_int() || self.is_float() || self.is_string() 97 | } 98 | 99 | #[inline] 100 | pub fn is_number(&self) -> bool { 101 | self.is_int() || self.is_float() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /crates/librush/src/eval/operators/conditional.rs: -------------------------------------------------------------------------------- 1 | //! Module implementating evaluation of conditonal operator AST nodes. 2 | 3 | use eval::{self, api, Context, Eval}; 4 | use parse::ast::ConditionalNode; 5 | 6 | 7 | /// Evaluate the ternary operator / conditional node. 8 | impl Eval for ConditionalNode { 9 | #[inline] 10 | fn eval(&self, context: &mut Context) -> eval::Result { 11 | let condition = try!( 12 | self.cond.eval(context).and_then(api::conv::bool) 13 | ).unwrap_bool(); 14 | if condition { 15 | self.then.eval(context) 16 | } else { 17 | self.else_.eval(context) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/librush/src/eval/operators/curried.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing evaluation of curried binary operator AST nodes. 2 | 3 | use eval::{self, Eval, Context, Value}; 4 | use eval::model::{Args, ArgCount, Arity, Function}; 5 | use parse::ast::{BinaryOpNode, CurriedBinaryOpNode}; 6 | 7 | 8 | impl Eval for CurriedBinaryOpNode { 9 | fn eval(&self, context: &mut Context) -> eval::Result { 10 | if let Some(ref left) = self.left { 11 | let arg = try!(left.eval(context)); 12 | return self.eval_with_left(arg); 13 | } 14 | if let Some(ref right) = self.right { 15 | let arg = try!(right.eval(context)); 16 | return self.eval_with_right(arg); 17 | } 18 | self.eval_with_none() 19 | } 20 | } 21 | 22 | impl CurriedBinaryOpNode { 23 | fn eval_with_left(&self, arg: Value) -> eval::Result { 24 | let op = self.op.clone(); 25 | let func = move |args: Args, ctx: &Context| { 26 | let other = try!(take_one_arg(args)); 27 | BinaryOpNode::eval_op(&op, arg.clone(), other, &ctx) 28 | }; 29 | Ok(Value::Function(Function::from_native_ctx(Arity::Exact(1), func))) 30 | } 31 | 32 | fn eval_with_right(&self, arg: Value) -> eval::Result { 33 | let op = self.op.clone(); 34 | let func = move |args: Args, ctx: &Context| { 35 | let other = try!(take_one_arg(args)); 36 | BinaryOpNode::eval_op(&op, other, arg.clone(), &ctx) 37 | }; 38 | Ok(Value::Function(Function::from_native_ctx(Arity::Exact(1), func))) 39 | } 40 | 41 | fn eval_with_none(&self) -> eval::Result { 42 | let op = self.op.clone(); 43 | let func = move |args: Args, ctx: &Context| { 44 | let (left, right) = try!(take_two_args(args)); 45 | BinaryOpNode::eval_op(&op, left, right, &ctx) 46 | }; 47 | Ok(Value::Function(Function::from_native_ctx(Arity::Exact(2), func))) 48 | } 49 | } 50 | 51 | 52 | // Utility functions 53 | 54 | fn take_one_arg(args: Args) -> eval::Result { 55 | try!(ensure_argcount(&args, 1)); 56 | Ok(args.into_iter().next().unwrap()) 57 | } 58 | fn take_two_args(args: Args) -> Result<(Value, Value), eval::Error> { 59 | try!(ensure_argcount(&args, 2)); 60 | let mut args = args.into_iter(); 61 | Ok((args.next().unwrap(), args.next().unwrap())) 62 | } 63 | 64 | fn ensure_argcount(args: &Args, count: ArgCount) -> Result<(), eval::Error> { 65 | if args.len() != count { 66 | return Err(eval::Error::new(&format!( 67 | "invalid number of arguments: expected {}, got {}", 68 | count, args.len() 69 | ))); 70 | } 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /crates/librush/src/eval/operators/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing evaluation of operator-related AST nodes. 2 | 3 | mod unary; 4 | mod binary; 5 | mod curried; 6 | mod conditional; 7 | -------------------------------------------------------------------------------- /crates/librush/src/eval/operators/unary.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing evaluation of unary operator AST nodes. 2 | 3 | use eval::{self, api, Eval, Context, Value}; 4 | use parse::ast::UnaryOpNode; 5 | 6 | 7 | impl Eval for UnaryOpNode { 8 | fn eval(&self, context: &mut Context) -> eval::Result { 9 | let arg = try!(self.arg.eval(context)); 10 | match &self.op[..] { 11 | "+" => UnaryOpNode::eval_plus(arg), 12 | "-" => UnaryOpNode::eval_minus(arg), 13 | "!" => UnaryOpNode::eval_bang(arg), 14 | _ => Err(eval::Error::new( 15 | &format!("unknown unary operator: `{}`", self.op) 16 | )) 17 | } 18 | } 19 | } 20 | 21 | impl UnaryOpNode { 22 | /// Evaluate the "+" operator for one value. 23 | fn eval_plus(arg: Value) -> eval::Result { 24 | eval1!(arg : Integer { arg }); 25 | eval1!(arg : Float { arg }); 26 | UnaryOpNode::err("+", &arg) 27 | } 28 | 29 | /// Evaluate the "-" operator for one value. 30 | fn eval_minus(arg: Value) -> eval::Result { 31 | eval1!(arg : Integer { -arg }); 32 | eval1!(arg : Float { -arg }); 33 | UnaryOpNode::err("-", &arg) 34 | } 35 | 36 | /// Evaluate the "!" operator for one value. 37 | #[inline] 38 | fn eval_bang(arg: Value) -> eval::Result { 39 | let arg = try!(api::conv::bool(arg)).unwrap_bool(); 40 | Ok(Value::Boolean(!arg)) 41 | } 42 | } 43 | 44 | impl UnaryOpNode { 45 | /// Produce an error about invalid argument for an operator. 46 | #[inline] 47 | fn err(op: &str, arg: &Value) -> eval::Result { 48 | Err(eval::Error::new(&format!( 49 | "invalid argument for `{}` operator: `{:?}`", op, arg 50 | ))) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/librush/src/eval/util/cmp.rs: -------------------------------------------------------------------------------- 1 | //! Custom traits related to ordering and comparison. 2 | 3 | use std::cmp::Ordering; 4 | 5 | 6 | /// Trait for values that can be optionally compared for a sort-order. 7 | /// 8 | /// Unlike PartialOrd, this one treats the unspecified ordering of values 9 | /// as an erroneous condition. As such, it is more similar to Ord, 10 | /// and also analogous to how TryFrom and TryInto traits from the conv crate 11 | /// relate to the standard From and Into traits. 12 | pub trait TryOrd { 13 | type Err; 14 | fn try_cmp(&self, other: &Rhs) -> Result; 15 | 16 | fn try_lt(&self, other: &Rhs) -> Result { 17 | self.try_cmp(other).map(|o| o == Ordering::Less) 18 | } 19 | fn try_le(&self, other: &Rhs) -> Result { 20 | self.try_cmp(other).map(|o| o == Ordering::Less || o == Ordering::Equal) 21 | } 22 | fn try_gt(&self, other: &Rhs) -> Result { 23 | self.try_cmp(other).map(|o| o == Ordering::Greater) 24 | } 25 | fn try_ge(&self, other: &Rhs) -> Result { 26 | self.try_cmp(other).map(|o| o == Ordering::Greater || o == Ordering::Equal) 27 | } 28 | } 29 | 30 | /// Trait for equality comparisons that may fail. 31 | /// 32 | /// Unlike Eq & PartialEq, this one treats the situation where two values cannot 33 | /// be compared as an error. As such, it is somewhat analogous to how 34 | /// TryFrom and TryInto traits from the conv crate relate to the standard 35 | /// From and Into traits. 36 | pub trait TryEq { 37 | type Err; 38 | fn try_eq(&self, other: &Rhs) -> Result; 39 | 40 | fn try_ne(&self, other: &Rhs) -> Result { 41 | self.try_eq(other).map(|b| !b) 42 | } 43 | } 44 | 45 | 46 | // Macros for implementing PartialX from TryX 47 | 48 | // These macros are necessary, because Rust only allows traits defined with current crate 49 | // to be impl'd for template params. This makes the following generic impl illegal: 50 | // 51 | // impl PartialOrd for T where T: TryOrd { ... } 52 | // 53 | // The macros allow to at least reduce the boilerplate for creating those impls 54 | // for a particular type to minimum. 55 | 56 | macro_rules! impl_partialord_for_tryord ( 57 | ($t:ty) => { 58 | impl ::std::cmp::PartialOrd for $t { 59 | fn partial_cmp(&self, other: &$t) -> Option<::std::cmp::Ordering> { 60 | self.try_cmp(other).ok() 61 | } 62 | } 63 | }; 64 | ); 65 | 66 | macro_rules! impl_partialeq_for_tryeq ( 67 | ($t:ty) => { 68 | impl ::std::cmp::PartialEq for $t { 69 | fn eq(&self, other: &$t) -> bool { 70 | self.try_eq(other).unwrap_or(false) 71 | } 72 | } 73 | }; 74 | ); 75 | -------------------------------------------------------------------------------- /crates/librush/src/eval/util/fmt.rs: -------------------------------------------------------------------------------- 1 | //! String formatting at runtime. 2 | //! Poached from https://github.com/panicbit/monster & refined a little. 3 | 4 | #![allow(dead_code)] 5 | 6 | 7 | use std::error; 8 | use std::fmt::{self, Display, Write}; 9 | use std::result; 10 | 11 | 12 | /// Format a string. 13 | /// The format syntax is similar to the one used by `std::fmt`, 14 | /// but very limited at the moment. 15 | /// 16 | /// # Example 17 | /// 18 | /// ``` 19 | /// let fmt = "You see {{{}}} tiny {}"; 20 | /// let result = format(fmt , &[&10, &"monsters"]); 21 | /// 22 | /// assert_eq!(result.unwrap(), "You see {10} tiny monsters"); 23 | /// ``` 24 | pub fn format(fmt: &str, args: &[&Display]) -> Result { 25 | let mut buffer = String::with_capacity(fmt.len()); 26 | try!(write_format(&mut buffer, fmt, args)); 27 | Ok(buffer) 28 | } 29 | 30 | /// Same as `format` but writes to a generic buffer instead. 31 | pub fn write_format(buffer: &mut W, fmt: &str, args: &[&Display]) -> Result<()> { 32 | let mut args = args.iter(); 33 | let mut state = Normal; 34 | 35 | for ch in fmt.chars() { 36 | match state { 37 | Normal => match ch { 38 | '{' => state = LeftBrace, 39 | '}' => state = RightBrace, 40 | _ => try!(buffer.write_char(ch)) 41 | }, 42 | LeftBrace => match ch { 43 | // An escaped '{' 44 | '{' => { 45 | try!(buffer.write_char(ch)); 46 | state = Normal 47 | }, 48 | // An escaped '}' 49 | '}' => { 50 | match args.next() { 51 | Some(arg) => try!(write!(buffer, "{}", arg)), 52 | None => return Err(Error::NotEnoughArgs) 53 | }; 54 | state = Normal 55 | }, 56 | // No named placeholders allowed 57 | _ => return Err(Error::UnexpectedChar) 58 | }, 59 | RightBrace => match ch { 60 | '}' => { 61 | try!(buffer.write_char(ch)); 62 | state = Normal 63 | }, 64 | // No standalone right brace allowed 65 | _ => return Err(Error::UnexpectedRightBrace) 66 | } 67 | } 68 | } 69 | Ok(()) 70 | } 71 | 72 | 73 | enum State { 74 | Normal, 75 | LeftBrace, 76 | RightBrace, 77 | } 78 | use self::State::*; 79 | 80 | 81 | // Error & result type 82 | 83 | pub type Result = result::Result; 84 | 85 | #[derive(Debug,Eq,PartialEq,Copy,Clone,Hash)] 86 | pub enum Error { 87 | NotEnoughArgs, 88 | UnexpectedChar, 89 | UnexpectedRightBrace, 90 | Unkown 91 | } 92 | 93 | impl fmt::Display for Error { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | use std::error::Error; 96 | write!(f, "Formatting error: {}", self.description()) 97 | } 98 | } 99 | 100 | impl error::Error for Error { 101 | fn description(&self) -> &str { 102 | match *self { 103 | Error::NotEnoughArgs => "not enough arguments passed", 104 | Error::UnexpectedChar => "unexpected character", 105 | Error::UnexpectedRightBrace => "unexpected right brace", 106 | Error::Unkown => "unknown error" 107 | } 108 | } 109 | } 110 | 111 | impl From for Error { 112 | fn from(_: fmt::Error) -> Error { 113 | Error::Unkown 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/librush/src/eval/util/macros.rs: -------------------------------------------------------------------------------- 1 | ///! Macros that make type-safe function definitions more concise. 2 | 3 | 4 | // A few tips on how to read and/or modify these macros: 5 | // 6 | // * Rules that are more general (like those where argtypes != return type) 7 | // come BEFORE rules that are more specific (and in turn use those general 8 | // rules in their implementation). 9 | // 10 | // This is because the macro parser scan them top-down and picks 11 | // the first match. The more general rules -- with additional tokens 12 | // over spcific rules, like `where` -- have thus to be excluded ASAP 13 | // if they don't apply. This way, the specific ones (which share much 14 | // of the syntax) have a chance to be matched. 15 | // 16 | // * Rules that generate variants operating on pointer types (e.g. &String) 17 | // have to be placed BEFORE those working with copied types (e.g. Integer). 18 | // For multi-argument variants, sequences of pointer type that are: 19 | // (1) longer -- e.g. (&a, &b) vs. (&a, b) 20 | // (2) closer to the beginning of argument list -- e.g. (&a, b) vs. (a, &b) 21 | // must come first. 22 | // 23 | // This ensures the (limited) Rust's macro parser can correctly identify 24 | // the variant to use by just scanning one token at a time (it's LL(1)). 25 | // Unfortunately, this also means certain patterns are ambiguous, 26 | // like (&a, b, c) and (&a, b, &c), and only one of them can be supported. 27 | // 28 | // * For simplicity, rules that come later should always refer to 29 | // the MOST general previous rule in their implementation. This way 30 | // we won't have to trace multi-layered macro expansions when debugging, 31 | // also reducing the risk of running into further limitations of Rust 32 | // macro parser. 33 | 34 | 35 | /// Evaluate a unary expression provided the argument match declared Value type. 36 | /// 37 | /// Example usage: 38 | /// eval1!(arg: Integer { arg + 42 }); 39 | /// 40 | macro_rules! eval1 { 41 | // (arg: &Foo) -> Bar where (pre()) { foo(arg) } 42 | (($x:ident: &$t:ident) -> $rt:ident where ($pre:expr) { $e:expr }) => { 43 | if let Value::$t(ref $x) = $x { 44 | if $pre { 45 | return Ok(Value::$rt($e)); 46 | } 47 | } 48 | }; 49 | // (arg: &Foo) -> Bar { foo(arg) } 50 | (($x:ident: &$t:ident) -> $rt:ident { $e:expr }) => { 51 | eval1!(($x: &$t) -> $rt where (true) { $e }); 52 | }; 53 | 54 | // (arg: Foo) -> Bar where (pre()) { foo(arg) } 55 | (($x:ident: $t:ident) -> $rt:ident where ($pre:expr) { $e:expr }) => { 56 | if let Value::$t($x) = $x { 57 | if $pre { 58 | return Ok(Value::$rt($e)); 59 | } 60 | } 61 | }; 62 | // (arg: Foo) -> Bar { foo(arg) } 63 | (($x:ident: $t:ident) -> $rt:ident { $e:expr }) => { 64 | eval1!(($x: $t) -> $rt where (true) { $e }); 65 | }; 66 | 67 | // arg : &Foo { foo(arg) } 68 | ($x:ident : &$t:ident { $e:expr }) => { 69 | eval1!(($x: &$t) -> $t where (true) { $e }); 70 | }; 71 | // arg : Foo { foo(arg) } 72 | ($x:ident : $t:ident { $e:expr }) => { 73 | eval1!(($x: $t) -> $t where (true) { $e }); 74 | }; 75 | } 76 | 77 | 78 | /// Evaluate a binary expression provided the argument match declared Value types. 79 | /// 80 | /// Example usage: 81 | /// eval2!(left, right: Integer { left + right }); 82 | /// 83 | macro_rules! eval2 { 84 | // (left: &Foo, right: &Bar) -> Baz where (pre()) { foo(left, right) } 85 | (($x:ident: &$t1:ident, $y:ident: &$t2:ident) -> $rt:ident where ($pre:expr) { $e:expr }) => { 86 | if let Value::$t1(ref $x) = $x { 87 | if let Value::$t2(ref $y) = $y { 88 | if $pre { 89 | return Ok(Value::$rt($e)); 90 | } 91 | } 92 | } 93 | }; 94 | // (left: &Foo, right: &Bar) -> Baz { foo(left, right) } 95 | (($x:ident: &$t1:ident, $y:ident: &$t2:ident) -> $rt:ident { $e:expr }) => { 96 | eval2!(($x: &$t1, $y: &$t2) -> $rt where (true) { $e }); 97 | }; 98 | 99 | // (left: &Foo, right: Bar) -> Baz where (pre()) { foo(left, right) } 100 | (($x:ident: &$t1:ident, $y:ident: $t2:ident) -> $rt:ident where ($pre:expr) { $e:expr }) => { 101 | if let Value::$t1(ref $x) = $x { 102 | if let Value::$t2($y) = $y { 103 | if $pre { 104 | return Ok(Value::$rt($e)); 105 | } 106 | } 107 | } 108 | }; 109 | // (left: &Foo, right: Bar) -> Baz { foo(left, right) } 110 | (($x:ident: &$t1:ident, $y:ident: $t2:ident) -> $rt:ident { $e:expr }) => { 111 | eval2!(($x: &$t1, $y: $t2) -> $rt where (true) { $e }); 112 | }; 113 | 114 | // (left: Foo, right: &Bar) -> Baz where (pre()) { foo(left, right) } 115 | (($x:ident: $t1:ident, $y:ident: &$t2:ident) -> $rt:ident where ($pre:expr) { $e:expr }) => { 116 | if let Value::$t1($x) = $x { 117 | if let Value::$t2(ref $y) = $y { 118 | if $pre { 119 | return Ok(Value::$rt($e)); 120 | } 121 | } 122 | } 123 | }; 124 | // (left: Foo, right: &Bar)-> Baz { foo(left, right) } 125 | (($x:ident: $t1:ident, $y:ident: &$t2:ident) -> $rt:ident { $e:expr }) => { 126 | eval2!(($x: $t1, $y: &$t2) -> $rt where (true) { $e }); 127 | }; 128 | 129 | // (left: Foo, right: Bar) -> Baz where (pre()) { foo(left, right) } 130 | (($x:ident: $t1:ident, $y:ident: $t2:ident) -> $rt:ident where ($pre:expr) { $e:expr }) => { 131 | if let Value::$t1($x) = $x { 132 | if let Value::$t2($y) = $y { 133 | if $pre { 134 | return Ok(Value::$rt($e)); 135 | } 136 | } 137 | } 138 | }; 139 | // (left: Foo, right: Bar) -> Baz { foo(left, right) } 140 | (($x:ident: $t1:ident, $y:ident: $t2:ident) -> $rt:ident { $e:expr }) => { 141 | eval2!(($x: $t1, $y: $t2) -> $rt where (true) { $e }); 142 | }; 143 | 144 | // left, right : &Foo { foo(left, right) } 145 | ($x:ident, $y:ident : &$t:ident { $e:expr }) => { 146 | eval2!(($x: &$t, $y: &$t) -> $t where (true) { $e }); 147 | }; 148 | // left, right : Foo { foo(left, right) } 149 | ($x:ident, $y:ident : $t:ident { $e:expr }) => { 150 | eval2!(($x: $t, $y: $t) -> $t where (true) { $e }); 151 | }; 152 | } 153 | 154 | 155 | // TODO(xion): define eval3!(...) 156 | -------------------------------------------------------------------------------- /crates/librush/src/eval/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utility module used by code that evaluates expressions. 2 | 3 | #[macro_use] 4 | mod macros; 5 | 6 | #[macro_use] 7 | pub mod cmp; 8 | pub mod fmt; 9 | -------------------------------------------------------------------------------- /crates/librush/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Root module of the rush library crate. 2 | 3 | #![allow(unknown_lints)] // for Clippy 4 | 5 | // NOTE: `nom` has to be declared before `log` because both define an error! 6 | // macro, and we want to use the one from `log`. 7 | #[macro_use] extern crate nom; 8 | #[macro_use] extern crate log; 9 | 10 | extern crate conv; 11 | extern crate csv; 12 | extern crate fnv; 13 | #[macro_use] extern crate lazy_static; 14 | #[macro_use] extern crate mopa; 15 | extern crate rand; 16 | extern crate regex; 17 | extern crate rustc_serialize; 18 | extern crate unicode_categories; 19 | extern crate unicode_normalization; 20 | extern crate unicode_segmentation; 21 | extern crate unidecode; 22 | 23 | 24 | mod eval; 25 | mod parse; 26 | mod wrappers; 27 | 28 | 29 | pub use self::eval::*; 30 | pub use self::parse::parse; 31 | pub use self::wrappers::*; 32 | -------------------------------------------------------------------------------- /crates/librush/src/parse/error.rs: -------------------------------------------------------------------------------- 1 | //! Parse error type. 2 | 3 | use std::error::Error as StdError; 4 | use std::fmt; 5 | 6 | use nom::Needed; 7 | 8 | 9 | /// Error from parsing an expression. 10 | #[derive(Clone,Debug)] 11 | pub enum Error { 12 | /// Empty input. 13 | Empty, 14 | /// Not an UTF8 input. 15 | Corrupted, 16 | /// Parse error (input doesn't follow valid expression syntax). 17 | // TODO(xion): include more information, like the offending character index 18 | Invalid, 19 | /// Extra input beyond what's allowed by expression syntax. 20 | Excess(String), 21 | /// Unexpected end of input. 22 | Incomplete(Needed), 23 | } 24 | 25 | impl Error { 26 | /// Whether the error can be interpreted as simple syntax error. 27 | pub fn is_syntax(&self) -> bool { 28 | match *self { 29 | Error::Empty | Error::Corrupted => false, 30 | _ => true 31 | } 32 | } 33 | } 34 | 35 | impl fmt::Display for Error { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 37 | match *self { 38 | Error::Excess(ref s) => 39 | write!(f, "expected end of expression, but found `{}`", s), 40 | Error::Incomplete(_) => 41 | write!(f, "unexpected end of expression"), 42 | _ => write!(f, "{}", self.description()), 43 | } 44 | } 45 | } 46 | 47 | impl StdError for Error { 48 | fn description(&self) -> &str { 49 | match *self { 50 | Error::Empty => "empty expression", 51 | Error::Corrupted => "non-UTF8 expression", 52 | Error::Invalid => "syntax error", 53 | Error::Excess(_) => "unexpected expression", 54 | Error::Incomplete(_) => "unexpected end of expression", 55 | } 56 | } 57 | 58 | #[allow(match_same_arms)] 59 | fn cause(&self) -> Option<&StdError> { 60 | match *self { 61 | Error::Empty | 62 | Error::Excess(_) | 63 | Error::Incomplete(_) => None, 64 | // TODO(xion): for the rest, we could store or recreate 65 | // the original Error to return it as cause here 66 | _ => None, 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/librush/src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | //! Parser code for the expression syntax. 2 | 3 | mod error; 4 | mod syntax; 5 | 6 | pub mod ast; 7 | pub use self::error::Error; 8 | 9 | 10 | use std::str::from_utf8; 11 | 12 | use nom::IResult; 13 | 14 | use eval::Eval; 15 | use self::syntax::expression; 16 | 17 | 18 | /// Parse given expression, returning the AST that represents it. 19 | pub fn parse(input: &str) -> Result, Error> { 20 | if input.is_empty() { 21 | return Err(Error::Empty); 22 | } 23 | 24 | match expression(input.trim().as_bytes()) { 25 | IResult::Done(input, node) => { 26 | if input.is_empty() { 27 | Ok(node) 28 | } else { 29 | Err(match from_utf8(input) { 30 | Ok(i) => Error::Excess(i.to_owned()), 31 | // TODO(xion): bubble the error from the various 32 | // from_utf8 calls in grammar rules 33 | _ => Error::Corrupted, 34 | }) 35 | } 36 | }, 37 | IResult::Incomplete(needed) => Err(Error::Incomplete(needed)), 38 | IResult::Error(_) => Err(Error::Invalid), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/librush/src/parse/syntax/literals.rs: -------------------------------------------------------------------------------- 1 | //! Parser for the literal ("atomic") values, like numbers and strings. 2 | 3 | use std::f64; 4 | use std::str::from_utf8; 5 | 6 | use nom::{self, alpha, alphanumeric, IResult}; 7 | use regex::Regex; 8 | 9 | use eval::{Eval, Value}; 10 | use eval::value::{FloatRepr, IntegerRepr, RegexRepr, StringRepr}; 11 | use parse::ast::{ArrayNode, ObjectNode, ScalarNode}; 12 | use super::structure::expression; 13 | 14 | 15 | // TODO(xion): switch from parsers expecting &[u8] to accepting &str; 16 | // this will get rid of the hack in float_literal() and possibly other cruft 17 | 18 | 19 | const RESERVED_WORDS: &'static [&'static str] = &[ 20 | "const", "do", "else", "false", "for", "if", "let", "true", "while", 21 | ]; 22 | 23 | const DIGITS: &'static str = "0123456789"; 24 | const FLOAT_REGEX: &'static str = r"(0|[1-9][0-9]*)\.[0-9]+([eE][+-]?[1-9][0-9]*)?"; 25 | const ESCAPE: &'static str = "\\"; 26 | 27 | const UNDERSCORE_SUFFIXES: &'static str = "bifs"; 28 | 29 | 30 | /// identifier ::== ('_' SUFFIX?) | (ALPHA ALPHANUMERIC*) 31 | named!(pub identifier( &[u8] ) -> String, alt!( 32 | string!(seq!(tag!("_"), maybe!(char_of!(UNDERSCORE_SUFFIXES)))) | 33 | map_res!(string!(seq!(alpha, many0!(alphanumeric))), |ident: String| { 34 | { 35 | let id: &str = &ident; 36 | if RESERVED_WORDS.contains(&id) { 37 | // TODO(xion): better error handling for the reserved word case 38 | // (note that map_res! generally discards errors so we may have 39 | // to use fix_error!, add_error!, or error!) 40 | return Err(()); 41 | } 42 | } 43 | Ok(ident) 44 | }) 45 | )); 46 | 47 | /// atom ::== PRIMITIVE | '(' expression ')' 48 | /// PRIMITIVE ::== NIL | OBJECT | ARRAY | BOOLEAN | FLOAT | INTEGER | SYMBOL | REGEX | STRING 49 | named!(pub atom( &[u8] ) -> Box, alt!( 50 | // 51 | // Note that order of those branches matters. 52 | // Literals that have special case'd identifiers as valid values 53 | // -- like floats with their NaN -- have to be before symbols! 54 | // 55 | nil_value | 56 | object_value | array_value | 57 | bool_value | float_value | int_value | symbol_value | 58 | regex_value | string_value | 59 | delimited!(multispaced!(tag!("(")), expression, multispaced!(tag!(")"))) 60 | )); 61 | 62 | named!(nil_value( &[u8] ) -> Box, map!(tag!("nil"), |_| { 63 | Box::new(ScalarNode::from(Value::Empty)) 64 | })); 65 | 66 | /// OBJECT ::== '{' [expression ':' expression] (',' expression ':' expression)* '}' 67 | named!(object_value( &[u8] ) -> Box, map!( 68 | delimited!( 69 | multispaced!(tag!("{")), 70 | separated_list!( 71 | multispaced!(tag!(",")), 72 | separated_pair!(expression, multispaced!(tag!(":")), expression) 73 | ), 74 | multispaced!(tag!("}")) 75 | ), 76 | |attrs| { Box::new(ObjectNode::new(attrs)) } 77 | )); 78 | 79 | /// ARRAY ::== '[' [expression] (',' expression)* ']' 80 | named!(array_value( &[u8] ) -> Box, map!( 81 | delimited!( 82 | multispaced!(tag!("[")), 83 | separated_list!(multispaced!(tag!(",")), expression), 84 | multispaced!(tag!("]")) 85 | ), 86 | |items| { Box::new(ArrayNode::new(items)) } 87 | )); 88 | 89 | named!(bool_value( &[u8] ) -> Box, alt!( 90 | tag!("false") => { |_| Box::new(ScalarNode::from(false)) } | 91 | tag!("true") => { |_| Box::new(ScalarNode::from(true)) } 92 | )); 93 | 94 | named!(symbol_value( &[u8] ) -> Box, map!(identifier, |value: String| { 95 | Box::new(ScalarNode::from(Value::Symbol(value))) 96 | })); 97 | 98 | named!(int_value( &[u8] ) -> Box, map_res!(int_literal, |value: String| { 99 | value.parse::().map(ScalarNode::from).map(Box::new) 100 | })); 101 | named!(int_literal( &[u8] ) -> String, string!(alt!( 102 | seq!(char_of!(&DIGITS[1..]), many0!(char_of!(DIGITS))) | tag!("0") 103 | ))); 104 | 105 | named!(float_value( &[u8] ) -> Box, alt!( 106 | tag!("Inf") => { |_| Box::new(ScalarNode::from(f64::INFINITY as FloatRepr)) } | 107 | tag!("NaN") => { |_| Box::new(ScalarNode::from(f64::NAN as FloatRepr)) } | 108 | map_res!(float_literal, |value: String| { 109 | value.parse::().map(ScalarNode::from).map(Box::new) 110 | }) 111 | )); 112 | fn float_literal(input: &[u8]) -> IResult<&[u8], String> { 113 | let (_, input) = try_parse!(input, expr_res!(from_utf8(input))); 114 | 115 | // TODO(xion): use *_static! variant when regexp_macros feature 116 | // can be used in stable Rust 117 | let regex = "^".to_owned() + FLOAT_REGEX; // 'coz we want immediate match 118 | let result = re_find!(input, ®ex); 119 | 120 | // This match has to be explicit (rather than try_parse! etc.) 121 | // because of the silly IResult::Error branch, which is seemingly no-op 122 | // but it forces the result to be of correct type (nom::Err<&[u8]> 123 | // rather than nom::Err<&str> returned by regex parser). 124 | // TODO(xion): consider switching all parsers to &str->&str 125 | // to avoid this hack and the various map_res!(..., from_utf8) elsewhere 126 | match result { 127 | IResult::Done(rest, parsed) => 128 | IResult::Done(rest.as_bytes(), String::from(parsed)), 129 | IResult::Incomplete(i) => IResult::Incomplete(i), 130 | IResult::Error(nom::Err::Code(e)) => IResult::Error(nom::Err::Code(e)), 131 | r => unreachable!("unexpected result from parsing float: {:?}", r), 132 | } 133 | } 134 | 135 | named!(regex_value( &[u8] ) -> Box, map!(regex_literal, |value: RegexRepr| { 136 | Box::new(ScalarNode::from(value)) 137 | })); 138 | fn regex_literal(input: &[u8]) -> IResult<&[u8], Regex> { 139 | let (mut input, _) = try_parse!(input, tag!("/")); 140 | 141 | // consume chacters until the closing slash 142 | let mut r = String::new(); 143 | loop { 144 | let (rest, chunk) = try_parse!(input, 145 | string!(take_until_and_consume!("/"))); 146 | r.push_str(&chunk); 147 | 148 | input = rest; 149 | if input.is_empty() { 150 | break; 151 | } 152 | 153 | // Try to parse what we've got so far as a regex; 154 | // if it succeeds, then this is our result. 155 | // Note that this will handle escaping of the slash 156 | // (to make it literal part of the regex) through character class [/], 157 | // since unterminated square bracket won't parse as regex. 158 | if let Ok(regex) = Regex::new(&r) { 159 | return IResult::Done(input, regex); 160 | } 161 | 162 | r.push('/'); 163 | } 164 | 165 | // If we exhausted the input, then whatever we've accumulated so far 166 | // may still be a valid regex, so try to parse it. 167 | expr_res!(input, Regex::new(&r)) 168 | } 169 | 170 | named!(string_value( &[u8] ) -> Box, map!(string_literal, |value: StringRepr| { 171 | Box::new(ScalarNode::from(Value::String(value))) 172 | })); 173 | #[allow(single_char_pattern)] 174 | fn string_literal(input: &[u8]) -> IResult<&[u8], String> { 175 | let (mut input, _) = try_parse!(input, tag!("\"")); 176 | 177 | // consume characters until the closing double quote 178 | let mut s = String::new(); 179 | loop { 180 | let (rest, chunk) = try_parse!(input, 181 | string!(take_until_and_consume!("\""))); 182 | input = rest; 183 | 184 | if chunk.is_empty() { 185 | break; 186 | } 187 | s.push_str(&chunk); 188 | 189 | // however, if the quote was escaped, the string continues beyond it 190 | // and requires parsing of another chunk 191 | if !chunk.ends_with(ESCAPE) { 192 | break; 193 | } 194 | s.push('"'); 195 | } 196 | 197 | // replace the escape sequences with corresponding characters 198 | s = s.replace(&format!("{}\"", ESCAPE), "\""); // double quotes 199 | s = s.replace(&format!("{}n", ESCAPE), "\n"); 200 | s = s.replace(&format!("{}r", ESCAPE), "\r"); 201 | s = s.replace(&format!("{}t", ESCAPE), "\t"); 202 | s = s.replace(&format!("{}{}", ESCAPE, ESCAPE), ESCAPE); // must be last 203 | 204 | IResult::Done(input, s) 205 | } 206 | -------------------------------------------------------------------------------- /crates/librush/src/parse/syntax/mod.rs: -------------------------------------------------------------------------------- 1 | //! Expression syntax. 2 | //! Uses nom's parser combinators to define the grammar. 3 | 4 | #[macro_use] 5 | mod util; 6 | 7 | mod literals; 8 | mod ops; 9 | mod structure; 10 | 11 | 12 | pub use self::structure::*; 13 | 14 | -------------------------------------------------------------------------------- /crates/librush/src/parse/syntax/ops.rs: -------------------------------------------------------------------------------- 1 | //! Operator symbols. 2 | 3 | 4 | named!(pub binary_op( &[u8] ) -> String, alt_complete!( 5 | functional_op | logical_op | comparison_op | 6 | power_op | multiplicative_op | // power_op needs to be before multiplicative_op 7 | additive_op 8 | )); 9 | 10 | named!(pub unary_op( &[u8] ) -> String, string!(multispaced!( 11 | char_of!("+-!") 12 | ))); 13 | 14 | 15 | // Binary operators 16 | 17 | named!(pub assignment_op( &[u8] ) -> String, string!(multispaced!( 18 | tag!("=") 19 | ))); 20 | named!(pub functional_op( &[u8] ) -> String, string!(multispaced!( 21 | char_of!("&$") 22 | ))); 23 | named!(pub logical_op( &[u8] ) -> String, string!(multispaced!(alt_complete!( 24 | tag!("&&") | tag!("||") 25 | )))); 26 | named!(pub comparison_op( &[u8] ) -> String, string!(multispaced!(alt_complete!( 27 | tag!("<=") | tag!(">=") | tag!("==") | tag!("!=") | char_of!("<>@") 28 | )))); 29 | named!(pub additive_op( &[u8] ) -> String, string!(multispaced!( 30 | char_of!("+-") 31 | ))); 32 | named!(pub multiplicative_op( &[u8] ) -> String, string!(multispaced!( 33 | char_of!("*/%") 34 | ))); 35 | named!(pub power_op( &[u8] ) -> String, string!(multispaced!( 36 | tag!("**") 37 | ))); 38 | -------------------------------------------------------------------------------- /crates/librush/src/parse/syntax/structure.rs: -------------------------------------------------------------------------------- 1 | //! Module defining grammar symbols that form the main structure of the syntax. 2 | 3 | use eval::{Eval, Function}; 4 | use parse::ast::*; 5 | use super::literals::{atom, identifier}; 6 | use super::ops::*; 7 | 8 | 9 | /// Root symbol of the grammar. 10 | named!(pub expression( &[u8] ) -> Box, chain!(e: assignment, || { e })); 11 | 12 | 13 | /// Macros shortening the repetitive parts of defining syntactical constructs 14 | /// involving binary operators. 15 | macro_rules! binary ( 16 | ($rule:ident($assoc:ident) => $first:ident ($op:ident $rest:ident)*) => ( 17 | named!($rule( &[u8] ) -> Box, chain!( 18 | first: $first ~ 19 | rest: many0!(pair!($op, $rest)), 20 | move || { 21 | if rest.is_empty() { first } 22 | else { Box::new( 23 | BinaryOpNode::new(Associativity::$assoc, first, rest) 24 | ) as Box } 25 | } 26 | )); 27 | ); 28 | ); 29 | macro_rules! left_assoc ( 30 | ($rule:ident => $first:ident ($op:ident $rest:ident)*) => ( 31 | binary!($rule(Left) => $first ($op $rest)*); 32 | ); 33 | ); 34 | macro_rules! right_assoc ( 35 | ($rule:ident => $first:ident ($op:ident $rest:ident)*) => ( 36 | binary!($rule(Right) => $first ($op $rest)*); 37 | ); 38 | ); 39 | 40 | 41 | /// assignment ::== functional (ASSIGNMENT_OP functional)* 42 | right_assoc!(assignment => functional (assignment_op functional)*); 43 | 44 | /// functional ::== joint (FUNCTIONAL_OP joint)* 45 | left_assoc!(functional => joint (functional_op joint)*); 46 | 47 | /// joint ::== conditional | lambda | curried_op 48 | named!(joint( &[u8] ) -> Box, alt!(conditional | lambda | curried_op)); 49 | 50 | /// lambda ::== '|' ARGS '|' joint 51 | named!(lambda( &[u8] ) -> Box, chain!( 52 | multispaced!(tag!("|")) ~ 53 | args: separated_list!(multispaced!(tag!(",")), identifier) ~ 54 | multispaced!(tag!("|")) ~ 55 | body: joint, 56 | move || { Box::new( 57 | ScalarNode::from(Function::from_lambda(args, body)) 58 | ) as Box } 59 | )); 60 | 61 | /// curried_op ::== '(' (atom BINARY_OP) | (BINARY_OP atom) | BINARY_OP ')' 62 | named!(curried_op( &[u8] ) -> Box, delimited!( 63 | multispaced!(tag!("(")), 64 | alt!( 65 | pair!(atom, binary_op) => { |(arg, op)| Box::new( 66 | CurriedBinaryOpNode::with_left(op, arg) 67 | ) as Box } 68 | | 69 | pair!(binary_op, atom) => { |(op, arg)| Box::new( 70 | CurriedBinaryOpNode::with_right(op, arg) 71 | ) as Box } 72 | | 73 | binary_op => { |op| Box::new( 74 | CurriedBinaryOpNode::with_none(op) 75 | ) as Box } 76 | ), 77 | multispaced!(tag!(")")) 78 | )); 79 | 80 | /// conditional ::== logical ['?' logical ':' conditional] 81 | named!(conditional( &[u8] ) -> Box, map!( 82 | pair!(logical, maybe!(chain!( 83 | multispaced!(tag!("?")) ~ 84 | then: logical ~ 85 | multispaced!(tag!(":")) ~ 86 | else_: conditional, 87 | move || (then, else_) 88 | ))), 89 | |(cond, maybe_then_else)| { 90 | match maybe_then_else { 91 | None => cond, 92 | Some((then, else_)) => Box::new( 93 | ConditionalNode::new(cond, then, else_) 94 | ) as Box, 95 | } 96 | } 97 | )); 98 | 99 | /// logical ::== comparison (LOGICAL_OP comparison)* 100 | left_assoc!(logical => comparison (logical_op comparison)*); 101 | 102 | /// comparison ::== argument [COMPARISON_OP argument] 103 | named!(comparison( &[u8] ) -> Box, chain!( 104 | // TODO(xion): consider supporting chained comparisons a'la Python 105 | // (we could use the left_assoc! macro then) 106 | left: argument ~ 107 | maybe_right: maybe!(pair!(comparison_op, argument)), 108 | move || { 109 | match maybe_right { 110 | None => left, 111 | Some(right) => Box::new( 112 | BinaryOpNode::new(Associativity::Left, left, vec![right]) 113 | ) as Box, 114 | } 115 | } 116 | )); 117 | 118 | 119 | /// argument ::== term (ADDITIVE_BIN_OP term)* 120 | left_assoc!(argument => term (additive_op term)*); 121 | 122 | /// term ::== factor (MULTIPLICATIVE_BIN_OP factor)* 123 | left_assoc!(term => factor (multiplicative_op factor)*); 124 | 125 | /// factor ::== power (POWER_OP power)* 126 | left_assoc!(factor => power (power_op power)*); 127 | 128 | /// power ::== UNARY_OP* atom trailer* 129 | named!(power( &[u8] ) -> Box, chain!( 130 | ops: many0!(unary_op) ~ 131 | power: atom ~ 132 | trailers: many0!(trailer), 133 | move || { 134 | let mut result = power; 135 | 136 | // trailers (subscripts & function calls) have higher priority 137 | // than any unary operators, so we build their AST node(s) first 138 | for trailer in trailers { 139 | result = match trailer { 140 | Trailer::Subscript(index) => 141 | Box::new(SubscriptNode::new(result, index)), 142 | Trailer::Args(args) => 143 | Box::new(FunctionCallNode::new(result, args)), 144 | }; 145 | } 146 | 147 | // then, we build nodes for any unary operators that may have been 148 | // prepended to the whole thing (in reverse order, 149 | // so that `---foo` means `-(-(-foo))`) 150 | for op in ops.into_iter().rev() { 151 | result = Box::new(UnaryOpNode::new(op, result)); 152 | } 153 | 154 | result 155 | } 156 | )); 157 | 158 | /// trailer ::== '[' INDEX ']' | '(' ARGS ')' 159 | enum Trailer { Subscript(Index), Args(Vec>) } 160 | named!(trailer( &[u8] ) -> Trailer, alt!( 161 | delimited!(multispaced!(tag!("[")), 162 | index, 163 | multispaced!(tag!("]"))) => { |idx| Trailer::Subscript(idx) } 164 | | 165 | delimited!(multispaced!(tag!("(")), 166 | separated_list!(multispaced!(tag!(",")), expression), 167 | multispaced!(tag!(")"))) => { |args| Trailer::Args(args) } 168 | )); 169 | named!(index( &[u8] ) -> Index, alt!( 170 | chain!( 171 | left: maybe!(expression) ~ 172 | multispaced!(tag!(":")) ~ 173 | right: maybe!(expression), 174 | move || { Index::Range(left, right) } 175 | ) | 176 | expression => { |expr| Index::Point(expr) } 177 | )); 178 | -------------------------------------------------------------------------------- /crates/librush/src/parse/syntax/util.rs: -------------------------------------------------------------------------------- 1 | //! Utility macros used by the syntax definition. 2 | 3 | 4 | /// Make the underlying byte parser assume UTF8-encoded input 5 | /// and output String objects. 6 | macro_rules! string ( 7 | ($i:expr, $submac:ident!( $($args:tt)* )) => ({ 8 | use std::str::from_utf8; 9 | map!($i, map_res!($submac!($($args)*), from_utf8), String::from) 10 | }); 11 | ($i:expr, $f:expr) => ( 12 | string!($i, call!($f)); 13 | ); 14 | ); 15 | 16 | 17 | /// Make the underlying parser optional, 18 | /// but unlike opt! it is treating incomplete input as parse error. 19 | macro_rules! maybe ( 20 | ($i:expr, $submac:ident!( $($args:tt)* )) => ( 21 | opt!($i, complete!($submac!($($args)*))); 22 | ); 23 | ($i:expr, $f:expr) => ( 24 | maybe!($i, call!($f)); 25 | ); 26 | ); 27 | 28 | /// Parse a sequence that matches the first parser followed by the second parser. 29 | /// Return consumed input as the result (like recognize! does). 30 | macro_rules! seq ( 31 | // TODO(xion): generalize to arbitrary number of arguments (using chain!()) 32 | ($i:expr, $submac:ident!( $($args:tt)* ), $submac2:ident!( $($args2:tt)* )) => ({ 33 | // Unfortunately, this cannot be implemented straightforwardly as: 34 | // recognize!($i, pair!($submac!($($args)*), $submac2!($($args2)*))); 35 | // because Rust compiler fails to carry out the type inference correctly 36 | // in the generated code. 37 | // 38 | // Below is therefore essentially a rewrite of nom's recognize!() macro. 39 | use nom::{HexDisplay, IResult}; 40 | match pair!($i, $submac!($($args)*), $submac2!($($args2)*)) { 41 | IResult::Error(a) => IResult::Error(a), 42 | IResult::Incomplete(i) => IResult::Incomplete(i), 43 | IResult::Done(i, _) => { 44 | let index = ($i).offset(i); 45 | IResult::Done(i, &($i)[..index]) 46 | }, 47 | } 48 | }); 49 | ($i:expr, $submac:ident!( $($args:tt)* ), $g:expr) => ( 50 | seq!($i, $submac!($($args)*), call!($g)); 51 | ); 52 | ($i:expr, $f:expr, $submac:ident!( $($args:tt)* )) => ( 53 | seq!($i, call!($f), $submac!($($args)*)); 54 | ); 55 | ($i:expr, $f:expr, $g:expr) => ( 56 | seq!($i, call!($f), call!($g)); 57 | ); 58 | ); 59 | 60 | 61 | /// Parses values that are optionally surrounded by arbitrary number of 62 | /// any of the whitespace characters. 63 | macro_rules! multispaced ( 64 | ($i:expr, $submac:ident!( $($args:tt)* )) => ({ 65 | use nom::multispace; 66 | delimited!($i, opt!(multispace), $submac!($($args)*), opt!(multispace)) 67 | }); 68 | ($i:expr, $f:expr) => ( 69 | multispaced!($i, call!($f)); 70 | ); 71 | ); 72 | 73 | /// Matches exactly one character from the specified string. 74 | /// This is like one_of!, but returns the matched char as &[u8] (assumming UTF8). 75 | macro_rules! char_of ( 76 | ($i:expr, $inp:expr) => ( 77 | map!($i, one_of!($inp), |c: char| &$i[0..c.len_utf8()]); 78 | ); 79 | ); 80 | -------------------------------------------------------------------------------- /crates/librush/tests/test.rs: -------------------------------------------------------------------------------- 1 | //! Test crate. 2 | //! 3 | //! Note that the actual test cases are in the `tests` submodule. 4 | //! 5 | //! They can't be in the tests/ root, because `cargo test` executes every 6 | //! root *.rs file as a separate binary, which would e.g. require repeating 7 | //! of the `extern crate` declarations and dealing with unused code warnings 8 | //! within the `util` module. 9 | 10 | extern crate conv; 11 | #[macro_use] 12 | extern crate maplit; 13 | extern crate regex; 14 | extern crate rustc_serialize; 15 | 16 | extern crate rush; 17 | 18 | 19 | #[macro_use] 20 | mod util; 21 | mod tests; 22 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/api/base.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the base API functions. 2 | #![allow(non_snake_case)] 3 | 4 | use std::collections::HashMap; 5 | 6 | use util::*; 7 | 8 | 9 | #[test] 10 | fn len() { 11 | const STRING: &'static str = "Hello, world"; 12 | const ARRAY: &'static [u32] = &[1, 1, 2, 3, 5, 8, 13, 21]; 13 | let OBJECT: HashMap = hashmap_owned!{"foo" => 1, "bar" => 2}; 14 | 15 | assert_eval_error("len(false)"); 16 | assert_eval_error("len(42)"); 17 | assert_eval_error("len(3.14)"); 18 | assert_eq!("3", eval("len(foo)")); 19 | assert_eval_error("len(/foo/)"); 20 | assert_eq!(STRING.len().to_string(), apply("len(_)", STRING)); 21 | assert_eq!(ARRAY.len().to_string(), 22 | eval(&format!("len({})", ARRAY.to_literal()))); 23 | assert_eq!(OBJECT.len().to_string(), 24 | eval(&format!("len({})", OBJECT.to_literal()))); 25 | assert_eval_error("len(|x| x)"); 26 | } 27 | 28 | mod rev { 29 | use util::*; 30 | 31 | #[test] 32 | fn strings() { 33 | assert_eq!("oof", apply("rev(_)", "foo")); 34 | assert_noop_apply("rev(_)", "racecar"); 35 | assert_apply_error("rev(_)", "42"); 36 | assert_apply_error("rev(_)", "13.42"); 37 | assert_apply_error("rev(_)", "false"); 38 | } 39 | 40 | #[test] 41 | fn arrays() { 42 | assert_eq!("", eval(&format!("rev({})", "[]"))); 43 | } 44 | 45 | #[test] 46 | fn objects() { 47 | assert_eq!("{}", eval(&format!("rev({})", "{}"))); 48 | } 49 | 50 | // TODO: more tests for arrays & objects 51 | } 52 | 53 | #[test] 54 | fn keys() { 55 | const STRING: &'static str = "Hello, world"; 56 | const ARRAY: &'static [u32] = &[1, 1, 2, 3, 5, 8, 13, 21]; 57 | let OBJECT: HashMap = hashmap_owned!{"foo" => 1, "bar" => 2}; 58 | 59 | assert_eval_error("keys(true)"); 60 | assert_eval_error("keys(42)"); 61 | assert_eval_error("keys(3.14)"); 62 | assert_eval_error("keys(/foo/)"); 63 | 64 | // for strings and arrays, the result is array of indices, in order 65 | assert_eq!(join(&(0..STRING.len()).collect::>(), "\n"), 66 | eval(&format!("keys({})", STRING.to_literal()))); 67 | assert_eq!(join(&(0..ARRAY.len()).collect::>(), "\n"), 68 | eval(&format!("keys({})", ARRAY.to_literal()))); 69 | 70 | // for objects, it's an array of keys (in any order) 71 | let object_keys = eval(&format!("keys({})", OBJECT.to_literal())); 72 | for key in OBJECT.keys() { 73 | assert!(object_keys.contains(key)); 74 | } 75 | 76 | assert_eval_error("keys(|x| x)") 77 | } 78 | // TODO: tests for values() 79 | 80 | mod index { 81 | use util::*; 82 | 83 | #[test] 84 | fn string() { 85 | const STRING: &'static str = "Hello, world"; 86 | 87 | assert_eval_error(&format!("index(true, {})", STRING.to_literal())); 88 | assert_eval_error(&format!("index(42, {})", STRING.to_literal())); 89 | assert_eval_error(&format!("index(3.14, {})", STRING.to_literal())); 90 | 91 | assert_eq!("0", eval(&format!("index(Hell, {})", STRING.to_literal()))); 92 | assert_eq!("6", eval(&format!("index(/ \\w+/, {})", STRING.to_literal()))); 93 | 94 | assert_eval_error(&format!("index([], {})", STRING.to_literal())); 95 | assert_eval_error(&format!("index({{}}, {})", STRING.to_literal())); 96 | assert_eval_error(&format!("index(|x| x, {})", STRING.to_literal())); 97 | } 98 | 99 | #[test] 100 | fn array() { 101 | const ARRAY: &'static [u32] = &[1, 1, 2, 3, 5, 8, 13, 21]; 102 | 103 | assert_eval_error(&format!("index(false, {})", ARRAY.to_literal())); 104 | 105 | assert_eq!("4", eval(&format!("index(5, {})", ARRAY.to_literal()))); 106 | 107 | assert_eval_error(&format!("index(42, {})", ARRAY.to_literal())); 108 | assert_eval_error(&format!("index(2.71, {})", ARRAY.to_literal())); 109 | assert_eval_error(&format!("index(foo, {})", ARRAY.to_literal())); 110 | assert_eval_error(&format!("index([], {})", ARRAY.to_literal())); 111 | assert_eval_error(&format!("index({{}}, {})", ARRAY.to_literal())); 112 | assert_eval_error(&format!("index(|x| x, {})", ARRAY.to_literal())); 113 | } 114 | 115 | #[test] 116 | fn errors() { 117 | assert_eval_error("index(42, false)"); 118 | assert_eval_error("index(42, 42)"); 119 | assert_eval_error("index(42, 3.14)"); 120 | assert_eval_error("index(42, foo)"); 121 | assert_eval_error("index(42, /foo/)"); 122 | assert_eval_error("index(42, [])"); 123 | assert_eval_error("index(42, {})"); 124 | assert_eval_error("index(42, [])"); 125 | assert_eval_error("index(42, |x| x)"); 126 | } 127 | } 128 | 129 | // TODO(xion): tests for sort() and sortby() 130 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/api/conv.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the conversion functions. 2 | 3 | use util::*; 4 | 5 | 6 | #[test] 7 | fn str_() { 8 | assert_noop_apply("str(_)", "foobar"); 9 | assert_noop_apply("str(_)", "42"); 10 | assert_noop_apply("str(_)", "13.42"); 11 | assert_noop_apply("str(_)", "false"); 12 | assert_eval_error(&format!("str({})", "[]")); 13 | assert_eval_error(&format!("str({})", "{}")); 14 | } 15 | 16 | #[test] 17 | fn int() { 18 | assert_apply_error("int(_)", "foobar"); 19 | assert_noop_apply("int(_)", "42"); 20 | assert_eq!("13", apply("int(_)", 13.42)); 21 | assert_eq!("0", apply("int(_)", false)); 22 | assert_eval_error(&format!("int({})", "[]")); 23 | assert_eval_error(&format!("int({})", "{}")); 24 | } 25 | 26 | #[test] 27 | fn float() { 28 | assert_apply_error("float(_)", "foobar"); 29 | assert_eq!("42.0", apply("float(_)", 42)); 30 | assert_noop_apply("float(_)", "13.42"); 31 | assert_eq!("0.0", apply("float(_)", false)); 32 | assert_eval_error(&format!("float({})", "[]")); 33 | assert_eval_error(&format!("float({})", "{}")); 34 | } 35 | 36 | #[test] 37 | fn bool() { 38 | assert_apply_error("bool(_)", "foobar"); 39 | assert_eq!("true", apply("bool(_)", 42)); 40 | assert_eq!("false", apply("bool(_)", 0)); 41 | assert_eq!("true", apply("bool(_)", 13.42)); 42 | assert_eq!("false", eval(&format!("bool({})", "[]"))); 43 | assert_eq!("false", eval(&format!("bool({})", "{}"))); 44 | assert_eq!("true", eval(&format!("bool({})", "[3]"))); 45 | assert_eq!("true", eval(&format!("bool({})", "{foo: 4}"))); 46 | } 47 | 48 | #[test] 49 | fn array() { 50 | assert_eq!(unlines!("f", "o", "o", "b", "a", "r"), apply("array(_)", "foobar")); 51 | assert_apply_error("array(_)", "42"); 52 | assert_apply_error("array(_)", "13.42"); 53 | assert_apply_error("array(_)", "false"); 54 | assert_eq!(unlines!("3", "4"), eval(&format!("array({})", "[3,4]"))); 55 | 56 | // the order of object keys is unspecified 57 | let keys: Vec<_> = eval(&format!("array({})", "{foo: 3, bar: 4}")).split("\n") 58 | .map(String::from).collect(); 59 | for &s in &["foo", "bar"] { 60 | assert!(keys.contains(&String::from(s))); 61 | } 62 | } 63 | 64 | // TODO(xion): tests for csv() function 65 | // TODO(xion): tests for json() function 66 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module with API tests. 2 | 3 | mod base; 4 | mod conv; 5 | mod strings; 6 | 7 | 8 | // TODO: tests for the random module 9 | 10 | // TODO(xion): tests for all() and any() 11 | // TODO(xion): tests for min(), max() and sum() 12 | // TODO(xion): tests for map(), filter(), reject(), and reduce() 13 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/api/strings.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the string-related API functions. 2 | 3 | use util::*; 4 | 5 | 6 | #[test] 7 | fn chr() { 8 | assert_eq!("A", apply("chr(_)", "65")); 9 | assert_eq!("a", apply("chr(_)", "97")); 10 | assert_apply_error("chr(_)", "a"); 11 | assert_apply_error("chr(_)", "foo"); 12 | assert_apply_error("chr(_)", "3.14"); 13 | assert_apply_error("chr(_)", "-1"); 14 | assert_eval_error("chr([])"); 15 | assert_eval_error("chr({})"); 16 | } 17 | 18 | #[test] 19 | fn ord() { 20 | assert_eq!("65", apply("ord(_)", "A")); 21 | assert_eq!("97", apply("ord(_)", "a")); 22 | assert_apply_error("ord(_)", "foo"); 23 | assert_apply_error("ord(_)", "42"); 24 | assert_apply_error("ord(_)", "-12"); 25 | assert_apply_error("ord(_)", "2.71"); 26 | assert_eval_error("ord([])"); 27 | assert_eval_error("ord({})"); 28 | } 29 | 30 | mod split { 31 | use util::*; 32 | 33 | #[test] 34 | fn strings() { 35 | assert_eq!("", apply("split(X, _)", "")); 36 | assert_eq!("foo", apply("split(X, _)", "foo")); 37 | assert_eq!(unlines!("foo", "bar"), apply("split(X, _)", "fooXbar")); 38 | assert_eq!(unlines!("foo", ""), apply("split(X, _)", "fooX")); 39 | assert_eq!(unlines!("", "foo"), apply("split(X, _)", "Xfoo")); 40 | assert_eq!(unlines!("", ""), apply("split(X, _)", "X")); 41 | } 42 | 43 | #[test] 44 | fn non_strings() { 45 | assert_apply_error("split(X, _)", "42"); 46 | assert_apply_error("split(X, _)", "13.42"); 47 | assert_apply_error("split(X, _)", "false"); 48 | assert_eval_error(&format!("split(X, {})", "[]")); 49 | assert_eval_error(&format!("split(X, {})", "{}")); 50 | } 51 | } 52 | 53 | #[test] 54 | fn join_() { 55 | assert_eq!("", apply_lines("join(X, _)", &[""])); 56 | assert_eq!("foo", apply_lines("join(X, _)", &["foo"])); 57 | assert_eq!("fooXbar", apply_lines("join(X, _)", &["foo", "bar"])); 58 | assert_eq!("falseXtrue", apply_lines("join(X, _)", &[false, true])); 59 | assert_eval_error(&format!("join(X, {})", "false")); 60 | assert_eval_error(&format!("join(X, {})", "foo")); 61 | assert_eval_error(&format!("join(X, {})", "42")); 62 | assert_eval_error(&format!("join(X, {})", "13.42")); 63 | assert_eval_error(&format!("join(X, {})", "{}")); 64 | } 65 | 66 | // TODO(xion): tests for sub() and sub1(), especially w/ regex and replacement function 67 | // TODO(xion): tests for rsub1() 68 | 69 | mod before { 70 | use util::*; 71 | 72 | #[test] 73 | fn string() { 74 | assert_eq!("", apply("before(\"\", _)", "")); 75 | assert_eq!("", apply("before(bar, _)", "")); 76 | assert_eq!("", apply("before(bar, _)", "bar")); 77 | assert_eq!("foo", apply("before(bar, _)", "foobar")); 78 | assert_eq!("", apply("before(baz, _)", "foobar")); 79 | assert_apply_error("before(bar, _)", "42"); 80 | assert_apply_error("before(bar, _)", "3.14"); 81 | assert_eval_error("before(bar, [])"); 82 | assert_eval_error("before(bar, {})"); 83 | } 84 | 85 | #[test] 86 | fn regex() { 87 | assert_eq!("", apply("before(//, _)", "")); 88 | assert_eq!("", apply("before(/bar/, _)", "")); 89 | assert_eq!("", apply("before(/bar/, _)", "bar")); 90 | assert_eq!("foo", apply("before(/bar/, _)", "foobar")); 91 | assert_eq!("", apply("before(/baz/, _)", "foobar")); 92 | assert_apply_error("before(/bar/, _)", "42"); 93 | assert_apply_error("before(/bar/, _)", "3.14"); 94 | assert_eval_error("before(/bar/, [])"); 95 | assert_eval_error("before(/bar/, {})"); 96 | } 97 | } 98 | 99 | mod after { 100 | use util::*; 101 | 102 | #[test] 103 | fn string() { 104 | assert_eq!("", apply("after(\"\", _)", "")); 105 | assert_eq!("", apply("after(foo, _)", "")); 106 | assert_eq!("", apply("after(foo, _)", "foo")); 107 | assert_eq!("bar", apply("after(foo, _)", "foobar")); 108 | assert_eq!("", apply("after(baz, _)", "foobar")); 109 | assert_apply_error("after(foo, _)", "42"); 110 | assert_apply_error("after(foo, _)", "3.14"); 111 | assert_eval_error("after(foo, [])"); 112 | assert_eval_error("after(foo, {})"); 113 | } 114 | 115 | #[test] 116 | fn regex() { 117 | assert_eq!("", apply("after(//, _)", "")); 118 | assert_eq!("", apply("after(/foo/, _)", "")); 119 | assert_eq!("", apply("after(/foo/, _)", "foo")); 120 | assert_eq!("bar", apply("after(/foo/, _)", "foobar")); 121 | assert_eq!("", apply("after(/baz/, _)", "foobar")); 122 | assert_apply_error("after(/foo/, _)", "42"); 123 | assert_apply_error("after(/foo/, _)", "3.14"); 124 | assert_eval_error("after(/foo/, [])"); 125 | assert_eval_error("after(/foo/, {})"); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/constants.rs: -------------------------------------------------------------------------------- 1 | //! Tests for constant expressions. 2 | 3 | use util::*; 4 | 5 | 6 | #[test] 7 | fn constant_boolean_true() { 8 | assert_noop_eval("true"); 9 | } 10 | 11 | #[test] 12 | fn constant_boolean_false() { 13 | assert_noop_eval("false"); 14 | } 15 | 16 | mod numbers { 17 | use util::*; 18 | 19 | #[test] 20 | fn integer() { 21 | assert_noop_eval("42"); 22 | } 23 | 24 | #[test] 25 | fn integer_negative() { 26 | // Note that this may actually be interpreted as unary minus expression, 27 | // but the user wouldn't care about that so we consider it constant. 28 | assert_noop_eval("-42"); 29 | } 30 | 31 | #[test] 32 | fn float() { 33 | assert_noop_eval("42.42"); 34 | } 35 | 36 | #[test] 37 | fn float_zero() { 38 | assert_noop_eval("0.0"); 39 | } 40 | 41 | #[test] 42 | fn float_fraction() { 43 | assert_noop_eval("0.42"); 44 | } 45 | 46 | #[test] 47 | fn float_scientific() { 48 | const EXPR: &'static str = "42.4e2"; 49 | let expected = EXPR.parse::().unwrap().to_string() + ".0"; 50 | assert_eq!(expected, eval(EXPR)); 51 | } 52 | 53 | #[test] 54 | fn float_negative() { 55 | // Note that this may actually be interpreted as unary minus expression, 56 | // but the user wouldn't care about that so we consider it constant. 57 | assert_noop_eval("-42.42"); 58 | } 59 | } 60 | 61 | 62 | mod string { 63 | use util::*; 64 | 65 | #[test] 66 | fn bare() { 67 | assert_noop_eval("foo"); 68 | } 69 | 70 | #[test] 71 | fn quoted() { 72 | const STRING: &'static str = "foo"; 73 | let expr = &format!("\"{}\"", STRING); 74 | assert_eq!(STRING, eval(expr)); 75 | // TODO(xion): test escape sequences 76 | } 77 | } 78 | 79 | mod array { 80 | use util::*; 81 | 82 | #[test] 83 | fn empty() { 84 | const EXPR: &'static str = "[]"; 85 | let expected = ""; 86 | assert_eq!(expected, eval(EXPR)); 87 | } 88 | 89 | #[test] 90 | fn one_element() { 91 | const ELEMENT: &'static str = "foo"; 92 | let expr = format!("[{}]", ELEMENT); 93 | assert_eq!(ELEMENT, eval(&expr)); 94 | } 95 | 96 | #[test] 97 | fn integers() { 98 | const ELEMENTS: &'static [i64] = &[13, 42, 100, 256]; 99 | let expr = format!("[{}]", join(ELEMENTS, ",")); 100 | let actual: Vec<_> = eval(&expr) 101 | .split('\n').map(|s| s.parse::().unwrap()).collect(); 102 | assert_eq!(ELEMENTS, &actual[..]); 103 | } 104 | 105 | #[test] 106 | fn floats() { 107 | const ELEMENTS: &'static [f64] = &[-13.5, 0.00002, 42.007, 999999999.7]; 108 | let expr = format!("[{}]", join(ELEMENTS, ",")); 109 | let actual: Vec<_> = eval(&expr) 110 | .split('\n').map(|s| s.parse::().unwrap()).collect(); 111 | assert_eq!(ELEMENTS, &actual[..]); 112 | } 113 | 114 | #[test] 115 | fn strings() { 116 | const ELEMENTS: &'static [&'static str] = &["foo", "bar", "baz"]; 117 | let expr = format!("[{}]", join(ELEMENTS, ",")); 118 | let actual: Vec<_> = eval(&expr).split('\n').map(String::from).collect(); 119 | assert_eq!(ELEMENTS, &actual[..]); 120 | } 121 | 122 | #[test] 123 | fn quoted_strings() { 124 | const ELEMENTS: &'static [&'static str] = &["Alice", "has", "a", "cat"]; 125 | let expr = format!("[{}]", ELEMENTS.iter() 126 | .map(|s| format!("\"{}\"", s)).collect::>().join(",")); 127 | let actual: Vec<_> = eval(&expr).split('\n').map(String::from).collect(); 128 | assert_eq!(ELEMENTS, &actual[..]); 129 | } 130 | 131 | #[test] 132 | fn booleans() { 133 | const ELEMENTS: &'static [bool] = &[true, false, false, true, true]; 134 | let expr = format!("[{}]", join(ELEMENTS, ",")); 135 | let actual: Vec<_> = eval(&expr) 136 | .split('\n').map(|s| s.parse::().unwrap()).collect(); 137 | assert_eq!(ELEMENTS, &actual[..]); 138 | } 139 | } 140 | 141 | mod object { 142 | use util::*; 143 | 144 | #[test] 145 | fn empty() { 146 | assert_noop_eval("{}"); 147 | } 148 | 149 | #[test] 150 | fn one_attribute() { 151 | assert_noop_eval("{\"a\":2}"); 152 | assert_eval_error("{2: 3}"); // because key has to be string 153 | } 154 | 155 | #[test] 156 | fn constant_object() { 157 | let elems = hashmap_owned!{"a" => "foo", "b" => "bar"}; 158 | let actual = parse_json_stringmap(&eval(&elems.to_literal())); 159 | assert_eq!(elems, actual); 160 | } 161 | 162 | #[test] 163 | fn duplicate_key() { 164 | let key = "a"; 165 | let first_value = "foo"; 166 | let second_value = "bar"; 167 | let elems = hashmap_owned!{key => first_value, key => second_value}; 168 | 169 | let actual = parse_json_stringmap(&eval(&elems.to_literal())); 170 | assert!(actual.contains_key(key)); 171 | assert_eq!(second_value, actual.get(key).unwrap()); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module with actual tests. 2 | 3 | mod api; 4 | mod constants; 5 | mod operators; 6 | mod trailers; 7 | 8 | 9 | /// These are miscellaneous tests that hasn't been moved to a dedicated submodule yet. 10 | /// They are mostly to prevent regressions of various bugs. 11 | mod misc { 12 | use rush::{Context, eval, Value}; 13 | 14 | /// Test that assigning a lambda to a name works. 15 | #[test] 16 | fn assign_lambda_variable() { 17 | let mut context = Context::new(); 18 | eval("inc = |x| x + 1", &mut context).unwrap(); 19 | assert_eq!(Value::Integer(43), eval("inc(42)", &mut context).unwrap()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/operators/binary/arith.rs: -------------------------------------------------------------------------------- 1 | //! Tests for arithmetic operators. 2 | 3 | 4 | mod plus { 5 | use util::*; 6 | 7 | #[test] 8 | fn constant_integers() { 9 | assert_eq!("0", eval("0 + 0")); 10 | assert_eq!("2", eval("0 + 2")); 11 | assert_eq!("4", eval("2 + 2")); 12 | assert_eq!("42", eval("-2 + 44")); 13 | } 14 | 15 | #[test] 16 | fn constant_floats() { 17 | assert_eq!("0.0", eval("0.0 + 0.0")); 18 | assert_eq!("2.0", eval("0 + 2.0")); 19 | assert_eq!("4.0", eval("2.0 + 2.0")); 20 | assert_eq!("42.0", eval("-2.5 + 44.5")); 21 | } 22 | 23 | #[test] 24 | fn constant_strings() { 25 | assert_eq!("foo", eval("\"\" + foo")); 26 | assert_eq!("foobar", eval("foo + bar")); 27 | assert_eq!("barbaz", eval("bar + \"baz\"")); 28 | } 29 | 30 | #[test] 31 | fn input_integers() { 32 | assert_noop_apply("_ + 0", "42"); 33 | assert_noop_apply("0 + _", "42"); 34 | assert_eq!("42", apply("_ + 40", "2")); 35 | assert_eq!("42", apply("40 + _", "2")); 36 | assert_eq!("6", apply("_ + _", "3")); 37 | assert_eq!("12", apply("_ + _ + _", "4")); 38 | } 39 | // TODO(xion): input_floats 40 | // TODO(xion): input_strings 41 | } 42 | 43 | mod minus { 44 | use util::*; 45 | 46 | #[test] 47 | fn constant_integers() { 48 | assert_eq!("0", eval("0 - 0")); 49 | assert_eq!("2", eval("2 - 0")); 50 | assert_eq!("3", eval("5 - 2")); 51 | assert_eq!("-4", eval("1 - 5")); 52 | assert_eq!("-2", eval("-1 - 1")); 53 | assert_eq!("1", eval("-3 - -4")); 54 | } 55 | 56 | #[test] 57 | fn constant_floats() { 58 | assert_eq!("0.0", eval("0.0 - 0.0")); 59 | assert_eq!("2.0", eval("2.0 - 0.0")); 60 | assert_eq!("3.0", eval("5.0 - 2.0")); 61 | assert_eq!("-4.0", eval("1.0 - 5.0")); 62 | assert_eq!("-2.0", eval("-1.0 - 1.0")); 63 | assert_eq!("1.0", eval("-3.0 - -4.0")); 64 | } 65 | 66 | #[test] 67 | fn input_integers() { 68 | assert_noop_apply("_ - 0", "42"); 69 | assert_eq!("-42", apply("0 - _", "42")); 70 | assert_eq!("40", apply("42 - _", "2")); 71 | assert_eq!("-2", apply("40 - _", "42")); 72 | assert_eq!("0", apply("_ - _", "42")); 73 | assert_eq!("-42", apply("_ - _ - _", "42")); 74 | assert_noop_apply("_ - (_ - _)", "42"); 75 | } 76 | // TODO(xion): input_floats 77 | } 78 | 79 | mod times { 80 | use util::*; 81 | 82 | #[test] 83 | fn constant_integers() { 84 | assert_eq!("0", eval("0 * 0")); 85 | assert_eq!("0", eval("2 * 0")); 86 | assert_eq!("3", eval("3 * 1")); 87 | assert_eq!("-4", eval("4 * -1")); 88 | assert_eq!("2", eval("-2 * -1")); 89 | } 90 | 91 | #[test] 92 | fn constant_floats() { 93 | assert_eq!("0.0", eval("0.0 * 0.0")); 94 | assert_eq!("0.0", eval("2.0 * 0.0")); 95 | assert_eq!("3.0", eval("3.0 * 1.0")); 96 | assert_eq!("-4.0", eval("4.0 * -1.0")); 97 | assert_eq!("2.0", eval("-2.0 * -1.0")); 98 | } 99 | // TODO: string * integer 100 | } 101 | 102 | // TODO(xion): tests for division 103 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/operators/binary/cmp.rs: -------------------------------------------------------------------------------- 1 | //! Tests for comparison operators. 2 | 3 | 4 | mod less { 5 | use util::*; 6 | 7 | #[test] 8 | fn constants() { 9 | assert_eval_true("1 < 2"); 10 | assert_eval_true("-5 < 0"); 11 | assert_eval_true("1.5 < 2"); 12 | assert_eval_true("8 < 10.0"); 13 | assert_eval_true("-3.14 < 3.14"); 14 | assert_eval_false("1 < 1"); 15 | assert_eval_false("0 < -10"); 16 | assert_eval_error("0 < foo"); 17 | assert_eval_error("foo < 42"); 18 | assert_eval_error("bar < true"); 19 | assert_eval_error("[] < []"); 20 | assert_eval_error("{} < {}"); 21 | } 22 | 23 | // TODO(xion): inputs 24 | } 25 | 26 | mod less_or_equal { 27 | use util::*; 28 | 29 | #[test] 30 | fn constants() { 31 | assert_eval_true("1 <= 2"); 32 | assert_eval_true("-5 <= 0"); 33 | assert_eval_true("1.5 <= 2"); 34 | assert_eval_true("8 <= 10.0"); 35 | assert_eval_true("-3.14 <= 3.14"); 36 | assert_eval_true("1 <= 1"); 37 | assert_eval_false("0 <= -10"); 38 | assert_eval_false("-8 <= -12"); 39 | assert_eval_error("0 <= foo"); 40 | assert_eval_error("foo <= 42"); 41 | assert_eval_error("bar <= true"); 42 | assert_eval_error("[] <= []"); 43 | assert_eval_error("{} <= {}"); 44 | } 45 | 46 | // TODO(xion): inputs 47 | } 48 | 49 | mod greater { 50 | use util::*; 51 | 52 | #[test] 53 | fn constants() { 54 | assert_eval_true("2 > 1"); 55 | assert_eval_true("0 > -5"); 56 | assert_eval_true("2 > 1.5"); 57 | assert_eval_true("10.0 > 8"); 58 | assert_eval_true("3.14 > -3.14"); 59 | assert_eval_false("1 > 1"); 60 | assert_eval_false("-10 > 0"); 61 | assert_eval_false("-12 > -8"); 62 | assert_eval_error("0 > foo"); 63 | assert_eval_error("foo > 42"); 64 | assert_eval_error("bar > true"); 65 | assert_eval_error("[] > []"); 66 | assert_eval_error("{} > {}"); 67 | } 68 | // TODO(xion): inputs 69 | } 70 | 71 | mod greater_or_equal { 72 | use util::*; 73 | 74 | #[test] 75 | fn constants() { 76 | assert_eval_true("2 >= 1"); 77 | assert_eval_true("0 >= -5"); 78 | assert_eval_true("2 >= 1.5"); 79 | assert_eval_true("10.0 >= 8"); 80 | assert_eval_true("3.14 >= -3.14"); 81 | assert_eval_true("1 >= 1"); 82 | assert_eval_false("-10 >= 0"); 83 | assert_eval_false("-12 >= -8"); 84 | assert_eval_error("0 >= foo"); 85 | assert_eval_error("foo >= 42"); 86 | assert_eval_error("bar >= true"); 87 | assert_eval_error("[] >= []"); 88 | assert_eval_error("{} >= {}"); 89 | } 90 | // TODO(xion): inputs 91 | } 92 | 93 | mod equal { 94 | use util::*; 95 | 96 | #[test] 97 | fn constants() { 98 | assert_eval_false("2 == 1"); 99 | assert_eval_false("0 == -5"); 100 | assert_eval_false("2 == 1.5"); 101 | assert_eval_false("10.0 == 8"); 102 | assert_eval_false("3.14 == -3.14"); 103 | assert_eval_false("-10 == 0"); 104 | assert_eval_false("-12 == -8"); 105 | assert_eval_true("1 == 1"); 106 | assert_eval_true("2.0 == 2"); 107 | assert_eval_true("3.0 == 3.0"); 108 | assert_eval_true("4 == 4.0"); 109 | assert_eval_true("[] == []"); 110 | assert_eval_true("{} == {}"); 111 | assert_eval_error("0 == foo"); 112 | assert_eval_error("foo == 42"); 113 | assert_eval_error("bar == true"); 114 | } 115 | // TODO(xion): inputs 116 | } 117 | 118 | mod not_equal { 119 | use util::*; 120 | 121 | #[test] 122 | fn constants() { 123 | assert_eval_true("2 != 1"); 124 | assert_eval_true("0 != -5"); 125 | assert_eval_true("2 != 1.5"); 126 | assert_eval_true("10.0 != 8"); 127 | assert_eval_true("3.14 != -3.14"); 128 | assert_eval_true("-10 != 0"); 129 | assert_eval_true("-12 != -8"); 130 | assert_eval_false("1 != 1"); 131 | assert_eval_false("2.0 != 2"); 132 | assert_eval_false("3.0 != 3.0"); 133 | assert_eval_false("4 != 4.0"); 134 | assert_eval_false("[] != []"); 135 | assert_eval_false("{} != {}"); 136 | assert_eval_error("0 != foo"); 137 | assert_eval_error("foo != 42"); 138 | assert_eval_error("bar != true"); 139 | } 140 | // TODO(xion): inputs 141 | } 142 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/operators/binary/mod.rs: -------------------------------------------------------------------------------- 1 | //! Tests for binary operators. 2 | 3 | mod arith; 4 | mod cmp; 5 | 6 | // TODO(xion): tests for logical operators 7 | // TODO(xion): tests for assignment operators 8 | // TODO: tests for string formatting 9 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/operators/curried.rs: -------------------------------------------------------------------------------- 1 | //! Tests for the curried versions of operators. 2 | 3 | use util::*; 4 | 5 | 6 | #[test] 7 | fn left_simple() { 8 | assert_eq!("4", eval("(2+) $ 2")); 9 | assert_eq!("9", eval("((3*))(3)")); // function call form 10 | assert_eq!("foofoofoo", eval("(foo*) $ 3")); 11 | } 12 | 13 | #[test] 14 | fn right_simple() { 15 | assert_eq!("4", eval("(*2) $ 2")); 16 | assert_eq!("6", eval("((*2))(3)")); // function call form 17 | assert_eval_error("(+42) $ 11"); // unary expression! 18 | assert_eq!("aaaaa", eval("(*5) $ a")); 19 | } 20 | 21 | #[test] 22 | fn none_simple() { 23 | assert_eq!("9", eval("(+) $ 5 $ 4")); // just application 24 | assert_eq!("7", eval("((+) $ 3)(4)")); // hybrid: application + call 25 | assert_eq!("7", eval("((+))(3) $ 4")); // hybrid: call + application 26 | assert_eq!("5", eval("((+))(1)(4)")); // just function calls 27 | } 28 | 29 | #[test] 30 | fn map_with_left() { 31 | let input = [1, 2, 3].to_literal(); 32 | assert_eq!(unlines![2, 3, 4], eval(&format!("map((1+), {})", input))); 33 | assert_eq!(unlines![2, 4, 6], eval(&format!("map((2*), {})", input))); 34 | assert_eq!(unlines![3, 9, 27], eval(&format!("map((3**), {})", input))); 35 | assert_eq!(unlines!["x", "xx", "xxx"], eval(&format!("map((x*), {})", input))); 36 | } 37 | 38 | #[test] 39 | fn map_with_right() { 40 | let input = [2, 4, 6].to_literal(); 41 | assert_eval_error(&format!("map((+1), {})", input)); // unary expression! 42 | assert_eq!(unlines![4, 8, 12], eval(&format!("map((*2), {})", input))); 43 | assert_eq!(unlines![8, 64, 216], eval(&format!("map((**3), {})", input))); 44 | assert_eq!(unlines![1, 2, 3], eval(&format!("map((/2), {})", input))); 45 | } 46 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/operators/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module with operator tests. 2 | 3 | mod binary; 4 | mod curried; 5 | mod unary; 6 | 7 | // TODO(xion): tests for the conditional operator 8 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/operators/unary.rs: -------------------------------------------------------------------------------- 1 | //! Tests for unary operators. 2 | 3 | mod plus { 4 | use util::*; 5 | 6 | #[test] 7 | fn integer() { 8 | assert_noop_apply("+_", "42"); 9 | assert_noop_apply("++_", "42"); 10 | assert_noop_apply("+++_", "42"); 11 | } 12 | 13 | #[test] 14 | fn float() { 15 | assert_noop_apply("+_", "42.42"); 16 | assert_noop_apply("++_", "42.42"); 17 | assert_noop_apply("+++_", "42.42"); 18 | } 19 | 20 | #[test] 21 | fn string() { 22 | assert_apply_error("+_", "foo"); 23 | } 24 | 25 | #[test] 26 | fn boolean() { 27 | assert_apply_error("+_", "true"); 28 | assert_apply_error("+_", "false"); 29 | } 30 | } 31 | 32 | mod minus { 33 | use util::*; 34 | 35 | #[test] 36 | fn integer() { 37 | const INPUT: &'static str = "42"; 38 | let negated = format!("-{}", INPUT); 39 | assert_eq!(negated, apply("-_", INPUT)); 40 | assert_eq!(INPUT, apply("--_", INPUT)); 41 | assert_eq!(negated, apply("---_", INPUT)); 42 | } 43 | 44 | #[test] 45 | fn float() { 46 | const INPUT: &'static str = "42.42"; 47 | let negated = format!("-{}", INPUT); 48 | assert_eq!(negated, apply("-_", INPUT)); 49 | assert_eq!(INPUT, apply("--_", INPUT)); 50 | assert_eq!(negated, apply("---_", INPUT)); 51 | } 52 | } 53 | 54 | mod bang { 55 | use util::*; 56 | 57 | #[test] 58 | fn constant() { 59 | assert_eq!("false", eval("!true")); 60 | assert_eq!("true", eval("!!true")); 61 | assert_eq!("false", eval("!!!true")); 62 | assert_eq!("true", eval("!false")); 63 | assert_eq!("false", eval("!!false")); 64 | assert_eq!("true", eval("!!!false")); 65 | } 66 | 67 | #[test] 68 | fn input() { 69 | assert_eq!("false", apply("!_", "true")); 70 | assert_eq!("true", apply("!_", "false")); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/librush/tests/tests/trailers.rs: -------------------------------------------------------------------------------- 1 | //! Tests for "trialers", i.e. final parts of atoms: subscripts & function calls. 2 | 3 | 4 | mod subscript { 5 | mod array { 6 | use util::*; 7 | 8 | #[test] 9 | fn constant() { 10 | assert_eq!("42", eval("[42][0]")); 11 | assert_eq!("42", eval("[13, 42][1]")); 12 | assert_eq!("42", eval("[[42]][0][0]")); 13 | assert_eq!("c", eval("[a, b, c][-1]")); 14 | assert_eval_error("[][0]"); 15 | assert_eval_error("[42][1]"); 16 | assert_eval_error("[42][-2]"); 17 | } 18 | 19 | #[test] 20 | fn input() { 21 | const INPUT: &'static [&'static str] = &["foo", "bar"]; 22 | assert_eq!("foo", apply_lines("_[0]", INPUT)); 23 | assert_eq!("bar", apply_lines("_[1]", INPUT)); 24 | assert_eq!("foo", apply_lines("[_][0][0]", INPUT)); 25 | assert_eq!("other", apply_lines("[_, [other]][1][0]", INPUT)); 26 | assert_apply_lines_error("_[42]", INPUT); 27 | } 28 | } 29 | 30 | mod string { 31 | use util::*; 32 | 33 | #[test] 34 | fn constant() { 35 | assert_eq!("f", eval("foo[0]")); 36 | assert_eq!("a", eval("\"bar\"[1]")); 37 | assert_eval_error("\"\"[]"); 38 | assert_eval_error("baz[42]"); 39 | } 40 | 41 | #[test] 42 | fn input() { 43 | const INPUT: &'static str = "hello"; 44 | assert_eq!("h", apply("_[0]", INPUT)); 45 | assert_eq!("l", apply("_[2]", INPUT)); 46 | assert_eq!("o", apply("_[-1]", INPUT)); 47 | assert_eq!("e", apply("_[-4]", INPUT)); 48 | assert_apply_error("_[42]", INPUT); 49 | assert_apply_error("_[-42]", INPUT); 50 | } 51 | } 52 | 53 | // TODO(xion): tests for subscript ranges 54 | } 55 | 56 | mod function_call { 57 | mod one_arg { 58 | use util::*; 59 | 60 | #[test] 61 | fn constant() { 62 | assert_eq!("42", eval("abs(42)")); 63 | assert_eq!("5", eval("len(hello)")); 64 | } 65 | 66 | #[test] 67 | fn input() { 68 | assert_noop_apply("abs(_)", "42"); 69 | assert_eq!("5", apply("len(_)", "hello")); 70 | } 71 | } 72 | 73 | mod two_args { 74 | use util::*; 75 | 76 | #[test] 77 | fn constant() { 78 | assert_eq!("he\n\no", eval("split(l, hello)")); 79 | } 80 | 81 | #[test] 82 | fn input() { 83 | assert_eq!("he\n\no", apply("split(l, _)", "hello")); 84 | } 85 | } 86 | 87 | mod three_args { 88 | use util::*; 89 | 90 | #[test] 91 | fn constant() { 92 | assert_eq!("pot", eval("sub(i, o, pit)")); 93 | assert_eq!("", eval("sub(a, \"\", aaa)")); 94 | } 95 | 96 | #[test] 97 | fn input() { 98 | assert_eq!("pot", apply("sub(i, o, _)", "pit")); 99 | assert_eq!("", apply("sub(a, \"\", _)", "aaa")); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/librush/tests/util/asserts.rs: -------------------------------------------------------------------------------- 1 | //! Assertion functions. 2 | 3 | use super::{apply, apply_ex, apply_lines_ex, eval, eval_ex}; 4 | 5 | 6 | // TODO(xion): allow for more fine grained error assertions 7 | 8 | pub fn assert_noop_eval(expr: &str) { 9 | assert_eq!(expr, eval(expr)); 10 | } 11 | 12 | pub fn assert_noop_apply(expr: &str, input: &str) { 13 | assert_eq!(input, apply(expr, input)); 14 | } 15 | 16 | pub fn assert_eval_error(expr: &str) { 17 | assert!(eval_ex(expr).is_err(), 18 | "Expression `{}` didn't cause an error!", expr); 19 | } 20 | 21 | pub fn assert_eval_true(expr: &str) { 22 | let result = eval(expr); 23 | let result_bool = result.parse::().expect(&format!( 24 | "Couldn't interpret result of `{}` as boolean: {}", expr, result 25 | )); 26 | assert!(result_bool, "unexpectedly false: {}", expr); 27 | } 28 | 29 | pub fn assert_eval_false(expr: &str) { 30 | let result = eval(expr); 31 | let result_bool = result.parse::().expect(&format!( 32 | "Couldn't interpret result of `{}` as boolean: {}", expr, result 33 | )); 34 | assert!(!result_bool, "unexpectedly true: {}", expr); 35 | } 36 | 37 | pub fn assert_apply_error(expr: &str, input: T) { 38 | let input = &input.to_string(); 39 | assert!(apply_ex(expr, input).is_err(), 40 | "Mapping `{}` for input `{}` didn't cause an error!", expr, input); 41 | } 42 | 43 | pub fn assert_apply_lines_error(expr: &str, input: &[T]) { 44 | assert!(apply_lines_ex(expr, input).is_err(), 45 | "Reducing `{}` on input `{}` didn't cause an error!"); 46 | } 47 | -------------------------------------------------------------------------------- /crates/librush/tests/util/literals.rs: -------------------------------------------------------------------------------- 1 | //! Module defining how to convert Rust objects into rush literals. 2 | //! This allows to easily supply test data for rush expressions. 3 | 4 | use std::collections::HashMap; 5 | use std::hash::Hash; 6 | 7 | use regex::Regex; 8 | 9 | 10 | /// Type to hold a literal rush representation of a Rust object. 11 | pub type Literal = String; 12 | 13 | /// Defines how a Rust object can be converted to a rush literal. 14 | pub trait ToLiteral { 15 | fn to_literal(&self) -> Literal; 16 | } 17 | 18 | 19 | /// Macro to implement ToLiteral using string formatting. 20 | /// 21 | /// It is necessary because making a blanket implementation for each T: Display 22 | /// introduces ambiguity when we also add the specialized cases for arrays & hashmaps. 23 | macro_rules! impl_toliteral_via_format { 24 | ($t:ty) => { 25 | impl ToLiteral for $t { 26 | fn to_literal(&self) -> Literal { format!("{}", self) } 27 | } 28 | } 29 | } 30 | 31 | impl_toliteral_via_format!(bool); 32 | impl_toliteral_via_format!(i32); 33 | impl_toliteral_via_format!(i64); 34 | impl_toliteral_via_format!(u32); 35 | impl_toliteral_via_format!(u64); 36 | impl_toliteral_via_format!(f32); 37 | impl_toliteral_via_format!(f64); 38 | 39 | impl<'s> ToLiteral for &'s str { 40 | fn to_literal(&self) -> Literal { 41 | format!("\"{}\"", self.to_owned() 42 | // TODO: handle the rest of escape symbols 43 | .replace("\\", "\\\\") 44 | .replace("\"", "\\\"") 45 | ) 46 | } 47 | } 48 | impl ToLiteral for String { 49 | fn to_literal(&self) -> Literal { 50 | (self as &str).to_literal() 51 | } 52 | } 53 | 54 | impl ToLiteral for Regex { 55 | fn to_literal(&self) -> Literal { 56 | let regex = self.as_str(); 57 | 58 | // regexes that'd require rush-specific escaping are not supported 59 | if regex.contains("/") || regex.contains("\\") { 60 | panic!("ToLiteral for regexes containing (back)slashes is NYI"); 61 | } 62 | 63 | String::from(regex) 64 | } 65 | } 66 | 67 | impl ToLiteral for [T] { 68 | fn to_literal(&self) -> Literal { 69 | format!("[{}]", self.iter() 70 | .map(T::to_literal).collect::>().join(",")) 71 | } 72 | } 73 | 74 | impl ToLiteral for HashMap 75 | where K: Hash + Eq + ToLiteral, V: ToLiteral 76 | { 77 | fn to_literal(&self) -> Literal { 78 | format!("{{{}}}", self.iter() 79 | .map(|(ref k, ref v)| format!("{}:{}", k.to_literal(), v.to_literal())) 80 | .collect::>().join(",")) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/librush/tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions used by tests. 2 | 3 | mod asserts; 4 | pub mod literals; // This has to be `pub` because of Rust bug (!) 5 | // making ToLiteral trait inaccessible otherwise. 6 | // Details: https://github.com/rust-lang/rust/issues/18241 7 | 8 | pub use self::asserts::*; 9 | pub use self::literals::*; 10 | 11 | 12 | use std::collections::HashMap; 13 | use std::io; 14 | use std::str::from_utf8; 15 | 16 | use conv::TryFrom; 17 | 18 | use rustc_serialize::json::Json; 19 | use rush::{self, Context}; 20 | 21 | 22 | /// Construct a hashmap where key & value is turned into its Owned version 23 | /// prior to inserting to the map. 24 | macro_rules! hashmap_owned { 25 | {$($key:expr => $value:expr),*} => { 26 | hashmap!{$($key.to_owned() => $value.to_owned()),*}; 27 | }; 28 | } 29 | 30 | /// Construct a string literal from given separate string literal lines 31 | /// by joining them all together. 32 | macro_rules! unlines ( 33 | ($($line:expr),*) => ({ 34 | $crate::util::join(&[$($line),*], "\n") 35 | }); 36 | ); 37 | 38 | 39 | /// Join a slice of stringifiable values. 40 | pub fn join(array: &[T], sep: &str) -> String { 41 | array.iter().map(T::to_string).collect::>().join(sep) 42 | } 43 | 44 | /// Parse JSON containing only string values. 45 | pub fn parse_json_stringmap(json: &str) -> HashMap { 46 | let json = Json::from_str(json).expect("failed to parse as JSON"); 47 | match json { 48 | Json::Object(o) => o.into_iter() 49 | .map(|(k, v)| (k, v.as_string().unwrap().to_owned())).collect(), 50 | _ => { panic!("expected a JSON object literal") }, 51 | } 52 | } 53 | 54 | 55 | // Wrappers around tested code. 56 | 57 | /// Evaluate the expression without any input. 58 | pub fn eval(expr: &str) -> String { 59 | match eval_ex(expr) { 60 | Ok(output) => output, 61 | Err(err) => { panic!("eval() error: {}", err); } 62 | } 63 | } 64 | 65 | pub fn eval_ex(expr: &str) -> io::Result { 66 | let result = try!(rush::eval(expr, &mut Context::new())); 67 | String::try_from(result) 68 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) 69 | } 70 | 71 | 72 | /// Applies an expression to input given as (single line) string. 73 | /// This is a special variant of map_lines(). 74 | /// Internally, this calls rush::map_lines. 75 | pub fn apply(expr: &str, input: T) -> String { 76 | match apply_ex(expr, input) { 77 | Ok(output) => output, 78 | Err(err) => { panic!("apply() error: {}", err); } 79 | } 80 | } 81 | 82 | pub fn apply_ex(expr: &str, input: T) -> io::Result { 83 | let input = input.to_string(); 84 | assert!(!input.contains("\n")); 85 | map_lines_ex(expr, input) 86 | } 87 | 88 | 89 | /// Applies an expression to input given as a string. 90 | /// 91 | /// Single- and multiline strings are handled automatically: 92 | /// multiline strings are split into individual lines & mapped over with `expr`. 93 | /// Howeever, if the input didn't end with a newline, output won't either. 94 | /// 95 | /// Internally, this calls rush::map_lines. 96 | #[allow(dead_code)] 97 | pub fn map_lines(expr: &str, input: T) -> String { 98 | match map_lines_ex(expr, input) { 99 | Ok(output) => output, 100 | Err(err) => { panic!("map_lines() error: {}", err); } 101 | } 102 | } 103 | 104 | pub fn map_lines_ex(expr: &str, input: T) -> io::Result { 105 | let mut extra_newline = false; 106 | let mut input = input.to_string(); 107 | if !input.ends_with("\n") { 108 | input.push('\n'); 109 | extra_newline = true; 110 | } 111 | 112 | let mut output: Vec = Vec::new(); 113 | try!(rush::map_lines(expr, input.as_bytes(), &mut output)); 114 | 115 | let mut result = try!( 116 | from_utf8(&output) 117 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e)) 118 | ).to_owned(); 119 | if extra_newline { 120 | result.pop(); // remove trailing \n 121 | } 122 | Ok(result) 123 | } 124 | 125 | 126 | /// Applies an expression to input given as slice of strings. 127 | /// This input is interpreted as an array by the given expression. 128 | /// 129 | /// Internally, this calls rush::apply_lines. 130 | pub fn apply_lines(expr: &str, input: &[T]) -> String { 131 | match apply_lines_ex(expr, input) { 132 | Ok(output) => output, 133 | Err(err) => { panic!("apply_lines() error: {}", err); } 134 | } 135 | } 136 | 137 | pub fn apply_lines_ex(expr: &str, input: &[T]) -> io::Result { 138 | let input = join(input, "\n"); 139 | 140 | let mut output: Vec = Vec::new(); 141 | try!(rush::apply_lines(expr, input.as_bytes(), &mut output)); 142 | 143 | // if the result turns out to be just a single line, 144 | // remove the trailing \n 145 | let mut result = try!( 146 | from_utf8(&output) 147 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e)) 148 | ).to_owned(); 149 | if result.ends_with("\n") && result.chars().filter(|c| *c == '\n').count() == 1 { 150 | result.pop(); 151 | } 152 | Ok(result) 153 | } 154 | -------------------------------------------------------------------------------- /crates/rush/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rh" 3 | version = "0.1.0" 4 | authors = ["Karol Kuczmarski "] 5 | license = "GPL-3.0" 6 | repository = "https://github.com/Xion/rush" 7 | readme = "../../README.md" 8 | publish = false 9 | 10 | [dependencies] 11 | case = "*" 12 | clap = { version = "2.2.4", features = ["suggestions"] } 13 | conv = "0.3.1" 14 | log = "0.3" 15 | rush = { path = "../librush" } 16 | 17 | [profile.release] 18 | lto = true 19 | 20 | 21 | [[bin]] 22 | name = "rh" 23 | path = "src/main.rs" 24 | doc = false 25 | -------------------------------------------------------------------------------- /crates/rush/src/logging.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing logging for the application. 2 | 3 | use std::io::{self, Write}; 4 | 5 | use log::{Log, LogRecord, LogLevel, LogMetadata, set_logger, SetLoggerError}; 6 | 7 | 8 | const MAX_LEVEL: LogLevel = LogLevel::Trace; 9 | 10 | 11 | pub fn init() -> Result<(), SetLoggerError> { 12 | set_logger(|max_log_level| { 13 | max_log_level.set(MAX_LEVEL.to_log_level_filter()); 14 | Box::new(Logger) 15 | }) 16 | } 17 | 18 | 19 | struct Logger; 20 | 21 | impl Log for Logger { 22 | #[inline] 23 | fn enabled(&self, metadata: &LogMetadata) -> bool { 24 | metadata.level() <= MAX_LEVEL 25 | } 26 | 27 | #[inline] 28 | fn log(&self, record: &LogRecord) { 29 | if self.enabled(record.metadata()) { 30 | writeln!(&mut io::stderr(), 31 | "{}: {}", record.level(), record.args()).unwrap(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/rush/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Module with the entry point of the binary. 2 | 3 | extern crate case; 4 | extern crate clap; 5 | extern crate conv; 6 | #[macro_use] 7 | extern crate log; 8 | 9 | extern crate rush; 10 | 11 | 12 | mod args; 13 | mod logging; 14 | mod rcfile; 15 | 16 | 17 | use std::error::Error; // for .cause() method 18 | use std::io::{self, Write}; 19 | use std::iter::repeat; 20 | use std::process::exit; 21 | 22 | use conv::TryFrom; 23 | use rush::Context; 24 | 25 | use args::InputMode; 26 | 27 | 28 | fn main() { 29 | logging::init().unwrap(); 30 | 31 | let opts = args::parse(); 32 | 33 | let before = opts.before.as_ref().map(|b| b as &str); 34 | let exprs: Vec<&str> = opts.expressions.iter().map(|e| e as &str).collect(); 35 | let after = opts.after.as_ref().map(|a| a as &str); 36 | 37 | match opts.input_mode { 38 | Some(mode) => { 39 | if let Err(error) = process_input(mode, before, &exprs, after) { 40 | handle_error(error); 41 | exit(1); 42 | } 43 | }, 44 | None => { 45 | if let Some(before) = before { 46 | println!("--before expression:"); 47 | print_ast(before); 48 | println!(""); 49 | } 50 | for expr in exprs { 51 | print_ast(expr); 52 | } 53 | if let Some(after) = after { 54 | println!(""); 55 | println!("--after expression:"); 56 | print_ast(after); 57 | } 58 | }, 59 | } 60 | } 61 | 62 | 63 | /// Process standard input through given expressions, writing results to stdout. 64 | fn process_input(mode: InputMode, 65 | before: Option<&str>, exprs: &[&str], after: Option<&str>) -> io::Result<()> { 66 | // Prepare a Context for the processing. 67 | // This includes evaluating any "before" expression within it. 68 | let mut context = Context::new(); 69 | try!(rcfile::load_into(&mut context) 70 | .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, 71 | format!("Error processing startup file: {}", err)))); 72 | if let Some(before) = before { 73 | try!(rush::exec(before, &mut context)); 74 | } 75 | 76 | // Do the processing. 77 | // 78 | // If there is an "after" expression provided, it is that expression that should produce 79 | // the only output of the program. So we'll just consume whatever results would normally 80 | // be printed otherwise. 81 | if after.is_some() { 82 | // HACK: Because the intermediate results have to be printed out -- even if only to /dev/null 83 | // -- we have to ensure there is always a non-empty value to use as the intermediate result. 84 | // This is necessary especially since with --after (and --before), intermediate expressions 85 | // are likely to be just assignments (and the result of an assignment is empty). 86 | // 87 | // We can make sure there is always a value to print simply by adding one more expression 88 | // at the end of the chain. It so happens that zero (or any number) is compatible with 89 | // all the input modes, so let's use that. 90 | let mut exprs = exprs.to_vec(); 91 | exprs.push("0"); 92 | try!(apply_multi_ctx(mode, &mut context, &exprs, &mut io::sink())); 93 | } else { 94 | try!(apply_multi_ctx(mode, &mut context, exprs, &mut io::stdout())); 95 | } 96 | 97 | // Evaluate the "after" expression, if provided, and return it as the result. 98 | if let Some(after) = after { 99 | let result = try!(rush::eval(after, &mut context)); 100 | let result_string = try!(String::try_from(result) 101 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))); 102 | 103 | // Make it so that the output always ends with a newline, 104 | // regardless whether it consists of a single value or multiple lines. 105 | return if result_string.ends_with("\n") { 106 | write!(&mut io::stdout(), "{}", result_string) 107 | } else { 108 | write!(&mut io::stdout(), "{}\n", result_string) 109 | }; 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | /// Apply the expressions to the standard input with given mode. 116 | /// This forms the bulk of the input processing. 117 | #[inline] 118 | fn apply_multi_ctx(mode: InputMode, 119 | context: &mut Context, exprs: &[&str], mut output: &mut Write) -> io::Result<()> { 120 | let func: fn(_, _, _, _) -> _ = match mode { 121 | InputMode::String => rush::apply_string_multi_ctx, 122 | InputMode::Lines => rush::map_lines_multi_ctx, 123 | InputMode::Words => rush::map_words_multi_ctx, 124 | InputMode::Chars => rush::map_chars_multi_ctx, 125 | InputMode::Bytes => rush::map_bytes_multi_ctx, 126 | InputMode::Files => rush::map_files_multi_ctx, 127 | }; 128 | func(context, exprs, io::stdin(), &mut output) 129 | } 130 | 131 | /// Handle an error that occurred while processing the input. 132 | fn handle_error(error: io::Error) { 133 | writeln!(&mut io::stderr(), "error: {}", error).unwrap(); 134 | 135 | // Print the error causes as an indented "tree". 136 | let mut cause = error.cause(); 137 | let mut indent = 0; 138 | while let Some(error) = cause { 139 | writeln!(&mut io::stderr(), "{}{}{}", 140 | repeat(" ").take(CAUSE_PREFIX.len() * indent).collect::(), 141 | CAUSE_PREFIX, 142 | error).unwrap(); 143 | indent += 1; 144 | cause = error.cause(); 145 | } 146 | } 147 | 148 | const CAUSE_PREFIX: &'static str = "└ "; // U+2514 149 | 150 | 151 | /// Print the AST for given expression to stdout. 152 | fn print_ast(expr: &str) { 153 | debug!("Printing the AST of: {}", expr); 154 | match rush::parse(expr) { 155 | Ok(ast) => println!("{:?}", ast), 156 | Err(error) => { error!("{:?}", error); exit(1); }, 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /crates/rush/src/rcfile.rs: -------------------------------------------------------------------------------- 1 | //! Module for handling of the .Xrc files. 2 | //! 3 | //! Those files -- which can be in various places in the system but mostly inside 4 | //! the home directory -- may define expressions to be executed right before 5 | //! processing the input. 6 | //! 7 | //! The expressions will be executed within the root Context that's reused between 8 | //! all expressions evaluated during an invocation of the binary. 9 | //! The primary application of this is to define additional functions & other symbols 10 | //! to be available to all expressions. 11 | 12 | use std::env; 13 | use std::convert::AsRef; 14 | use std::fs::{self, File}; 15 | use std::io::{self, BufRead, BufReader, Read}; 16 | use std::path::{Path, PathBuf}; 17 | 18 | use rush::{self, Context}; 19 | 20 | 21 | /// Stems of the .Xrc filenames, of files that we'll load the symbols from. 22 | /// Note they shouldn't have the leading dot nor the actual "rc" suffix. 23 | const STEMS: &'static [&'static str] = &["rush", "rh"]; 24 | 25 | /// Prefix of comment lines. 26 | const COMMENT_PREFIX: &'static str = "//"; 27 | 28 | 29 | /// Load the definitions from all available .Xrc files into given Context. 30 | pub fn load_into(context: &mut Context) -> io::Result<()> { 31 | for path in list_rcfiles() { 32 | debug!("Loading symbols from {}", path.display()); 33 | let file = try!(File::open(&path)); 34 | let exprs = try!(read_rcfile(file)); 35 | let expr_count = exprs.len(); 36 | for expr in exprs { 37 | try!(rush::exec(&expr, context)); 38 | } 39 | info!("Loaded {} symbol(s) from {}", expr_count, path.display()); 40 | } 41 | Ok(()) 42 | } 43 | 44 | 45 | /// Read an .Xrc file, discarding all the comments & empty lines. 46 | /// Returns the list of expressions that remained. 47 | fn read_rcfile(file: R) -> io::Result> { 48 | let mut result = Vec::new(); 49 | let reader = BufReader::new(file); 50 | for line in reader.lines() { 51 | let line = try!(line); 52 | 53 | let trimmed = line.trim(); 54 | if trimmed.is_empty() || trimmed.starts_with(COMMENT_PREFIX) { 55 | continue; 56 | } 57 | result.push(trimmed.to_owned()); 58 | } 59 | Ok(result) 60 | } 61 | 62 | 63 | /// List the full paths to all .Xrc files in the system, 64 | /// in the order they should be read. 65 | fn list_rcfiles() -> Vec { 66 | // List directories eligible for having their .Xrc files read. 67 | // Note that the order matters: directories with higher priority should be 68 | // considered last, so that definitions inside their .Xrc files can override 69 | // the ones that come before them. 70 | let mut dirs: Vec = Vec::new(); 71 | if let Some(homedir) = env::home_dir() { 72 | dirs.push(homedir); 73 | } 74 | match env::current_dir() { 75 | Ok(cwd) => dirs.push(cwd.to_owned()), 76 | Err(err) => warn!("Couldn't retrieve current directory: {}", err), 77 | } 78 | 79 | // Return those .Xrc files that actually exist. 80 | let mut result = Vec::new(); 81 | for dir in dirs.into_iter().map(PathBuf::from) { 82 | for name in rc_filenames() { 83 | let path = dir.join(name); 84 | if file_exists(&path) { 85 | result.push(path); 86 | } 87 | } 88 | } 89 | result 90 | } 91 | 92 | /// Get the possible names of .Xrc files within any directory. 93 | fn rc_filenames() -> Vec { 94 | let mut result: Vec = Vec::new(); 95 | for stem in STEMS { 96 | result.push(PathBuf::from(format!(".{}rc", stem))); 97 | } 98 | for stem in STEMS { 99 | result.push(PathBuf::from(".config").join(format!(".{}rc", stem))); 100 | } 101 | result 102 | } 103 | 104 | // Utility functions 105 | 106 | fn file_exists>(path: P) -> bool { 107 | match fs::metadata(&path) { 108 | Ok(_) => true, 109 | Err(err) => { 110 | if err.kind() != io::ErrorKind::NotFound { 111 | warn!("Couldn't determine if {} exists: {}", path.as_ref().display(), err); 112 | } 113 | false 114 | } 115 | } 116 | } 117 | 118 | #[allow(dead_code)] 119 | fn directory_exists>(path: P) -> bool { 120 | match fs::metadata(&path) { 121 | Ok(metadata) => metadata.is_dir(), 122 | Err(err) => { 123 | if err.kind() != io::ErrorKind::NotFound { 124 | warn!("Couldn't determine if {} exists: {}", path.as_ref().display(), err); 125 | } 126 | false 127 | }, 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /docs/.docsignore: -------------------------------------------------------------------------------- 1 | # 2 | # List of files that should be ignored when building documentation 3 | # (this ignoring is handled by `inv build.docs` task, not mkdocs itself) 4 | # 5 | 6 | partials 7 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # rush 2 | 3 | Succinct & readable processing language for the shell 4 | 5 | ## Documentation 6 | 7 | * [Expression API reference](api.md) 8 | 9 | 10 | (Original index.md below) 11 | 12 | (TODO: incorporate `mkdocs serve` into automation tasks somewhere) 13 | 14 | 15 | # Welcome to MkDocs 16 | 17 | For full documentation visit [mkdocs.org](http://mkdocs.org). 18 | 19 | ## Commands 20 | 21 | * `mkdocs new [dir-name]` - Create a new project. 22 | * `mkdocs serve` - Start the live-reloading docs server. 23 | * `mkdocs build` - Build the documentation site. 24 | * `mkdocs help` - Print this help message. 25 | 26 | ## Project layout 27 | 28 | mkdocs.yml # The configuration file. 29 | docs/ 30 | index.md # The documentation homepage. 31 | ... # Other markdown pages, images and other files. 32 | -------------------------------------------------------------------------------- /docs/partials/function.md.jinja: -------------------------------------------------------------------------------- 1 | {#- 2 | Template generating documentation for a single function of the expression API 3 | 4 | :param func: Function object, as defined in tasks/util/docs.py 5 | -#} 6 | 7 | ### {{ func.name -}} ( {{- func.arguments|join(", ") -}} ) 8 | 9 | {{ func.description }} 10 | -------------------------------------------------------------------------------- /docs/partials/module.md.jinja: -------------------------------------------------------------------------------- 1 | {#- 2 | Template generating documentation for a single module of the expression API. 3 | 4 | :param mod: Module object, as defined in tasks/util/docs.py 5 | -#} 6 | 7 | ## {{ mod.name }} 8 | 9 | {{ mod.description }} 10 | 11 | {# TODO: render submodules #} 12 | {%- for func in mod.functions %} 13 | {#- Intentional empty line #} 14 | {{- func.render() }} 15 | {#- Intentional empty line #} 16 | {% endfor -%} 17 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: rush 2 | site_description: "Succinct & readable processing language for the shell" 3 | site_author: "Karol Kuczmarski" 4 | 5 | theme: 'material' 6 | 7 | docs_dir: 'docs' 8 | site_dir: 'release/docs' 9 | 10 | pages: 11 | - API: 'api.md' 12 | 13 | markdown_extensions: 14 | - smarty 15 | - toc: 16 | permalink: True 17 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Python packages used in development 3 | # 4 | 5 | invoke>=0.13 6 | 7 | # Dependencies used directly by automation tasks 8 | flake8 9 | pathlib 10 | semver 11 | glob2 12 | jinja2 13 | 14 | # Documentation generation 15 | mkdocs 16 | mkdocs-material 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | ; Configuration file for auxiliary Python-related tools 2 | 3 | [flake8] 4 | ignore = W503, E402, E731 5 | show-source = 1 6 | -------------------------------------------------------------------------------- /syntax.md: -------------------------------------------------------------------------------- 1 | # rush 2 | 3 | Expression syntax overview 4 | 5 | TODO(xion): expand further, move to docs, and split 6 | 7 | ## Data types 8 | 9 | * string (default if no explicit annotation/function/coercion is used) 10 | * number: int or float 11 | * booleans (true or false) 12 | # regexes 13 | * arrays 14 | * objects (hashmaps string -> value) 15 | 16 | ## Identifiers & values 17 | 18 | Identifier starts with a letter (NOT underscore, because see below) 19 | and can contain letters, numbers, and the underscore character. 20 | 21 | If an identifier doesn't refer to a known function or variable, it is treated as literal string. 22 | 23 | Strings are surrounded with double quotes. \" to escape a quote, \\ to escape a backslash. 24 | 25 | Integers are `[+-]?[1-9][0-9]*`. 26 | Floats are additionally `[+-]?([0-9]\.)?[0-9]+(e$INTEGER)?` (i.e. regular & scientific notation). 27 | 28 | Arrays are enclosed in brackets: `[1, 2, 3]`. 29 | 30 | Objects use JS(ON) notation: `{foo: "bar", "baz": qux}`. Note that both key & value are expressions, 31 | so `foo` may be either a string `"foo"` or the value of `foo` variable. 32 | 33 | Regexes are enclosed in slashes: `/foo+/`. (Slashes are **not** used to perform regex operations, though, 34 | unlike sed or Perl). 35 | 36 | ## Special symbols 37 | 38 | * `_` (underscore) -- Current item. Meaning depends on the flags, e.g. for `-l` (default) this will be the current line. 39 | 40 | ## Operators 41 | 42 | * logical: `&&`, `||` 43 | * comparison: `<`, `>`, `<=`, `>=`, `==`, `!=`, `@` (membership & regex matching) 44 | * arithmetic: `+`, `-`, `*`, `**`, `/`, `%`; operate on numbers 45 | * strings: `+` (concatentation), `*` (repeat), `%` (formatting), `/` (split), `*` (join (array * string)) 46 | * ternary operator: `?:` 47 | 48 | ## Functions 49 | 50 | Function names are identifiers. 51 | 52 | Function invocation always involves parentheses around its argument list (even if empty). 53 | Multiple arguments are separated with comma. 54 | 55 | Anonymous functions are defined using `|` (pipe), an argument list, another pipe, and expression, e.g.: 56 | 57 | || 42 58 | |x| x + 2 59 | |x,y| x + y 60 | 61 | `&` is the "reverse function composition" (piping) operator: 62 | 63 | int & abs & |x| x / 2 === |x| abs(int(x)) / 2 64 | 65 | Functions are automatically curried when given fewer than minimum number of arguments: 66 | 67 | split(",") === |s| split(",", s) 68 | 69 | These features can of course be combined: 70 | 71 | $ echo '1,2,3' | rh 'split(",") & map(int & |x| x * 2) & join(",")' 72 | 2,4,6 73 | 74 | There is also a Haskell-like syntax for (partial application of) operator functions 75 | `(+)`, `(2+)`, `(*5)`, etc. 76 | 77 | ## Reserved syntactic elements 78 | 79 | All "special" characters (symbols on the numeric row) are reserved for possible future use. 80 | If string is to contain them, it must be surrounded by quotes. 81 | 82 | Some possible future keywords are also reserved, e.g.: `if else while for do`. 83 | 84 | ## Result handling 85 | 86 | Depending on the type of the overall expression, the result of its execution is the following: 87 | 88 | * if the type is a unary function, it is applied to the current item and its result 89 | is the output for the item 90 | * if the type is a plain value, it is executed with `_` bound to current item 91 | and the result is the output for the item 92 | * otherwise (e.g. function with more than one argument) it is a fatal error 93 | 94 | Alternately, an expression such as `_ + 2` can be thought as a shorthand for `|x| x + 2`. 95 | -------------------------------------------------------------------------------- /tasks/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automation tasks, aided by the Invoke package. 3 | 4 | Most of them mimic the Rust's Cargo binary, but they do what makes sense for 5 | either the library crate, binary crate, or both. 6 | """ 7 | import logging 8 | import os 9 | import sys 10 | 11 | from invoke import Collection, task 12 | 13 | 14 | BIN = 'rush' 15 | LIB = 'librush' 16 | 17 | 18 | @task(default=True) 19 | def default(ctx): 20 | """Run all the tests & lints.""" 21 | test.all(ctx) 22 | lint.all(ctx) 23 | 24 | 25 | # Task setup 26 | 27 | ns = Collection() 28 | ns.add_task(default) 29 | 30 | from tasks import build, clean, lint, release, run, test 31 | ns.add_collection(build) 32 | ns.add_collection(clean) 33 | ns.add_collection(lint) 34 | ns.add_collection(run) 35 | ns.add_collection(release) 36 | ns.add_collection(test) 37 | 38 | from tasks import misc 39 | ns.add_task(misc.print_grammar) 40 | 41 | 42 | # This precondition makes it easier to localize various auxiliary files 43 | # that the tasks need, like Cargo.toml's of crates, etc. 44 | if not os.path.exists(os.path.join(os.getcwd(), '.gitignore')): 45 | logging.fatal( 46 | "Automation tasks can only be invoked from project's root directory!") 47 | sys.exit(1) 48 | -------------------------------------------------------------------------------- /tasks/build.py: -------------------------------------------------------------------------------- 1 | """ 2 | Build tasks. 3 | """ 4 | from __future__ import print_function 5 | 6 | from itertools import chain, imap 7 | import logging 8 | import os 9 | from pathlib import Path 10 | import re 11 | try: 12 | from shlex import quote 13 | except ImportError: 14 | from pipes import quote 15 | import shutil 16 | import sys 17 | 18 | from invoke import task 19 | import semver 20 | 21 | from tasks import BIN, LIB 22 | from tasks.util import cargo, docs 23 | 24 | 25 | MIN_RUSTC_VERSION = '1.10.0' 26 | 27 | HELP = { 28 | 'release': "Whether to build artifacts in release mode", 29 | 'verbose': "Whether to show verbose logging output of the build", 30 | } 31 | 32 | 33 | @task(help=HELP, default=True) 34 | def all(ctx, release=False, verbose=False): 35 | """Build the project.""" 36 | # calling lib() is unnecessary because the binary crate 37 | # depeends on the library crate, so it will be rebuilt as well 38 | bin(ctx, release=release, verbose=verbose) 39 | docs_(ctx, release=release, verbose=verbose) 40 | print("\nBuild finished.", file=sys.stderr) 41 | 42 | 43 | @task(help=HELP) 44 | def bin(ctx, release=False, verbose=False): 45 | """Build the binary crate.""" 46 | ensure_rustc_version(ctx) 47 | cargo_build = cargo(ctx, 'build', *get_rustc_flags(release, verbose), 48 | crate=BIN, pty=True) 49 | if not cargo_build.ok: 50 | logging.critical("Failed to build the binary crate") 51 | return cargo_build.return_code 52 | 53 | # TODO: everything below should probably its own separate task 54 | 55 | # run the resulting binary to obtain command line help 56 | binary = cargo(ctx, 'run', crate=BIN, hide=True) 57 | if not binary.ok: 58 | logging.critical("Compiled binary return error code %s; stderr:\n%s", 59 | binary.return_code, binary.stderr) 60 | return binary.return_code 61 | help_lines = binary.stdout.strip().splitlines() 62 | 63 | # beautify it before pasting into README 64 | while not help_lines[0].startswith("USAGE"): 65 | del help_lines[0] # remove "About" line & other fluff 66 | del help_lines[0] # remove "USAGE:" header 67 | flags = re.findall(r'--(?:\w|[-_])+', help_lines[0]) 68 | help_lines[1:] = [ 69 | # remove the description lines of flags that aren't in the usage string 70 | line for line in help_lines[1:] 71 | if not '--' in line or any(flag in line for flag in flags) 72 | ] 73 | help_lines[:1] = ( 74 | # make the usage line more readable by splitting long flags into 75 | # separate lines, and fix binary name 76 | ']\n '.join(help_lines[0].strip().split('] ')) 77 | .replace("rush", "rh") 78 | .splitlines() 79 | ) 80 | help = os.linesep.join(' ' + line for line in help_lines) 81 | 82 | # paste the modified help into README 83 | verbose and logging.info("Updating README to add binary's help string") 84 | with (Path.cwd() / 'README.md').open('r+t', encoding='utf-8') as f: 85 | readme_lines = [line.rstrip() for line in f.readlines()] 86 | 87 | # determine the line indices of the region to replace, 88 | # which is between the header titled "Usage" and the immediate next one 89 | begin_idx, end_idx = None, None 90 | for i, line in enumerate(readme_lines): 91 | if not line.startswith('#'): 92 | continue 93 | if begin_idx is None: 94 | if "# Usage" in line: 95 | begin_idx = i 96 | else: 97 | end_idx = i 98 | break 99 | if begin_idx is None or end_idx is None: 100 | logging.critical("usage begin or end markers not found in README " 101 | "(begin:%s, end:%s)", begin_idx, end_idx) 102 | return 2 103 | 104 | # reassemble the modified content of the README, with help inside 105 | readme_content = os.linesep.join([ 106 | os.linesep.join(readme_lines[:begin_idx + 1]), 107 | '', help, '', 108 | os.linesep.join(readme_lines[end_idx:]), 109 | '', # ensure newline at the end of file 110 | ]) 111 | 112 | f.seek(0) 113 | f.truncate() 114 | f.write(readme_content) 115 | 116 | 117 | @task(help=HELP) 118 | def lib(ctx, release=False, verbose=False): 119 | """Build the library crate.""" 120 | ensure_rustc_version(ctx) 121 | return cargo( 122 | ctx, 'build', *get_rustc_flags(release, verbose), crate=LIB, pty=True 123 | ).return_code 124 | 125 | 126 | @task(name='docs', help=HELP) 127 | def docs_(ctx, release=False, verbose=False, dump_api=False): 128 | """Build the project documentation. 129 | 130 | This includes analyzing the Rust modules that implement expression API 131 | in order to extract their in-code documentation before putting it in 132 | the dedicated documentation page as Markdown. 133 | 134 | It also removes some of the superfluous files from the docs output 135 | directory in release mode. 136 | """ 137 | # describe the API modules and functions contained therein, 138 | # rendering the documentation as Markdown into the designated doc page 139 | is_root_mod_rs = lambda p: p.stem == 'mod' and p.parent.stem == 'api' 140 | module_paths = [ 141 | mod for mod in Path('./crates', LIB, 'src/eval/api').rglob('**/*.rs') 142 | if not is_root_mod_rs(mod)] 143 | modules = docs.describe_rust_api(*module_paths) 144 | docs.insert_api_docs(modules, into='./docs/api.md') 145 | 146 | # build the docs in output format 147 | args = ['--strict'] 148 | if release: 149 | args.append('--clean') 150 | if verbose: 151 | args.append('--verbose') 152 | build_run = ctx.run('mkdocs build ' + ' '.join(map(quote, args)), pty=True) 153 | if not build_run.ok: 154 | logging.fatal("mkdocs build failed, aborting.") 155 | return build_run.return_code 156 | 157 | mkdocs_config = docs.read_mkdocs_config() 158 | source_dir = Path.cwd() / mkdocs_config.get('docs_dir', 'docs') 159 | output_dir = Path.cwd() / mkdocs_config.get('site_dir', 'site') 160 | 161 | # purge any HTML comments that have been carried from Markdown 162 | for path in output_dir.rglob('*.html'): 163 | docs.scrub_html_comment_markers(path) 164 | 165 | # for release doc builds, clean some of the output files that get 166 | # copied verbatim since mkdocs doesn't support ignoring them 167 | if release: 168 | # read the list of ignored path patterns from a file 169 | ignored = [] 170 | ignore_file = source_dir / '.docsignore' 171 | if ignore_file.exists(): 172 | verbose and logging.info( 173 | "%s file found, applying ignore patterns...", ignore_file) 174 | with ignore_file.open(encoding='utf-8') as f: 175 | ignored = [ 176 | line.rstrip() for line in f.readlines() 177 | if line.strip() and not line.lstrip().startswith('#')] 178 | else: 179 | verbose and logging.info( 180 | "%s not found, not removing any ignored files.", ignore_file) 181 | 182 | # resolve the patterns to see what files in the output dir 183 | # they correspond to, if any 184 | if ignored: 185 | ignored = chain.from_iterable(imap(output_dir.glob, ignored)) 186 | 187 | # "ignore" them, i.e. delete from output directory 188 | for path in ignored: 189 | verbose and logging.info( 190 | "Removing ignored file/directory '%s'", path) 191 | if path.is_dir(): 192 | shutil.rmtree(str(path)) 193 | else: 194 | path.unlink() 195 | 196 | 197 | # Utility functions 198 | 199 | def ensure_rustc_version(ctx): 200 | """Terminates the build unless the Rust compiler is recent enough.""" 201 | rustc_v = ctx.run('rustc --version', hide=True) 202 | if not rustc_v.ok: 203 | logging.critical("Rust compiler not found, aborting build.") 204 | sys.exit(rustc_v.return_code) 205 | 206 | _, version, _ = rustc_v.stdout.split(None, 2) 207 | if not semver.match(version, '>=' + MIN_RUSTC_VERSION): 208 | logging.error("Build requires at least Rust %s, found %s", 209 | MIN_RUSTC_VERSION, version) 210 | sys.exit(1) 211 | 212 | return True 213 | 214 | 215 | def get_rustc_flags(release, verbose): 216 | """Return a list of Rust compiler flags corresponding to given params.""" 217 | flags = [] 218 | if release: 219 | flags.append('--release') 220 | if verbose: 221 | flags.append('--verbose') 222 | return flags 223 | -------------------------------------------------------------------------------- /tasks/clean.py: -------------------------------------------------------------------------------- 1 | """ 2 | Clean tasks. 3 | """ 4 | from __future__ import print_function 5 | 6 | import logging 7 | import shutil 8 | import sys 9 | 10 | from invoke import task 11 | 12 | from tasks import BIN, LIB 13 | from tasks.util import cargo 14 | from tasks.util.docs import get_docs_output_dir 15 | 16 | 17 | HELP = {'release': "Whether the to clean release artifacts."} 18 | 19 | 20 | @task(help=HELP, default=True) 21 | def all(ctx, release=False): 22 | """Clean all of the project's build artifacts.""" 23 | lib(ctx, release=release) 24 | bin(ctx, release=release) 25 | docs(ctx) 26 | print("\nAll cleaned.", file=sys.stderr) 27 | 28 | 29 | @task(help=HELP) 30 | def bin(ctx, release=False): 31 | """Clean the binary crate's build artifacts.""" 32 | args = ['--release'] if release else [] 33 | return cargo(ctx, 'clean', *args, crate=BIN, pty=True).return_code 34 | 35 | 36 | @task 37 | def docs(ctx): 38 | """Clean the built documentation.""" 39 | output_dir = get_docs_output_dir() 40 | if output_dir.is_dir(): 41 | try: 42 | shutil.rmtree(str(output_dir)) 43 | except (OSError, shutil.Error) as e: 44 | logging.warning("Error while cleaning docs' output dir: %s", e) 45 | 46 | 47 | @task(help=HELP) 48 | def lib(ctx, release=False): 49 | """Clean the library crate's build artifacts.""" 50 | args = ['--release'] if release else [] 51 | return cargo(ctx, 'clean', *args, crate=LIB, pty=True).return_code 52 | -------------------------------------------------------------------------------- /tasks/lint.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lint tasks. 3 | """ 4 | from invoke import task 5 | 6 | 7 | @task 8 | def tasks(ctx): 9 | """Lint the Invoke tasks' code.""" 10 | return ctx.run('flake8 tasks').return_code 11 | 12 | 13 | @task(default=True, pre=[tasks]) 14 | def all(ctx): 15 | """Run all the lint tasks.""" 16 | -------------------------------------------------------------------------------- /tasks/misc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Miscellaneous tasks. 3 | """ 4 | from __future__ import print_function 5 | 6 | from glob import glob 7 | 8 | from invoke import task 9 | 10 | from tasks import LIB 11 | 12 | 13 | @task(name="print-grammar") 14 | def print_grammar(ctx): 15 | """Prints the language's grammar rules. 16 | Uses comments in the syntax definition & parsing code. 17 | """ 18 | syntax_package = './crates/%s/src/parse/syntax' % (LIB,) 19 | for filename in glob('%s/*.rs' % syntax_package): 20 | with open(filename) as f: 21 | for line in f.readlines(): 22 | # TODO: support grammar rules spanning more than one line 23 | if '::==' in line: 24 | line = line.lstrip('/').strip() 25 | print(line) 26 | -------------------------------------------------------------------------------- /tasks/release.py: -------------------------------------------------------------------------------- 1 | """ 2 | Release tasks. 3 | """ 4 | from invoke import task 5 | 6 | 7 | # TODO: rewrite the ./tools/release script in Python into nested tasks 8 | # like release.deb or release.rpm or release.brew 9 | @task(default=True) 10 | def all(ctx): 11 | """Create the release packages for various operating systems.""" 12 | return ctx.run('./tools/release', pty=True).return_code 13 | -------------------------------------------------------------------------------- /tasks/run.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run tasks. 3 | """ 4 | import logging 5 | import sys 6 | import threading 7 | import webbrowser 8 | 9 | from invoke import task 10 | 11 | from tasks import BIN 12 | from tasks.util import cargo 13 | 14 | 15 | @task(default=True) 16 | def bin(ctx): 17 | """Run the binary crate.""" 18 | # Because we want to accept arbitrary arguments, we have to ferret them out 19 | # of sys.argv manually. 20 | cargo(ctx, 'run', *sys.argv[2:], crate=BIN, wait=False) 21 | 22 | 23 | @task(help={ 24 | 'port': "Port to have the docs' HTTP server listen on", 25 | 'reload': "Whether live reload of the docs should be enabled", 26 | 'verbose': "Whether to display verbose logging output of the server", 27 | }) 28 | def docs(ctx, port=8000, reload=False, verbose=False): 29 | """"Run" the docs. 30 | 31 | This starts an HTTP server for serving the docs (with optional live reload) 32 | and previews them in the default web browser. 33 | """ 34 | addr = '127.0.0.1:%s' % port 35 | 36 | build_error_event = threading.Event() 37 | 38 | def open_browser(url): 39 | """Open browser with compiled docs. 40 | Runs in a separate thread to sleep for a while while the docs build. 41 | """ 42 | # few seconds should be enough for a successful build 43 | build_error_event.wait(timeout=2.0) 44 | if not build_error_event.is_set(): 45 | webbrowser.open_new_tab(url) 46 | 47 | opener_thread = threading.Thread(target=open_browser, 48 | args=('http://%s' % addr,)) 49 | opener_thread.start() 50 | 51 | # run server which will take some time to execute the build; 52 | # if that fails, we signal the event to prevent the browser from opening 53 | server = ctx.run('mkdocs serve --dev-addr %s --%slivereload %s' % ( 54 | addr, 55 | '' if reload else 'no-', 56 | '--verbose' if verbose else '', 57 | ), pty=True) 58 | if not server.ok: 59 | logging.critical("Failed to start the mkdocs server") 60 | build_error_event.set() 61 | opener_thread.join() 62 | return server.return_code 63 | -------------------------------------------------------------------------------- /tasks/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test tasks. 3 | """ 4 | from invoke import task 5 | 6 | from tasks import BIN, LIB 7 | from tasks.util import cargo 8 | 9 | 10 | @task(default=True) 11 | def all(ctx): 12 | """Execute the project's tests.""" 13 | lib(ctx) 14 | bin(ctx) 15 | 16 | 17 | @task 18 | def bin(ctx): 19 | """Execute the binary crate's tests.""" 20 | return cargo( 21 | ctx, 'test', '--no-fail-fast', crate=BIN, pty=True).return_code 22 | 23 | 24 | @task 25 | def lib(ctx): 26 | """Execute the library crate's tests.""" 27 | return cargo( 28 | ctx, 'test', '--no-fail-fast', crate=LIB, pty=True).return_code 29 | -------------------------------------------------------------------------------- /tasks/util/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions used by multiple task collections. 3 | """ 4 | import os 5 | try: 6 | from shlex import quote 7 | except ImportError: 8 | from pipes import quote 9 | 10 | 11 | __all__ = ['cargo'] 12 | 13 | 14 | def cargo(ctx, cmd, *args, **kwargs): 15 | """Run Cargo as if inside the specified crate directory. 16 | 17 | :param ctx: Invoke's Context object 18 | :param cmd: Cargo command to run 19 | 20 | The following are optional keyword arguments: 21 | 22 | :param crate: Name of the crate to run Cargo against 23 | :param wait: Whether to wait for the Cargo process to finish (True), 24 | or to replace the whole Invoke process with it (False) 25 | 26 | :return: If wait=True, the Invoke's Result object of the run. 27 | """ 28 | cargo_args = [cmd] 29 | 30 | crate = kwargs.pop('crate', None) 31 | if crate: 32 | cargo_args.append('--manifest-path') 33 | cargo_args.append(os.path.join('crates', crate, 'Cargo.toml')) 34 | 35 | cargo_args.extend(args) 36 | 37 | wait = kwargs.pop('wait', True) 38 | if wait: 39 | return ctx.run('cargo ' + ' '.join(map(quote, cargo_args)), **kwargs) 40 | else: 41 | argv = ['cargo'] + cargo_args # execvp() needs explicit argv[0] 42 | os.execvp('cargo', argv) 43 | -------------------------------------------------------------------------------- /tools/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script for preparing release bundles with the rush binary 4 | # 5 | # This essentially a wrapper script around fpm (https://github.com/jordansissel/fpm). 6 | # Requires fpm to be installed first, which may in turn require Ruby with C headers. 7 | # 8 | # Refer to fpm's README for instructions. 9 | # 10 | 11 | # Package information 12 | NAME='rush' 13 | VERSION='0.1.0' 14 | DESCRIPTON='Succinct & readable processing language' 15 | URL='http://github.com/Xion/rush' 16 | LICENSE='GPL v3' 17 | MAINTAINER='Karol Kuczmarski' 18 | # TODO(xion): share these between this script, Cargo.toml, and the argument parser 19 | # (the last one probably requires custom build.rs) 20 | 21 | CRATE_DIR='./crates/rush' 22 | SOURCE_DIR="$CRATE_DIR/target/release" 23 | BIN='rh' 24 | LICENSE_FILE='./LICENSE' 25 | OUTPUT_DIR="./release" 26 | 27 | INSTALL_DIR='/usr/bin' 28 | 29 | 30 | main() { 31 | [ -f ".gitignore" ] || fatal "Release script must be run from project's root!" 32 | 33 | require cargo 34 | require fpm 35 | 36 | log "Building the release binary..." 37 | cargo_build 38 | 39 | log "Creating release bundles..." 40 | ensure_output_dir 41 | prepare_deb_package 42 | prepare_rpm_package 43 | 44 | log "Release finished." 45 | } 46 | 47 | 48 | ensure_output_dir() { 49 | if [ ! -d "$OUTPUT_DIR" ]; then 50 | log "Creating output directory ($OUTPUT_DIR)..." 51 | mkdir -p "$OUTPUT_DIR" 52 | fi 53 | } 54 | 55 | cargo_build() { 56 | cargo build --release --manifest-path "$CRATE_DIR/Cargo.toml" 57 | if [ ! -f "${SOURCE_DIR}/$BIN" ]; then 58 | fatal "Failed to build the release binary, aborting." 59 | fi 60 | 61 | # strip the binary of debug symbols to reduce its size 62 | if exists strip; then 63 | strip "${SOURCE_DIR}/$BIN" 64 | else 65 | warn "'strip' not found, binary will retain debug symbols." 66 | fi 67 | 68 | # ensure a license file is available in the source directory 69 | cp --force --no-target-directory "$LICENSE_FILE" "$SOURCE_DIR/$LICENSE_FILE" 70 | # TODO(xion): include the license file in the final bundles 71 | } 72 | 73 | 74 | prepare_deb_package() { 75 | log "Preparing Debian package..." 76 | 77 | bundle deb --prefix "$INSTALL_DIR" 78 | if [ "$?" -ne 0 ]; then 79 | fatal "Failed to create Debian package." 80 | fi 81 | } 82 | 83 | prepare_rpm_package() { 84 | if ! exists rpm; then 85 | warn "`rpm` not found -- skipping creation of the RedHat package." 86 | return 1 87 | fi 88 | 89 | log "Preparing RedHat package..." 90 | 91 | bundle rpm --prefix "$INSTALL_DIR" 92 | if [ "$?" -ne 0 ]; then 93 | fatal "Failed to create RedHat package." 94 | fi 95 | } 96 | 97 | bundle() { 98 | # Create a release bundle by invoking `fpm` with common parameters 99 | local target="$1" ; shift 100 | 101 | local iteration=$(git rev-parse --short HEAD) # Use Git SHA of HEAD as iteration. 102 | fpm --force --log error \ 103 | --name "$NAME" --version "$VERSION" --iteration="$iteration" \ 104 | --description="$DESCRIPTON" --license "$LICENSE" --url "$URL" \ 105 | --maintainer "$MAINTAINER" --vendor '' \ 106 | -s dir -t "$target" -C "$SOURCE_DIR" --package "${OUTPUT_DIR}/${NAME}.$target" \ 107 | "$@" \ 108 | "$BIN" 109 | } 110 | 111 | 112 | # General utility functions 113 | 114 | exists() { 115 | which "$1" >/dev/null 116 | } 117 | 118 | require() { 119 | # Require for an external program to exist, abort the script if not found 120 | local prog="$1" 121 | local msg="${2-$prog not found, aborting.}" 122 | exists "$prog" || fatal "$msg\n" 123 | } 124 | 125 | fatal() { 126 | local fmt="$1" ; shift 127 | log "FATAL: $fmt" "$@" 128 | exit 1 129 | } 130 | 131 | warn() { 132 | local fmt="$1" ; shift 133 | log "WARN: $fmt" "$@" 134 | } 135 | 136 | log() { 137 | local fmt="$1" ; shift 138 | printf >&2 "\n>>> $fmt\n" "$@" 139 | } 140 | 141 | 142 | main "$@" 143 | --------------------------------------------------------------------------------