├── .github └── workflows │ └── publish.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── agora ├── Cargo.toml └── src │ ├── args.rs │ ├── contest.rs │ ├── errors.rs │ ├── log_loader.rs │ ├── main.rs │ ├── model_loader.rs │ └── runner.rs ├── docs ├── README.md └── reference │ ├── boolean-expressions.md │ ├── expressions.md │ ├── identifiers.md │ ├── matches.md │ ├── models.md │ ├── predicates.md │ ├── rational-expressions.md │ ├── statements.md │ ├── substitutions.md │ ├── toc.md │ └── when-clauses.md ├── lang ├── Cargo.toml └── src │ ├── coercion.rs │ ├── context.rs │ ├── expressions │ ├── binary.rs │ ├── boolean_algebra.rs │ ├── comparisons.rs │ ├── expr_stack.rs │ ├── linears.rs │ ├── mod.rs │ └── primitives.rs │ ├── language.rs │ ├── lib.rs │ ├── matching.rs │ ├── parse_error_tests.rs │ ├── parse_errors.rs │ ├── parser.rs │ ├── prelude.rs │ └── tests.rs ├── node-plugin ├── Cargo.toml ├── README.md ├── lib │ ├── index.d.ts │ ├── index.js │ └── index.test.js └── src │ ├── build.rs │ └── lib.rs ├── package.json └── yarn.lock /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # Creates and publishes assets for a new release. 2 | # To manually trigger this workflow, create a release in the GitHub UI. 3 | 4 | name: "Publish binaries" 5 | 6 | on: 7 | push: 8 | tags: 9 | - '*' 10 | 11 | jobs: 12 | publish-github: 13 | strategy: 14 | # Allowing jobs to fail until 'node-pre-gyp-github' supports failing gracefully if file already exists 15 | # (https://github.com/bchr02/node-pre-gyp-github/issues/42) 16 | fail-fast: false 17 | matrix: 18 | node_version: [18, 19, 20] 19 | system: 20 | - os: macos-12 21 | target: x86_64-apple-darwin 22 | - os: ubuntu-22.04 23 | target: x86_64-unknown-linux-gnu 24 | include: 25 | ## ARM64 builds are not working. No ARM64 GitHub Action runners available out of box. Need to nail down cross compile 26 | # - node_version: 16 27 | # system: 28 | # os: macos-latest 29 | # target: aarch64-apple-darwin 30 | - node_version: 17 31 | system: 32 | os: ubuntu-20.04 33 | target: x86_64-unknown-linux-gnu 34 | - node_version: 17 35 | system: 36 | os: macos-11 37 | target: x86_64-apple-darwin 38 | runs-on: ${{ matrix.system.os }} 39 | steps: 40 | - name: Checkout the repo 41 | uses: actions/checkout@v2 42 | - name: Set up Node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v2.1.5 44 | with: 45 | node-version: ${{ matrix.node_version }} 46 | - name: Setup Rust 47 | uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: stable 51 | target: ${{ matrix.system.target }} 52 | override: true 53 | - name: Setup python 54 | run: python3 -m pip install setuptools 55 | - name: Install dependencies 56 | working-directory: ./ 57 | run: yarn install --ignore-scripts 58 | - name: Compile binary, test, package, and publish to Github release page 59 | env: 60 | NODE_PRE_GYP_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | CARGO_BUILD_TARGET: ${{ matrix.system.target }} 62 | working-directory: ./ 63 | run: yarn build-test-pack-publish 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node-plugin/native/target 2 | node-plugin/native/index.node 3 | node-plugin/native/artifacts.json 4 | **/node_modules 5 | /target 6 | **/.DS_Store 7 | .idea 8 | Cargo.lock 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "lang", 5 | "agora", 6 | "node-plugin", 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 The Graph Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Agora 2 | 3 | ## The evaluation tool 4 | 5 | The source for the evaluation tool lives under this repo at `./agora/`. 6 | 7 | ### Installation 8 | 9 | To compile and run from source: 10 | 11 | **Install Rustup** 12 | If you don't have `cargo` already, install it by following the instructions here: https://www.rust-lang.org/tools/install 13 | 14 | **Compile and Run** 15 | 16 | The first step in using the tool is to compile it with `cargo`. 17 | 18 | ```shell 19 | cd ./agora 20 | cargo build --release 21 | ``` 22 | 23 | The tool will be compiled to `/target/release/`. 24 | 25 | ```shell 26 | cd target/release/ 27 | ./agora -h 28 | ``` 29 | 30 | This should print the available command-line arguments, which, at the time of this writing will look like this: 31 | 32 | ``` 33 | agora 0.1.0 34 | 35 | USAGE: 36 | agora [OPTIONS] 37 | 38 | FLAGS: 39 | -h, --help Prints help information 40 | -V, --version Prints version information 41 | 42 | OPTIONS: 43 | -c, --cost A cost model to use for costing 44 | --globals 45 | --grt-per-time 46 | -l, --load-log ... Load request log file(s) supports json and tree-buf 47 | --sample Take a sample of the request log. Unit interval [default: 1.0] 48 | --save-log Save the request log file. Only tree-buf is supported 49 | ``` 50 | 51 | **Usage** 52 | 53 | The arguments to the tool are meant to be combined to instruct `agora` to accomplish tasks. What follows are just examples: 54 | 55 | ```shell 56 | # Load two log files (from json lines format) 57 | # Sample the logs at 10% 58 | # Save the result as a single tree-buf file 59 | ./agora \ 60 | --load-log ./log1.jsonl \ 61 | --load-log ./log2.jsonl \ 62 | --sample 0.1 \ 63 | --save-log ./logs.treebuf 64 | 65 | # Load the sampled/combined file 66 | # And evaluate the effectiveness of our pricing 67 | ./agora \ 68 | --load-log ./logs.treebuf \ 69 | --globals ./globals.json \ 70 | --grt-per-time 0.0001 \ 71 | --cost ./cost-model.agora 72 | ``` 73 | 74 | **Cost Models** 75 | 76 | Agora uses a specialized language for defining cost models that can react to changes in Query structure 77 | and parameters. See the [documentation](https://github.com/graphprotocol/agora/blob/master/docs/README.md) to learn about the Agora cost model language. 78 | 79 | ## Copyright 80 | 81 | Copyright © 2020 The Graph Foundation. 82 | 83 | Licensed under the [MIT license](./LICENSE). 84 | -------------------------------------------------------------------------------- /agora/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "agora" 3 | version = "0.1.0" 4 | authors = ["Zac Burns "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | cost-model = { path = "../lang" } 11 | # num-bigint is behind so we can use num-format 12 | num-bigint = "0.4.5" 13 | fraction = { version = "0.15.2", features = ["with-bigint"] } 14 | serde = { version = "1.0.115", features = ["derive"] } 15 | serde_json = "1.0" 16 | tree-buf = "0.10.0" 17 | rayon = "1.4.0" 18 | rand = "0.8.5" 19 | structopt = "0.3.14" 20 | num-format = { version = "0.4.0", features = ["with-num-bigint"] } 21 | anyhow = "1.0.33" 22 | libflate = "2.0.0" 23 | -------------------------------------------------------------------------------- /agora/src/args.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[derive(StructOpt, Debug)] 4 | pub struct Args { 5 | /// Load request log file(s) supports json and tree-buf. 6 | #[structopt(short, long)] 7 | pub load_log: Vec, 8 | 9 | /// Save the request log file. Only tree-buf is supported. 10 | #[structopt(long)] 11 | pub save_log: Option, 12 | 13 | /// Take a sample of the request log. Unit interval. 14 | #[structopt(long, default_value = "1.0")] 15 | pub sample: f64, 16 | 17 | /// A cost model to use for costing 18 | #[structopt(long, short)] 19 | pub cost: Option, 20 | 21 | #[structopt(long, requires("cost"))] 22 | pub globals: Option, 23 | 24 | #[structopt(long, requires("cost"))] 25 | pub grt_per_time: Option, 26 | } 27 | 28 | pub fn load() -> Args { 29 | Args::from_args() 30 | } 31 | -------------------------------------------------------------------------------- /agora/src/contest.rs: -------------------------------------------------------------------------------- 1 | pub struct Contest { 2 | winners: Vec<(usize, T)>, 3 | } 4 | 5 | /// Stores a set of up to 'capacity' unique entries sorted by their score. 6 | impl Contest { 7 | pub fn new(capacity: usize) -> Self { 8 | Self { 9 | winners: Vec::with_capacity(capacity), 10 | } 11 | } 12 | 13 | pub fn take(self) -> Vec { 14 | self.winners.into_iter().map(|(_, item)| item).collect() 15 | } 16 | 17 | #[inline] 18 | pub fn insert_unique(&mut self, score: usize, value: T, equal: impl Fn(&T, &T) -> bool) { 19 | self.insert_with_unique(score, move || value, equal) 20 | } 21 | 22 | pub fn insert_with_unique( 23 | &mut self, 24 | score: usize, 25 | value: impl FnOnce() -> T, 26 | equal: impl Fn(&T, &T) -> bool, 27 | ) { 28 | if self.winners.len() == self.winners.capacity() 29 | && self.winners[self.winners.len() - 1].0 > score 30 | { 31 | return; 32 | } 33 | 34 | let value = value(); 35 | 36 | // Well, none of the below turned out to be very elegant. 37 | for (i, (other, winner)) in self.winners.iter().enumerate() { 38 | if equal(winner, &value) { 39 | if score > *other { 40 | self.winners.remove(i); 41 | break; 42 | } 43 | return; 44 | } 45 | } 46 | for (i, (other, _)) in self.winners.iter().enumerate() { 47 | if score > *other { 48 | if self.winners.len() == self.winners.capacity() { 49 | self.winners.pop(); 50 | } 51 | self.winners.insert(i, (score, value)); 52 | return; 53 | } 54 | } 55 | if self.winners.len() < self.winners.capacity() { 56 | self.winners.push((score, value)); 57 | } 58 | } 59 | } 60 | 61 | impl Contest { 62 | pub fn cloned(&self) -> Vec { 63 | self.winners.iter().map(|(_, item)| item.clone()).collect() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /agora/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | use std::fmt::Debug; 4 | use std::path::{Path, PathBuf}; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct WithPath { 8 | path: PathBuf, 9 | inner: Err, 10 | } 11 | 12 | impl WithPath { 13 | pub fn context( 14 | path: impl AsRef, 15 | f: impl FnOnce(&Path) -> Result, 16 | ) -> Result> { 17 | match f(path.as_ref()) { 18 | Ok(val) => Ok(val), 19 | Err(inner) => Err(WithPath { 20 | path: path.as_ref().to_owned(), 21 | inner, 22 | }), 23 | } 24 | } 25 | } 26 | 27 | impl fmt::Display for WithPath { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | writeln!(f, "{}", &self.inner)?; 30 | writeln!(f, "With path: {}", self.path.display())?; 31 | Ok(()) 32 | } 33 | } 34 | 35 | impl Error for WithPath {} 36 | -------------------------------------------------------------------------------- /agora/src/log_loader.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::WithPath; 2 | use anyhow::Result; 3 | use libflate::gzip::Decoder; 4 | use rand::{thread_rng, Rng}; 5 | use serde::{ 6 | de::DeserializeOwned, 7 | {Deserialize, Serialize}, 8 | }; 9 | use std::fs::File; 10 | use std::io::{BufRead, BufReader, ErrorKind, Lines, Read}; 11 | use std::marker::PhantomData; 12 | use std::path::Path; 13 | use std::time::Instant; 14 | use tree_buf::prelude::*; 15 | 16 | struct ChunkLoader { 17 | start: Instant, 18 | logs: as IntoIterator>::IntoIter, 19 | sample: f64, 20 | current: Option, 21 | _marker: PhantomData<*const T>, 22 | } 23 | 24 | impl ChunkLoader { 25 | fn elapsed(&self) -> std::time::Duration { 26 | Instant::now() - self.start 27 | } 28 | } 29 | 30 | impl Iterator for ChunkLoader 31 | where 32 | T: tree_buf::Decodable + DeserializeOwned, 33 | // TODO: Tree-buf should be able to figure out this bound 34 | Vec: tree_buf::Decodable, 35 | { 36 | type Item = Result>; 37 | 38 | fn next(&mut self) -> Option>> { 39 | loop { 40 | // Use the current file, if any 41 | match &mut self.current { 42 | // Load chunk from file and continue to next file if need be 43 | Some(current) => match current.load_chunk(self.sample) { 44 | Ok(Some(sample)) => { 45 | println!("{:?} Loaded {} queries", self.elapsed(), sample.len()); 46 | return Some(Ok(sample)); 47 | } 48 | Ok(None) => self.current = None, 49 | Err(e) => return Some(Err(e)), 50 | }, 51 | // Load next file 52 | None => { 53 | match self.logs.next() { 54 | Some(log) => { 55 | println!("{:?}, Loading file: {}", self.elapsed(), &log); 56 | match AnyLoader::new(log) { 57 | Ok(loader) => self.current = Some(loader), 58 | Err(e) => return Some(Err(e)), 59 | } 60 | } 61 | // No more logs, all done. 62 | None => { 63 | println!("{:?} Finished", self.elapsed()); 64 | return None; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | pub fn load_all_chunks(logs: &[String], sample: f64) -> impl Iterator>> 74 | where 75 | T: tree_buf::Decodable + DeserializeOwned, 76 | // TODO: Tree-buf should be able to figure out this bound 77 | Vec: tree_buf::Decodable, 78 | { 79 | let start = Instant::now(); 80 | let logs = logs.to_vec(); 81 | let logs = logs.into_iter(); 82 | 83 | println!(); 84 | 85 | ChunkLoader { 86 | start, 87 | logs, 88 | current: None, 89 | _marker: PhantomData, 90 | sample, 91 | } 92 | } 93 | 94 | #[derive(Serialize, Deserialize, Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Debug)] 95 | #[serde(rename_all = "camelCase")] 96 | pub struct Query { 97 | subgraph: String, 98 | query: String, 99 | variables: String, 100 | time: u32, 101 | } 102 | 103 | enum AnyLoader { 104 | Gz(JsonLinesLoader>), 105 | Json(JsonLinesLoader), 106 | TreeBuf(TreeBufLoader), 107 | } 108 | 109 | impl AnyLoader { 110 | pub fn new>(path: P) -> Result { 111 | let file = WithPath::context(&path, |p| File::open(p))?; 112 | let s = match path.as_ref().extension().and_then(|ext| ext.to_str()) { 113 | Some("gz") => Self::Gz(JsonLinesLoader::new(Decoder::new(file)?)?), 114 | Some("jsonl") => Self::Json(JsonLinesLoader::new(file)?), 115 | Some("treebuf") => Self::TreeBuf(TreeBufLoader::new(file)?), 116 | _ => panic!("Expecting json or treebuf file"), 117 | }; 118 | Ok(s) 119 | } 120 | 121 | fn load_chunk( 122 | &mut self, 123 | sample: f64, 124 | ) -> Result>> 125 | where 126 | Vec: tree_buf::Decodable, 127 | { 128 | match self { 129 | Self::Json(inner) => inner.load_chunk(sample), 130 | Self::TreeBuf(inner) => inner.load_chunk(sample), 131 | Self::Gz(inner) => inner.load_chunk(sample), 132 | } 133 | } 134 | } 135 | 136 | struct JsonLinesLoader { 137 | lines: Lines>, 138 | } 139 | 140 | impl JsonLinesLoader 141 | where 142 | T: Read, 143 | { 144 | fn new(file: T) -> Result { 145 | let file = BufReader::new(file); 146 | let lines = file.lines(); 147 | Ok(Self { lines }) 148 | } 149 | 150 | fn load_chunk(&mut self, sample: f64) -> Result>> { 151 | let mut rand = thread_rng(); 152 | 153 | let mut result = Vec::new(); 154 | 155 | for line in &mut self.lines { 156 | let prob: f64 = rand.gen(); 157 | if prob >= sample { 158 | continue; 159 | } 160 | 161 | let deserialized = serde_json::from_str(&line?)?; 162 | 163 | result.push(deserialized); 164 | 165 | if result.len() == crate::CHUNK_SIZE_HINT { 166 | break; 167 | } 168 | } 169 | Ok(if result.is_empty() { 170 | None 171 | } else { 172 | Some(result) 173 | }) 174 | } 175 | } 176 | 177 | struct TreeBufLoader { 178 | file: File, 179 | } 180 | 181 | impl TreeBufLoader { 182 | fn new(file: File) -> Result { 183 | Ok(Self { file }) 184 | } 185 | 186 | fn load_chunk(&mut self, sample: f64) -> Result>> 187 | where 188 | Vec: tree_buf::Decodable, 189 | { 190 | let mut chunk_size: [u8; 8] = Default::default(); 191 | 192 | match self.file.read_exact(&mut chunk_size) { 193 | Ok(()) => (), 194 | Err(e) if e.kind() == ErrorKind::UnexpectedEof => { 195 | // TODO: This isn't quite right, because we should 196 | // err bytes read is not 0. 197 | return Ok(None); 198 | } 199 | err => err?, 200 | } 201 | 202 | let chunk_size = u64::from_le_bytes(chunk_size); 203 | let mut buf = vec![0; chunk_size as usize]; 204 | 205 | self.file.read_exact(&mut buf)?; 206 | 207 | let mut result: Vec = decode(&buf)?; 208 | 209 | if sample < 1.0 { 210 | let mut rand = thread_rng(); 211 | let mut i = result.len(); 212 | while i != 0 { 213 | i -= 1; 214 | 215 | let prob: f64 = rand.gen(); 216 | if prob >= sample { 217 | result.swap_remove(i); 218 | } 219 | } 220 | } 221 | 222 | Ok(Some(result)) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /agora/src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod contest; 3 | mod errors; 4 | mod log_loader; 5 | mod model_loader; 6 | mod runner; 7 | 8 | use anyhow::{anyhow, Result}; 9 | use errors::WithPath; 10 | use num_bigint::BigUint; 11 | use tree_buf::prelude::*; 12 | 13 | const CHUNK_SIZE_HINT: usize = 262144 * 4; 14 | 15 | fn execute_args(args: args::Args) -> Result<()> { 16 | if let Some(model) = &args.cost { 17 | // Convert decimal GRT to wei 18 | let grt_per_time = args 19 | .grt_per_time 20 | .as_ref() 21 | .map(|s| { 22 | let real = cost_model::parse_real(s)?; 23 | let cost = cost_model::fract_to_cost(real) 24 | .map_err(|()| anyhow!("Failed to convert --grt-per-time to wei"))?; 25 | Result::<_>::Ok(cost) 26 | }) 27 | .transpose()?; 28 | 29 | cost_many( 30 | model, 31 | args.globals.as_deref(), 32 | &args.load_log, 33 | args.sample, 34 | grt_per_time.as_ref(), 35 | )?; 36 | } 37 | 38 | if let Some(save_log) = &args.save_log { 39 | save(save_log, &args.load_log, args.sample)?; 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | /// Loads, processes, and saves query logs. This can... 46 | /// * Load multiple log files in treebuf and/or jsonl format. 47 | /// * Load a cost model, cost each log and report on various metrics 48 | /// * Sample logs 49 | /// * Save logs in the treebuf format 50 | /// For usage details, see the command-line help 51 | fn main() { 52 | let args = args::load(); 53 | 54 | match execute_args(args) { 55 | Ok(()) => (), 56 | Err(e) => { 57 | eprintln!("Failed with error: {}", e); 58 | std::process::exit(1); 59 | } 60 | } 61 | 62 | // TODO: Ideas: 63 | // Group data in each category by both aggregate and shape hash 64 | // Command-line arguments for: 65 | // filter shape hash 66 | // bucket size? Might be important if running on very large queries and using too much memory. 67 | // filter by subgraph id 68 | // Comparing results across runs 69 | } 70 | 71 | fn cost_many( 72 | model: &str, 73 | globals: Option<&str>, 74 | logs: &[String], 75 | sample: f64, 76 | grt_per_time: Option<&BigUint>, 77 | ) -> Result<()> { 78 | let model = model_loader::load(model, globals)?; 79 | let mut result: runner::QueryCostSummary = Default::default(); 80 | for chunk in log_loader::load_all_chunks::(logs, sample) { 81 | let update = runner::cost_many(&model, chunk?, grt_per_time); 82 | result = result.merge(update); 83 | } 84 | 85 | println!("{}", &result); 86 | Ok(()) 87 | } 88 | 89 | fn save(path: &str, logs: &[String], sample: f64) -> Result<()> { 90 | use std::{fs::File, io::Write}; 91 | let mut out_chunk = Vec::new(); 92 | let mut out_file = WithPath::context(path, |p| File::create(p))?; 93 | 94 | let mut flush = move |data: &mut Vec| { 95 | // Makes the file smaller 96 | data.sort_unstable(); 97 | let bin = encode(data); 98 | let size = (bin.len() as u64).to_le_bytes(); 99 | out_file.write_all(&size)?; 100 | out_file.write_all(&bin)?; 101 | Result::<_>::Ok(()) 102 | }; 103 | 104 | for chunk in log_loader::load_all_chunks::(logs, sample) { 105 | let mut chunk = chunk?; 106 | if chunk.len() >= CHUNK_SIZE_HINT { 107 | flush(&mut chunk)?; 108 | } else { 109 | out_chunk.extend(chunk); 110 | if out_chunk.len() >= CHUNK_SIZE_HINT { 111 | flush(&mut out_chunk)?; 112 | out_chunk.clear(); 113 | } 114 | } 115 | } 116 | if !out_chunk.is_empty() { 117 | flush(&mut out_chunk)?; 118 | } 119 | 120 | Ok(()) 121 | } 122 | -------------------------------------------------------------------------------- /agora/src/model_loader.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::WithPath; 2 | use anyhow::Result; 3 | use cost_model::CostModel; 4 | use std::fs; 5 | use std::path::Path; 6 | 7 | pub fn load, P2: AsRef>(model: P1, globals: Option) -> Result { 8 | let model = WithPath::context(model, |p| fs::read_to_string(p))?; 9 | let globals = if let Some(globals) = globals { 10 | WithPath::context(globals, |p| fs::read_to_string(p))? 11 | } else { 12 | "".to_owned() 13 | }; 14 | Ok(CostModel::compile(model, &globals)?) 15 | } 16 | -------------------------------------------------------------------------------- /agora/src/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::contest::Contest; 2 | use cost_model::{wei_to_grt, CostError, CostModel}; 3 | use fraction::BigFraction; 4 | use num_bigint::{BigInt, BigUint}; 5 | use num_format::{Locale, ToFormattedString as _}; 6 | use rayon::prelude::*; 7 | use serde::{Deserialize, Serialize}; 8 | use std::collections::HashMap; 9 | use std::fmt; 10 | use tree_buf::prelude::*; 11 | 12 | #[derive(Encode, Decode, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] 13 | pub struct Query { 14 | query: String, 15 | variables: String, 16 | time: u32, 17 | } 18 | 19 | #[derive(Default)] 20 | pub struct QueryCostSummary { 21 | successes: usize, 22 | total_grt: BigUint, 23 | /// The difference between the goal stated 24 | /// in GRT/time over all queries in the summary, 25 | /// and the total_grt in the summary. 26 | total_err: Option, 27 | total_squared_err: Option, 28 | failures: HashMap<&'static str, FailureBucket>, 29 | } 30 | 31 | fn fail_name(err: CostError) -> &'static str { 32 | match err { 33 | CostError::FailedToParseQuery => "Failed to parse query", 34 | CostError::QueryNotCosted => "Query not costed", 35 | CostError::QueryNotSupported => "Query not supported", 36 | CostError::QueryInvalid => "Query invalid", 37 | CostError::CostModelFail => "Cost model failure", 38 | CostError::FailedToParseVariables => "Failed to parse variables", 39 | } 40 | } 41 | 42 | impl fmt::Display for QueryCostSummary { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | writeln!( 45 | f, 46 | "Successes: {} queries", 47 | self.successes.to_formatted_string(&Locale::en) 48 | )?; 49 | 50 | let total_grt = BigFraction::new(self.total_grt.clone(), wei_to_grt()); 51 | writeln!(f, "Total Value: {:.1$} GRT", total_grt, 2)?; 52 | 53 | if self.successes != 0 { 54 | if let Some(total_err) = &self.total_err { 55 | let mut total_err = BigFraction::from(total_err.clone()); 56 | total_err *= BigFraction::new(BigUint::from(1u32), wei_to_grt()); 57 | let per_query = total_err.clone() 58 | * BigFraction::new(BigUint::from(1u32), BigUint::from(self.successes)); 59 | writeln!( 60 | f, 61 | "Total Err: {:.2$} GRT. ({:.3$} GRT per query)", 62 | total_err, per_query, 2, 5 63 | )?; 64 | } 65 | } 66 | if let Some(mse) = self.mean_squared_error() { 67 | let mut mse = BigFraction::from(mse); 68 | mse *= BigFraction::new(BigUint::from(1u32), wei_to_grt() * wei_to_grt()); 69 | writeln!(f, "Mean Squared Err: {:.1$} GRT²", mse, 2)?; 70 | } 71 | 72 | writeln!(f)?; 73 | writeln!( 74 | f, 75 | "Failures: {}", 76 | self.failures 77 | .values() 78 | .map(|bucket| bucket.count) 79 | .sum::() 80 | .to_formatted_string(&Locale::en) 81 | )?; 82 | for (name, bucket) in self.failures.iter() { 83 | writeln!(f, "\t{:?} {}", name, bucket)?; 84 | for example in bucket.examples.cloned() { 85 | writeln!( 86 | f, 87 | "\t\t{} | variables: {}", 88 | &example.query, &example.variables 89 | )?; 90 | } 91 | } 92 | 93 | Ok(()) 94 | } 95 | } 96 | 97 | pub struct FailureBucket { 98 | count: usize, 99 | total_grt: BigUint, 100 | examples: Contest, 101 | } 102 | 103 | impl FailureBucket { 104 | pub fn new(capacity: usize) -> Self { 105 | Self { 106 | count: 0, 107 | total_grt: BigUint::from(0u32), 108 | examples: Contest::new(capacity), 109 | } 110 | } 111 | 112 | fn merge(&mut self, other: FailureBucket) { 113 | self.count += other.count; 114 | self.total_grt += other.total_grt; 115 | for example in other.examples.take() { 116 | self.examples.insert_unique( 117 | failed_query_complexity(&example), 118 | example, 119 | contest_query_cmp, 120 | ) 121 | } 122 | } 123 | } 124 | 125 | fn failed_query_complexity(query: &Query) -> usize { 126 | // Does not underflow because that would imply going over the memory limit 127 | usize::MAX - query.query.len() - query.variables.len() 128 | } 129 | 130 | fn contest_query_cmp(a: &Query, b: &Query) -> bool { 131 | // Ignoring the time because that is partly random, 132 | // and ignoring the variables because they are meant to be 133 | // different but may not materially affect the query. 134 | a.query == b.query 135 | } 136 | 137 | impl fmt::Display for FailureBucket { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | let total_grt = BigFraction::new(self.total_grt.clone(), wei_to_grt()); 140 | write!( 141 | f, 142 | "count: {} total_grt: {:.2$} GRT", 143 | self.count.to_formatted_string(&Locale::en), 144 | total_grt, 145 | 2 146 | ) 147 | } 148 | } 149 | 150 | struct CostedQuery { 151 | expected: Option, 152 | actual: Result, 153 | query: Query, 154 | } 155 | 156 | impl QueryCostSummary { 157 | fn add(&mut self, result: CostedQuery) { 158 | match result.actual { 159 | Ok(cost) => { 160 | if let Some(expected) = result.expected { 161 | let err = BigInt::from(cost.clone()) - BigInt::from(expected); 162 | *self.total_squared_err.get_or_insert_with(Default::default) += 163 | err.clone() * err.clone(); 164 | *self.total_err.get_or_insert_with(Default::default) += err; 165 | } 166 | self.successes += 1; 167 | self.total_grt += cost; 168 | } 169 | Err(e) => { 170 | let bucket = self.failure_bucket(fail_name(e)); 171 | bucket.count += 1; 172 | if let Some(expected) = result.expected { 173 | bucket.total_grt += expected; 174 | } 175 | bucket.examples.insert_unique( 176 | failed_query_complexity(&result.query), 177 | result.query, 178 | contest_query_cmp, 179 | ); 180 | } 181 | } 182 | } 183 | 184 | pub fn merge(mut self, mut other: Self) -> Self { 185 | self.successes += other.successes; 186 | self.total_grt += other.total_grt; 187 | if let Some(total_err) = other.total_err { 188 | *self.total_err.get_or_insert_with(Default::default) += total_err; 189 | } 190 | if let Some(total_squared_err) = other.total_squared_err { 191 | *self.total_squared_err.get_or_insert_with(Default::default) += total_squared_err; 192 | } 193 | for (key, bucket) in other.failures.drain() { 194 | self.failure_bucket(key).merge(bucket); 195 | } 196 | self 197 | } 198 | 199 | pub fn mean_squared_error(&self) -> Option { 200 | if let Some(total_squared_err) = &self.total_squared_err { 201 | if self.successes != 0 { 202 | return Some(total_squared_err.clone() / BigInt::from(self.successes)); 203 | } 204 | } 205 | None 206 | } 207 | 208 | fn failure_bucket(&mut self, name: &'static str) -> &mut FailureBucket { 209 | self.failures 210 | .entry(name) 211 | .or_insert_with(|| FailureBucket::new(4)) 212 | } 213 | } 214 | 215 | fn cost_one(model: &CostModel, query: Query, grt_per_time: Option<&BigUint>) -> CostedQuery { 216 | let cost = model.cost(&query.query, &query.variables); 217 | let expected = grt_per_time.map(|g| g * query.time); 218 | CostedQuery { 219 | actual: cost, 220 | expected, 221 | query, 222 | } 223 | } 224 | 225 | pub fn cost_many( 226 | model: &CostModel, 227 | entries: Vec, 228 | grt_per_time: Option<&BigUint>, 229 | ) -> QueryCostSummary { 230 | entries 231 | .into_par_iter() 232 | .map(|entry| cost_one(model, entry, grt_per_time)) 233 | .fold(QueryCostSummary::default, |mut acc, value| { 234 | acc.add(value); 235 | acc 236 | }) 237 | .reduce(QueryCostSummary::default, QueryCostSummary::merge) 238 | } 239 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Agora 2 | 3 | Agora is a flexible language for pricing queries. There are a wide variety of use-cases that it must support. 4 | 5 | For example, an Indexer may want to express value-based pricing - where the price of a query is based on a notion of the value of that query to a Consumer. Or, the Indexer may want to express cost-based pricing where the price is based on the cost to the Indexer for serving the query. Additionally, an Indexer may want to make frequent adjustments in response to changing market conditions, finely tune prices based on multiple elements within the query, or even ban some queries outright. 6 | 7 | Agora is intended to provide this flexibility while still being readable, performant, and easy-to-use. 8 | 9 | It is recommended when learning Agora to read the [Language Reference](./reference/toc.md) starting at [Models](./reference/models.md) and paying special attention to the section on [Matches](./reference/matches.md). 10 | 11 | -------------------------------------------------------------------------------- /docs/reference/boolean-expressions.md: -------------------------------------------------------------------------------- 1 | # Boolean Expressions 2 | A _BooleanExpression_ evaluates to `true` or `false`. Syntactically, it may take any of the following forms: 3 | 4 | * Const 5 | * _Substitution_ 6 | * (_BooleanExpression_) 7 | * _BooleanExpression_ _BooleanBinaryOperator_ _BooleanExpression_ 8 | * _LinearExpression_ _ComparisonBinaryOperator_ _LinearExpression_ 9 | 10 | ``` 11 | # A Const BooleanExpression 12 | true 13 | 14 | # A Substitution BooleanExpression 15 | $UNDER_LOAD 16 | 17 | # A Parenthesized BooleanExpression 18 | (false) 19 | 20 | # A BooleanExpression using a BooleanBinaryOperator 21 | true || false 22 | 23 | # BooleanExpression using a ComparisonBinaryOperator 24 | 1 >= 2 25 | 26 | # A BooleanExpression combining several of the above 27 | ($skip > 1000 || $first > 500) && $UNDER_LOAD 28 | ``` 29 | 30 | ## Boolean Binary Operators 31 | The following BooleanBinaryOperators are supported and are applied in order: 32 | 33 | * `&&` And 34 | * `||` Or 35 | 36 | ## Comparison Binary Operators 37 | The following ComparisonBinaryOperators are supported: 38 | 39 | * `==` Equal 40 | * `!=` Not Equal 41 | * `>` Greater Than 42 | * `<` Less Than 43 | * `>=` Greater Than Or Equal 44 | * `<=` Less Than Or Equal 45 | 46 | ## See also 47 | * [Table of Contents](./toc.md) 48 | * [Expressions](./expressions.md) 49 | * [Substitutions](./substitutions.md) -------------------------------------------------------------------------------- /docs/reference/expressions.md: -------------------------------------------------------------------------------- 1 | # Expressions 2 | 3 | Within a _Statement_, the type of _Expression_ is based on the context and the expected type of that _Expression_. Depending on the expected type, different operators may be available and the expected type of for a _Substitution_ will be different. 4 | 5 | There are two types of _Expressions_: _RationalExpressions_ and _BooleanExpressions_. 6 | 7 | 8 | ## See also 9 | * [Table of Contents](./toc.md) 10 | * [Substitutions](./substitutions.md) 11 | * [Rational Expressions](./rational-expressions.md) 12 | * [Boolean Expressions](./boolean-expressions.md) 13 | * [Statements](./statements.md) 14 | * [Matches](./matches.md) -------------------------------------------------------------------------------- /docs/reference/identifiers.md: -------------------------------------------------------------------------------- 1 | # Identifiers 2 | An Identifier starts with an ASCII letter or an underscore, and is followed by zero or more ASCII letters, numbers, and/or underscores. 3 | 4 | ``` 5 | # Valid identifiers: 6 | BE 7 | _KIND_ 8 | _2 9 | One 10 | another 11 | 12 | # Not identifiers: 13 | 42 14 | Fun! 15 | λ 16 | ``` 17 | 18 | ## See also 19 | * [Table of Contents](./toc.md) 20 | * [Substitutions](./substitutions.md) -------------------------------------------------------------------------------- /docs/reference/matches.md: -------------------------------------------------------------------------------- 1 | # Matches 2 | 3 | A _Match_ describes a set of GraphQL queries that it selects for. _Matches_ come in 2 forms: _DefaultMatch_ and _QueryMatch_. 4 | 5 | ## Default Match 6 | 7 | A _DefaultMatch_ will select for all GraphQL Queries. 8 | 9 | ``` 10 | # This default match 11 | default 12 | 13 | # Will select this query 14 | { tokens pairs } 15 | 16 | # And all other queries as well 17 | { unexpected(value_in: [2]) { id field } } 18 | ``` 19 | 20 | ## Query Match 21 | 22 | A _QueryMatch_ is specified by the tag `query` followed a `Query shorthand` as defined in the [GraphQL Spec](https://spec.graphql.org/June2018/#sec-Language.Operations) 23 | 24 | ``` 25 | query { tokens } 26 | ``` 27 | 28 | In order for a _QueryMatch_ to select a query, the entire `SelectionSet` of the _QueryMatch_ must have an exact match within the query. 29 | 30 | ``` 31 | # Given this Query Match: 32 | query { tokens mints } 33 | 34 | # This query is selected: 35 | { mints tokens } 36 | 37 | # And this query is selected: 38 | { tokens mints pairs } 39 | 40 | # But this query is NOT selected 41 | { tokens } 42 | ``` 43 | 44 | It is also possible to match arguments in a query. 45 | 46 | ``` 47 | # Given this Query Match: 48 | query { tokens(first: 100) } 49 | 50 | # This query will be selected: 51 | { tokens(first: 100) { id } } 52 | 53 | # But this query will not be selected: 54 | { tokens(first: 200) { id } } 55 | 56 | # And neither will this one: 57 | { tokens { id } } 58 | ``` 59 | 60 | It is possible to match any GraphQL value in an argument, including lists, objects, strings, etc. 61 | 62 | ### Captures 63 | 64 | The above examples matching query arguments match only very narrow sets of queries, since the arguments supplied must match exactly. 65 | 66 | Captures allow you to select for any value in the named argument position and are specified using the GraphQL variable syntax. 67 | 68 | ``` 69 | # This query match: 70 | query { tokens(first: $first, skip: $skip) } 71 | 72 | # Will select this query: 73 | query { tokens(first: 50, skip: 2000) { id } } 74 | 75 | # And 'capture' the values 76 | { "first": 50, "skip": 2000 } 77 | ``` 78 | 79 | Once a value is captured, it can be used in any _Expression_ in the current statement. This includes the _BooleanExpression_ of the optional _WhenClause_, as well as the _RationalExpression_ for the cost. 80 | 81 | 82 | ### Query Normalization 83 | 84 | An input query is treated as though it were in a normalized form with all of it's fragments expanded and all of it's variables substituted. 85 | 86 | **Examples:** 87 | ``` 88 | # This query: 89 | query pairs($skip: Int!) { 90 | pairs(skip: $skip) { ...fields } 91 | } 92 | fragment fields on Name { 93 | id, reserveUSD 94 | } 95 | # With these variables: 96 | { "skip": 1 } 97 | 98 | # Is treated the same as this query 99 | { pairs(skip: 1) { id reserveUSD } } 100 | ``` 101 | 102 | 103 | ## See also 104 | * [Table of Contents](./toc.md) 105 | * [Expressions](./expressions.md) 106 | * [When Clauses](./when-clauses.md) 107 | * [Predicates](./predicates.md) 108 | * [Substitutions](./substitutions.md) -------------------------------------------------------------------------------- /docs/reference/models.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | A _Model_ is a sequence of _Statements_ which execute in order. 4 | 5 | Before a query is priced by a _Model_, it is broken up into multiple top-level queries. Each top-level query is priced separately, and the results are summed to produce the final price. 6 | 7 | ``` 8 | # This query: 9 | { tokens($first: 10) { id } transactions { buyer seller } } 10 | 11 | # Will be priced as the sum of these two queries: 12 | { tokens($first: 10) { id } } 13 | { transactions { buyer seller} } 14 | ``` 15 | 16 | For each top-level query, the first _Statement_ which matches and prices a query determines the price for that query. Subsequent _Statements_ will be ignored. Because of this, it is necessary to order _Statements_ from most to least specific. 17 | 18 | If any matching _Statement_ produces an error then the entire query will produce an error. 19 | 20 | 21 | 22 | ## See also 23 | * [Table of Contents](./toc.md) 24 | * [Statements](./statements.md) -------------------------------------------------------------------------------- /docs/reference/predicates.md: -------------------------------------------------------------------------------- 1 | # Predicates 2 | 3 | Predicates are used for matching GraphQL queries. 4 | 5 | A Predicate is composed of a Match, followed by an optional When Clause. 6 | 7 | **Examples:** 8 | ``` 9 | # A match for a query 10 | query { tokens } 11 | 12 | # A match that uses a when clause 13 | query { tokens } when $RATE_LIMITED 14 | ``` 15 | 16 | ## See also 17 | * [Table of Contents](./toc.md) 18 | * [Matches](./matches.md) 19 | * [When Clauses](./when-clauses.md) 20 | * [Statements](./statements.md) -------------------------------------------------------------------------------- /docs/reference/rational-expressions.md: -------------------------------------------------------------------------------- 1 | # Rational Expressions 2 | A _RationalExpression_ evaluates to a rational number. 3 | 4 | Syntactically, it may any of the following forms: 5 | 6 | * _Const_ 7 | * _Sustitution_ 8 | * (_RationalExpression_) 9 | * _RationalExpression_ _BinaryOperator_ _RationalExpression_ 10 | 11 | ``` 12 | # A const Rational Expression 13 | 10.00198 14 | 15 | # A Substitution Rational Expression 16 | $sub 17 | 18 | #A parenthesized Rational Expression 19 | (1) 20 | 21 | # A Rational Expression using a Binary Operator 22 | 2 + 4 23 | 24 | # A Rational expression using all of the above 25 | 500.0 + ($skip + 10) * $ENTITY_COST 26 | ``` 27 | 28 | ## Binary Operators in Rational Expressions 29 | The following binary operators are supported and are applied order: 30 | 31 | * `*` Mult 32 | * `/` Div 33 | * `+` Add 34 | * `-` Sub 35 | 36 | All math is lossless during the execution of an expression, but is rounded toward zero and clamped between 0 (inclusive) and 2^256 (exclusive) GRT expressed in wei when outputting the final cost. 37 | 38 | A divide-by-zero will cause the expression to fail and not output a cost. 39 | 40 | ## See also 41 | * [Table of Contents](./toc.md) 42 | * [Expressions](./expressions.md) 43 | * [Substitutions](./substitutions.md) -------------------------------------------------------------------------------- /docs/reference/statements.md: -------------------------------------------------------------------------------- 1 | # Statements 2 | 3 | A statement is comprised of a _Predicate_, which is used for matching GraphQL queries, and a _CostExpression_ which when evaluated outputs a cost in decimal GRT. 4 | 5 | ``` 6 | default => 1; 7 | ``` 8 | This is the simplest statement. The Predicate `default` matches any GraphQL. The Cost Expression `1` evaluates to 1.0 GRT. 9 | 10 | The Predicate and Cost Function are separated by `=>` and terminated with `;`. 11 | 12 | Statements can express a complex set of rules: 13 | 14 | ``` 15 | query { pairs(skip: $skip) { id } } when $skip > 2000 => 0.0001 * $skip; 16 | ``` 17 | 18 | would cost the query `{ pairs(skip: 5000) { id } }` at 0.5 GRT but would not match the query `{ token }`. 19 | 20 | ## Comments 21 | A Statement may be preceded by explanatory text, called a _Comment_. A comment starts with a `#` and continues until the end of the line. 22 | 23 | ``` 24 | # This is a comment about the following statement. 25 | default => 0.001; 26 | ``` 27 | 28 | **See also** 29 | * [Table of Contents](./toc.md) 30 | * [Predicates](./predicates.md) 31 | * [Expressions](./expressions.md) 32 | * [Models](./models.md) 33 | -------------------------------------------------------------------------------- /docs/reference/substitutions.md: -------------------------------------------------------------------------------- 1 | # Substitutions 2 | Substitutions are placeholders in _Expressions_ for values that will not be known until runtime. They include _Captures_ that were captured in the _Match_ part of a statement, as well as _Globals_. 3 | 4 | Syntactically, a Substitution always starts with a `$` and is followed by an _Identifier_ 5 | 6 | _Globals_ and _Captures_ have the same syntax and are used in the same positions. In order to prevent a name collision, it is recommended that globals use $UPPER_CASE_SNAKE_CASE, and _Captures_ use $lowerCamelCase. 7 | 8 | In the event that a _Global_ and _Capture_ do have a name collision, the _Capture_ will take precedence. 9 | 10 | ``` 11 | # These are all valid globals conforming convention: 12 | $GAS_PRICE 13 | $ENABLE_SURGE_PRICING 14 | $AVG_RESPONSE_BYTES 15 | 16 | # And these are all valid captures conforming to convention: 17 | $first 18 | $skip 19 | 20 | # This is a When Clause that uses a substitution 21 | when $skip > 1000 22 | 23 | # And this is a Cost expression using substitutions 24 | (500 + $skip) * $AVG_TIME 25 | ``` 26 | 27 | If the value of a _Substitution_ cannot be found, the _Statement_ will return an error and the query will not be costed. This can be used to ban problematic or unknown queries, if desired. 28 | 29 | ``` 30 | # A statement preventing matching queries from being costed 31 | query { problem } => $_BAN_; 32 | ``` 33 | 34 | 35 | ## Globals 36 | A global is a value that is supplied at runtime from outside of the cost model. This enables making efficient incremental changes to a cost model in response to market conditions, traffic spikes, and the like. 37 | 38 | ## Captures 39 | Captures are covered in more detail in [Matches](./matches.md). 40 | 41 | ## Type Coercion 42 | The following coercions are supported: 43 | 44 | ### Converting to bool 45 | null => false 46 | int => int != 0 47 | string => string.len() != 0 48 | list => list.len() != 0 49 | object => true 50 | 51 | ### Converting to rational 52 | true => 1 53 | false => 0 54 | null => 0 55 | string => parseDecimal(string) 56 | 57 | If the coercion fails, the pricing will return an error and the query will not be costed. 58 | 59 | Note that because of limitations of JSON, large numbers and numbers with decimals must be passed in as strings. 60 | 61 | # See also 62 | * [Table of Contents](./toc.md) 63 | * [Identifiers](./identifiers.md) 64 | * [Matches](./matches.md) 65 | * [Expressions](./expressions.md) 66 | * [Rational Expressions](./rational-expressions.md) 67 | * [Boolean Expressions](./boolean-expressions.md) -------------------------------------------------------------------------------- /docs/reference/toc.md: -------------------------------------------------------------------------------- 1 | # Agora Language Reference TOC 2 | 3 | * [Boolean Expressions](./boolean-expressions.md) 4 | * [Expressions](./expressions.md) 5 | * [Identifiers](./identifiers.md) 6 | * [Matches](./matches.md) 7 | * [Models](./models.md) 8 | * [Predicates](./predicates.md) 9 | * [Rational Expressions](./rational-expressions.md) 10 | * [Statements](./statements.md) 11 | * [Substitutions](./substitutions.md) 12 | * [When Clauses](./when-clauses.md) -------------------------------------------------------------------------------- /docs/reference/when-clauses.md: -------------------------------------------------------------------------------- 1 | # When Clauses 2 | 3 | A _WhenClause_ is the keyword `when` followed by a _BooleanExpression_ that must evaluate to `true` for a GraphQL query to be filtered by it's containing _Predicate_. 4 | 5 | ``` 6 | # A when clause used in a statement 7 | query { tokens } when true => 1; 8 | 9 | # A more typical use with a Substitution 10 | query { tokens(first: $first) } when $first > 1000 => $SURCHARGE; 11 | ``` 12 | 13 | **See Also** 14 | * [Table of Contents](./toc.md) 15 | * [Predicates](./predicates.md) 16 | * [Boolean Expressions](./boolean-expressions.md) 17 | * [Substitutions](./substitutions.md) 18 | * [Matches](./matches.md) -------------------------------------------------------------------------------- /lang/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cost-model" 3 | version = "0.1.0" 4 | authors = ["Zac Burns "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | firestorm = "0.5" 11 | fraction = { version = "0.15.2", features = ["with-bigint"] } 12 | graphql = { git = "https://github.com/edgeandnode/toolshed", tag = "graphql-v0.2.0", default-features = false } 13 | itertools = "0.13.0" 14 | lazy_static = "1.4.0" 15 | nom = "7.1.3" 16 | num-bigint = "0.4.5" 17 | num-traits = "0.2.12" 18 | serde_json = "1.0" 19 | -------------------------------------------------------------------------------- /lang/src/coercion.rs: -------------------------------------------------------------------------------- 1 | use fraction::BigFraction; 2 | use graphql::graphql_parser::query as q; 3 | use q::Value::*; 4 | 5 | /// This is like TryInto, but more liberal 6 | pub trait Coerce { 7 | type Error; 8 | fn coerce(&self) -> Result; 9 | } 10 | 11 | // TODO: Consider that coercing to a bool and int from a "BigInt" style string may have surprising results 12 | // For the case "0", this coerces to 0, which would coerce to false. But, coercing the string "0" to bool directly 13 | // yields true. This is JSON's fault. It may be necessary to address this with docs 14 | // Eg: An int < i32::MAX should never be sent as a str 15 | // Or - possibly strangely, require that there are never leading '0s' including the case of 0 which would be "". 16 | 17 | impl<'t, Text: q::Text<'t>> Coerce for q::Value<'t, Text> { 18 | type Error = (); 19 | fn coerce(&self) -> Result { 20 | match self { 21 | Boolean(b) => Ok(*b), 22 | Null => Ok(false), 23 | Int(i) => Ok(*i != q::Number::from(0)), 24 | String(s) => Ok(!s.is_empty()), 25 | List(l) => Ok(!l.is_empty()), 26 | Object(_) => Ok(true), 27 | Variable(_) | Float(_) | Enum(_) => Err(()), 28 | } 29 | } 30 | } 31 | 32 | impl<'t, Text: q::Text<'t>> Coerce for q::Value<'t, Text> { 33 | type Error = (); 34 | fn coerce(&self) -> Result { 35 | match self { 36 | Boolean(b) => Ok(if *b { 1.into() } else { 0.into() }), 37 | Null => Ok(0.into()), 38 | Int(i) => Ok(i.as_i64().ok_or(())?.into()), 39 | String(s) => crate::parse_real(s).map_err(|_| ()), 40 | List(_) | Object(_) | Variable(_) | Float(_) | Enum(_) => Err(()), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lang/src/context.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::CostError; 3 | use graphql::{graphql_parser::query as q, QueryVariables}; 4 | 5 | #[derive(Clone)] 6 | pub struct Context<'q> { 7 | pub operations: Vec>, 8 | pub fragments: Vec>, 9 | pub variables: QueryVariables, 10 | } 11 | 12 | impl<'q> Context<'q> { 13 | pub fn new(query: &'q str, variables: &'q str) -> Result { 14 | profile_method!(new); 15 | 16 | let variables = 17 | crate::parse_vars(variables).map_err(|_| CostError::FailedToParseVariables)?; 18 | let query = q::parse_query(query).map_err(|_| CostError::FailedToParseQuery)?; 19 | let (operations, fragments) = crate::split_definitions(query.definitions); 20 | 21 | Ok(Self { 22 | variables, 23 | fragments, 24 | operations, 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lang/src/expressions/binary.rs: -------------------------------------------------------------------------------- 1 | // TODO: The simplest way to make this recursion free would be 2 | // to use a stack machine for execution. 3 | /// An expression like 1 + 1 consisting of 4 | /// a left-hand-side expression, an operator, and 5 | /// a right-hand-side expression. 6 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 7 | pub struct BinaryExpression { 8 | pub(crate) lhs: LHS, 9 | pub(crate) op: Op, 10 | pub(crate) rhs: RHS, 11 | } 12 | 13 | impl BinaryExpression { 14 | pub fn new(lhs: LHS, op: Op, rhs: RHS) -> Self { 15 | Self { lhs, op, rhs } 16 | } 17 | } 18 | 19 | /// An operator combining the values of two binary expressions 20 | pub trait BinaryOperator { 21 | type Type; 22 | #[inline(always)] 23 | fn short_circuit(&self, _lhs: &T) -> Result, ()> { 24 | Ok(None) 25 | } 26 | fn exec(&self, lhs: T, rhs: T) -> Result; 27 | } 28 | -------------------------------------------------------------------------------- /lang/src/expressions/boolean_algebra.rs: -------------------------------------------------------------------------------- 1 | //! Boolean Algebra operators 2 | 3 | use super::*; 4 | 5 | macro_rules! boolean_op { 6 | ($($Name:ident: $op:tt,)+) => { 7 | $( 8 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 9 | pub struct $Name; 10 | 11 | impl BinaryOperator for $Name { 12 | type Type = bool; 13 | fn exec(&self, lhs: bool, rhs: bool) -> Result { 14 | Ok(lhs $op rhs) 15 | } 16 | } 17 | 18 | impl From<$Name> for AnyBooleanOp { 19 | #[inline(always)] 20 | fn from(_op: $Name) -> Self { 21 | AnyBooleanOp::$Name 22 | } 23 | } 24 | )+ 25 | 26 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 27 | pub enum AnyBooleanOp { 28 | $( 29 | $Name, 30 | )+ 31 | } 32 | 33 | impl BinaryOperator for AnyBooleanOp { 34 | type Type = bool; 35 | fn exec(&self, lhs: bool, rhs: bool) -> Result { 36 | match self { 37 | $( 38 | Self::$Name => $Name.exec(lhs, rhs), 39 | )+ 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | boolean_op![ 47 | And: &&, 48 | Or: ||, 49 | ]; 50 | -------------------------------------------------------------------------------- /lang/src/expressions/comparisons.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | macro_rules! comparisons { 4 | ($($Name:ident: $op:tt $T:ident,)+) => { 5 | $( 6 | #[derive(Copy, Clone, Eq, PartialEq)] 7 | pub struct $Name; 8 | 9 | impl BinaryOperator for $Name { 10 | type Type = bool; 11 | fn exec(&self, lhs: T, rhs: T) -> Result { 12 | Ok(lhs $op rhs) 13 | } 14 | } 15 | 16 | impl From<$Name> for AnyComparison { 17 | #[inline(always)] 18 | fn from(_op: $Name) -> Self { 19 | AnyComparison::$Name 20 | } 21 | } 22 | )+ 23 | 24 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 25 | pub enum AnyComparison { 26 | $( 27 | $Name, 28 | )+ 29 | } 30 | 31 | impl BinaryOperator for AnyComparison where 32 | T: PartialEq + PartialOrd 33 | { 34 | type Type = bool; 35 | fn exec(&self, lhs: T, rhs: T) -> Result { 36 | match self { 37 | $(Self::$Name => $Name.exec(lhs, rhs),)+ 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | comparisons![ 45 | Eq: == PartialEq, 46 | Ne: != PartialEq, 47 | Gt: > PartialOrd, 48 | Lt: < PartialOrd, 49 | Ge: >= PartialOrd, 50 | Le: <= PartialOrd, 51 | ]; 52 | -------------------------------------------------------------------------------- /lang/src/expressions/expr_stack.rs: -------------------------------------------------------------------------------- 1 | use crate::expressions::*; 2 | use crate::language::*; 3 | use crate::prelude::*; 4 | use fraction::BigFraction; 5 | 6 | pub enum Atom<'a, Expr, Op> { 7 | Expr(&'a Expr), 8 | Op(Op), 9 | } 10 | pub struct Stack<'a, E, O, V, C> { 11 | queue: Vec>, 12 | values: Vec, 13 | pub(crate) context: C, 14 | } 15 | 16 | pub type LinearStack<'a, 'c> = 17 | Stack<'a, LinearExpression, AnyLinearOperator, BigFraction, &'c Captures>; 18 | 19 | pub type CondStack<'a, 'c> = Stack<'a, Condition, AnyBooleanOp, bool, LinearStack<'a, 'c>>; 20 | 21 | pub trait Schedule<'e, Stack> { 22 | fn schedule(&'e self, stack: &mut Stack) -> Result<(), ()>; 23 | } 24 | 25 | impl<'a, 'c> Schedule<'a, LinearStack<'a, 'c>> for LinearExpression { 26 | fn schedule(&'a self, stack: &mut LinearStack<'a, 'c>) -> Result<(), ()> { 27 | match self { 28 | LinearExpression::Const(c) => stack.push_value(c.eval()), 29 | LinearExpression::Variable(v) => stack.push_value(v.eval(stack.context)?), 30 | LinearExpression::Error(()) => return Err(()), 31 | LinearExpression::BinaryExpression(bin) => { 32 | stack.queue.push(Atom::Op(bin.op)); 33 | stack.push_expr(&bin.rhs); 34 | stack.push_expr(&bin.lhs); 35 | } 36 | } 37 | 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl<'a, 'c> Schedule<'a, CondStack<'a, 'c>> for Condition { 43 | fn schedule(&'a self, stack: &mut CondStack<'a, 'c>) -> Result<(), ()> { 44 | match self { 45 | Condition::Const(c) => stack.push_value(c.eval()), 46 | Condition::Variable(v) => stack.push_value(v.eval(stack.context.context)?), 47 | Condition::Comparison(c) => { 48 | let lhs = stack.context.execute(&c.lhs)?; 49 | let rhs = stack.context.execute(&c.rhs)?; 50 | let value = c.op.exec(lhs, rhs)?; 51 | stack.push_value(value); 52 | } 53 | Condition::Error(()) => return Err(()), 54 | Condition::Boolean(bin) => { 55 | stack.queue.push(Atom::Op(bin.op)); 56 | stack.push_expr(&bin.rhs); 57 | stack.push_expr(&bin.lhs); 58 | } 59 | } 60 | Ok(()) 61 | } 62 | } 63 | 64 | impl<'a, Expr, Op, V, C> Stack<'a, Expr, Op, V, C> { 65 | pub fn new(context: C) -> Self { 66 | Self { 67 | queue: Vec::new(), 68 | values: Vec::new(), 69 | context, 70 | } 71 | } 72 | 73 | pub fn push_expr(&mut self, expr: &'a Expr) { 74 | let expr = Atom::Expr(expr); 75 | self.queue.push(expr); 76 | } 77 | 78 | pub fn push_value(&mut self, value: V) { 79 | self.values.push(value); 80 | } 81 | } 82 | 83 | impl<'a, Expr, Op, V, C> Stack<'a, Expr, Op, V, C> { 84 | pub fn execute(&mut self, expr: &'a Expr) -> Result 85 | where 86 | Expr: Schedule<'a, Self>, 87 | Op: BinaryOperator, 88 | { 89 | profile_fn!(execute); 90 | 91 | // TODO: (Performance) Could re-use a stack in the context. 92 | // But these need to clean up memory on Err in execute if used too long 93 | // See also 1ba86b41-3fe2-4802-ad21-90e65fb8d91f 94 | let len = self.queue.len(); 95 | let values_len = self.values.len(); 96 | self.push_expr(expr); 97 | 98 | while self.queue.len() > len { 99 | let next = self.queue.pop().unwrap(); 100 | 101 | match next { 102 | Atom::Expr(expr) => { 103 | expr.schedule(self)?; 104 | } 105 | Atom::Op(op) => { 106 | let rhs = self.values.pop().unwrap(); 107 | let lhs = self.values.pop().unwrap(); 108 | let value = op.exec(lhs, rhs)?; 109 | self.values.push(value); 110 | } 111 | } 112 | } 113 | assert!(self.values.len() == values_len + 1); 114 | Ok(self.values.pop().unwrap()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lang/src/expressions/linears.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::ops; 3 | 4 | macro_rules! linear_op { 5 | ($($Name:ident: $op:tt,)+) => { 6 | $( 7 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 8 | pub struct $Name; 9 | 10 | impl BinaryOperator for $Name { 11 | type Type = T::Output; 12 | #[inline(always)] 13 | fn exec(&self, lhs: T, rhs: T) -> Result { 14 | Ok(lhs $op rhs) 15 | } 16 | } 17 | 18 | impl From<$Name> for AnyLinearOperator { 19 | #[inline(always)] 20 | fn from(_op: $Name) -> Self { 21 | AnyLinearOperator::$Name 22 | } 23 | } 24 | )+ 25 | 26 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 27 | pub enum AnyLinearOperator { 28 | $( 29 | $Name, 30 | )+ 31 | } 32 | 33 | impl BinaryOperator for AnyLinearOperator where 34 | $(T: ops::$Name,)+ 35 | { 36 | type Type = T; 37 | fn exec(&self, lhs: T, rhs: T) -> Result { 38 | match self { 39 | $(Self::$Name => $Name.exec(lhs, rhs),)+ 40 | } 41 | } 42 | } 43 | 44 | } 45 | } 46 | 47 | linear_op![ 48 | Add: +, 49 | Sub: -, 50 | Mul: *, 51 | Div: /, 52 | ]; 53 | -------------------------------------------------------------------------------- /lang/src/expressions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod binary; 2 | pub mod boolean_algebra; 3 | pub mod comparisons; 4 | pub mod expr_stack; 5 | pub mod linears; 6 | pub mod primitives; 7 | use crate::language::Captures; 8 | pub use binary::*; 9 | pub use boolean_algebra::*; 10 | pub use comparisons::*; 11 | pub use linears::*; 12 | pub use primitives::*; 13 | -------------------------------------------------------------------------------- /lang/src/expressions/primitives.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::coercion::Coerce; 3 | use std::marker::PhantomData; 4 | use graphql::StaticValue; 5 | 6 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 7 | struct Unowned(PhantomData<*const T>); 8 | impl Unowned { 9 | pub fn new() -> Self { 10 | Self(PhantomData) 11 | } 12 | } 13 | 14 | unsafe impl Send for Unowned {} 15 | unsafe impl Sync for Unowned {} 16 | 17 | /// A placeholder value 18 | #[derive(Debug, Clone, Eq, PartialEq)] 19 | pub struct Variable { 20 | name: String, 21 | _marker: Unowned, 22 | } 23 | 24 | impl Variable { 25 | pub fn new>(name: S) -> Self { 26 | Self { 27 | name: name.into(), 28 | _marker: Unowned::new(), 29 | } 30 | } 31 | 32 | pub fn name(&self) -> &str { 33 | &self.name 34 | } 35 | } 36 | 37 | impl Variable 38 | where 39 | StaticValue: Coerce, 40 | { 41 | pub fn eval(&self, captures: &Captures) -> Result { 42 | if let Some(Ok(v)) = captures.get_as(&self.name) { 43 | Ok(v) 44 | } else { 45 | Err(()) 46 | } 47 | } 48 | } 49 | 50 | /// Always the same value 51 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 52 | pub struct Const { 53 | pub(crate) value: T, 54 | } 55 | 56 | impl Const { 57 | pub fn new(value: T) -> Self { 58 | Self { value } 59 | } 60 | } 61 | 62 | impl From for Const { 63 | fn from(value: T) -> Self { 64 | Self::new(value) 65 | } 66 | } 67 | 68 | impl Const { 69 | pub fn eval(&self) -> T { 70 | self.value.clone() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lang/src/language.rs: -------------------------------------------------------------------------------- 1 | use crate::coercion::Coerce; 2 | use crate::expressions::expr_stack::*; 3 | use crate::expressions::*; 4 | use crate::matching::{get_capture_names_field, match_query}; 5 | use crate::prelude::*; 6 | use fraction::BigFraction; 7 | use graphql::{graphql_parser::query as q, IntoStaticValue, QueryVariables, StaticValue}; 8 | use std::collections::HashMap; 9 | 10 | #[derive(Debug, PartialEq)] 11 | pub struct Document<'a> { 12 | pub statements: Vec>, 13 | } 14 | 15 | enum Visit<'a, 't> { 16 | Document(&'a mut Document<'t>), 17 | Statement(&'a mut Statement<'t>), 18 | Predicate(&'a mut Predicate<'t>), 19 | LinearExpression(&'a mut LinearExpression), 20 | Match(&'a Match<'t>), 21 | WhenClause(&'a mut WhenClause), 22 | Condition(&'a mut Condition), 23 | Field(&'a q::Field<'t, &'t str>), 24 | } 25 | 26 | pub fn substitute_globals(document: &mut Document, globals: &QueryVariables) -> Result<(), ()> { 27 | let mut queue = Vec::new(); 28 | queue.push(Visit::Document(document)); 29 | let mut capture_names = Vec::new(); 30 | 31 | // Security: Uses a visit queue to avoid stack overflow 32 | while let Some(next) = queue.pop() { 33 | match next { 34 | Visit::Document(doc) => doc.substitute_globals(&mut queue), 35 | Visit::Statement(statement) => { 36 | statement.substitute_globals(&mut capture_names, &mut queue) 37 | } 38 | Visit::Predicate(predicate) => predicate.substitute_globals(&mut queue), 39 | Visit::LinearExpression(linear_expression) => { 40 | linear_expression.substitute_globals(&mut queue, &capture_names, globals) 41 | } 42 | Visit::Match(match_) => { 43 | match_.get_capture_names(&mut queue); 44 | } 45 | Visit::WhenClause(when_clause) => when_clause.substitute_globals(&mut queue), 46 | // Security: Relying on GraphQL parsing to not have stack overflow here. 47 | // Could refactor like the above to unlimit depth. 48 | // See also 01205a6c-4e1a-4b35-8dc6-d400c499d423 49 | Visit::Field(field) => get_capture_names_field(field, &mut capture_names)?, 50 | Visit::Condition(condition) => { 51 | condition.substitute_globals(&mut queue, &capture_names, globals) 52 | } 53 | } 54 | } 55 | 56 | Ok(()) 57 | } 58 | 59 | impl<'t> Document<'t> { 60 | fn substitute_globals<'a, 'b: 'a>(&'b mut self, queue: &'a mut Vec>) { 61 | for statement in self.statements.iter_mut() { 62 | queue.push(Visit::Statement(statement)); 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug, PartialEq)] 68 | pub struct Statement<'a> { 69 | pub predicate: Predicate<'a>, 70 | pub cost_expr: LinearExpression, 71 | } 72 | 73 | impl<'s> Statement<'s> { 74 | pub fn try_cost<'a, 't: 'a, T: q::Text<'t>>( 75 | &self, 76 | query: &'a q::Field<'t, T>, 77 | fragments: &'a [q::FragmentDefinition<'t, T>], 78 | variables: &QueryVariables, 79 | captures: &mut Captures, 80 | ) -> Result, ()> { 81 | if !self 82 | .predicate 83 | .match_with_vars(query, fragments, variables, captures)? 84 | { 85 | return Ok(None); 86 | } 87 | 88 | // TODO: (Performance) Could re-use a stack in the context. 89 | // But these need to clean up memory on Err in execute if used too long 90 | // See also 1ba86b41-3fe2-4802-ad21-90e65fb8d91f 91 | let mut stack = LinearStack::new(captures); 92 | let cost = stack.execute(&self.cost_expr)?; 93 | Ok(Some(cost)) 94 | } 95 | 96 | fn substitute_globals<'a, 'b: 'a>( 97 | &'b mut self, 98 | capture_names_scratch: &mut Vec<&'s str>, 99 | queue: &'a mut Vec>, 100 | ) { 101 | // First we get the capture names from the predicate. 102 | // This is necessary because captures override globals. 103 | // So, we can only substitute a global if it is not a capture. 104 | capture_names_scratch.clear(); 105 | // The order here matters. We have to push the predicate last 106 | // in order to keep the captures around when looking at the cost 107 | // expression. 108 | queue.push(Visit::LinearExpression(&mut self.cost_expr)); 109 | queue.push(Visit::Predicate(&mut self.predicate)); 110 | } 111 | } 112 | 113 | #[derive(Debug, PartialEq)] 114 | pub enum Match<'a> { 115 | GraphQL(q::Field<'a, &'a str>), 116 | Default, 117 | } 118 | 119 | impl<'m> Match<'m> { 120 | fn match_with_vars<'a, 't: 'a, T: q::Text<'t>>( 121 | &self, 122 | item: &'a q::Field<'t, T>, 123 | fragments: &'a [q::FragmentDefinition<'t, T>], 124 | variables: &QueryVariables, 125 | captures: &mut Captures, 126 | ) -> Result { 127 | match self { 128 | Self::GraphQL(selection) => { 129 | match_query(selection, item, fragments, variables, captures) 130 | } 131 | Self::Default => Ok(true), 132 | } 133 | } 134 | 135 | fn get_capture_names<'a, 'b: 'a>(&'b self, queue: &'a mut Vec>) { 136 | match self { 137 | Self::GraphQL(selection) => queue.push(Visit::Field(selection)), 138 | Self::Default => {} 139 | } 140 | } 141 | } 142 | 143 | #[derive(Debug, PartialEq)] 144 | pub struct Predicate<'a> { 145 | pub match_: Match<'a>, 146 | pub when_clause: Option, 147 | } 148 | 149 | impl<'p> Predicate<'p> { 150 | fn match_with_vars<'a, 't: 'a, T: q::Text<'t>>( 151 | &self, 152 | item: &'a q::Field<'t, T>, 153 | fragments: &'a [q::FragmentDefinition<'t, T>], 154 | variables: &QueryVariables, 155 | captures: &mut Captures, 156 | ) -> Result { 157 | captures.clear(); 158 | 159 | if !self 160 | .match_ 161 | .match_with_vars(item, fragments, variables, captures)? 162 | { 163 | return Ok(false); 164 | } 165 | 166 | if let Some(when_clause) = &self.when_clause { 167 | // TODO: (Performance) Could re-use a stack in the context. 168 | // But these need to clean up memory on Err in execute if used too long 169 | // See also 1ba86b41-3fe2-4802-ad21-90e65fb8d91f 170 | let stack = LinearStack::new(captures); 171 | let mut stack = CondStack::new(stack); 172 | if !(stack.execute(&when_clause.condition)?) { 173 | return Ok(false); 174 | } 175 | } 176 | 177 | Ok(true) 178 | } 179 | 180 | fn substitute_globals<'a, 'b: 'a>(&'b mut self, queue: &'a mut Vec>) { 181 | if let Some(when_clause) = &mut self.when_clause { 182 | queue.push(Visit::WhenClause(when_clause)); 183 | } 184 | // The order here matters. Need to process match (which gets capture names) 185 | // before when clause (which uses capture names) 186 | queue.push(Visit::Match(&self.match_)); 187 | } 188 | } 189 | 190 | #[derive(Debug, PartialEq)] 191 | pub struct WhenClause { 192 | pub condition: Condition, 193 | } 194 | 195 | impl WhenClause { 196 | fn substitute_globals<'a, 'b: 'a>(&'b mut self, queue: &'a mut Vec>) { 197 | queue.push(Visit::Condition(&mut self.condition)); 198 | } 199 | } 200 | 201 | #[derive(Debug, PartialEq, Eq, Clone)] 202 | pub enum LinearExpression { 203 | Const(Const), 204 | Variable(Variable), 205 | BinaryExpression(Box>), 206 | Error(()), 207 | } 208 | 209 | impl LinearExpression { 210 | fn substitute_globals<'a, 'b: 'a>( 211 | &'b mut self, 212 | queue: &'a mut Vec>, 213 | capture_names: &[&str], 214 | globals: &QueryVariables, 215 | ) { 216 | use LinearExpression::*; 217 | match self { 218 | Const(_) | Error(()) => {} 219 | Variable(var) => { 220 | // Duplicated code 221 | // See also 9195a627-cfa1-4bd4-81bb-b9fc90867e8c 222 | let name = var.name(); 223 | // Captures shadow globals 224 | if capture_names.contains(&name) { 225 | return; 226 | } 227 | // If it's not a capture, it must be a global. 228 | // But if we can't find it, the expr will always be an error so jump straight there. 229 | // TODO: (Performance) This means that later in the code we can assume the variable will be there. 230 | *self = match globals.get(name).map(|v| v.coerce()) { 231 | Some(Ok(value)) => { 232 | LinearExpression::Const(crate::expressions::Const::new(value)) 233 | } 234 | _ => LinearExpression::Error(()), 235 | } 236 | } 237 | BinaryExpression(binary_expression) => { 238 | queue.push(Visit::LinearExpression(&mut binary_expression.lhs)); 239 | queue.push(Visit::LinearExpression(&mut binary_expression.rhs)); 240 | } 241 | } 242 | } 243 | } 244 | 245 | #[derive(Debug, PartialEq, Eq, Clone)] 246 | pub enum Condition { 247 | Comparison(BinaryExpression), 248 | Boolean(Box>), 249 | Variable(Variable), 250 | Const(Const), 251 | Error(()), 252 | } 253 | 254 | impl Condition { 255 | fn substitute_globals<'a, 'b: 'a>( 256 | &'b mut self, 257 | queue: &'a mut Vec>, 258 | capture_names: &[&str], 259 | globals: &QueryVariables, 260 | ) { 261 | use Condition::*; 262 | match self { 263 | Comparison(comparison) => { 264 | queue.push(Visit::LinearExpression(&mut comparison.lhs)); 265 | queue.push(Visit::LinearExpression(&mut comparison.rhs)); 266 | } 267 | Boolean(boolean) => { 268 | queue.push(Visit::Condition(&mut boolean.lhs)); 269 | queue.push(Visit::Condition(&mut boolean.rhs)); 270 | } 271 | Variable(var) => { 272 | // Duplicated code 273 | // See also 9195a627-cfa1-4bd4-81bb-b9fc90867e8c 274 | let name = var.name(); 275 | // Captures shadow globals 276 | if capture_names.contains(&name) { 277 | return; 278 | } 279 | // If it's not a capture, it must be a global. 280 | // But if we can't find it, the expr will always be an error so jump straight there. 281 | // TODO: (Performance) This means that later in the code we can assume the variable will be there. 282 | *self = match globals.get(name).map(|v| v.coerce()) { 283 | Some(Ok(value)) => Condition::Const(crate::expressions::Const::new(value)), 284 | _ => Condition::Error(()), 285 | } 286 | } 287 | Const(_) | Error(_) => {} 288 | } 289 | } 290 | } 291 | 292 | #[derive(Default, Debug)] 293 | pub struct Captures { 294 | values: HashMap, 295 | } 296 | 297 | impl Captures { 298 | pub fn new() -> Self { 299 | Default::default() 300 | } 301 | 302 | pub fn insert(&mut self, name: impl Into, value: impl IntoStaticValue) { 303 | self.values.insert(name.into(), value.to_graphql()); 304 | } 305 | 306 | pub fn get_as( 307 | &self, 308 | name: impl AsRef, 309 | ) -> Option>::Error>> 310 | where 311 | StaticValue: Coerce, 312 | { 313 | profile_fn!(get_as); 314 | 315 | self.values.get(name.as_ref()).map(Coerce::coerce) 316 | } 317 | 318 | pub fn clear(&mut self) { 319 | self.values.clear() 320 | } 321 | } 322 | 323 | #[cfg(test)] 324 | pub(crate) mod test_helpers { 325 | use super::*; 326 | 327 | impl From<()> for Captures { 328 | fn from(_: ()) -> Captures { 329 | Captures::new() 330 | } 331 | } 332 | 333 | impl From<(&'_ str, T0)> for Captures { 334 | fn from(value: (&'_ str, T0)) -> Captures { 335 | let mut v = Captures::new(); 336 | v.insert(value.0, value.1.to_graphql()); 337 | v 338 | } 339 | } 340 | 341 | impl From<((&'_ str, T0), (&'_ str, T1))> for Captures { 342 | fn from(value: ((&'_ str, T0), (&'_ str, T1))) -> Captures { 343 | let mut v = Captures::new(); 344 | v.insert((value.0).0, (value.0).1.to_graphql()); 345 | v.insert((value.1).0, (value.1).1.to_graphql()); 346 | v 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /lang/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | mod coercion; 5 | mod context; 6 | mod expressions; 7 | mod language; 8 | mod matching; 9 | #[macro_use] 10 | mod parse_errors; 11 | mod parser; 12 | 13 | pub(crate) mod prelude; 14 | use prelude::*; 15 | 16 | use fraction::{BigFraction, GenericFraction, Sign}; 17 | use graphql::graphql_parser::query as q; 18 | pub use graphql::QueryVariables; 19 | use language::*; 20 | use num_bigint::BigUint; 21 | use std::{error, fmt}; 22 | 23 | pub use context::Context; 24 | 25 | pub struct CostModel { 26 | // Rust does not have a memory model, nor does it have a proper `uintptr_t` equivalent. So a 27 | // `*const u8` is used here, since the C99 standard explicitly allows casting from a C `char` 28 | // the type of an object being accessed through a pointer (C99 §6.5/7). This assumes that all 29 | // platforms being targeted will define C `char` as either a signed or unsigned byte. 30 | document: *const u8, 31 | // The `document` field uses references to the `text` field. In order to ensure safety, `text` 32 | // must be owned by this struct and be dropped after `document`. 33 | #[allow(dead_code)] 34 | text: String, 35 | } 36 | 37 | unsafe impl Send for CostModel {} 38 | unsafe impl Sync for CostModel {} 39 | 40 | impl Drop for CostModel { 41 | fn drop(&mut self) { 42 | let _ = unsafe { Box::::from_raw(self.document as *mut Document) }; 43 | } 44 | } 45 | 46 | impl fmt::Debug for CostModel { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | profile_method!(fmt); 49 | write!(f, "CostModel {{}}") 50 | } 51 | } 52 | 53 | #[derive(Debug, PartialEq, Eq, Clone)] 54 | pub enum CostError { 55 | FailedToParseQuery, 56 | FailedToParseVariables, 57 | QueryInvalid, 58 | QueryNotSupported, 59 | QueryNotCosted, 60 | CostModelFail, 61 | } 62 | 63 | lazy_static! { 64 | static ref MAX_COST: BigUint = 65 | "115792089237316195423570985008687907853269984665640564039457584007913129639935" 66 | .parse() 67 | .unwrap(); 68 | } 69 | 70 | impl error::Error for CostError {} 71 | 72 | impl fmt::Display for CostError { 73 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 74 | profile_method!(fmt); 75 | 76 | use CostError::*; 77 | match self { 78 | FailedToParseQuery => write!(f, "Failed to parse query"), 79 | FailedToParseVariables => write!(f, "Failed to parse variables"), 80 | QueryNotSupported => write!(f, "Query not supported"), 81 | QueryInvalid => write!(f, "Query invalid"), 82 | QueryNotCosted => write!(f, "Query not costed"), 83 | CostModelFail => write!(f, "Cost model failure"), 84 | } 85 | } 86 | } 87 | 88 | pub(crate) fn parse_vars(vars: &str) -> Result { 89 | profile_fn!(parse_vars); 90 | 91 | let vars = vars.trim(); 92 | if ["{}", "null", ""].contains(&vars) { 93 | Ok(QueryVariables::default()) 94 | } else { 95 | serde_json::from_str(vars) 96 | } 97 | } 98 | 99 | // Performance TODO: Can avoid the pro-active formatting 100 | // by using another rental struct here. 101 | #[derive(Debug)] 102 | pub enum CompileError { 103 | DocumentParseError(String), 104 | GlobalsParseError(serde_json::error::Error), 105 | // TODO: Get rid of this by making all the errors known 106 | Unknown, 107 | } 108 | 109 | impl fmt::Display for CompileError { 110 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 111 | profile_method!(fmt); 112 | 113 | match self { 114 | CompileError::DocumentParseError(inner) => { 115 | writeln!(f, "Failed to parse cost model.")?; 116 | write!(f, "{}", inner)?; 117 | } 118 | CompileError::GlobalsParseError(inner) => { 119 | writeln!(f, "Failed to parse globals.")?; 120 | write!(f, "{}", inner)?; 121 | } 122 | CompileError::Unknown => { 123 | writeln!(f, "Unknown error.")?; 124 | } 125 | } 126 | 127 | Ok(()) 128 | } 129 | } 130 | 131 | impl std::error::Error for CompileError {} 132 | 133 | impl CostModel { 134 | pub fn document(&self) -> &Document { 135 | unsafe { &*(self.document as *const Document) } 136 | } 137 | 138 | pub fn compile(text: impl Into, globals: &str) -> Result { 139 | profile_method!(compile); 140 | 141 | let text = text.into(); 142 | let mut document = parser::parse_document(&text) 143 | .map_err(|e| CompileError::DocumentParseError(format!("{}", e)))?; 144 | let globals = parse_vars(globals).map_err(CompileError::GlobalsParseError)?; 145 | substitute_globals(&mut document, &globals).map_err(|_| CompileError::Unknown)?; 146 | let document = Box::into_raw(Box::new(document)) as *const u8; 147 | Ok(CostModel { document, text }) 148 | } 149 | 150 | pub fn cost(&self, query: &str, variables: &str) -> Result { 151 | profile_method!(cost); 152 | 153 | let context = Context::new(query, variables)?; 154 | self.cost_with_context(&context) 155 | } 156 | 157 | pub fn contains_statement_field(&self, statement: &str) -> bool { 158 | profile_method!(contains_statement); 159 | 160 | let statement = statement.trim(); 161 | self.document() 162 | .statements 163 | .iter() 164 | .any(|stmt| matches!(&stmt.predicate.match_, Match::GraphQL(field) if statement == field.name)) 165 | } 166 | 167 | /// This may be more efficient when costing a single query against multiple models 168 | pub fn cost_with_context(&self, context: &Context) -> Result { 169 | profile_method!(cost_with_context); 170 | 171 | let mut captures: Captures = Default::default(); 172 | 173 | let mut result = BigFraction::from(0); 174 | 175 | for operation in context.operations.iter() { 176 | profile_section!(operation_definition); 177 | 178 | // TODO: (Performance) We could move the search for top level fields 179 | // into the Context. But, then it would have to be self-referential 180 | let top_level_fields = 181 | get_top_level_fields(operation, &context.fragments, &context.variables)?; 182 | 183 | for top_level_field in top_level_fields.into_iter() { 184 | profile_section!(operation_field); 185 | 186 | let mut this_cost = None; 187 | 188 | for statement in &self.document().statements { 189 | profile_section!(field_statement); 190 | 191 | match statement.try_cost( 192 | top_level_field, 193 | &context.fragments, 194 | &context.variables, 195 | &mut captures, 196 | ) { 197 | Ok(None) => continue, 198 | Ok(cost) => { 199 | this_cost = cost; 200 | break; 201 | } 202 | Err(_) => return Err(CostError::CostModelFail), 203 | } 204 | } 205 | if let Some(this_cost) = this_cost { 206 | result += this_cost; 207 | } else { 208 | return Err(CostError::QueryNotCosted); 209 | } 210 | } 211 | } 212 | 213 | // Convert to an in-range value 214 | fract_to_cost(result).map_err(|()| CostError::CostModelFail) 215 | } 216 | } 217 | pub fn fract_to_cost(fract: BigFraction) -> Result { 218 | profile_fn!(fract_to_cost); 219 | 220 | match fract { 221 | GenericFraction::Rational(sign, mut ratio) => match sign { 222 | Sign::Plus => { 223 | // Convert to wei 224 | ratio *= wei_to_grt(); 225 | // Rounds toward 0 226 | let mut int = ratio.to_integer(); 227 | if int > *MAX_COST { 228 | int.clone_from(&MAX_COST) 229 | }; 230 | Ok(int) 231 | } 232 | Sign::Minus => Ok(BigUint::from(0u32)), 233 | }, 234 | // Used to clamp Inf, but the only way to get Inf 235 | // right now is to divide by 0. It makes more 236 | // sense to treat that like an error instead. 237 | /* 238 | GenericFraction::Infinity(sign) => match sign { 239 | Sign::Plus => Ok(MAX_COST.clone()), 240 | Sign::Minus => Ok(BigUint::from(0u32)), 241 | },*/ 242 | GenericFraction::Infinity(_) => Err(()), 243 | GenericFraction::NaN => Err(()), 244 | } 245 | } 246 | 247 | pub fn wei_to_grt() -> BigUint { 248 | BigUint::from(1000000000000000000u64) 249 | } 250 | 251 | pub(crate) fn split_definitions<'a, T: q::Text<'a>>( 252 | definitions: Vec>, 253 | ) -> ( 254 | Vec>, 255 | Vec>, 256 | ) { 257 | profile_fn!(split_definitions); 258 | 259 | let mut operations = Vec::new(); 260 | let mut fragments = Vec::new(); 261 | for definition in definitions.into_iter() { 262 | match definition { 263 | q::Definition::Fragment(fragment) => fragments.push(fragment), 264 | q::Definition::Operation(operation) => operations.push(operation), 265 | } 266 | } 267 | (operations, fragments) 268 | } 269 | 270 | fn get_top_level_fields<'a, 's, T: q::Text<'s>>( 271 | op: &'a q::OperationDefinition<'s, T>, 272 | fragments: &'a [q::FragmentDefinition<'s, T>], 273 | variables: &QueryVariables, 274 | ) -> Result>, CostError> { 275 | profile_fn!(get_top_level_fields); 276 | 277 | fn get_top_level_fields_from_set<'a1, 's1, T: q::Text<'s1>>( 278 | set: &'a1 q::SelectionSet<'s1, T>, 279 | fragments: &'a1 [q::FragmentDefinition<'s1, T>], 280 | variables: &QueryVariables, 281 | result: &mut Vec<&'a1 q::Field<'s1, T>>, 282 | ) -> Result<(), CostError> { 283 | profile_fn!(get_top_level_fields_from_set); 284 | 285 | for item in set.items.iter() { 286 | match item { 287 | q::Selection::Field(field) => { 288 | if !matching::exclude(&field.directives, variables) 289 | .map_err(|()| CostError::QueryNotSupported)? 290 | { 291 | result.push(field) 292 | } 293 | } 294 | q::Selection::FragmentSpread(fragment_spread) => { 295 | // Find the fragment from the fragment declarations 296 | let fragment = fragments 297 | .iter() 298 | .find(|frag| frag.name == fragment_spread.fragment_name); 299 | let fragment = if let Some(fragment) = fragment { 300 | fragment 301 | } else { 302 | return Err(CostError::QueryInvalid); 303 | }; 304 | 305 | // Exclude the fragment if either the fragment itself or the spread 306 | // has a directive indicating that. 307 | if matching::exclude(&fragment_spread.directives, variables) 308 | .map_err(|()| CostError::QueryNotSupported)? 309 | { 310 | continue; 311 | } 312 | 313 | if matching::exclude(&fragment.directives, variables) 314 | .map_err(|()| CostError::QueryNotSupported)? 315 | { 316 | continue; 317 | } 318 | 319 | // Treat each field within the fragment as a top level field 320 | // TODO: (Security) Recursion 321 | get_top_level_fields_from_set( 322 | &fragment.selection_set, 323 | fragments, 324 | variables, 325 | result, 326 | )?; 327 | } 328 | q::Selection::InlineFragment(inline_fragment) => { 329 | if matching::exclude(&inline_fragment.directives, variables) 330 | .map_err(|()| CostError::QueryNotSupported)? 331 | { 332 | continue; 333 | } 334 | 335 | get_top_level_fields_from_set( 336 | &inline_fragment.selection_set, 337 | fragments, 338 | variables, 339 | result, 340 | )?; 341 | } 342 | } 343 | } 344 | Ok(()) 345 | } 346 | 347 | let mut result = Vec::new(); 348 | 349 | match op { 350 | q::OperationDefinition::Query(query) => { 351 | if !query.directives.is_empty() { 352 | return Err(CostError::QueryNotSupported); 353 | } 354 | get_top_level_fields_from_set(&query.selection_set, fragments, variables, &mut result)?; 355 | } 356 | q::OperationDefinition::SelectionSet(set) => { 357 | get_top_level_fields_from_set(set, fragments, variables, &mut result)?; 358 | } 359 | q::OperationDefinition::Mutation(_) | q::OperationDefinition::Subscription(_) => { 360 | return Err(CostError::QueryNotSupported); 361 | } 362 | } 363 | 364 | Ok(result) 365 | } 366 | 367 | #[derive(Debug)] 368 | pub struct RealParseError { 369 | from: String, 370 | } 371 | 372 | impl fmt::Display for RealParseError { 373 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 374 | profile_method!(fmt); 375 | 376 | write!(f, "Failed to parse number from {}", &self.from) 377 | } 378 | } 379 | 380 | impl std::error::Error for RealParseError {} 381 | 382 | pub fn parse_real(s: &str) -> Result { 383 | profile_fn!(parse_real); 384 | 385 | match crate::parser::real(s) { 386 | Ok(("", i)) => Ok(i), 387 | _ => Err(RealParseError { from: s.to_owned() }), 388 | } 389 | } 390 | 391 | #[cfg(test)] 392 | mod parse_error_tests; 393 | #[cfg(test)] 394 | mod tests; 395 | -------------------------------------------------------------------------------- /lang/src/matching.rs: -------------------------------------------------------------------------------- 1 | use crate::language::Captures; 2 | use crate::prelude::*; 3 | use graphql::{graphql_parser::query as q, IntoStaticValue, QueryVariables}; 4 | use itertools::Itertools as _; 5 | use std::borrow::Borrow; 6 | use std::collections::BTreeMap; 7 | 8 | struct MatchingContext<'var, 'cap, 'frag, 'fragt: 'frag, TF: q::Text<'fragt>> { 9 | fragments: &'frag [q::FragmentDefinition<'fragt, TF>], 10 | variables: &'var QueryVariables, 11 | captures: &'cap mut Captures, 12 | } 13 | 14 | fn match_selections<'l, 'r, 'c, TL: q::Text<'l>, TR: q::Text<'r>, TC: q::Text<'c>>( 15 | predicate: &q::Selection<'l, TL>, 16 | query: &q::Selection<'r, TR>, 17 | context: &mut MatchingContext<'_, '_, '_, 'c, TC>, 18 | ) -> Result { 19 | profile_fn!(match_selections); 20 | 21 | match (predicate, query) { 22 | // A fragment spread on the lhs has nothing to draw the fragment contents from. 23 | (q::Selection::FragmentSpread(_), _) => Err(()), 24 | (q::Selection::Field(predicate), q::Selection::Field(query)) => { 25 | match_fields(predicate, query, context) 26 | } 27 | (_, q::Selection::FragmentSpread(fragment_spread)) => { 28 | if exclude(&fragment_spread.directives, context.variables)? { 29 | return Ok(false); 30 | } 31 | let fragment_definition = context 32 | .fragments 33 | .iter() 34 | .find(|def| def.name.as_ref() == fragment_spread.fragment_name.as_ref()); 35 | if let Some(fragment_definition) = fragment_definition { 36 | // TODO: Check the spec... what if there are 2 fragment definitions 37 | // with opposing directives? Does it mean "include the fragment" if such, 38 | // or does it mean "include the fields". In one case 2 fragments with the 39 | // same name might be valid. In the other, not. If the former, we would want 40 | // to move the check for excluding a fragment to find and then if there 41 | // is no fragment with a matching name then treat it as empty? 42 | if exclude(&fragment_definition.directives, context.variables)? { 43 | return Ok(false); 44 | } 45 | 46 | // TODO: A fragment definition always has a type condition. So, 47 | // this sometimes needs to match an inline fragment? 48 | 49 | any_ok( 50 | fragment_definition.selection_set.items.iter(), 51 | |selection| match_selections(predicate, selection, context), 52 | ) 53 | } else { 54 | Err(()) 55 | } 56 | } 57 | (_, q::Selection::InlineFragment(q_inline)) => { 58 | if exclude(&q_inline.directives, context.variables)? { 59 | return Ok(false); 60 | } 61 | if let Some(q_type) = &q_inline.type_condition { 62 | // If the fragment has a type condition, then match a fragment 63 | // with the same type condition as the predicate 64 | if let q::Selection::InlineFragment(p_inline) = predicate { 65 | if let Some(p_type) = &p_inline.type_condition { 66 | match (p_type, q_type) { 67 | (q::TypeCondition::On(p_type), q::TypeCondition::On(q_type)) => { 68 | if p_type.as_ref() == q_type.as_ref() { 69 | // Two fragments with the same type condition. 70 | return match_selection_sets( 71 | &p_inline.selection_set, 72 | &q_inline.selection_set, 73 | context, 74 | ); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | Ok(false) 81 | } else { 82 | any_ok(&q_inline.selection_set.items, |item| { 83 | match_selections(predicate, item, context) 84 | }) 85 | } 86 | } 87 | (q::Selection::InlineFragment(inline_fragment), q::Selection::Field(_)) => { 88 | // Can't support directives here in any meaningful way, except maybe from $GLOBALS... 89 | // but I'm not sure I want to think about what bringing substitutions in here would 90 | // mean fully as of yet. 91 | if !inline_fragment.directives.is_empty() { 92 | return Err(()); 93 | } 94 | if inline_fragment.type_condition.is_some() { 95 | return Ok(false); 96 | } 97 | 98 | // TODO: Code may need to be re-structured a little bit to support this. 99 | // An inline fragment with no condition could be said to 'extend' it's containing 100 | // selection set. So, what we probably would need to do is to first 'collect' the 101 | // fields of the predicate in the outer loop, then match them all rather than finding 102 | // this case here. Doing this in a pre-process would be good to have consistency about 103 | // when error conditions arise. We could also cache things like capture names during pre-processing. 104 | // There's probably no reason to support an inline fragment with no type condition 105 | // in the predicate though. 106 | Err(()) 107 | } 108 | } 109 | } 110 | 111 | fn get_capture_names_selection<'l>( 112 | predicate: &q::Selection<'l, &'l str>, 113 | names: &mut Vec<&'l str>, 114 | ) -> Result<(), ()> { 115 | profile_fn!(get_capture_names_selection); 116 | 117 | match predicate { 118 | q::Selection::Field(field) => get_capture_names_field(field, names), 119 | q::Selection::InlineFragment(inline) => get_capture_names_inline_fragment(inline, names), 120 | q::Selection::FragmentSpread(spread) => get_capture_names_fragment_spread(spread, names), 121 | } 122 | } 123 | 124 | fn get_capture_names_inline_fragment<'l>( 125 | predicate: &q::InlineFragment<'l, &'l str>, 126 | names: &mut Vec<&'l str>, 127 | ) -> Result<(), ()> { 128 | profile_fn!(get_capture_names_inline_fragment); 129 | 130 | if !predicate.directives.is_empty() { 131 | return Err(()); 132 | } 133 | 134 | get_capture_names_selection_set(&predicate.selection_set, names) 135 | } 136 | 137 | fn get_capture_names_fragment_spread<'l>( 138 | _predicate: &q::FragmentSpread<'l, &'l str>, 139 | _names: &mut [&'l str], 140 | ) -> Result<(), ()> { 141 | profile_fn!(get_capture_names_fragment_spread); 142 | Err(()) // Nowhere to get the fragment from the name. 143 | } 144 | 145 | fn get_capture_names_selection_set<'l>( 146 | predicate: &q::SelectionSet<'l, &'l str>, 147 | names: &mut Vec<&'l str>, 148 | ) -> Result<(), ()> { 149 | profile_fn!(get_capture_names_selection_set); 150 | 151 | for selection in predicate.items.iter() { 152 | get_capture_names_selection(selection, names)?; 153 | } 154 | 155 | Ok(()) 156 | } 157 | 158 | pub fn match_query<'l, 'r, 'f, 'tf: 'f, TF: q::Text<'tf>, TL: q::Text<'l>, TR: q::Text<'r>>( 159 | predicate: &q::Field<'l, TL>, 160 | query: &q::Field<'r, TR>, 161 | fragments: &'f [q::FragmentDefinition<'tf, TF>], 162 | variables: &QueryVariables, 163 | captures: &mut Captures, 164 | ) -> Result { 165 | profile_fn!(match_query); 166 | 167 | // TODO: (Security) Prevent stackoverflow by using 168 | // MatchingContext as a queue of requirement 169 | let mut context = MatchingContext { 170 | fragments, 171 | variables, 172 | captures, 173 | }; 174 | match_fields(predicate, query, &mut context) 175 | } 176 | 177 | // Iterates over each item in 'iter' and returns: 178 | // Ok(true) if f(item) => Ok(true) 179 | // Err(e) if f(item) => Err(e) 180 | // Ok(false) if the above conditions are not reached 181 | fn any_ok( 182 | iter: T, 183 | mut f: impl FnMut(T::Item) -> Result, 184 | ) -> Result { 185 | profile_fn!(any_ok); 186 | 187 | let iter = iter.into_iter(); 188 | for item in iter { 189 | if f(item)? { 190 | return Ok(true); 191 | } 192 | } 193 | 194 | Ok(false) 195 | } 196 | 197 | fn get_if_argument<'a, T: q::Text<'a>>( 198 | directive: &q::Directive<'a, T>, 199 | variables: &QueryVariables, 200 | ) -> Result { 201 | profile_fn!(get_if_argument); 202 | 203 | match directive.arguments.iter().exactly_one() { 204 | Ok((k, arg)) if k.as_ref() == "if" => match arg { 205 | q::Value::Boolean(b) => Ok(*b), 206 | q::Value::Variable(name) => match variables.get(name.as_ref()) { 207 | Some(q::Value::Boolean(b)) => Ok(*b), 208 | _ => Err(()), 209 | }, 210 | _ => Err(()), 211 | }, 212 | _ => Err(()), 213 | } 214 | } 215 | 216 | // TODO: Check the spec to make sure the semantics are correct here. 217 | // We are treating each directive as its own independent filter. 218 | // So: Eg: `@skip(if: true) @skip(if: false)` would skip. But, 219 | // it's not clear for sure if `@skip(true) @include(true)` for example 220 | // should behave the same way this function does. 221 | pub fn exclude<'a, T: q::Text<'a>>( 222 | directives: &[q::Directive<'a, T>], 223 | variables: &QueryVariables, 224 | ) -> Result { 225 | profile_fn!(exclude); 226 | 227 | for directive in directives.iter() { 228 | match directive.name.as_ref() { 229 | "skip" => { 230 | if get_if_argument(directive, variables)? { 231 | return Ok(true); 232 | } 233 | } 234 | "include" => { 235 | if !get_if_argument(directive, variables)? { 236 | return Ok(true); 237 | } 238 | } 239 | _ => return Err(()), 240 | } 241 | } 242 | 243 | Ok(false) 244 | } 245 | 246 | fn match_fields<'l, 'r, 'c, TL: q::Text<'l>, TR: q::Text<'r>, TC: q::Text<'c>>( 247 | predicate: &q::Field<'l, TL>, 248 | query: &q::Field<'r, TR>, 249 | context: &mut MatchingContext<'_, '_, '_, 'c, TC>, 250 | ) -> Result { 251 | profile_fn!(match_fields); 252 | 253 | if predicate.name.as_ref() != query.name.as_ref() { 254 | return Ok(false); 255 | } 256 | 257 | if !predicate.directives.is_empty() { 258 | return Err(()); 259 | } 260 | 261 | if !predicate.directives.is_empty() { 262 | return Err(()); 263 | } 264 | 265 | // If a directive says that a field should not be included, 266 | // then it won't be counted toward a match. 267 | if exclude(&query.directives, context.variables)? { 268 | return Ok(false); 269 | } 270 | 271 | for p_argument in predicate.arguments.iter() { 272 | let p_argument = (p_argument.0.as_ref(), &p_argument.1); 273 | if !any_ok(query.arguments.iter(), |q_argument| { 274 | let q_argument = (q_argument.0.as_ref(), &q_argument.1); 275 | match_named_value(p_argument, q_argument, context) 276 | })? { 277 | return Ok(false); 278 | } 279 | } 280 | 281 | if !match_selection_sets(&predicate.selection_set, &query.selection_set, context)? { 282 | return Ok(false); 283 | } 284 | 285 | // TODO: Support alias? 286 | 287 | Ok(true) 288 | } 289 | 290 | fn match_selection_sets<'l, 'r, 'c, TL: q::Text<'l>, TR: q::Text<'r>, TC: q::Text<'c>>( 291 | predicate: &q::SelectionSet<'l, TL>, 292 | query: &q::SelectionSet<'r, TR>, 293 | context: &mut MatchingContext<'_, '_, '_, 'c, TC>, 294 | ) -> Result { 295 | profile_fn!(match_selection_sets); 296 | 297 | for p_selection in predicate.items.iter() { 298 | if !any_ok(query.items.iter(), |q_selection| { 299 | match_selections(p_selection, q_selection, context) 300 | })? { 301 | return Ok(false); 302 | } 303 | } 304 | Ok(true) 305 | } 306 | 307 | pub fn get_capture_names_field<'l>( 308 | predicate: &q::Field<'l, &'l str>, 309 | names: &mut Vec<&'l str>, 310 | ) -> Result<(), ()> { 311 | profile_fn!(get_capture_names_field); 312 | 313 | for (_, value) in predicate.arguments.iter() { 314 | get_capture_names_value(value, names)?; 315 | } 316 | 317 | get_capture_names_selection_set(&predicate.selection_set, names) 318 | } 319 | 320 | fn match_named_value< 321 | 'l, 322 | 'r, 323 | 'c, 324 | TL: q::Text<'l>, 325 | TR: q::Text<'r>, 326 | VP: Borrow>, 327 | VQ: Borrow>, 328 | TC: q::Text<'c>, 329 | >( 330 | predicate: (&str, VP), 331 | query: (&str, VQ), 332 | context: &mut MatchingContext<'_, '_, '_, 'c, TC>, 333 | ) -> Result { 334 | profile_fn!(match_named_value); 335 | 336 | if predicate.0 != query.0 { 337 | return Ok(false); 338 | } 339 | 340 | match_value(predicate.1.borrow(), query.1.borrow(), context) 341 | } 342 | 343 | fn match_value<'l, 'r, 'c, TR: q::Text<'r>, TL: q::Text<'l>, TC: q::Text<'c>>( 344 | predicate: &q::Value<'l, TL>, 345 | query: &q::Value<'r, TR>, 346 | context: &mut MatchingContext<'_, '_, '_, 'c, TC>, 347 | ) -> Result { 348 | profile_fn!(match_value); 349 | use q::Value::*; 350 | 351 | match (predicate, query) { 352 | (_, Variable(var)) => { 353 | if let Some(var) = context.variables.get(var.as_ref()) { 354 | match_value(predicate, var, context) 355 | } else { 356 | Err(()) 357 | } 358 | } 359 | // TODO: Performance: Borrow keys in Captures 360 | (Variable(var), q) => { 361 | context.captures.insert(var.as_ref(), q.to_graphql()); 362 | Ok(true) 363 | } 364 | (Int(p), Int(q)) => Ok(p == q), 365 | (Float(p), Float(q)) => Ok(p == q), 366 | (String(p), String(q)) => Ok(p == q), 367 | (Boolean(p), Boolean(q)) => Ok(p == q), 368 | (Null, Null) => Ok(true), 369 | (Enum(p), Enum(q)) => Ok(p.as_ref() == q.as_ref()), 370 | (List(p), List(q)) => match_list(p, q, context), 371 | (Object(p), Object(q)) => match_object(p, q, context), 372 | _ => Ok(false), 373 | } 374 | } 375 | 376 | fn get_capture_names_value<'l>( 377 | value: &q::Value<'l, &'l str>, 378 | names: &mut Vec<&'l str>, 379 | ) -> Result<(), ()> { 380 | profile_fn!(get_capture_names_value); 381 | 382 | use q::Value::*; 383 | match value { 384 | Variable(var) => { 385 | // It's not possible to capture into a single name from multiple places in the query 386 | if names.contains(var) { 387 | Err(()) 388 | } else { 389 | names.push(var); 390 | Ok(()) 391 | } 392 | } 393 | List(values) => { 394 | for value in values.iter() { 395 | get_capture_names_value(value, names)?; 396 | } 397 | Ok(()) 398 | } 399 | Object(props) => { 400 | for value in props.values() { 401 | get_capture_names_value(value, names)?; 402 | } 403 | Ok(()) 404 | } 405 | Int(_) | Float(_) | String(_) | Boolean(_) | Null | Enum(_) => Ok(()), 406 | } 407 | } 408 | 409 | fn match_list<'l, 'r, 'c, TR: q::Text<'r>, TL: q::Text<'l>, TC: q::Text<'c>>( 410 | predicate: &[q::Value<'l, TL>], 411 | query: &[q::Value<'r, TR>], 412 | context: &mut MatchingContext<'_, '_, '_, 'c, TC>, 413 | ) -> Result { 414 | profile_fn!(match_list); 415 | 416 | if predicate.len() != query.len() { 417 | return Ok(false); 418 | } 419 | 420 | for (p, q) in predicate.iter().zip(query.iter()) { 421 | if !(match_value(p, q, context))? { 422 | return Ok(false); 423 | } 424 | } 425 | Ok(true) 426 | } 427 | 428 | fn match_object<'l, 'r, 'c, TR: q::Text<'r>, TL: q::Text<'l>, TC: q::Text<'c>>( 429 | predicate: &BTreeMap>, 430 | query: &BTreeMap>, 431 | context: &mut MatchingContext<'_, '_, '_, 'c, TC>, 432 | ) -> Result { 433 | profile_fn!(match_object); 434 | 435 | for p_arg in predicate.iter() { 436 | let p_arg = (p_arg.0.as_ref(), p_arg.1); 437 | if !any_ok(query.iter(), |q_arg| { 438 | let q_arg = (q_arg.0.as_ref(), q_arg.1); 439 | match_named_value(p_arg, q_arg, context) 440 | })? { 441 | return Ok(false); 442 | } 443 | } 444 | Ok(true) 445 | } 446 | -------------------------------------------------------------------------------- /lang/src/parse_error_tests.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | fn assert_err_text(model: &str, text: &str) { 4 | let err = CostModel::compile(model, "{}").unwrap_err(); 5 | let display = format!("{}", err); 6 | println!("{}", &display); 7 | assert_eq!(text, &display); 8 | } 9 | 10 | #[test] 11 | fn mismatched_close_paren() { 12 | let model = "default => 1 + 2);"; 13 | let expect = "\ 14 | Failed to parse cost model.\n\ 15 | When parsing statement at (line: 0, column: 0)\n\ 16 | default => 1 + 2);\n\ 17 | ^\n\ 18 | Expected: \";\" at (line: 0, column: 16)\n\ 19 | default => 1 + 2);\n ^\n\ 20 | "; 21 | assert_err_text(model, expect); 22 | } 23 | 24 | #[test] 25 | fn mismatched_open_paren() { 26 | let model = "default => (1 + 2;"; 27 | let expect = "Failed to parse cost model.\nWhen parsing rational expression at (line: 0, column: 11)\ndefault => (1 + 2;\n ^\nExpected: unknown at (line: 0, column: 17)\ndefault => (1 + 2;\n ^\n"; 28 | assert_err_text(model, expect); 29 | } 30 | 31 | #[test] 32 | fn missing_semicolon_middle_statement() { 33 | let model = " 34 | query { b } => 1;\n\ 35 | query { a } => 2\n\ 36 | default => 1;\n\ 37 | "; 38 | 39 | let expect = "Failed to parse cost model.\nWhen parsing statement at (line: 2, column: 0)\nquery { a } => 2\n^\nExpected: \";\" at (line: 2, column: 16)\nquery { a } => 2\n ^\n"; 40 | assert_err_text(model, expect); 41 | } 42 | 43 | #[test] 44 | fn missing_semicolon_last_statement() { 45 | let model = "\ 46 | query { a } => 2;\n\ 47 | default => 1\n\ 48 | "; 49 | 50 | let expect = "Failed to parse cost model.\nWhen parsing statement at (line: 1, column: 0)\ndefault => 1\n^\nExpected: \";\" at (line: 1, column: 12)\ndefault => 1\n ^\n"; 51 | assert_err_text(model, expect); 52 | } 53 | 54 | #[test] 55 | fn in_when_clause() { 56 | let model = "default when a => 1;"; 57 | let expect = "Failed to parse cost model.\nWhen parsing when clause at (line: 0, column: 8)\ndefault when a => 1;\n ^\nExpected: \"(\" at (line: 0, column: 13)\ndefault when a => 1;\n ^\nExpected: \"false\" at (line: 0, column: 13)\ndefault when a => 1;\n ^\nExpected: \"true\" at (line: 0, column: 13)\ndefault when a => 1;\n ^\nWhen parsing variable at (line: 0, column: 13)\ndefault when a => 1;\n ^\nExpected: \"$\" at (line: 0, column: 13)\ndefault when a => 1;\n ^\nWhen parsing rational expression at (line: 0, column: 13)\ndefault when a => 1;\n ^\nExpected: \"(\" at (line: 0, column: 13)\ndefault when a => 1;\n ^\nWhen parsing variable at (line: 0, column: 13)\ndefault when a => 1;\n ^\nExpected: \"$\" at (line: 0, column: 13)\ndefault when a => 1;\n ^\nWhen parsing number at (line: 0, column: 13)\ndefault when a => 1;\n ^\nUnknown at (line: 0, column: 13)\ndefault when a => 1;\n ^\n"; 58 | assert_err_text(model, expect); 59 | } 60 | 61 | #[test] 62 | fn across_lines() { 63 | let model = "\ 64 | query { 65 | a { b, c } 66 | } => x; 67 | "; 68 | let expect = "Failed to parse cost model.\nWhen parsing rational expression at (line: 2, column: 13)\n } => x;\n ^\nExpected: \"(\" at (line: 2, column: 13)\n } => x;\n ^\nWhen parsing variable at (line: 2, column: 13)\n } => x;\n ^\nExpected: \"$\" at (line: 2, column: 13)\n } => x;\n ^\nWhen parsing number at (line: 2, column: 13)\n } => x;\n ^\nUnknown at (line: 2, column: 13)\n } => x;\n ^\n"; 69 | assert_err_text(model, expect); 70 | } 71 | 72 | #[test] 73 | fn thin_arrow() { 74 | let model = "default -> 2;"; 75 | let expect = "Failed to parse cost model.\nWhen parsing statement at (line: 0, column: 0)\ndefault -> 2;\n^\nExpected: \"=>\" at (line: 0, column: 8)\ndefault -> 2;\n ^\n"; 76 | assert_err_text(model, expect); 77 | } 78 | 79 | #[test] 80 | fn invalid_identifier_in_variable_later_char() { 81 | let model = "default => $_a²;"; 82 | let expect = "Failed to parse cost model.\nWhen parsing statement at (line: 0, column: 0)\ndefault => $_a²;\n^\nExpected: \";\" at (line: 0, column: 14)\ndefault => $_a²;\n ^\n"; 83 | assert_err_text(model, expect); 84 | } 85 | 86 | #[test] 87 | fn invalid_identifier_in_variable_first_char() { 88 | let model = "default => $1a;"; 89 | let expect = "Failed to parse cost model.\nWhen parsing identifier at (line: 0, column: 12)\ndefault => $1a;\n ^\nExpected: \"_\" at (line: 0, column: 12)\ndefault => $1a;\n ^\nUnknown at (line: 0, column: 12)\ndefault => $1a;\n ^\n"; 90 | assert_err_text(model, expect); 91 | } 92 | 93 | #[test] 94 | fn bad_graphql() { 95 | let model = "query { a % } => 1;"; 96 | let expect = "\ 97 | Failed to parse cost model.\n\ 98 | When parsing query at (line: 0, column: 0)\n\ 99 | query { a % } => 1;\n\ 100 | ^\n\ 101 | Failed to parse GraphQL\n\ 102 | Parse error follows.\n\ 103 | Note that within this error, line and column numbers are relative.\n\ 104 | query parse error: Parse error at 1:11\n\ 105 | Unexpected `unexpected character \'%\'`\nExpected `}`\n\ 106 | \n \ 107 | at (line: 0, column: 0)\n\ 108 | query { a % } => 1;\n\ 109 | ^\n\ 110 | "; 111 | assert_err_text(model, expect); 112 | } 113 | -------------------------------------------------------------------------------- /lang/src/parse_errors.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use graphql::graphql_parser::query::ParseError as GraphQLParseError; 3 | use nom::error::{ErrorKind, ParseError}; 4 | use nom::{Err as NomErr, IResult, InputLength}; 5 | use std::cmp::Ordering; 6 | use std::fmt; 7 | use std::ops::Deref; 8 | 9 | /// If something failed, this notes what we were 10 | /// trying to do when it failed. 11 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 12 | pub enum ErrorContext { 13 | Statement, 14 | Predicate, 15 | RationalExpression, 16 | WhenClause, 17 | Match, 18 | Identifier, 19 | Variable, 20 | RealNumber, 21 | GraphQLQuery, 22 | Comparison, 23 | } 24 | 25 | impl fmt::Display for ErrorContext { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | profile_method!(fmt); 28 | 29 | write!(f, "When parsing ")?; 30 | use ErrorContext::*; 31 | match self { 32 | Statement => write!(f, "statement"), 33 | Predicate => write!(f, "predicate"), 34 | RationalExpression => write!(f, "rational expression"), 35 | WhenClause => write!(f, "when clause"), 36 | Match => write!(f, "match"), 37 | Identifier => write!(f, "identifier"), 38 | Variable => write!(f, "variable"), 39 | RealNumber => write!(f, "number"), 40 | GraphQLQuery => write!(f, "query"), 41 | Comparison => write!(f, "comparison"), 42 | } 43 | } 44 | } 45 | 46 | // When I started I tried to separate some of these into contextual enums that nested. 47 | // But, intricacies of Nom's error system turned this into a nightmare. 48 | // So I rolled that back and wrote a long comment - but the comment turned into a nightmare. 49 | // So I rolled that back too. 50 | // Some of the problem has been fixed by rolling different types into the aggregator 51 | #[derive(Debug)] 52 | pub enum ValidationError { 53 | FailedToParseGraphQL(GraphQLParseError), 54 | ExpectedQueryOperationDefinition, 55 | MatchingQueryNameIsUnsupported(I), 56 | VariablesAreUnsupported, 57 | DirectivesAreUnsupported, 58 | SelectionSetMustContainSingleField, 59 | } 60 | 61 | impl fmt::Display for ValidationError<&'_ str> { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | profile_method!(fmt); 64 | 65 | use ValidationError::*; 66 | match self { 67 | FailedToParseGraphQL(inner) => { 68 | writeln!( 69 | f, 70 | "Failed to parse GraphQL\n\ 71 | Parse error follows.\n\ 72 | Note that within this error, line and column numbers are relative." 73 | )?; 74 | writeln!(f, "{}", inner)?; 75 | } 76 | ExpectedQueryOperationDefinition => { 77 | writeln!( 78 | f, 79 | "GraphQL must have a single operation definition of type query." 80 | )?; 81 | } 82 | MatchingQueryNameIsUnsupported(name) => { 83 | writeln!(f, "Matching a query name is unsupported. Got: {}", name)?; 84 | } 85 | VariablesAreUnsupported => { 86 | writeln!(f, "Defining variables in the GraphQL query is unsupported.")?; 87 | } 88 | DirectivesAreUnsupported => { 89 | writeln!( 90 | f, 91 | "Matching directives in the GraphQL query is unsupported." 92 | )?; 93 | writeln!(f, "Note that standard directives such as skip and filter will be handled in query normalization.")?; 94 | } 95 | SelectionSetMustContainSingleField => { 96 | writeln!( 97 | f, 98 | "GraphQL query selection set must contain exactly one field." 99 | )?; 100 | writeln!(f, "Note that when multiple fields exist in the query, they will be costed individually and summed.")?; 101 | } 102 | } 103 | Ok(()) 104 | } 105 | } 106 | 107 | #[derive(Debug, Clone)] 108 | pub enum ExpectationError { 109 | Tag(&'static str), 110 | AlphaNumeric, 111 | Alpha, 112 | Or(Box<(ExpectationError, ExpectationError)>), 113 | // Hacks? 114 | TODO, 115 | Nom(ErrorKind), 116 | } 117 | 118 | impl fmt::Display for ExpectationError { 119 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 120 | profile_method!(fmt); 121 | 122 | let mut queue = vec![self]; 123 | write!(f, "Expected: ")?; 124 | while let Some(next) = queue.pop() { 125 | use ExpectationError::*; 126 | match next { 127 | Tag(s) => write!(f, "\"{}\"", s)?, 128 | TODO | Nom(_) => write!(f, "unknown")?, 129 | Or(lr) => { 130 | queue.push(&lr.0); 131 | queue.push(&lr.1); 132 | } 133 | Alpha => write!(f, "[a-z] or [A-Z]")?, 134 | AlphaNumeric => write!(f, "[a-z] or [A-Z] or [0-9]")?, 135 | } 136 | if !queue.is_empty() { 137 | write!(f, " or ")?; 138 | } 139 | } 140 | Ok(()) 141 | } 142 | } 143 | 144 | pub type ExpectationAtom = ErrorAtom; 145 | pub type ValidationAtom = ErrorAtom>; 146 | pub type ContextAtom = ErrorAtom; 147 | 148 | // Turns any Err into a Failure 149 | pub fn fail_fast(mut f: F) -> impl FnMut(I) -> IResult 150 | where 151 | F: FnMut(I) -> IResult, 152 | { 153 | move |i: I| match f(i) { 154 | Err(NomErr::Error(e)) => Err(NomErr::Failure(e)), 155 | other => other, 156 | } 157 | } 158 | 159 | /* 160 | pub fn remap_kind( 161 | k: ExpectationError, 162 | f: F, 163 | ) -> impl Fn(I) -> IResult> 164 | where 165 | F: Fn(I) -> IResult>, 166 | { 167 | remap_err(f, move |e| { 168 | // The intent here is just to map errs from Nom into something else. 169 | // We don't want to clobber errors, that's what context is for. 170 | debug_assert!(matches!(&e.kind, ExpectationError::Nom(_))); 171 | e.kind = k; 172 | }) 173 | } 174 | 175 | /// Allows you to run a fn on a return error (whether that 176 | /// be a Error or a Failure). This is useful for changing 177 | /// from an AgoraParseErrorKind::Nom to some other kind 178 | pub fn remap_err( 179 | f: F, 180 | map: impl Fn(&mut ErrorAtom), 181 | ) -> impl Fn(I) -> IResult> 182 | where 183 | F: Fn(I) -> IResult>, 184 | { 185 | move |i: I| { 186 | let mut result = f(i); 187 | if let Err(err) = &mut result { 188 | match err { 189 | NomErr::Error(err) => map(err.errors.first_mut().unwrap()), 190 | NomErr::Failure(err) => map(err.errors.first_mut().unwrap()), 191 | NomErr::Incomplete(_) => {} 192 | } 193 | } 194 | result 195 | } 196 | } 197 | */ 198 | 199 | /// Adds additional error context to any error. 200 | /// This does the error prone work for you of stashing 201 | /// the input at call time so that both the outer and 202 | /// inner error may reference a unique part of the string. 203 | pub fn with_context( 204 | context: ErrorContext, 205 | mut f: F, 206 | ) -> impl FnMut(I) -> IResult> 207 | where 208 | I: Clone, 209 | F: FnMut(I) -> IResult>, 210 | { 211 | profile_fn!(with_context); 212 | 213 | move |i: I| { 214 | let input = i.clone(); 215 | let result = f(i); 216 | match result { 217 | Err(NomErr::Error(err)) => { 218 | let context = ErrorAtom::new(input, context); 219 | Err(NomErr::Error(ErrorAggregator::Context( 220 | context, 221 | Box::new(err), 222 | ))) 223 | } 224 | Err(NomErr::Failure(err)) => { 225 | let context = ErrorAtom::new(input, context); 226 | Err(NomErr::Failure(ErrorAggregator::Context( 227 | context, 228 | Box::new(err), 229 | ))) 230 | } 231 | r => r, 232 | } 233 | } 234 | } 235 | 236 | /// This is the smallest unit of error 237 | #[derive(Debug)] 238 | pub struct ErrorAtom { 239 | pub input: I, 240 | pub kind: E, 241 | } 242 | 243 | /// This aggregates multiple of the above. 244 | #[derive(Debug)] 245 | pub enum ErrorAggregator { 246 | // Expected something. For example, expected a `;` or expected a number. 247 | Expectation(ExpectationAtom), 248 | // Multiple branches at this level are possible. This forms a 249 | // tree, because we could expect different things within different 250 | // contexts. Eg: Expected a `;` within statement, or 251 | // `+` within expression within that statement. 252 | // The assumption in all code dealing with this right now 253 | // is that each branch has the same unparsed input length. 254 | Or(Box<(ErrorAggregator, ErrorAggregator)>), 255 | // Validation takes precedence over Expectations. This means 256 | // we parsed something successfully, but the result was invalid. 257 | // TODO: Use NomErr::Failure for Validations 258 | Validation(ValidationAtom), 259 | // Failed during some larger task. Eg: expected a [blank] within a statement. 260 | Context(ContextAtom, Box>), 261 | Unknown(ErrorAtom), 262 | } 263 | 264 | impl ErrorAggregator { 265 | fn unparsed_input_len(&self) -> usize { 266 | profile_fn!(unparsed_input_len); 267 | 268 | let mut queue = self; 269 | loop { 270 | match queue { 271 | ErrorAggregator::Expectation(atom) => return atom.input.input_len(), 272 | ErrorAggregator::Or(items) => queue = &items.0, 273 | ErrorAggregator::Validation(atom) => return atom.input.input_len(), 274 | ErrorAggregator::Context(_, inner) => queue = inner, 275 | ErrorAggregator::Unknown(atom) => return atom.input.input_len(), 276 | } 277 | } 278 | } 279 | } 280 | 281 | impl ErrorAggregator {} 282 | 283 | /// This makes convenient the use of atoms in functions that use IResult 284 | impl From> for NomErr> 285 | where 286 | ErrorAggregator: From>, 287 | { 288 | fn from(i: ErrorAtom) -> Self { 289 | NomErr::Error(i.into()) 290 | } 291 | } 292 | 293 | impl From> for ErrorAggregator { 294 | fn from(error: ExpectationAtom) -> Self { 295 | ErrorAggregator::Expectation(error) 296 | } 297 | } 298 | 299 | impl From> for ErrorAggregator { 300 | fn from(error: ValidationAtom) -> Self { 301 | ErrorAggregator::Validation(error) 302 | } 303 | } 304 | 305 | /// In order to finally impl Error and print a useful indication 306 | /// of where in the document something went wrong, we need to 307 | /// capture the original input as well. So, that's what this does. 308 | #[derive(Debug)] 309 | pub struct AgoraParseError { 310 | input: I, 311 | aggregator: ErrorAggregator, 312 | } 313 | 314 | struct Pos<'a> { 315 | line_number: usize, 316 | line: &'a str, 317 | column_number: usize, 318 | } 319 | 320 | impl<'a> AgoraParseError<&'a str> { 321 | pub fn new(input: &'a str, aggregator: ErrorAggregator<&'a str>) -> Self { 322 | Self { input, aggregator } 323 | } 324 | fn pos(&self, atom: &ErrorAtom<&'a str, E>) -> Pos<'a> { 325 | profile_fn!(pos); 326 | 327 | let mut line_start = 0; 328 | let mut line_number = 0; 329 | let mut column_number = 0; 330 | 331 | let mut chars = self.input.char_indices(); 332 | // TODO: Better to verify that one is actually a subslice of the other. 333 | let prefix_start = self.input.len().checked_sub(atom.input.len()).unwrap(); 334 | let mut first_prefix_char = None; 335 | 336 | for (i, c) in chars.by_ref() { 337 | if i == prefix_start { 338 | first_prefix_char = Some((c, i)); 339 | break; 340 | } 341 | 342 | if c == '\n' { 343 | line_number += 1; 344 | line_start = i + 1; 345 | column_number = 0; 346 | } else { 347 | column_number += 1; 348 | } 349 | } 350 | 351 | let line_end = match first_prefix_char { 352 | Some(('\n', i)) => i, 353 | _ => { 354 | let mut line_end = self.input.len(); 355 | for (i, c) in chars { 356 | if c == '\n' { 357 | line_end = i; 358 | break; 359 | } 360 | } 361 | line_end 362 | } 363 | }; 364 | 365 | Pos { 366 | line_number, 367 | column_number, 368 | line: &self.input[line_start..line_end], 369 | } 370 | } 371 | 372 | fn write_one( 373 | &self, 374 | f: &mut fmt::Formatter<'_>, 375 | atom: &ErrorAtom<&'a str, E>, 376 | ) -> fmt::Result { 377 | profile_fn!(write_one); 378 | 379 | let pos = self.pos(atom); 380 | writeln!( 381 | f, 382 | "{} at (line: {}, column: {})", 383 | &atom.kind, pos.line_number, pos.column_number 384 | )?; 385 | writeln!(f, "{}", pos.line)?; 386 | // Write a caret indicating the position. 387 | writeln!(f, "{}^", " ".repeat(pos.column_number))?; 388 | Ok(()) 389 | } 390 | } 391 | 392 | impl fmt::Display for AgoraParseError<&'_ str> { 393 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 394 | profile_method!(fmt); 395 | 396 | let mut queue = vec![&self.aggregator]; 397 | 398 | // Starting with just dumping all this info. 399 | // TODO: Format this sanely. 400 | while let Some(next) = queue.pop() { 401 | match next { 402 | ErrorAggregator::Expectation(e) => self.write_one(f, e)?, 403 | ErrorAggregator::Validation(e) => self.write_one(f, e)?, 404 | ErrorAggregator::Context(c, e) => { 405 | // Only write the context if it is the smallest context for that item. 406 | // This avoids writing tons of eg: when parsing this document, when parsing this 407 | // statement, when parsing this... (etc). But, does allow you do see what would 408 | // have been needed to correctly close the 1 thing you are interested in. Sometimes 409 | // a multiple contexts will still be printed, eg: when there is an Or which has their 410 | // own contexts. That is good, because it could display things like need ';' to close 411 | // the statement, or '+' to continue the linear expression. This check allows us 412 | // to be much more liberal in adding context to everything without overwhelming the user. 413 | if !matches!(e.deref(), &ErrorAggregator::Context(_, _)) { 414 | self.write_one(f, c)?; 415 | } 416 | queue.push(e); 417 | } 418 | ErrorAggregator::Or(items) => { 419 | queue.push(&items.0); 420 | queue.push(&items.1); 421 | } 422 | ErrorAggregator::Unknown(u) => self.write_one( 423 | f, 424 | &ErrorAtom { 425 | input: u.input, 426 | kind: "Unknown", 427 | }, 428 | )?, 429 | } 430 | } 431 | Ok(()) 432 | } 433 | } 434 | 435 | impl std::error::Error for AgoraParseError<&'_ str> {} 436 | 437 | impl ErrorAtom { 438 | pub fn new(input: I, kind: E) -> Self { 439 | Self { input, kind } 440 | } 441 | 442 | pub fn err(input: I, kind: E) -> Result { 443 | Err(Self::new(input, kind)) 444 | } 445 | } 446 | 447 | impl ParseError for ExpectationAtom { 448 | fn from_error_kind(input: I, kind: ErrorKind) -> Self { 449 | Self { 450 | input, 451 | kind: ExpectationError::Nom(kind), 452 | } 453 | } 454 | fn append(_input: I, _kind: ErrorKind, other: Self) -> Self { 455 | // TODO 456 | debug_assert!(false, "Called append"); 457 | other 458 | } 459 | // Clever idea taken from the nom docs. This prefers branches which parsed further 460 | // into the input. 461 | fn or(self, other: Self) -> Self { 462 | profile_fn!(or); 463 | 464 | match self.input.input_len().cmp(&other.input.input_len()) { 465 | Ordering::Equal => Self { 466 | input: self.input, 467 | kind: ExpectationError::Or(Box::new((self.kind, other.kind))), 468 | }, 469 | Ordering::Greater => other, 470 | Ordering::Less => self, 471 | } 472 | } 473 | } 474 | 475 | impl ParseError for ErrorAggregator { 476 | fn from_error_kind(input: I, kind: ErrorKind) -> Self { 477 | ErrorAggregator::Unknown(ErrorAtom::new(input, kind)) 478 | } 479 | fn append(_input: I, _kind: ErrorKind, other: Self) -> Self { 480 | // TODO 481 | //debug_assert!(false, "Called append"); 482 | other 483 | } 484 | // Clever idea taken from the nom docs. This prefers branches which parsed further 485 | // into the input. In the case where the parse length is equal, use an actual Or 486 | fn or(self, other: Self) -> Self { 487 | profile_method!(or); 488 | 489 | match self.unparsed_input_len().cmp(&other.unparsed_input_len()) { 490 | Ordering::Equal => ErrorAggregator::Or(Box::new((self, other))), 491 | Ordering::Greater => other, 492 | Ordering::Less => self, 493 | } 494 | } 495 | } 496 | 497 | macro_rules! ensure { 498 | ($cond:expr, $err:expr) => { 499 | if !($cond) { 500 | Err($err)? 501 | } 502 | }; 503 | } 504 | -------------------------------------------------------------------------------- /lang/src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::parse_errors::{ 2 | ErrorAggregator, ErrorAtom as ErrAtom, ErrorContext, ExpectationError, ValidationError, 3 | }; 4 | use crate::prelude::*; 5 | use crate::{expressions::*, language::*, parse_errors::*}; 6 | use fraction::BigFraction; 7 | use graphql::graphql_parser::query as q; 8 | use itertools::Itertools as _; 9 | use nom::{ 10 | branch::alt, 11 | bytes::complete::{is_not, take_while1}, 12 | character::complete::{alpha1, alphanumeric1, char, digit1}, 13 | combinator::{map, opt, recognize}, 14 | error::ParseError as NomParseError, 15 | multi::many0, 16 | sequence::{pair, tuple}, 17 | sequence::{preceded, terminated}, 18 | Compare, Err as NomErr, IResult as NomIResult, InputLength, InputTake, InputTakeAtPosition, 19 | }; 20 | use num_bigint::BigUint; 21 | use num_traits::Pow as _; 22 | 23 | // Change Nom default error type from (I, ErrorKind) to ErrorAggregator 24 | type IResult> = NomIResult; 25 | 26 | fn graphql_query(input: &str) -> IResult<&str, q::Field<'_, &str>> { 27 | profile_fn!(graphql_query); 28 | 29 | with_context(ErrorContext::GraphQLQuery, |input: &str| { 30 | tag("query")(input)?; 31 | fail_fast(|input: &str| { 32 | let (query, input) = q::consume_definition::<'_, &str>(input) 33 | .map_err(|e| ErrAtom::new(input, ValidationError::FailedToParseGraphQL(e)))?; 34 | 35 | let query = match query { 36 | q::Definition::Operation(q::OperationDefinition::Query(query)) => query, 37 | _ => ErrAtom::err(input, ValidationError::ExpectedQueryOperationDefinition)?, 38 | }; 39 | 40 | ensure!( 41 | query.name.is_none(), 42 | ErrAtom::new( 43 | input, 44 | ValidationError::MatchingQueryNameIsUnsupported(query.name.unwrap()) 45 | ) 46 | ); 47 | 48 | ensure!( 49 | query.variable_definitions.is_empty(), 50 | ErrAtom::new(input, ValidationError::VariablesAreUnsupported) 51 | ); 52 | 53 | ensure!( 54 | query.directives.is_empty(), 55 | ErrAtom::new(input, ValidationError::DirectivesAreUnsupported) 56 | ); 57 | 58 | match query.selection_set.items.into_iter().exactly_one() { 59 | Ok(q::Selection::Field(field)) => Ok((input, field)), 60 | _ => ErrAtom::err(input, ValidationError::SelectionSetMustContainSingleField)?, 61 | } 62 | })(input) 63 | })(input) 64 | } 65 | 66 | fn whitespace(input: I) -> IResult 67 | where 68 | I: InputTakeAtPosition + Clone + InputLength, 69 | { 70 | profile_fn!(whitespace); 71 | take_while1(char::is_whitespace)(input) 72 | } 73 | 74 | /// Calls nom tag and remaps the error from the opaque nom 75 | /// type to an expectation 76 | fn tag(s: &'static str) -> impl Fn(I) -> IResult 77 | where 78 | I: InputTake + Clone + InputLength + Compare<&'static str>, 79 | { 80 | move |i: I| match nom::bytes::complete::tag(s)(i) { 81 | Ok(o) => Ok(o), 82 | Err(NomErr::Error(ErrorAtom { 83 | kind: ExpectationError::Nom(_), 84 | input, 85 | })) => Err(NomErr::Error( 86 | ErrorAtom { 87 | kind: ExpectationError::Tag(s), 88 | input, 89 | } 90 | .into(), 91 | )), 92 | _ => unreachable!("Tag did not produce error"), 93 | } 94 | } 95 | 96 | fn when_clause(input: &str) -> IResult<&str, WhenClause> { 97 | profile_fn!(when_clause); 98 | 99 | with_context(ErrorContext::WhenClause, |input| { 100 | // Fail fast ensures that if we are expecting a when condition and find 101 | // the keyword that any subsequent error gets propagated up instead of 102 | // dropped by opt when parsing the statement. 103 | preceded( 104 | tag("when"), 105 | fail_fast(preceded( 106 | whitespace, 107 | map(condition, |c| WhenClause { condition: c }), 108 | )), 109 | )(input) 110 | })(input) 111 | } 112 | 113 | fn const_bool(input: &str) -> IResult<&str, Const> { 114 | profile_fn!(const_bool); 115 | 116 | let (input, value) = alt((map(tag("true"), |_| true), map(tag("false"), |_| false)))(input)?; 117 | Ok((input, Const::new(value))) 118 | } 119 | 120 | /// Conceptually a tree, but stored flat 121 | struct FlatTree { 122 | leaves: Vec, 123 | branches: Vec, 124 | } 125 | 126 | impl FlatTree { 127 | pub fn new() -> Self { 128 | Self { 129 | leaves: Vec::new(), 130 | branches: Vec::new(), 131 | } 132 | } 133 | 134 | pub fn leaf_required(&self) -> bool { 135 | self.leaves.len() == self.branches.len() 136 | } 137 | } 138 | 139 | // TODO: Remove all this collapse tree nonsense and use the shunting-yard algorithm. 140 | // https://en.wikipedia.org/wiki/Shunting-yard_algorithm That should fix any recursion 141 | // problem as well. 142 | impl> FlatTree { 143 | fn collapse( 144 | self, 145 | input: &str, 146 | kind: impl Into, 147 | mut join: impl FnMut(Leaf, Branch, Leaf) -> Leaf, 148 | ) -> IResult<&str, Self> { 149 | profile_method!(collapse); 150 | 151 | let FlatTree { leaves, branches } = self; 152 | let mut out = Self::new(); 153 | 154 | ensure!( 155 | leaves.len() == branches.len() + 1, 156 | ErrAtom::new(input, ExpectationError::TODO) 157 | ); 158 | 159 | let mut leaves = leaves.into_iter(); 160 | let branches = branches.into_iter(); 161 | 162 | // Safety: We know this unwrap will not panic because 163 | // we verified that leaves.len() > 0 (inferred 164 | // because branches.len() != -1) 165 | out.leaves.push(leaves.next().unwrap()); 166 | 167 | let kind = kind.into(); 168 | for (op, expr) in branches.zip(leaves) { 169 | if kind == op { 170 | let prev = out.leaves.pop().unwrap(); 171 | out.leaves.push(join(prev, op, expr)); 172 | } else { 173 | out.leaves.push(expr); 174 | out.branches.push(op); 175 | } 176 | } 177 | 178 | Ok((input, out)) 179 | } 180 | } 181 | 182 | enum ParenOrLeaf { 183 | Paren, 184 | Leaf(T), 185 | } 186 | 187 | fn open_paren_or_leaf(leaf: F) -> impl FnMut(I) -> IResult> 188 | where 189 | F: Fn(I) -> IResult, 190 | I: InputTakeAtPosition 191 | + Clone 192 | + InputTake 193 | + Compare 194 | + Compare<&'static str> 195 | + InputLength, 196 | { 197 | alt(( 198 | map(leaf, ParenOrLeaf::Leaf), 199 | map(tuple((tag("("), opt(whitespace))), |_| ParenOrLeaf::Paren), 200 | )) 201 | } 202 | 203 | fn close_paren_or_leaf(leaf: F) -> impl FnMut(I) -> IResult> 204 | where 205 | F: Fn(I) -> IResult, 206 | I: InputTakeAtPosition 207 | + Clone 208 | + InputTake 209 | + Compare 210 | + Compare<&'static str> 211 | + InputLength, 212 | { 213 | alt(( 214 | map(leaf, ParenOrLeaf::Leaf), 215 | map(tuple((opt(whitespace), tag(")"))), |_| ParenOrLeaf::Paren), 216 | )) 217 | } 218 | 219 | fn parse_tree_with_parens( 220 | mut input: &str, 221 | leaf: impl Fn(&str) -> IResult<&str, Leaf>, 222 | branch: impl Fn(&str) -> IResult<&str, Branch> + Clone, 223 | try_collapse: impl Fn(&str, FlatTree) -> IResult<&str, Leaf>, 224 | ) -> IResult<&str, Leaf> { 225 | profile_fn!(parse_tree_with_parens); 226 | 227 | let mut leaf = open_paren_or_leaf(leaf); 228 | let mut branch_paren = opt(close_paren_or_leaf(branch.clone())); 229 | let mut branch = opt(map(branch, ParenOrLeaf::Leaf)); 230 | 231 | let mut queue = vec![FlatTree::::new()]; 232 | loop { 233 | let queue_len = queue.len(); 234 | let top = queue.last_mut().unwrap(); 235 | 236 | // If a leaf is required, parse that 237 | if top.leaf_required() { 238 | let (i, atom) = leaf(input)?; 239 | input = i; 240 | match atom { 241 | // Increase nesting 242 | ParenOrLeaf::Paren => queue.push(FlatTree::new()), 243 | ParenOrLeaf::Leaf(leaf) => top.leaves.push(leaf), 244 | } 245 | // If a leaf is not required, then we can either close the sub-expr with ), 246 | // or increase it with an operator. 247 | } else { 248 | let (i, op) = { 249 | // This is tricky here. Ensure we don't decrease nesting 250 | // that we didn't create, because it could be a part of an outer parser. 251 | // Panic safety: See also c5cd18c0-0314-43d8-9daf-b80bbd07e802 252 | if queue_len > 1 { 253 | branch_paren(input)? 254 | } else { 255 | branch(input)? 256 | } 257 | }; 258 | input = i; 259 | match op { 260 | None => break, 261 | Some(ParenOrLeaf::Paren) => { 262 | // Decrease nesting 263 | // Panic safety: See also c5cd18c0-0314-43d8-9daf-b80bbd07e802 264 | let top = queue.pop().unwrap(); 265 | let (i, op) = try_collapse(input, top)?; 266 | input = i; 267 | // Panic safety: See also c5cd18c0-0314-43d8-9daf-b80bbd07e802 268 | queue.last_mut().unwrap().leaves.push(op); 269 | } 270 | Some(ParenOrLeaf::Leaf(op)) => { 271 | top.branches.push(op); 272 | } 273 | } 274 | } 275 | } 276 | 277 | match queue.into_iter().exactly_one() { 278 | Ok(item) => try_collapse(input, item), 279 | Err(_) => ErrAtom::err(input, ExpectationError::TODO)?, 280 | } 281 | } 282 | 283 | fn condition(input: &str) -> IResult<&str, Condition> { 284 | profile_fn!(condition); 285 | 286 | fn try_collapse( 287 | input: &str, 288 | tree: FlatTree, 289 | ) -> IResult<&str, Condition> { 290 | fn join(lhs: Condition, op: AnyBooleanOp, rhs: Condition) -> Condition { 291 | Condition::Boolean(Box::new(BinaryExpression::new(lhs, op, rhs))) 292 | } 293 | let (input, tree) = tree.collapse(input, And, join)?; 294 | let (input, mut tree) = tree.collapse(input, Or, join)?; 295 | assert!(tree.leaves.len() == 1); 296 | assert!(tree.branches.is_empty()); 297 | 298 | Ok((input, tree.leaves.pop().unwrap())) 299 | } 300 | 301 | fn condition_atom(input: &str) -> IResult<&str, Condition> { 302 | alt(( 303 | map(comparison, Condition::Comparison), 304 | map(variable, Condition::Variable), 305 | map(const_bool, Condition::Const), 306 | ))(input) 307 | } 308 | 309 | fn condition_op(input: &str) -> IResult<&str, AnyBooleanOp> { 310 | surrounded_by(whitespace, any_boolean_operator)(input) 311 | } 312 | 313 | parse_tree_with_parens(input, condition_atom, condition_op, try_collapse) 314 | } 315 | 316 | fn comparison(input: &str) -> IResult<&str, BinaryExpression> { 317 | profile_fn!(comparison); 318 | 319 | with_context(ErrorContext::Comparison, |input: &str| { 320 | let (input, lhs) = linear_expression(input)?; 321 | let (input, op) = surrounded_by( 322 | opt(whitespace), 323 | alt(( 324 | |input| binary_operator(input, "==", Eq), 325 | |input| binary_operator(input, "!=", Ne), 326 | |input| binary_operator(input, ">=", Ge), 327 | |input| binary_operator(input, "<=", Le), 328 | |input| binary_operator(input, ">", Gt), 329 | |input| binary_operator(input, "<", Lt), 330 | )), 331 | )(input)?; 332 | let (input, rhs) = linear_expression(input)?; 333 | 334 | Ok((input, BinaryExpression::new(lhs, op, rhs))) 335 | })(input) 336 | } 337 | 338 | fn identifier(input: &str) -> IResult<&str, &str> { 339 | profile_fn!(identifier); 340 | 341 | with_context( 342 | ErrorContext::Identifier, 343 | recognize(tuple(( 344 | alt((alpha1, tag("_"))), 345 | many0(alt((alphanumeric1, tag("_")))), 346 | ))), 347 | )(input) 348 | } 349 | 350 | fn variable(input: &str) -> IResult<&str, Variable> { 351 | profile_fn!(variable); 352 | 353 | with_context( 354 | ErrorContext::Variable, 355 | preceded(tag("$"), fail_fast(map(identifier, Variable::new))), 356 | )(input) 357 | } 358 | 359 | fn surrounded_by, F, G>( 360 | mut outer: F, 361 | mut inner: G, 362 | ) -> impl FnMut(I) -> IResult 363 | where 364 | F: FnMut(I) -> IResult, 365 | G: FnMut(I) -> IResult, 366 | { 367 | move |input: I| { 368 | profile_fn!(surrounded_by); 369 | 370 | let (input, _) = outer(input)?; 371 | let (input, result) = inner(input)?; 372 | let (input, _) = outer(input)?; 373 | Ok((input, result)) 374 | } 375 | } 376 | 377 | pub fn real(input: &str) -> IResult<&str, BigFraction> { 378 | with_context(ErrorContext::RealNumber, |input: &str| { 379 | profile_fn!(real); 380 | 381 | let (input, neg) = opt(tag("-"))(input)?; 382 | let (input, numerator) = digit1(input)?; 383 | let (input, denom) = opt(preceded(tag("."), digit1))(input)?; 384 | 385 | let numerator: BigUint = numerator.parse().unwrap(); 386 | let one = BigUint::from(1u32); 387 | let mut result = if neg.is_some() { 388 | BigFraction::new_neg(numerator, one) 389 | } else { 390 | BigFraction::new(numerator, one) 391 | }; 392 | 393 | if let Some(denom) = denom { 394 | let ten = BigUint::from(10u32); 395 | let div = ten.pow(denom.len()); 396 | let denom: BigUint = denom.parse().unwrap(); 397 | let add = BigFraction::new(denom, div); 398 | result += add; 399 | } 400 | 401 | Ok((input, result)) 402 | })(input) 403 | } 404 | 405 | fn any_boolean_operator(input: &str) -> IResult<&str, AnyBooleanOp> { 406 | profile_fn!(any_boolean_operator); 407 | 408 | alt(( 409 | |input| binary_operator(input, "||", Or), 410 | |input| binary_operator(input, "&&", And), 411 | ))(input) 412 | } 413 | 414 | fn linear_expression(input: &str) -> IResult<&str, LinearExpression> { 415 | profile_fn!(linear_expression); 416 | 417 | fn try_collapse( 418 | input: &str, 419 | tree: FlatTree, 420 | ) -> IResult<&str, LinearExpression> { 421 | fn join( 422 | lhs: LinearExpression, 423 | op: AnyLinearOperator, 424 | rhs: LinearExpression, 425 | ) -> LinearExpression { 426 | match (lhs, rhs) { 427 | // TODO: (Performance) Do not do constant propagation in 428 | // the parsing pass, but instead after global substitution 429 | // Constant propagation 430 | (LinearExpression::Error(()), _) => LinearExpression::Error(()), 431 | (_, LinearExpression::Error(())) => LinearExpression::Error(()), 432 | (LinearExpression::Const(lhs), LinearExpression::Const(rhs)) => { 433 | match op.exec(lhs.value, rhs.value) { 434 | Ok(value) => LinearExpression::Const(Const::new(value)), 435 | Err(e) => LinearExpression::Error(e), 436 | } 437 | } 438 | (lhs, rhs) => LinearExpression::BinaryExpression(Box::new(BinaryExpression::new( 439 | lhs, op, rhs, 440 | ))), 441 | } 442 | } 443 | let (input, tree) = tree.collapse(input, Mul, join)?; 444 | let (input, tree) = tree.collapse(input, Div, join)?; 445 | let (input, tree) = tree.collapse(input, Add, join)?; 446 | let (input, mut tree) = tree.collapse(input, Sub, join)?; 447 | assert!(tree.leaves.len() == 1); 448 | assert!(tree.branches.is_empty()); 449 | 450 | Ok((input, tree.leaves.pop().unwrap())) 451 | } 452 | 453 | fn linear_expression_leaf(input: &str) -> IResult<&str, LinearExpression> { 454 | alt(( 455 | map(real, |r| LinearExpression::Const(Const::new(r))), 456 | map(variable, LinearExpression::Variable), 457 | ))(input) 458 | } 459 | 460 | fn any_linear_binary_operator(input: &str) -> IResult<&str, AnyLinearOperator> { 461 | surrounded_by( 462 | whitespace, 463 | alt(( 464 | |input| binary_operator(input, "+", Add), 465 | |input| binary_operator(input, "-", Sub), 466 | |input| binary_operator(input, "*", Mul), 467 | |input| binary_operator(input, "/", Div), 468 | )), 469 | )(input) 470 | } 471 | 472 | with_context(ErrorContext::RationalExpression, |input| { 473 | parse_tree_with_parens( 474 | input, 475 | linear_expression_leaf, 476 | any_linear_binary_operator, 477 | try_collapse, 478 | ) 479 | })(input) 480 | } 481 | 482 | fn binary_operator<'a, O>( 483 | input: &'a str, 484 | tag_: &'static str, 485 | op: impl Into + Copy, 486 | ) -> IResult<&'a str, O> { 487 | profile_fn!(binary_operator); 488 | 489 | map(tag(tag_), |_| op.into())(input) 490 | } 491 | 492 | fn match_(input: &str) -> IResult<&str, Match> { 493 | profile_fn!(match_); 494 | 495 | with_context( 496 | ErrorContext::Match, 497 | alt(( 498 | map(tag("default"), |_| Match::Default), 499 | map(graphql_query, Match::GraphQL), 500 | )), 501 | )(input) 502 | } 503 | 504 | fn predicate(input: &str) -> IResult<&str, Predicate> { 505 | profile_fn!(predicate); 506 | 507 | with_context(ErrorContext::Predicate, |input| { 508 | // Whitespace is optional here because graphql_query is greedy and takes it. 509 | // Shouldn't be a problem though for ambiguity since `default=> 1` or `query { a }=> 1` 510 | // both seem unambiguous and readable. 511 | let (input, match_) = terminated(match_, opt(whitespace))(input)?; 512 | 513 | // TODO: The use of opt here makes error messages less informative. 514 | // The when_clause call has all the information we need to say something like 515 | // expected "when". So that if we have a statement like: 516 | // query { a } X 517 | // It could say expected "when" or "=>" at ^ 518 | // where the 'X' character is. But since the optional when_clause always 519 | // succeeds in parsing that error information is lost and we currently 520 | // only get expected "=>" 521 | // 522 | // This problem seems general to all uses of opt. It could be fixed 523 | // by moving the expectation for the "=>" into the predicate instead 524 | // of the statement where it is currently and using alt instead of opt. 525 | // Eg: Instead of tuple(pre, opt(post)) use alt((pre, tuple((pre, post)))) 526 | // But this quickly runs into combinatorics problems as there can be multiple 527 | // things which are opt (here there is optional whitespace for example). 528 | // This solution also moves code into places which do not match the mental model 529 | // in that the "=>" moves from the statement to the predicate (this also would 530 | // mess up context in error messages) 531 | // 532 | // Another possibility would be to start accumulating error information in I 533 | // such that if parsing proceeds 'potential' error information gets dropped. 534 | // In this case we return Ok(&str) - which is just the unparsed input. Instead, 535 | // we would return something like Ok(&str, Option) which would 536 | // allow for combining later if the context fails. I need to think through the details, 537 | // and this would mean yet another refactoring of the error code that I don't 538 | // have time for at this moment. 539 | // 540 | // Addendum: Thinking about this some more, this can extend beyond just opt... 541 | // it generalizes to whatever the most recently successfully parsed items were. 542 | // Since opt is always successful, it falls into this general category of a 543 | // recently successfully parsed item. Each such item may have continuations. 544 | // Consider for example the statement "default => $_a²;" The problem is the attempt 545 | // to use the '²' in an identifier, but the current model identifies the problem as 546 | // expecting a ';' at the end of the statement because from the point of view of the 547 | // parser the identifier parsed successfully! As a part of the set of things that finished 548 | // parsing on that character, we could interpret errors also as failed attempts at 549 | // extending items that were just parsed. 550 | let (input, when_clause) = opt(terminated(when_clause, whitespace))(input)?; 551 | 552 | let predicate = Predicate { 553 | match_, 554 | when_clause, 555 | }; 556 | Ok((input, predicate)) 557 | })(input) 558 | } 559 | 560 | fn statement(input: &str) -> IResult<&str, Statement> { 561 | profile_fn!(statement); 562 | 563 | with_context(ErrorContext::Statement, |input| { 564 | // Start by dropping whitespace and comments. 565 | // A comment is allowed to preceed a statement. 566 | // This used to be handled in the GraphQL parser, 567 | // but that made it impossible to comment default matches. 568 | // Handling it here also allows us to do a query check in the 569 | // graphql parser which enables better error handling. 570 | let (input, _) = many0(alt((whitespace, recognize(pair(char('#'), is_not("\n"))))))(input)?; 571 | let (input, predicate) = predicate(input)?; 572 | let (input, _) = tuple((tag("=>"), whitespace))(input)?; 573 | let (input, cost_expr) = linear_expression(input)?; 574 | let (input, _) = tag(";")(input)?; 575 | let (input, _) = opt(whitespace)(input)?; 576 | 577 | let statement = Statement { 578 | predicate, 579 | cost_expr, 580 | }; 581 | Ok((input, statement)) 582 | })(input) 583 | } 584 | 585 | fn document(mut input: &str) -> Result, ErrorAggregator<&str>> { 586 | profile_fn!(document); 587 | 588 | // This function breaks the pattern of using IResult because we assume 589 | // that you need to parse to the end, which means you need to retain the 590 | // final error. (Otherwise you need to try to parse a statement on the 591 | // remaining error again to retrieve it.) 592 | let mut statements = Vec::new(); 593 | while !input.is_empty() { 594 | match statement(input) { 595 | Ok((remaining, statement)) => { 596 | statements.push(statement); 597 | input = remaining 598 | } 599 | Err(e) => match e { 600 | NomErr::Error(e) => return Err(e), 601 | NomErr::Failure(e) => return Err(e), 602 | NomErr::Incomplete(_) => unreachable!("Incomplete input"), 603 | }, 604 | } 605 | } 606 | Ok(Document { statements }) 607 | } 608 | 609 | pub fn parse_document(input: &str) -> Result> { 610 | profile_fn!(parse_document); 611 | 612 | // Mapping from ErrorAggregator to AgoraParseError, 613 | // which requires the 'original' input. 614 | match document(input) { 615 | Ok(doc) => Ok(doc), 616 | Err(e) => Err(AgoraParseError::new(input, e)), 617 | } 618 | } 619 | 620 | #[cfg(test)] 621 | mod tests { 622 | use super::*; 623 | use crate::expressions::expr_stack::*; 624 | use fraction::BigFraction; 625 | use num_bigint::BigInt; 626 | 627 | fn assert_expr(s: &str, expect: impl Into, v: impl Into) { 628 | let v = v.into(); 629 | let (rest, expr) = linear_expression(s).unwrap(); 630 | assert!(rest.is_empty()); 631 | let mut stack = LinearStack::new(&v); 632 | let result = stack.execute(&expr); 633 | assert_eq!(Ok(expect.into()), result) 634 | } 635 | 636 | fn assert_clause(s: &str, expect: bool, v: impl Into) { 637 | let v = v.into(); 638 | let (rest, clause) = when_clause(s).unwrap(); 639 | assert!(rest.is_empty()); 640 | let stack = LinearStack::new(&v); 641 | let mut stack = CondStack::new(stack); 642 | let result = stack.execute(&clause.condition); 643 | assert_eq!(Ok(expect), result); 644 | } 645 | 646 | #[test] 647 | fn binary_expr() { 648 | assert_expr("1 + 2", 3, ()); 649 | } 650 | 651 | #[test] 652 | fn operator_precedence() { 653 | assert_expr("1 + 10 * 2", 21, ()); 654 | assert_expr("10 * 2 + 1", 21, ()); 655 | } 656 | 657 | #[test] 658 | fn parenthesis() { 659 | assert_expr("(1 + 10) * 2", 22, ()); 660 | } 661 | 662 | #[test] 663 | fn left_to_right_after_precedence() { 664 | assert_expr("1 - 10 - 2", -11, ()); 665 | } 666 | 667 | #[test] 668 | fn when_clauses() { 669 | assert_clause("when 1 > 2", false, ()); 670 | assert_clause("when $a == $b", true, (("a", 2), ("b", 2))); 671 | assert!(when_clause("when .").is_err()); 672 | } 673 | 674 | #[test] 675 | fn boolean_precedence() { 676 | assert_clause("when true || 1 == 0 && false", true, ()); 677 | assert_clause("when 1 == 0 && 1 == 0 || $a", true, ("a", true)); 678 | 679 | assert_clause("when true || false && false", true, ()); 680 | assert_clause("when (true || false) && false", false, ()); 681 | 682 | assert_clause("when false && false || true", true, ()); 683 | assert_clause("when false && (false || true)", false, ()); 684 | } 685 | 686 | #[test] 687 | fn when_parens() { 688 | assert_clause("when ($a != $a)", false, ("a", 1)); 689 | assert_clause("when (1 == 0 && 1 == 1) || 1 == 1", true, ()); 690 | } 691 | 692 | #[test] 693 | fn statements() { 694 | assert!(statement("query { users(skip: $skip) { tokens } } when 5 == 5 => 1;").is_ok()) 695 | } 696 | 697 | // TODO: (Idea) It would be nice sometimes to optionally capture 698 | // variables and have defaults. This applies to $first in particular, 699 | // which has an implicit 100 700 | 701 | // TODO: (Idea) It would be nice to allow rules to combine, somehow. 702 | // One way to do this would be to use fragments in the cost model, 703 | // or named queries/fragments that could call each other or something. 704 | // Consider this usage: 705 | // query { tokens { id } } => + 10; 706 | // query { tokens { name } } => + 100; 707 | // query { tokens(first: $first) } => * $first; 708 | // query { tokens(skip: $skip) } => * $skip; 709 | // Where each match that succeeds contributes to the cost. 710 | // This would remove the redundancy that exists now. 711 | // But, a * might apply to too much - all previous lines 712 | // may include lines from unrelated queries. 713 | 714 | #[test] 715 | fn doc() { 716 | let file = " 717 | query { users(skip: $skip) { tokens } } when $skip > 1000 => 100 + $skip * 10; 718 | # Bob is evil 719 | query { users(name: \"Bob\") { tokens } } => 999999; 720 | "; 721 | 722 | //println!("{}", document(file).unwrap_err()); 723 | assert!(document(file).is_ok()) 724 | } 725 | 726 | #[test] 727 | fn const_folding() { 728 | let result = linear_expression("1 + 2 * (15 / 10)"); 729 | let expect = LinearExpression::Const(Const::new(BigFraction::from(BigInt::from(4)))); 730 | assert_eq!(result.expect("Should have compiled"), ("", expect)); 731 | } 732 | 733 | #[test] 734 | fn condition_does_not_stack_overflow() { 735 | const DEPTH: usize = 1000; 736 | let text = "(true && ".repeat(DEPTH) + "$a" + ")".repeat(DEPTH).as_str(); 737 | let (text, expr) = condition(&text).unwrap(); 738 | assert_eq!(text.len(), 0); 739 | let captures = ("a", false).into(); 740 | let linear_stack = LinearStack::new(&captures); 741 | let mut cond_stack = CondStack::new(linear_stack); 742 | let result = cond_stack.execute(&expr); 743 | assert_eq!(result, Ok(false)); 744 | } 745 | 746 | #[test] 747 | fn expr_does_not_stack_overflow() { 748 | const DEPTH: usize = 1000; 749 | let text = "(1 + ".repeat(DEPTH) + "$a" + ")".repeat(DEPTH).as_str(); 750 | let (text, expr) = linear_expression(&text).unwrap(); 751 | assert_eq!(text.len(), 0); 752 | let captures = ("a", 2).into(); 753 | let mut stack = LinearStack::new(&captures); 754 | let result = stack.execute(&expr); 755 | 756 | assert_eq!(result, Ok((DEPTH + 2).into())); 757 | } 758 | 759 | #[test] 760 | fn expressions_in_conditions_do_not_stack_overflow() { 761 | const DEPTH: usize = 500; 762 | let expr = "(1 + ".repeat(DEPTH) + "$a" + ")".repeat(DEPTH).as_str(); 763 | let cond = ("(".to_owned() + expr.as_str() + " == " + expr.as_str() + " && ") 764 | .as_str() 765 | .repeat(DEPTH) 766 | + "$b" 767 | + ")".repeat(DEPTH).as_str(); 768 | 769 | let (remain, expr) = condition(&cond).unwrap(); 770 | assert!(remain.is_empty()); 771 | let captures = (("a", 2), ("b", (DEPTH + 2) as i32)).into(); 772 | let linear_stack = LinearStack::new(&captures); 773 | let mut cond_stack = CondStack::new(linear_stack); 774 | let result = cond_stack.execute(&expr); 775 | 776 | assert_eq!(result, Ok(true)); 777 | } 778 | 779 | #[test] 780 | fn paren_condition() { 781 | let text = "when (1 != 1)"; 782 | assert_clause(text, false, ()); 783 | } 784 | 785 | #[test] 786 | fn paren_expr() { 787 | let text = "(1 + 1)"; 788 | assert_expr(text, 2, ()); 789 | } 790 | 791 | #[test] 792 | fn ambiguous_paren() { 793 | // Is the paren a part of the linear expr or the conditional expr? 794 | let text = "when 0 > (1 + 2)"; 795 | assert_clause(text, false, ()); 796 | } 797 | 798 | #[test] 799 | fn ambiguous_paren_harder() { 800 | // Is the paren a part of the linear expr or the conditional expr? 801 | let text = "when (((1 + 1) > 2) || (0 > (1 + 2)))"; 802 | assert_clause(text, false, ()); 803 | } 804 | 805 | #[test] 806 | fn using_fract() { 807 | let text = "when 4 * 1.25 == $five"; 808 | assert_clause(text, true, ("five", "5.0".to_owned())); 809 | } 810 | } 811 | -------------------------------------------------------------------------------- /lang/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use firestorm::{profile_fn, profile_method, profile_section}; 2 | -------------------------------------------------------------------------------- /lang/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::*; 3 | use num_bigint::BigUint; 4 | 5 | trait IntoTestResult { 6 | fn into(self) -> Result; 7 | } 8 | 9 | impl IntoTestResult for u64 { 10 | fn into(self) -> Result { 11 | Ok(BigUint::from(self) * wei_to_grt()) 12 | } 13 | } 14 | 15 | impl IntoTestResult for BigUint { 16 | fn into(self) -> Result { 17 | Ok(self) 18 | } 19 | } 20 | 21 | impl IntoTestResult for CostError { 22 | fn into(self) -> Result { 23 | Err(self) 24 | } 25 | } 26 | 27 | trait IntoModel { 28 | fn into(self) -> CostModel; 29 | } 30 | 31 | impl IntoModel for &str { 32 | fn into(self) -> CostModel { 33 | CostModel::compile(self, "").unwrap() 34 | } 35 | } 36 | 37 | impl IntoModel for (&str, &str) { 38 | fn into(self) -> CostModel { 39 | CostModel::compile(self.0, self.1).unwrap() 40 | } 41 | } 42 | 43 | trait IntoQuery { 44 | fn into(self) -> (&'static str, &'static str); 45 | } 46 | 47 | impl IntoQuery for &'static str { 48 | fn into(self) -> (&'static str, &'static str) { 49 | (self, "") 50 | } 51 | } 52 | 53 | impl IntoQuery for (&'static str, &'static str) { 54 | fn into(self) -> Self { 55 | self 56 | } 57 | } 58 | 59 | fn test(model: impl IntoModel, query: impl IntoQuery, result: impl IntoTestResult) { 60 | profile_fn!(test); 61 | 62 | let model = model.into(); 63 | let (query, variables) = query.into(); 64 | let cost = model.cost(query, variables); 65 | assert_eq!(result.into(), cost); 66 | } 67 | 68 | #[test] 69 | fn query_match() { 70 | let model = " 71 | query { a } when true => 11; 72 | query { b } when false => 12; 73 | query { b } when 1 == 1 => 2 + 2; 74 | # Never used, because the above matches the same conditions. 75 | query { b } when true => 7; 76 | "; 77 | test(model, "query { a }", 11); 78 | test(model, "query { b }", 4); 79 | } 80 | 81 | #[test] 82 | fn test_contains_field() { 83 | let model = " 84 | query { a } when true => 11; 85 | query { b } when false => 12; 86 | query { b } when 1 == 1 => 2 + 2; 87 | # Never used, because the above matches the same conditions. 88 | query { sql } => 1; 89 | "; 90 | let model = IntoModel::into(model); 91 | assert!(model.contains_statement_field("sql")); 92 | } 93 | 94 | #[test] 95 | fn test_not_contains_field() { 96 | let model = " 97 | query { a } when true => 11; 98 | query { b } when false => 12; 99 | query { b } when 1 == 1 => 2 + 2; 100 | # Never used, because the above matches the same conditions. 101 | query { b } when true => 7; 102 | "; 103 | let model = IntoModel::into(model); 104 | assert!(!model.contains_statement_field("sql")); 105 | } 106 | 107 | #[test] 108 | fn field_args() { 109 | let model = " 110 | query { a(skip: 10) } => 15; 111 | query { a(skip: $skip) } when $skip > 10 => $skip * (2 + 0); 112 | query { a } => 55; 113 | # This is a comment 114 | query { b(skip: $skip, bob: $bob) } when $skip == $bob && true => $bob; # This is also a comment 115 | query { b } => 99; 116 | "; 117 | test(model, "query { a(skip: 10) }", 15); 118 | test(model, "query { a(skip: 11) }", 22); 119 | test(model, "query { a(skip: 9) }", 55); 120 | test(model, "query { a }", 55); 121 | test(model, "query { b }", 99); 122 | test(model, "query { b(skip: 9) }", 99); 123 | test(model, "query { b(skip: 9, bob: 10) }", 99); 124 | test(model, "query { b(skip: 10, bob: 10) }", 10); 125 | test(model, "query { b(skip: 10, bob: 10), a }", 65); 126 | } 127 | 128 | #[test] 129 | fn sums_top_levels() { 130 | let model = " 131 | query { a(skip: $skip) } => $skip; 132 | query { b(bob: $bob) } => 10; 133 | query { c } => 9; 134 | query { a } => 99; 135 | query { d } => 1; 136 | "; 137 | test(model, "query { a(skip: 10), b }", CostError::QueryNotCosted); 138 | test(model, "query { a(skip: 10), b(bob: 5) }", 20); 139 | } 140 | 141 | #[test] 142 | fn var_substitutions() { 143 | let query = "query pairs($skip: Int!) { pairs(skip: $skip) { id } }"; 144 | let variables = "{\"skip\":1}"; 145 | let model = "query { pairs(skip: $k) } => $k;"; 146 | 147 | test(model, (query, variables), 1); 148 | } 149 | 150 | #[test] 151 | fn default() { 152 | let query = "query { nonsense }"; 153 | let model = "query { abc } => 2; default => 10;"; 154 | test(model, query, 10); 155 | } 156 | 157 | #[test] 158 | fn matching_object() { 159 | let model = " 160 | query { a(where: { age_gt: 18 }) } => 1; 161 | query { a(where: $where) } => 2; 162 | default => 3; 163 | "; 164 | 165 | test(model, "query { a(where: { age_gt: 18, non: 1 }) }", 1); 166 | test(model, "query { a(where: { age_gt: 21 }) }", 2); 167 | test(model, "query { a }", 3); 168 | } 169 | 170 | #[test] 171 | fn matching_list() { 172 | let model = " 173 | query { a(val_in: [1, 2]) } => 1; 174 | query { a(val_in: $in) } => 2; 175 | default => 3; 176 | "; 177 | 178 | test(model, "query { a(val_in: [1, 2]) }", 1); 179 | test(model, "query { a(val_in: [2, 3]) }", 2); 180 | test(model, "query { a }", 3); 181 | } 182 | 183 | #[test] 184 | fn fragments() { 185 | let model = " 186 | query { pairs(skip: $skip) { id reserveUSD } } => 1; 187 | query { pairs(skip: $skip) { id } } => 2; 188 | default => 3; 189 | "; 190 | 191 | let query_1 = " 192 | { 193 | pairs(skip: 1) { ...fields } 194 | } 195 | fragment fields on Name { 196 | id, reserveUSD 197 | } 198 | "; 199 | 200 | let query_2 = " 201 | { 202 | pairs(skip: 1) { ...fields } 203 | } 204 | fragment fields on Name { 205 | id 206 | } 207 | "; 208 | 209 | let query_3 = " 210 | { 211 | pairs(skip: 1) { ...fields } 212 | } 213 | fragment fields on Name { 214 | reserveUSD 215 | } 216 | "; 217 | 218 | test(model, query_1, 1); 219 | test(model, query_2, 2); 220 | test(model, query_3, 3); 221 | } 222 | 223 | #[test] 224 | fn invalid_query() { 225 | test("default => 1;", "blah", CostError::FailedToParseQuery); 226 | } 227 | 228 | #[test] 229 | fn invalid_variables() { 230 | test( 231 | "default => 1;", 232 | ("query { a }", "blah"), 233 | CostError::FailedToParseVariables, 234 | ); 235 | } 236 | 237 | #[test] 238 | fn invalid_model() { 239 | for case in &[ 240 | // Missing semicolon between 2 statements 241 | "query { a } => 1 query { b } => 2;", 242 | // Garbage data after valid statement 243 | "query { a } => 1; garbage", 244 | ] { 245 | assert!(CostModel::compile(*case, "").is_err()); 246 | } 247 | } 248 | 249 | #[test] 250 | fn nested_query() { 251 | let model = " 252 | query { users { id tokens { id } } } => 1; 253 | query { users { id } } => 2; 254 | default => 3; 255 | "; 256 | test(model, "query { users { id tokens { id } } }", 1); 257 | test(model, "query { users { id tokens { id and } } }", 1); 258 | test(model, "query { users { id tokens { and } } }", 2); 259 | test( 260 | model, 261 | "query { we { are { the { knights { who { say { ni } } } } } } }", 262 | 3, 263 | ); 264 | } 265 | 266 | #[test] 267 | fn query_not_costed() { 268 | test("query { a } => 2;", "{ b }", CostError::QueryNotCosted); 269 | } 270 | 271 | #[test] 272 | fn div_by_zero_does_not_panic() { 273 | test("default => 1 / 0;", "{ a }", CostError::CostModelFail); 274 | } 275 | 276 | #[test] 277 | fn lossless_math() { 278 | // If the cost model were implemented by truncating at each operation, 279 | // the result would be 0. 280 | test("default => 100 * (1 / 2);", "{ a }", 50); 281 | 282 | // Underflows (below 0) temporarily 283 | test( 284 | "default => ((-1 / 2) * 5) + 5;", 285 | "{ a }", 286 | BigUint::from(2500000000000000000u64), 287 | ); 288 | } 289 | 290 | #[test] 291 | fn overflow_clamp() { 292 | // Underflow 293 | test("default => 100 - 200;", "{ a }", 0); 294 | test("default => 115792089237316195423570985008687907853269984665640564039457584007913129639931 + 10;", "{ a }", MAX_COST.clone()); 295 | } 296 | 297 | #[test] 298 | fn decimals() { 299 | test( 300 | "query { a(d: $d) } => 1.5 * 2.99 * 3.8 * $d;", 301 | "{ a(d: \"20.5\") }", 302 | BigUint::from(349381500000000000000u128), 303 | ); 304 | } 305 | 306 | #[test] 307 | fn infinity_cancel_is_err() { 308 | test( 309 | "default => (1 / 0) + (-1 / 0);", 310 | "{ a }", 311 | CostError::CostModelFail, 312 | ); 313 | } 314 | 315 | #[test] 316 | fn arg_only() { 317 | test( 318 | "query { tokens(first: $first) } => 1;", 319 | "{ tokens(first: 100) { id } }", 320 | 1, 321 | ) 322 | } 323 | 324 | /// If we try to capture to the same name in 2 positions, this should fail to compile 325 | #[test] 326 | fn capture_twice_fails_to_compile() { 327 | let model = "query { tokens(first: $skip, skip: $skip) } => 1;"; 328 | assert!(CostModel::compile(model, "").is_err()); 329 | } 330 | 331 | #[test] 332 | fn globals_in_cost() { 333 | let model = "default => $GLOBAL;"; 334 | let model = (model, "{ \"GLOBAL\": 15 }"); 335 | test(model, "{ a }", 15); 336 | } 337 | 338 | #[test] 339 | fn globals_in_where() { 340 | let model = "query { a } when $COND => 1; default => 2;"; 341 | test((model, "{ \"COND\": true }"), "{ a }", 1); 342 | test((model, "{ \"COND\": false }"), "{ a }", 2); 343 | } 344 | 345 | #[test] 346 | fn default_with_where() { 347 | let model = "default when $COND => 1; default => 2;"; 348 | test((model, "{ \"COND\": true }"), "{ a }", 1); 349 | test((model, "{ \"COND\": false }"), "{ a }", 2); 350 | 351 | let model = "default when true => 1; default => 2;"; 352 | test(model, "{ a }", 1); 353 | } 354 | 355 | /// When there is a capture, that will be preferred over a global 356 | #[test] 357 | fn capture_shadows_global() { 358 | let model = "query { a(first: $GLOBAL) } => $GLOBAL;"; 359 | let model = (model, "{ \"GLOBAL\": 15 }"); 360 | test(model, "{ a(first: 30) }", 30); 361 | } 362 | 363 | #[test] 364 | fn ban() { 365 | let model = "default => $BAN;"; 366 | test(model, "{ a }", CostError::CostModelFail); 367 | } 368 | 369 | mod global_when_to_bool { 370 | use super::*; 371 | 372 | const MODEL: &str = "query { a } when $A => 1; default => 2;"; 373 | 374 | #[test] 375 | fn coerce_nonempty_str() { 376 | test((MODEL, "{ \"A\": \"A\" }"), "{ a }", 1); 377 | } 378 | 379 | #[test] 380 | fn coerce_true() { 381 | test((MODEL, "{ \"A\": true }"), "{ a }", 1); 382 | } 383 | 384 | #[test] 385 | fn coerce_nonzero_int() { 386 | test((MODEL, "{ \"A\": 1 }"), "{ a }", 1); 387 | } 388 | 389 | #[test] 390 | fn coerce_empty_str() { 391 | test((MODEL, "{ \"A\": \"\" }"), "{ a }", 2); 392 | } 393 | 394 | #[test] 395 | fn coerce_false() { 396 | test((MODEL, "{ \"A\": false }"), "{ a }", 2); 397 | } 398 | 399 | #[test] 400 | fn coerce_zero() { 401 | test((MODEL, "{ \"A\": 0 }"), "{ a }", 2); 402 | } 403 | } 404 | 405 | mod standard_directives { 406 | use super::*; 407 | 408 | const MODEL: &str = " 409 | query { a } => 1; 410 | query { b } => 10; 411 | query { c { c1 c2 } } => 100; 412 | query { c { c1 } } => 1000; 413 | "; 414 | 415 | #[test] 416 | fn include_true_root() { 417 | test( 418 | MODEL, 419 | ( 420 | "query Q($includeB: Boolean!) { a b @include(if: $includeB) }", 421 | "{\"includeB\": true}", 422 | ), 423 | 11, 424 | ); 425 | } 426 | 427 | #[test] 428 | fn include_true_nested() { 429 | test( 430 | MODEL, 431 | ( 432 | "query Q($includeC2: Boolean!) { c { c1 c2 @include(if: $includeC2) } }", 433 | "{\"includeC2\": true}", 434 | ), 435 | 100, 436 | ); 437 | } 438 | 439 | #[test] 440 | fn include_false_root() { 441 | test( 442 | MODEL, 443 | ( 444 | "query Q($includeB: Boolean!) { a b @include(if: $includeB) }", 445 | "{\"includeB\": false}", 446 | ), 447 | 1, 448 | ); 449 | } 450 | 451 | #[test] 452 | fn include_false_nested() { 453 | test( 454 | MODEL, 455 | ( 456 | "query Q($includeC2: Boolean!) { c { c1 c2 @include(if: $includeC2) } }", 457 | "{\"includeC2\": false}", 458 | ), 459 | 1000, 460 | ); 461 | } 462 | 463 | #[test] 464 | fn skip_true_root() { 465 | test( 466 | MODEL, 467 | ( 468 | "query Q($skipB: Boolean!) { a b @skip(if: $skipB) }", 469 | "{\"skipB\": true}", 470 | ), 471 | 1, 472 | ); 473 | } 474 | 475 | #[test] 476 | fn skip_false_root() { 477 | test( 478 | MODEL, 479 | ( 480 | "query Q($skipB: Boolean!) { a b @skip(if: $skipB) }", 481 | "{\"skipB\": false}", 482 | ), 483 | 11, 484 | ); 485 | } 486 | 487 | #[test] 488 | fn skip_false_nested() { 489 | test( 490 | MODEL, 491 | ( 492 | "query Q($skipC2: Boolean!) { c { c1 c2 @skip(if: $skipC2) } }", 493 | "{\"skipC2\": false}", 494 | ), 495 | 100, 496 | ); 497 | } 498 | 499 | #[test] 500 | fn skip_true_nested() { 501 | test( 502 | MODEL, 503 | ( 504 | "query Q($skipC2: Boolean!) { c { c1 c2 @skip(if: $skipC2) } }", 505 | "{\"skipC2\": true}", 506 | ), 507 | 1000, 508 | ); 509 | } 510 | } 511 | 512 | #[test] 513 | fn root_fragment() { 514 | let model = " 515 | query { a } => 1; 516 | query { b } => 10; 517 | query { c } => 100; 518 | query { d } => 1000; 519 | "; 520 | 521 | let query = " 522 | fragment f on Any { a b } 523 | query { ...f c } 524 | "; 525 | 526 | // Ensures that we didn't miss the "splitting" up of root fields 527 | // by viewing them through a fragment. If the fragment is treated 528 | // as just one root field, we expect to see 1 returned, since 529 | // the whole fragment will match as a superset of the first statement's 530 | // predicate: { a } 531 | test(model, query, 111); 532 | } 533 | 534 | mod inline_fragments { 535 | use super::*; 536 | 537 | const MODEL: &str = " 538 | query { a { ... on X { x } ... on Y { y } } } => 1; 539 | query { a { ... on Y { y } } } => 10; 540 | query { a } => 100; 541 | "; 542 | 543 | #[test] 544 | fn multiple_match() { 545 | test(MODEL, "{ a { ... on Y { y m } ... on X { x } } }", 1); 546 | } 547 | 548 | #[test] 549 | fn partial_match() { 550 | test(MODEL, "{ a { ... on Y { y m } } }", 10); 551 | } 552 | 553 | #[test] 554 | fn no_match() { 555 | test(MODEL, "{ a { b } }", 100); 556 | } 557 | } 558 | 559 | mod recursions { 560 | use super::*; 561 | 562 | #[test] 563 | fn substitute_globals() { 564 | let model = format!("default => {}$a{};", "(1 + ".repeat(5000), ")".repeat(5000)); 565 | 566 | test((model.as_str(), "{\"a\": 2}"), "{ a }", 5002); 567 | } 568 | } 569 | -------------------------------------------------------------------------------- /node-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cost-model-node-plugin" 3 | version = "0.1.0" 4 | authors = ["Zac Burns "] 5 | license = "MIT" 6 | build = "src/build.rs" 7 | edition = "2018" 8 | exclude = ["/lib"] 9 | 10 | [lib] 11 | name = "node_plugin" 12 | crate-type = ["cdylib"] 13 | 14 | [build-dependencies] 15 | neon-build = "0.10.0" 16 | 17 | [dependencies] 18 | neon = "0.10.0" 19 | cost-model = { path = "../lang" } 20 | -------------------------------------------------------------------------------- /node-plugin/README.md: -------------------------------------------------------------------------------- 1 | Node plugin for the Graph Protocol cost model -------------------------------------------------------------------------------- /node-plugin/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface CostModel { 2 | costAsync(query: string, variables?: string): Promise 3 | } 4 | 5 | export function compileAsync(code: string, globals: string): Promise 6 | -------------------------------------------------------------------------------- /node-plugin/lib/index.js: -------------------------------------------------------------------------------- 1 | var addon = require('../native') 2 | 3 | function promisify(f) { 4 | return new Promise((resolve, reject) => 5 | f((err, result) => { 6 | if (err) { 7 | reject(err) 8 | } else { 9 | resolve(result) 10 | } 11 | }) 12 | ) 13 | } 14 | 15 | class CostModel { 16 | constructor(native) { 17 | this._native = native 18 | } 19 | 20 | /** 21 | * query: A graphQL query string 22 | * variables: (Optional) a JSON string of variables to be substituted into the query 23 | */ 24 | async costAsync(query, variables) { 25 | let cost = await promisify((r) => this._native.cost(r, query, variables)) 26 | // See also e5d47bc3-9b14-490d-abec-90c286330a2d 27 | if (!cost) { 28 | throw new Error('Failed to cost query') 29 | } 30 | return cost 31 | } 32 | } 33 | 34 | /** 35 | * Compiles a cost model from text. 36 | * Throws if the cost model is invalid. 37 | * If this compiles successfully, you can call costAsync on the result. 38 | * 39 | * Performance Tip: Re-use the compiled cost model for many queries. 40 | * 41 | * code: A cost model as text. 42 | * globals: (Optional) a JSON object string of global variables for the cost model. 43 | */ 44 | async function compileAsync(code, globals) { 45 | let native = new addon.CostModel(code, globals) 46 | 47 | let result = await promisify(native.compile.bind(native)) 48 | // See also e5d47bc3-9b14-490d-abec-90c286330a2d 49 | if (!result) { 50 | throw new Error('Failed to compile cost model') 51 | } 52 | 53 | return new CostModel(native) 54 | } 55 | 56 | // TODO: Move this to a unit test framework 57 | /* 58 | async function test_one(code, query, variables) { 59 | let model; 60 | try { 61 | model = await compileAsync(code); 62 | } 63 | catch (err) { 64 | throw (err); 65 | } 66 | 67 | return await model.costAsync(query, variables); 68 | } 69 | 70 | // These tests caught various problems (including 3 conditions that caused the process to exit, and one segfault...) 71 | async function test() { 72 | async function throws(f, msg) { 73 | let fail = false; 74 | try { 75 | await f(); 76 | fail = true; 77 | } catch { 78 | 79 | } 80 | if (fail) { 81 | throw msg; 82 | } 83 | } 84 | // Invalid model 85 | await throws(() => test_one("query;", "query { a }"), "Costed invalid model"); 86 | 87 | // Invalid query 88 | await throws(() => test_one("query { a } => 10;", "fail"), "Costed invalid query"); 89 | 90 | // Ok 91 | let cost = await test_one("query { a } => 10;", "{ a }"); 92 | if (cost !== "10") { 93 | throw "Cost != 10"; 94 | } 95 | 96 | // No match 97 | await throws(() => test_one("query { a } => 10;", "{ b }"), "Costed unmatched query"); 98 | 99 | // With vars 100 | cost = await test_one("query { a(skip: 3) { b } } => 9;", "query Skip($skip: Int) { a(skip: $skip) { b } }", "{ \"skip\": 3 }"); 101 | if (cost !== "9") { 102 | throw "Cost != 9"; 103 | } 104 | 105 | console.log("SUCCESS"); 106 | } 107 | */ 108 | 109 | module.exports = { 110 | compileAsync, 111 | } 112 | -------------------------------------------------------------------------------- /node-plugin/lib/index.test.js: -------------------------------------------------------------------------------- 1 | const { compileAsync } = require('.') 2 | 3 | describe('Cost model', () => { 4 | test('Basic parsing', async () => { 5 | await expect(compileAsync('default => 1;', '{}')).resolves.toBeTruthy() 6 | }) 7 | 8 | test('Invalid model parsing fails', async () => { 9 | await expect(compileAsync('default => 1', '{}')).rejects.toStrictEqual( 10 | new Error('Failed to compile cost model') 11 | ) 12 | }) 13 | 14 | test('Invalid globals parsing fails', async () => { 15 | await expect( 16 | compileAsync('default => 1;', '!#@!%@$!') 17 | ).rejects.toStrictEqual(new Error('Failed to compile cost model')) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /node-plugin/src/build.rs: -------------------------------------------------------------------------------- 1 | extern crate neon_build; 2 | 3 | fn main() { 4 | neon_build::setup(); 5 | } 6 | -------------------------------------------------------------------------------- /node-plugin/src/lib.rs: -------------------------------------------------------------------------------- 1 | // , \ / , 2 | // / \ )\__/( / \ 3 | // / \ (_\ /_) / \ 4 | // ____/_____\__\@ @/___/_____\____ 5 | // | |\../| | 6 | // | \VV/ | 7 | // | HERE BE DRAGONS | 8 | // |_________________________________| 9 | // | /\ / \\ \ /\ | 10 | // | / V )) V \ | 11 | // |/ ` // ' \| 12 | // 13 | 14 | use cost_model::CostModel; 15 | use neon::prelude::*; 16 | use std::ops::{Deref, DerefMut}; 17 | use std::sync::Arc; 18 | use std::sync::Mutex; 19 | 20 | struct CostModelArgs { 21 | code: String, 22 | globals: String, 23 | } 24 | 25 | /// This enum exists because js has (mandatory) constructors, 26 | /// which can't be async. Our compile fn should be async. 27 | /// Ergo, we need to have js classes with not yet valid states. 28 | /// So, the js class transitions through these states and we 29 | /// fixup the API on the JS side by not exposing the class. 30 | /// (Can still be reached through some underhanded methods 31 | /// which are not worth fixing) 32 | enum State { 33 | Initialized(CostModelArgs), 34 | Compiling, 35 | Compiled(Arc), 36 | Fail, 37 | } 38 | 39 | pub struct Wrapper { 40 | data: Arc>, 41 | } 42 | 43 | impl Clone for Wrapper { 44 | fn clone(&self) -> Self { 45 | Self { 46 | data: self.data.clone(), 47 | } 48 | } 49 | } 50 | 51 | impl State { 52 | fn new(args: CostModelArgs) -> Self { 53 | Self::Initialized(args) 54 | } 55 | } 56 | 57 | struct CompileTask { 58 | model: Wrapper, 59 | } 60 | 61 | macro_rules! cas { 62 | ($name:ident, $( $pattern:pat )|+ $( if $guard: expr )?, $val:expr) => { 63 | match $name { 64 | $( $pattern )|+ $( if $guard )? => { 65 | let mut val = $val; 66 | ::std::mem::swap($name, &mut val); 67 | val 68 | } 69 | _ => $val 70 | } 71 | } 72 | } 73 | 74 | impl Task for CompileTask { 75 | type Output = (); 76 | type Error = &'static str; 77 | type JsEvent = JsBoolean; 78 | 79 | fn perform(&self) -> Result { 80 | // Transform the state from Initialized to Compiling, taking the code out. 81 | let args = { 82 | let mut lock = self.model.data.lock().unwrap(); 83 | let lock = lock.deref_mut(); 84 | if let State::Initialized(args) = cas!(lock, State::Initialized(_), State::Compiling) { 85 | args 86 | } else { 87 | return Err("Expected initialized cost model"); 88 | } 89 | }; 90 | 91 | let (state, result) = match CostModel::compile(args.code, &args.globals) { 92 | Ok(model) => (State::Compiled(Arc::new(model)), Ok(())), 93 | // Intentionally disregarding the actual contents of the error, 94 | // because the Gateway has no way to recover. If an Indexer's 95 | // model is broke, they just lose out on queries. So don't 96 | // spend performance formatting the error then serializing 97 | // that to JS to be dropped. 98 | Err(_) => (State::Fail, Err("Failed to compile cost model")), 99 | }; 100 | 101 | let mut lock = self.model.data.lock().unwrap(); 102 | let lock = lock.deref_mut(); 103 | // Don't need to check that state transition succeeded this time. 104 | cas!(lock, State::Compiling, state); 105 | 106 | result 107 | } 108 | 109 | // There's no microtask here because we already performed the necessary state update. 110 | fn complete( 111 | self, 112 | mut cx: TaskContext, 113 | result: Result, 114 | ) -> JsResult { 115 | // TODO: Wanted to propagate the error message here by 116 | // calling result.unwrap(), since the docs indicate that 117 | // a panic is propagated with an Error in JS but this results in... 118 | // "fatal runtime error: failed to initiate panic, error 5" 119 | // and the process is aborted. 🙁 120 | // Then tried just returning Err(neon::result::Throw) if there is an 121 | // error, but this results in a segfault! 😦 122 | // Returning a bool in frustration and checking for this on the js side. 123 | // Can do better later and clean up some of this wonky task stuff. 124 | // See also e5d47bc3-9b14-490d-abec-90c286330a2d 125 | Ok(cx.boolean(result.is_ok())) 126 | } 127 | } 128 | 129 | struct CostTask { 130 | model: Wrapper, 131 | query: String, 132 | variables: Option, 133 | } 134 | 135 | impl Task for CostTask { 136 | type Output = String; 137 | type Error = String; 138 | type JsEvent = JsString; 139 | 140 | fn perform(&self) -> Result { 141 | let model = { 142 | let lock = self.model.data.lock().unwrap(); 143 | match lock.deref() { 144 | State::Compiled(model) => model.clone(), 145 | _ => return Err("Expected compiled cost model".to_string()), 146 | } 147 | }; 148 | 149 | let cost = model.cost(&self.query, self.variables.as_deref().unwrap_or("")); 150 | let cost = match cost { 151 | Ok(cost) => cost, 152 | Err(e) => return Err(format!("{}", e)), 153 | }; 154 | Ok(cost.to_str_radix(10)) 155 | } 156 | 157 | fn complete( 158 | self, 159 | mut cx: TaskContext, 160 | result: Result, 161 | ) -> JsResult { 162 | // This suffers the same fate as 163 | // See also e5d47bc3-9b14-490d-abec-90c286330a2d 164 | // For this one, use the empty string as an err. 165 | match result { 166 | // TODO: (Performance) Find a version which does not copy the string 167 | Ok(s) => Ok(cx.string(s)), 168 | Err(_) => Ok(cx.string("")), 169 | } 170 | } 171 | } 172 | 173 | fn this(cx: &mut CallContext) -> Wrapper { 174 | let this = cx.this(); 175 | let guard = cx.lock(); 176 | let borrow = this.borrow(&guard); 177 | borrow.clone() 178 | } 179 | 180 | declare_types! { 181 | pub class JsCostModel for Wrapper { 182 | init(mut cx) { 183 | let code = cx.argument::(0)?.value(); 184 | let globals = cx.argument::(1)?.value(); 185 | let args = CostModelArgs { 186 | code, globals 187 | }; 188 | let state = State::new(args); 189 | let wrapper = Wrapper { data: Arc::new(Mutex::new(state)) }; 190 | Ok(wrapper) 191 | } 192 | 193 | method compile(mut cx) { 194 | let cb = cx.argument::(0)?; 195 | let model = this(&mut cx); 196 | 197 | let task = CompileTask { model }; 198 | task.schedule(cb); 199 | Ok(cx.undefined().upcast()) 200 | } 201 | 202 | method cost(mut cx) { 203 | let cb = cx.argument::(0)?; 204 | let query = cx.argument::(1)?.value(); 205 | let variables = cx.argument_opt(2).and_then(|arg| { 206 | // Extra check necessary because from the js side were passing 207 | // optional variable argument through, which ends up being interpreted 208 | // here as a specified argument of type undefined. 209 | if arg.is_a::() || arg.is_a::() { 210 | None 211 | } else { 212 | Some(arg.downcast::().or_throw(&mut cx).unwrap().value()) 213 | } 214 | }); 215 | let model = this(&mut cx); 216 | 217 | let task = CostTask { model, variables, query }; 218 | task.schedule(cb); 219 | Ok(cx.undefined().upcast()) 220 | } 221 | } 222 | } 223 | 224 | register_module!(mut m, { 225 | m.export_class::("CostModel")?; 226 | Ok(()) 227 | }); 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphprotocol/cost-model", 3 | "version": "0.1.18", 4 | "description": "Cost model", 5 | "main": "node-plugin/lib/index.js", 6 | "types": "/node-plugin/lib/index.d.ts", 7 | "files": [ 8 | "/node-plugin/lib", 9 | "/node-plugin/src", 10 | "/node-plugin/Cargo.toml", 11 | "/lang/" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/graphprotocol/agora.git" 16 | }, 17 | "author": "Zac Burns ", 18 | "license": "MIT", 19 | "os": [ 20 | "darwin", 21 | "linux" 22 | ], 23 | "cpu": [ 24 | "x64", 25 | "arm", 26 | "arm64" 27 | ], 28 | "scripts": { 29 | "build": "cargo-cp-artifact -a cdylib node_plugin ./node-plugin/native/index.node -- cargo build --manifest-path ./node-plugin/Cargo.toml --message-format=json-render-diagnostics", 30 | "build-debug": "yarn build --", 31 | "build-release": "yarn build --release", 32 | "package": "node-pre-gyp package", 33 | "publish-github-draft": "node-pre-gyp-github publish", 34 | "publish-github": "node-pre-gyp-github publish --release", 35 | "build-test-pack-publish": "yarn build-release && yarn test && yarn package && yarn publish-github", 36 | "install": "node-pre-gyp install --fallback-to-build=false --update-binary || yarn build-release", 37 | "test": "jest", 38 | "clean": "cargo clean && rm -rf ./node-plugin/native ./build ./node_modules ./target" 39 | }, 40 | "dependencies": { 41 | "@mapbox/node-pre-gyp": "1.0.11", 42 | "cargo-cp-artifact": "0.1.8" 43 | }, 44 | "devDependencies": { 45 | "jest": "27.5.1", 46 | "node-pre-gyp-github": "1.4.4" 47 | }, 48 | "binary": { 49 | "module_name": "index", 50 | "module_path": "./node-plugin/native", 51 | "host": "https://github.com/graphprotocol/agora/releases/download/", 52 | "remote_path": "v{version}", 53 | "package_name": "graphprotocol-agora-plugin-v{version}-{node_abi}-{platform}-{arch}.tar.gz" 54 | } 55 | } 56 | --------------------------------------------------------------------------------