From cb0b3a17902c58bf1d32ac4c1b6ab5a296d174ae Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Thu, 9 Feb 2023 10:56:54 -0800 Subject: [PATCH] Code from github.com/anakrish/rego-rs Authored by anakrish and mingweishih Signed-off-by: Anand Krishnamoorthi --- Cargo.toml | 22 + build.rs | 10 + docs/grammar.md | 174 ++ scripts/coverage | 49 + scripts/make-docs | 15 + scripts/pre-commit | 25 + scripts/pre-push | 15 + scripts/rego-eval | 13 + scripts/rego-lex | 27 + scripts/rego-parse | 8 + scripts/yaml-test-eval | 8 + scripts/yaml-test-parse | 8 + snippets/2.rego | 23 + src/ast.rs | 284 +++ src/builtins/compare.rs | 27 + src/builtins/mod.rs | 3 + src/interpreter.rs | 1618 +++++++++++++++++ src/lexer.rs | 506 ++++++ src/lib.rs | 15 + src/parser.rs | 1572 ++++++++++++++++ src/value.rs | 325 ++++ tests/interpreter/cases/arithmetic/mod.rs | 45 + tests/interpreter/cases/basic_001.yaml | 32 + tests/interpreter/cases/builtins/compare.yaml | 234 +++ tests/interpreter/cases/call/basic.yaml | 44 + .../cases/compr/array-vs-compr-tricky.yaml | 132 ++ tests/interpreter/cases/compr/mod.rs | 123 ++ tests/interpreter/cases/compr/object.yaml | 67 + tests/interpreter/cases/default/basic.yaml | 56 + tests/interpreter/cases/in/mod.rs | 126 ++ tests/interpreter/cases/mod.rs | 7 + tests/interpreter/cases/multi/basic.yaml | 32 + tests/interpreter/cases/rule/contains.yaml | 32 + tests/interpreter/cases/rule/dependency.yaml | 41 + tests/interpreter/cases/rule/object.yaml | 35 + tests/interpreter/cases/rule/old_set.yaml | 31 + .../interpreter/cases/snippets/snippets.yaml | 249 +++ tests/interpreter/cases/variables/basic.yaml | 75 + tests/interpreter/cases/variables/mod.rs | 54 + tests/interpreter/mod.rs | 386 ++++ tests/lexer/cases/all.yaml | 34 + tests/lexer/cases/boolean.yaml | 10 + tests/lexer/cases/comment.yaml | 71 + tests/lexer/cases/eof.yaml | 35 + tests/lexer/cases/identifier.yaml | 38 + tests/lexer/cases/keyword.yaml | 10 + tests/lexer/cases/newline.yaml | 13 + tests/lexer/cases/number.yaml | 184 ++ tests/lexer/cases/rawstring.yaml | 37 + tests/lexer/cases/string.yaml | 114 ++ tests/lexer/cases/symbol.yaml | 23 + tests/lexer/cases/whitespace.yaml | 96 + tests/lexer/mod.rs | 335 ++++ tests/parser/cases/every/every.yaml | 60 + .../parser/cases/expressions/arithmetic.yaml | 40 + .../parser/cases/expressions/array-compr.yaml | 209 +++ tests/parser/cases/expressions/array.yaml | 108 ++ tests/parser/cases/expressions/bin.yaml | 64 + tests/parser/cases/expressions/bool.yaml | 43 + tests/parser/cases/expressions/call.yaml | 48 + tests/parser/cases/expressions/in.yaml | 59 + .../parser/cases/expressions/membership.yaml | 53 + tests/parser/cases/expressions/object.yaml | 163 ++ tests/parser/cases/expressions/set-compr.yaml | 209 +++ tests/parser/cases/expressions/set.yaml | 112 ++ tests/parser/cases/import/future.yaml | 154 ++ tests/parser/cases/import/import.yaml | 308 ++++ tests/parser/cases/package/package.yaml | 129 ++ tests/parser/cases/rules/basic.yaml | 44 + tests/parser/cases/rules/else.yaml | 250 +++ tests/parser/cases/rules/set.yaml | 135 ++ tests/parser/cases/some/some.in.yaml | 226 +++ tests/parser/cases/some/some.vars.yaml | 74 + tests/parser/mod.rs | 795 ++++++++ tests/tests.rs | 7 + .../arith_0.rego | 18 + .../basic.rego | 20 + .../input_0.json | 4 + .../parse_0.rego | 37 + .../query.rego | 3 + .../rego_0.rego | 5 + .../rule_assign_0.rego | 25 + .../tmp.rego | 12 + .../variables_0.rego | 6 + tests/value/mod.rs | 176 ++ 85 files changed, 11144 insertions(+) create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 docs/grammar.md create mode 100755 scripts/coverage create mode 100755 scripts/make-docs create mode 100755 scripts/pre-commit create mode 100755 scripts/pre-push create mode 100755 scripts/rego-eval create mode 100755 scripts/rego-lex create mode 100755 scripts/rego-parse create mode 100755 scripts/yaml-test-eval create mode 100755 scripts/yaml-test-parse create mode 100644 snippets/2.rego create mode 100644 src/ast.rs create mode 100644 src/builtins/compare.rs create mode 100644 src/builtins/mod.rs create mode 100644 src/interpreter.rs create mode 100644 src/lexer.rs create mode 100644 src/lib.rs create mode 100644 src/parser.rs create mode 100644 src/value.rs create mode 100644 tests/interpreter/cases/arithmetic/mod.rs create mode 100644 tests/interpreter/cases/basic_001.yaml create mode 100644 tests/interpreter/cases/builtins/compare.yaml create mode 100644 tests/interpreter/cases/call/basic.yaml create mode 100644 tests/interpreter/cases/compr/array-vs-compr-tricky.yaml create mode 100644 tests/interpreter/cases/compr/mod.rs create mode 100644 tests/interpreter/cases/compr/object.yaml create mode 100644 tests/interpreter/cases/default/basic.yaml create mode 100644 tests/interpreter/cases/in/mod.rs create mode 100644 tests/interpreter/cases/mod.rs create mode 100644 tests/interpreter/cases/multi/basic.yaml create mode 100644 tests/interpreter/cases/rule/contains.yaml create mode 100644 tests/interpreter/cases/rule/dependency.yaml create mode 100644 tests/interpreter/cases/rule/object.yaml create mode 100644 tests/interpreter/cases/rule/old_set.yaml create mode 100644 tests/interpreter/cases/snippets/snippets.yaml create mode 100644 tests/interpreter/cases/variables/basic.yaml create mode 100644 tests/interpreter/cases/variables/mod.rs create mode 100644 tests/interpreter/mod.rs create mode 100644 tests/lexer/cases/all.yaml create mode 100644 tests/lexer/cases/boolean.yaml create mode 100644 tests/lexer/cases/comment.yaml create mode 100644 tests/lexer/cases/eof.yaml create mode 100644 tests/lexer/cases/identifier.yaml create mode 100644 tests/lexer/cases/keyword.yaml create mode 100644 tests/lexer/cases/newline.yaml create mode 100644 tests/lexer/cases/number.yaml create mode 100644 tests/lexer/cases/rawstring.yaml create mode 100644 tests/lexer/cases/string.yaml create mode 100644 tests/lexer/cases/symbol.yaml create mode 100644 tests/lexer/cases/whitespace.yaml create mode 100644 tests/lexer/mod.rs create mode 100644 tests/parser/cases/every/every.yaml create mode 100644 tests/parser/cases/expressions/arithmetic.yaml create mode 100644 tests/parser/cases/expressions/array-compr.yaml create mode 100644 tests/parser/cases/expressions/array.yaml create mode 100644 tests/parser/cases/expressions/bin.yaml create mode 100644 tests/parser/cases/expressions/bool.yaml create mode 100644 tests/parser/cases/expressions/call.yaml create mode 100644 tests/parser/cases/expressions/in.yaml create mode 100644 tests/parser/cases/expressions/membership.yaml create mode 100644 tests/parser/cases/expressions/object.yaml create mode 100644 tests/parser/cases/expressions/set-compr.yaml create mode 100644 tests/parser/cases/expressions/set.yaml create mode 100644 tests/parser/cases/import/future.yaml create mode 100644 tests/parser/cases/import/import.yaml create mode 100644 tests/parser/cases/package/package.yaml create mode 100644 tests/parser/cases/rules/basic.yaml create mode 100644 tests/parser/cases/rules/else.yaml create mode 100644 tests/parser/cases/rules/set.yaml create mode 100644 tests/parser/cases/some/some.in.yaml create mode 100644 tests/parser/cases/some/some.vars.yaml create mode 100644 tests/parser/mod.rs create mode 100644 tests/tests.rs create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/arith_0.rego create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/basic.rego create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/input_0.json create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/parse_0.rego create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/query.rego create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/rego_0.rego create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/rule_assign_0.rego create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/tmp.rego create mode 100644 tests/tmp-files-to-be-added-as-yaml-tests/variables_0.rego create mode 100644 tests/value/mod.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7983516 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "rego-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.66" +ordered-float = "3.4.0" +serde = {version = "1.0.150", features = ["derive", "rc"] } +serde_json = "1.0.89" +log = "0.4.17" +env_logger="0.10.0" + +[dev-dependencies] +serde_yaml = "0.9.16" +test-generator = "0.3.1" +walkdir = "2.3.2" + +[build-dependencies] +anyhow = "1.0.66" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..02bcc5e --- /dev/null +++ b/build.rs @@ -0,0 +1,10 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +use anyhow::Result; + +fn main() -> Result<()> { + // Copy hooks to appropriate location so that git will run them. + std::fs::copy("./scripts/pre-commit", "./.git/hooks/pre-commit")?; + std::fs::copy("./scripts/pre-push", "./.git/hooks/pre-push")?; + Ok(()) +} diff --git a/docs/grammar.md b/docs/grammar.md new file mode 100644 index 0000000..fd1598c --- /dev/null +++ b/docs/grammar.md @@ -0,0 +1,174 @@ + +``` +module: package imports { rule } + +package: "package" path-ref + +imports: { "import" path-ref [ "as" var ] } + +path-ref: path-ref NO_WS "." NO_WS IDENT +| path-ref NO_WS "[" STRING "]" +| IDENT + +rule: default-rule +| spec-rule + +default-rule: "default" rule-ref assign-op term + +spec-rule: rule-head rule-bodies + +rule-head: func-rule +| contains-rule +| object-rule +| set-rule +| compr-rule + +func-rule: rule-ref "(" term { "," term } [","] ")" [ rule-assign ] + +contains-rule: rule-ref "contains" or-expr + +object-rule: rule-ref NO_WS "[" membership-expr "]" rule-assign + +set-rule: rule-ref NO_WS "[" membership-expr "]" + +compr-rule: rule-ref [ rule-assign ] + +rule-ref: rule-ref NO_WS "." NO_WS var +| path-ref NO_WS "[" membership-expr "]" +| var + +rule-assign: assign-op membership-expr + +rule-bodies: "if" "{" query "}" alternatives +| "if" literal-stmt alternatives +| "{" query "}" alternatives + +alternatives: query-blocks +| else-blocks + +query-blocks: { "{" query "}" } + +else-blocks: { else-block } + +else-block: "else" [rule-assign] "if" "{" query "}" +| "else" [rule-assign] "if" literal-stmt +| "else" [rule-assign] "{" query "}" + +assign-op: "=" | ":=" + +query: literal-stmt { sep literal-stmt } + +sep: ";" | "\n" | "\r\n" + +literal-stmt: literal with-modifiers + +with-modifiers: { "with" path-ref "as" in-expr } + +literal: some +| every +| expr +| not-expr + +some: some-vars +| some-in + +some-vars: "some" var { "," var } + +some-in: "some" ref [ "," ref ] "in" + +every: "every" var [ "," var ] "in" bool-expr "{" query "}" + +expr: assign-expr + +not-expr: "not" assign-expr + +assign-expr: ref assign-op membership-expr + +membership-expr: membership-expr "in" bool-expr +| bool-expr "," bool-expr +| bool-expr + +in-expr: in-expr "in" bool-expr +| bool-expr + +bool-expr: bool-expr bool-op or-expr +| or-expr + +bool-op: "<" | "<=" | "==" | ">=" | ">" | "!=" + +or-expr: or-expr "|" and-expr +| and-expr + +and-expr: and-expr "&" arith-expr +| arith-expr + +arith-expr: arith-expr ("+" | "-") mul-div-expr +| mul-div-expr + +mul-div-expr: mul-div-expr ("*" | "/") term +| term + +term: ref + +ref: scalar-or-var +| compr-set-or-object +| compr-or-array +| unary-expr +| parens-expr +| ref-dot +| ref-brack +| call-expr + +ref-dot: ref NO_WS "." NO_WS var + +ref-brack: ref NO_WS "[" in-expr "]" + +call-expr: path-ref NO_WS "(" call-args [","] ")" + +call-args: in-expr { "," in-expr } + +parens-expr: "(" membership-expr ")" + +unary-expr: "-" in-expr + +compr-set-or-object: set-compr +| set +| object-compr +| object + +set-compr: "{" compr "}" + +# Set must have at least one item. +set: "{" in-expr { "," in-expr } [","] "}" +| "set(" ")" # empty set + +object: "{" field { "," field } [","] "}" +| "{" "}" # empty object + +field: in-expr ":" in-expr + +# Comprehension or array +compr-or-array: array-compr +| array + +array-compr: "[" compr "]" + +array: "[" in-expr { "," in-expr } [","] "]" +| "[" "]" # Empty array + +# Comprehension +compr: ref "|" query + +scalar-or-var: var +| NUMBER +| STRING +| RAWSTRING +| "null" +| "true" +| "false" + +var: IDENT +| non-imported-future-keyword + +non-imported-future-future-keyword: "contains" | "every" | "if" | "in" +``` diff --git a/scripts/coverage b/scripts/coverage new file mode 100755 index 0000000..458f898 --- /dev/null +++ b/scripts/coverage @@ -0,0 +1,49 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -e + +if ! command -v grcov > /dev/null; then + cargo install grcov +fi + +if ! command -v llvm-profdata > /dev/null; then + rustup component add llvm-tools-preview +fi + +#export LLVM_PROFILE_FILE='target/cargo-test-%p-%m.profraw' +#export CARGO_INCREMENTAL=1 +#export RUSTFLAGS='-Cinstrument-coverage' + +echo "Building with instrumentation" +cargo build --all-targets + +if [ "$1" == "--no-run" ]; then + exit 0 +fi + +# Remove existing coverage information. +rm -f target/*.profraw +rm -rf target/coverage +mkdir -p target/coverage + +echo "Running tests" +cargo test + +# Generate html +grcov target/ --binary-path ./target/x86_64-unknown-linux-musl/debug/deps -s src/ -t html \ +--branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html + +if [ "$1" == "--show" ]; then + echo "Opening report in browser" + xdg-open target/coverage/html/src/index.html 2>/dev/null + echo "Done" +fi + +# Generate markdown +grcov target/ --binary-path ./target/x86_64-unknown-linux-musl/debug/deps -s src/ -t markdown \ +--branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/markdown +cat target/coverage/markdown + +#TODO: Maybe use coveralls format (json) and query data to lockdown code coverage. diff --git a/scripts/make-docs b/scripts/make-docs new file mode 100755 index 0000000..b8ace5a --- /dev/null +++ b/scripts/make-docs @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +git stash +cargo doc --no-deps +git checkout docs +rm -rf docs +cp -r target/x86_64-unknown-linux-musl/doc ./docs +echo "" > docs/index.html +git add docs +git commit -s +git push +git checkout - +git stash pop diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..0897847 --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,25 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -eo pipefail + +if [ -f Cargo.toml ]; then + # Ensure that all targets can be built. + scripts/coverage --no-run + + #Ensure that code is correctly formatted. + cargo fmt --check || (echo "Run cargo fmt to fix formatting" && exit 1) + + # Ensure that clippy warnings are addressed. + cargo clippy --all-targets --no-deps -- -Dwarnings + + # Ensure that all modifications are included. + # TODO refine status checking. + if git status -s | grep -e "MM " -e "??" -e "AM " -e " M " > /dev/null; then + printf "\nUnstaged changes found:\n" + git status -s | grep -e "MM " -e "??" -e "AM " -e " M " + echo "Stage them and try again" + exit 1 + fi +fi diff --git a/scripts/pre-push b/scripts/pre-push new file mode 100755 index 0000000..e851671 --- /dev/null +++ b/scripts/pre-push @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -eo pipefail + +if [ -f Cargo.toml ]; then + # Run precommit checks + dir=$(dirname "${BASH_SOURCE[0]}") + "$dir/pre-commit" + + # Ensure that all tests pass + # Also generate coverage information. + scripts/coverage +fi diff --git a/scripts/rego-eval b/scripts/rego-eval new file mode 100755 index 0000000..1883402 --- /dev/null +++ b/scripts/rego-eval @@ -0,0 +1,13 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -e +rego=$(realpath -e $1) + +if [ ! -z "$2" ]; then + input=$(realpath -e $2) + cargo test interpreter::one_file -- --include-ignored --nocapture "$rego" "$input" +else + cargo test interpreter::one_file -- --include-ignored --nocapture "$rego" +fi diff --git a/scripts/rego-lex b/scripts/rego-lex new file mode 100755 index 0000000..d08296e --- /dev/null +++ b/scripts/rego-lex @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -e + +usage="usage: rego-lex [-v]" + +if [ -z "$1" ]; then + echo "$usage" + exit 1 +fi + +rego=$(realpath -e $1) + +case "$2" in + "-v") + verbose="verbose" + ;; + *) + if [ ! -z "$2" ]; then + echo "$usage" + exit 1 + fi +esac + +eval "cargo test lexer::one_file -- --include-ignored --nocapture $rego $verbose" diff --git a/scripts/rego-parse b/scripts/rego-parse new file mode 100755 index 0000000..b4d0e2f --- /dev/null +++ b/scripts/rego-parse @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -e + +rego=$(realpath -e $1) +cargo test parser::one_file -- --include-ignored --nocapture "$rego" diff --git a/scripts/yaml-test-eval b/scripts/yaml-test-eval new file mode 100755 index 0000000..80aff37 --- /dev/null +++ b/scripts/yaml-test-eval @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -e +yaml=$(realpath -e $1) + +RUST_BACKTRACE=1 cargo test interpreter::one_yaml -- --include-ignored --nocapture "$yaml" diff --git a/scripts/yaml-test-parse b/scripts/yaml-test-parse new file mode 100755 index 0000000..ca10ffe --- /dev/null +++ b/scripts/yaml-test-parse @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +set -e +yaml=$(realpath -e $1) + +RUST_BACKTRACE=1 cargo test parser::one_yaml -- --include-ignored --nocapture "$yaml" diff --git a/snippets/2.rego b/snippets/2.rego new file mode 100644 index 0000000..5f30ffb --- /dev/null +++ b/snippets/2.rego @@ -0,0 +1,23 @@ +Cpackage play + +a := {4} + +mydoc(x) := path { + path := "data.play.a" +} + +x := [ y | + y := data.play.a | data.play.b with data.play.a as {5} with data.play.b as {6} +] + +r := [ m | m := data.play.p with data.play.p as 5 + 6; true ] + + +allow { + input.x + == 5 + + input.y == 5 + input.y + == 5 +} \ No newline at end of file diff --git a/src/ast.rs b/src/ast.rs new file mode 100644 index 0000000..141b791 --- /dev/null +++ b/src/ast.rs @@ -0,0 +1,284 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. + +use crate::lexer::*; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum BinOp { + And, + Or, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum ArithOp { + Add, + Sub, + Mul, + Div, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum BoolOp { + Lt, + Le, + Eq, + Ge, + Gt, + Ne, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum AssignOp { + Eq, + ColEq, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum Expr<'source> { + // Simple items that only have a span as content. + String(Span<'source>), + RawString(Span<'source>), + Number(Span<'source>), + True(Span<'source>), + False(Span<'source>), + Null(Span<'source>), + Var(Span<'source>), + + // array + Array { + span: Span<'source>, + items: Vec>, + }, + + // set + Set { + span: Span<'source>, + items: Vec>, + }, + + Object { + span: Span<'source>, + fields: Vec<(Span<'source>, Expr<'source>, Expr<'source>)>, + }, + + // Comprehensions + ArrayCompr { + span: Span<'source>, + term: Box>, + query: Query<'source>, + }, + + SetCompr { + span: Span<'source>, + term: Box>, + query: Query<'source>, + }, + + ObjectCompr { + span: Span<'source>, + key: Box>, + value: Box>, + query: Query<'source>, + }, + + Call { + span: Span<'source>, + fcn: Box>, + params: Vec>, + }, + + UnaryExpr { + span: Span<'source>, + expr: Box>, + }, + + // ref + RefDot { + span: Span<'source>, + refr: Box>, + field: Span<'source>, + }, + + RefBrack { + span: Span<'source>, + refr: Box>, + index: Box>, + }, + + // Infix expressions + BinExpr { + span: Span<'source>, + op: BinOp, + lhs: Box>, + rhs: Box>, + }, + BoolExpr { + span: Span<'source>, + op: BoolOp, + lhs: Box>, + rhs: Box>, + }, + + ArithExpr { + span: Span<'source>, + op: ArithOp, + lhs: Box>, + rhs: Box>, + }, + + AssignExpr { + span: Span<'source>, + op: AssignOp, + lhs: Box>, + rhs: Box>, + }, + + Membership { + span: Span<'source>, + key: Box>, + value: Box>>, + collection: Box>, + }, +} + +impl<'source> Expr<'source> { + pub fn span(&self) -> &Span<'source> { + use Expr::*; + match self { + String(s) | RawString(s) | Number(s) | True(s) | False(s) | Null(s) | Var(s) => s, + Array { span, .. } + | Set { span, .. } + | Object { span, .. } + | ArrayCompr { span, .. } + | SetCompr { span, .. } + | ObjectCompr { span, .. } + | Call { span, .. } + | UnaryExpr { span, .. } + | RefDot { span, .. } + | RefBrack { span, .. } + | BinExpr { span, .. } + | BoolExpr { span, .. } + | ArithExpr { span, .. } + | AssignExpr { span, .. } + | Membership { span, .. } => span, + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum Literal<'source> { + SomeVars { + span: Span<'source>, + vars: Vec>, + }, + SomeIn { + span: Span<'source>, + key: Expr<'source>, + value: Option>, + collection: Expr<'source>, + }, + Expr { + span: Span<'source>, + expr: Expr<'source>, + }, + NotExpr { + span: Span<'source>, + expr: Expr<'source>, + }, + Every { + span: Span<'source>, + key: Span<'source>, + value: Option>, + domain: Expr<'source>, + query: Query<'source>, + }, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct WithModifier<'source> { + pub span: Span<'source>, + pub refr: Expr<'source>, + pub r#as: Expr<'source>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct LiteralStmt<'source> { + pub span: Span<'source>, + pub literal: Literal<'source>, + pub with_mods: Vec>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct Query<'source> { + pub span: Span<'source>, + pub stmts: Vec>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct RuleAssign<'source> { + pub span: Span<'source>, + pub op: AssignOp, + pub value: Expr<'source>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct RuleBody<'source> { + pub span: Span<'source>, + pub assign: Option>, + pub query: Query<'source>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum RuleHead<'source> { + Compr { + span: Span<'source>, + refr: Expr<'source>, + assign: Option>, + }, + Set { + span: Span<'source>, + refr: Expr<'source>, + key: Option>, + }, + Func { + span: Span<'source>, + refr: Expr<'source>, + args: Vec>, + assign: Option>, + }, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum Rule<'source> { + Spec { + span: Span<'source>, + head: RuleHead<'source>, + bodies: Vec>, + }, + Default { + span: Span<'source>, + refr: Expr<'source>, + op: AssignOp, + value: Expr<'source>, + }, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct Package<'source> { + pub span: Span<'source>, + pub refr: Expr<'source>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct Import<'source> { + pub span: Span<'source>, + pub refr: Expr<'source>, + pub r#as: Option>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct Module<'source> { + pub package: Package<'source>, + pub imports: Vec>, + pub policy: Vec>, +} diff --git a/src/builtins/compare.rs b/src/builtins/compare.rs new file mode 100644 index 0000000..8860240 --- /dev/null +++ b/src/builtins/compare.rs @@ -0,0 +1,27 @@ +use crate::ast::*; +use crate::value::{Value::*, *}; + +pub fn eq(v1: &Value, v2: &Value) -> Value { + match (v1, v2) { + (Undefined, _) | (_, Undefined) => Undefined, + _ => Bool(v1 == v2), + } +} + +pub fn ne(v1: &Value, v2: &Value) -> Value { + match (v1, v2) { + (Undefined, _) | (_, Undefined) => Undefined, + _ => Bool(v1 != v2), + } +} + +pub fn compare(op: &BoolOp, v1: &Value, v2: &Value) -> Value { + match op { + BoolOp::Eq => eq(v1, v2), + BoolOp::Ne => ne(v1, v2), + BoolOp::Ge => Bool(v1 >= v2), + BoolOp::Gt => Bool(v1 > v2), + BoolOp::Le => Bool(v1 <= v2), + BoolOp::Lt => Bool(v1 < v2), + } +} diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs new file mode 100644 index 0000000..04b9eaf --- /dev/null +++ b/src/builtins/mod.rs @@ -0,0 +1,3 @@ +mod compare; + +pub use self::compare::*; diff --git a/src/interpreter.rs b/src/interpreter.rs new file mode 100644 index 0000000..b2c28dd --- /dev/null +++ b/src/interpreter.rs @@ -0,0 +1,1618 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +use crate::ast::*; +use crate::builtins; +use crate::lexer::Span; +use crate::parser::Parser; +use crate::value::*; + +use anyhow::{anyhow, bail, Result}; +use log::info; +use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}; +use std::rc::Rc; + +type Scope = BTreeMap; + +pub struct Interpreter<'source> { + modules: Vec<&'source Module<'source>>, + module: Option<&'source Module<'source>>, + current_module_path: String, + input: Value, + data: Value, + scopes: Vec, + // TODO: handle recursive calls where same expr could have different values. + loop_var_values: BTreeMap<&'source Expr<'source>, Value>, + contexts: Vec>, + functions: HashMap>, + rules: HashMap>>, + default_rules: HashMap, Option)>>, + processed: BTreeSet<&'source Rule<'source>>, + active_rules: Vec<&'source Rule<'source>>, +} + +#[derive(Debug)] +struct Variable { + value: Value, + partial: bool, + _has_default: bool, +} + +#[derive(Debug, Clone)] +struct Context<'source> { + key_expr: Option<&'source Expr<'source>>, + output_expr: Option<&'source Expr<'source>>, + value: Value, +} + +#[derive(Debug)] +struct LoopExpr<'source> { + span: &'source Span<'source>, + expr: &'source Expr<'source>, + value: &'source Expr<'source>, +} + +impl<'source> Interpreter<'source> { + pub fn new(modules: Vec<&'source Module<'source>>) -> Result> { + Ok(Interpreter { + modules, + module: None, + current_module_path: String::default(), + input: Value::new_object(), + data: Value::new_object(), + scopes: vec![Scope::new()], + contexts: vec![], + loop_var_values: BTreeMap::new(), + functions: HashMap::new(), + rules: HashMap::new(), + default_rules: HashMap::new(), + processed: BTreeSet::new(), + active_rules: vec![], + }) + } + + fn current_module(&self) -> Result<&'source Module<'source>> { + match &self.module { + Some(m) => Ok(m), + _ => bail!("internal error: current module not set"), + } + } + + #[inline(always)] + fn add_variable( + &mut self, + name: &str, + partial: bool, + default: Option, + ) -> Result<(String, Value)> { + let name = name.to_string(); + + // Only add the variable if the key is not "_" + let value = if name != "_" { + let (value, _has_default) = if let Some(default) = default { + (default, true) + } else { + (Value::Undefined, false) + }; + + let variable = Variable { + value: value.clone(), + partial, + _has_default, + }; + + match self.scopes.last_mut() { + Some(scope) => { + scope.insert(name.to_string(), variable); + } + _ => bail!("internal error: no active scope"), + } + value + } else { + Value::Undefined + }; + Ok((name, value)) + } + + fn add_variable_or( + &mut self, + name: &str, + partial: bool, + default: Option, + ) -> Result<(String, Value, bool)> { + for scope in self.scopes.iter().rev() { + if let Some(variable) = scope.get(&name.to_string()) { + return Ok((name.to_string(), variable.value.clone(), variable.partial)); + } + } + + let (name, value) = self.add_variable(name, partial, default)?; + Ok((name, value, partial)) + } + + // TODO: optimize this + fn variables_assignment(&mut self, name: &str, value: &Value) -> Result<()> { + match self.scopes.last_mut() { + Some(scope) => { + if let Some(variable) = scope.get_mut(name) { + variable.value = value.clone(); + } else { + return Err(anyhow!("variable {} is undefined", name)); + } + } + _ => bail!("internal error: no active scope"), + } + + Ok(()) + } + + fn eval_chained_ref_dot_or_brack(&mut self, mut expr: &'source Expr<'source>) -> Result { + // Collect a chaing of '.field' or '["field"]' + let mut path = vec![]; + loop { + match expr { + // Stop path collection upon encountering the leading variable. + Expr::Var(v) => { + path.reverse(); + return self.lookup_var(v.text(), &path[..]); + } + // Accumulate chained . field accesses. + Expr::RefDot { refr, field, .. } => { + expr = refr; + path.push(field.text()); + } + Expr::RefBrack { refr, index, .. } => match index.as_ref() { + // refr["field"] is the same as refr.field + Expr::String(s) => { + expr = refr; + path.push(s.text()); + } + // Handle other forms of refr. + // Note, we have the choice to evaluate a non-string index + _ => { + path.reverse(); + let obj = self.eval_expr(refr)?; + let index = self.eval_expr(index)?; + return Ok(Self::get_value_chained(obj[&index].clone(), &path[..])); + } + }, + _ => { + path.reverse(); + return Ok(Self::get_value_chained(self.eval_expr(expr)?, &path[..])); + } + } + } + } + + fn is_loop_var(&self, ident: &str) -> bool { + // TODO: check for vars that are declared using some-vars + // TODO: check for vars that are not declared and dont exist in any scope including global. + ident == "_" + } + + fn hoist_loops_impl(&self, expr: &'source Expr<'source>, loops: &mut Vec>) { + use Expr::*; + match expr { + RefBrack { refr, index, span } => { + // First hoist any loops in refr + self.hoist_loops_impl(refr, loops); + + // Then hoist the current bracket operation. + match index.as_ref() { + Var(ident) if self.is_loop_var(ident.text()) => loops.push(LoopExpr { + span, + expr, + //var: ident.text(), + value: refr, + }), + _ => { + // hoist any loops in index expression. + self.hoist_loops_impl(index, loops); + } + } + } + + // Primitives + String(_) | RawString(_) | Number(_) | True(_) | False(_) | Null(_) | Var(_) => (), + + // Recurse into expressions in other variants. + Array { items, .. } | Set { items, .. } | Call { params: items, .. } => { + for item in items { + self.hoist_loops_impl(item, loops); + } + } + + Object { fields, .. } => { + for (_, key, value) in fields { + self.hoist_loops_impl(key, loops); + self.hoist_loops_impl(value, loops); + } + } + + RefDot { refr: expr, .. } | UnaryExpr { expr, .. } => { + self.hoist_loops_impl(expr, loops) + } + + BinExpr { lhs, rhs, .. } + | BoolExpr { lhs, rhs, .. } + | ArithExpr { lhs, rhs, .. } + | AssignExpr { lhs, rhs, .. } => { + self.hoist_loops_impl(lhs, loops); + self.hoist_loops_impl(rhs, loops); + } + + Membership { + key, + value, + collection, + .. + } => { + self.hoist_loops_impl(key, loops); + if let Some(value) = value.as_ref() { + self.hoist_loops_impl(value, loops); + } + self.hoist_loops_impl(collection, loops); + } + + // The output expressions of comprehensions must be subject to hoisting + // only after evaluating the body of the comprehensions since the output + // expressions may depend on variables defined within the body. + ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => (), + } + } + + fn hoist_loops(&self, literal: &'source Literal<'source>) -> Vec> { + let mut loops = vec![]; + use Literal::*; + match literal { + SomeVars { .. } => (), + SomeIn { + key, + value, + collection, + .. + } => { + self.hoist_loops_impl(key, &mut loops); + if let Some(value) = value { + self.hoist_loops_impl(value, &mut loops); + } + self.hoist_loops_impl(collection, &mut loops); + } + Every { + domain: collection, .. + } => self.hoist_loops_impl(collection, &mut loops), + Expr { expr, .. } | NotExpr { expr, .. } => self.hoist_loops_impl(expr, &mut loops), + } + loops + } + + fn eval_bool_expr( + &mut self, + op: &BoolOp, + lhs_expr: &'source Expr<'source>, + rhs_expr: &'source Expr<'source>, + ) -> Result { + let lhs = self.eval_expr(lhs_expr)?; + let rhs = self.eval_expr(rhs_expr)?; + Ok(builtins::compare(op, &lhs, &rhs)) + } + + fn eval_bin_expr( + &mut self, + op: &BinOp, + lhs: &'source Expr<'source>, + rhs: &'source Expr<'source>, + ) -> Result { + let lhs = self.eval_expr(lhs)?; + let rhs = self.eval_expr(rhs)?; + + let lhs = if let Value::Set(set) = lhs { + set + } else { + return Err(anyhow!("expect {:?} to be a set", lhs)); + }; + + let rhs = if let Value::Set(set) = rhs { + set + } else { + return Err(anyhow!("expect {:?} to be a set", rhs)); + }; + + info!( + "eval_bin_expr, op: {:?}, lhs: {:?}, rhs: {:?}", + op, lhs, rhs + ); + + Ok(Value::from_set(match op { + BinOp::Or => lhs.union(&rhs).cloned().collect(), + BinOp::And => lhs.intersection(&rhs).cloned().collect(), + })) + } + + fn eval_arith_expr( + &mut self, + op: &ArithOp, + lhs: &'source Expr<'source>, + rhs: &'source Expr<'source>, + ) -> Result { + let lhs = self.eval_expr(lhs)?; + let rhs = self.eval_expr(rhs)?; + + // Handle special case for set difference. + if let (Value::Set(lhs), ArithOp::Sub, Value::Set(rhs)) = (&lhs, op, &rhs) { + return Ok(Value::from_set(lhs.difference(rhs).cloned().collect())); + } + + let lhs = if let Value::Number(number) = lhs { + number.0 + } else { + return Err(anyhow!("expect {:?} to be a number", lhs)); + }; + + let rhs = if let Value::Number(number) = rhs { + number.0 + } else { + return Err(anyhow!("expect {:?} to be a number", rhs)); + }; + + let result = match op { + ArithOp::Add => lhs + rhs, + ArithOp::Sub => lhs - rhs, + ArithOp::Mul => lhs * rhs, + ArithOp::Div => lhs / rhs, + }; + + info!( + "eval_arith_expr, op: {:?}, lhs: {:?}, rhs: {:?}", + op, lhs, rhs + ); + + Ok(Value::Number(Number(result))) + } + + fn eval_assign_expr( + &mut self, + op: &AssignOp, + lhs: &'source Expr<'source>, + rhs: &'source Expr<'source>, + ) -> Result { + let lhs = if let Expr::Var(span) = lhs { + span.text() + } else { + return Err(anyhow!("expect a variable, got: {:?}", lhs)); + }; + + let (_, variable, _) = self.add_variable_or(lhs, false, None)?; + + let rhs = self.eval_expr(rhs)?; + + // TODO: handle iterations + if variable[0] != Value::Undefined { + return Err(anyhow!("Redefinition for variable {:?}", lhs)); + } + + // TODO: optimize this + self.variables_assignment(lhs, &rhs)?; + + info!( + "eval_assign_expr before, op: {:?}, lhs: {:?}, rhs: {:?}", + op, lhs, rhs + ); + + Ok(Value::Bool(true)) + } + + fn eval_stmt(&mut self, stmt: &'source LiteralStmt<'source>) -> Result { + let mut to_restore = vec![]; + for wm in &stmt.with_mods { + // Evaluate value and ref + let value = self.eval_expr(&wm.r#as)?; + let path = Parser::get_path_ref_components(&wm.refr)?; + let mut path: Vec<&str> = path.iter().map(|s| s.text()).collect(); + + // TODO: multiple modules and qualified path + if path.len() > 2 && format!("{}.{}", path[0], path[1]) == self.current_module_path { + path = path[1..].to_vec(); + } + + // Set new values in modifications table + let mut saved = false; + for (i, _) in path.iter().enumerate() { + let vref = Self::make_or_get_value_mut(&mut self.data, &path[0..i])?; + if vref == &Value::Undefined { + to_restore.push((path[0..i].to_vec(), vref.clone())); + saved = false; + break; + } + } + + // TODO: input + let vref = Self::make_or_get_value_mut(&mut self.data, &path[..])?; + if !saved { + to_restore.push((path, vref.clone())); + } + + *vref = value; + } + + let r = Ok(match &stmt.literal { + Literal::Expr { expr, .. } => { + let value = self.eval_expr(expr)?; + if let Value::Bool(bool) = value { + bool + } else { + // panic!(); + // TODO: confirm this + // For non-booleans, treat anything other than undefined as true + value != Value::Undefined + } + } + Literal::SomeVars { vars, .. } => { + for var in vars { + let name = var.text(); + if let Ok((_, variable, _)) = self.add_variable_or(name, false, None) { + if variable != Value::Undefined { + return Err(anyhow!( + "duplicated definition of local variable {}", + name + )); + } + } + } + true + } + Literal::SomeIn { + key, + value, + collection, + .. + } => { + let value = self.eval_membership(key, value, collection)?; + if let Value::Bool(bool) = value { + bool + } else { + panic!(); + } + } + _ => unimplemented!(), + }); + + for (path, value) in to_restore.into_iter().rev() { + if value == Value::Undefined { + unimplemented!("handle undefined restore"); + } else { + let vref = Self::make_or_get_value_mut(&mut self.data, &path[..])?; + *vref = value; + } + } + r + } + + fn eval_stmts_in_loop( + &mut self, + stmts: &'source [LiteralStmt<'source>], + loops: &[LoopExpr<'source>], + ) -> Result { + if loops.is_empty() { + if !stmts.is_empty() { + // Evaluate the current statement whose loop expressions have been hoisted. + if !self.eval_stmt(&stmts[0])? { + return Ok(false); + } + self.eval_stmts(&stmts[1..]) + } else { + self.eval_stmts(stmts) + } + } else { + let loop_expr = &loops[0]; + let mut result = false; + match self.eval_expr(loop_expr.value)? { + Value::Array(items) => { + for v in items.iter() { + self.loop_var_values.insert(loop_expr.expr, v.clone()); + result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result; + } + } + Value::Set(items) => { + for v in items.iter() { + self.loop_var_values.insert(loop_expr.expr, v.clone()); + result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result; + } + } + Value::Object(obj) => { + for (_, v) in obj.iter() { + self.loop_var_values.insert(loop_expr.expr, v.clone()); + result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result; + } + } + _ => { + return Err(loop_expr.span.source.error( + loop_expr.span.line, + loop_expr.span.col, + "item cannot be indexed", + )); + } + } + self.loop_var_values.remove(loop_expr.expr); + // Return true if at least on iteration returned true + Ok(result) + } + } + + fn eval_output_expr_in_loop(&mut self, loops: &[LoopExpr<'source>]) -> Result { + if loops.is_empty() { + let (key_expr, output_expr) = self.get_exprs_from_context()?; + + match (key_expr, output_expr) { + (Some(ke), Some(oe)) => { + let key = self.eval_expr(ke)?; + let value = self.eval_expr(oe)?; + + let ctx = self.contexts.last_mut().unwrap(); + if key != Value::Undefined && value != Value::Undefined { + let map = ctx.value.as_object_mut()?; + match map.get(&key) { + Some(pv) if *pv != value => { + let span = ke.span(); + return Err(span.source.error( + span.line, + span.col, + format!( + "value for key `{}` generated multiple times: `{}` and `{}`", + serde_json::to_string_pretty(&key)?, + serde_json::to_string_pretty(&pv)?, + serde_json::to_string_pretty(&value)?, + ) + .as_str(), + )); + } + _ => map.insert(key, value), + }; + } else { + ctx.value = Value::Undefined; + }; + } + (None, Some(oe)) => { + let output = self.eval_expr(oe)?; + let ctx = self.contexts.last_mut().unwrap(); + if output != Value::Undefined { + match &mut ctx.value { + Value::Array(a) => { + Rc::make_mut(a).push(output); + } + Value::Set(ref mut s) => { + Rc::make_mut(s).insert(output); + } + _ => bail!("internal error: invalid context value"), + } + } else { + ctx.value = Value::Undefined; + } + } + // No output expression. + // TODO: should we just push a Bool(true)? + _ => (), + } + + // Push the context back so that it is available to the caller. + // self.contexts.push(ctx); + return Ok(true); + } + + // Try out values in current loop expr. + let loop_expr = &loops[0]; + let mut result = false; + match self.eval_expr(loop_expr.value)? { + Value::Array(items) => { + for v in items.iter() { + self.loop_var_values.insert(loop_expr.expr, v.clone()); + result = self.eval_output_expr_in_loop(&loops[1..])? || result; + } + } + Value::Set(items) => { + for v in items.iter() { + self.loop_var_values.insert(loop_expr.expr, v.clone()); + result = self.eval_output_expr_in_loop(&loops[1..])? || result; + } + } + Value::Object(obj) => { + for (_, v) in obj.iter() { + self.loop_var_values.insert(loop_expr.expr, v.clone()); + result = self.eval_output_expr_in_loop(&loops[1..])? || result; + } + } + _ => { + return Err(loop_expr.span.source.error( + loop_expr.span.line, + loop_expr.span.col, + "item cannot be indexed", + )); + } + } + self.loop_var_values.remove(loop_expr.expr); + Ok(result) + } + + fn get_current_context(&self) -> Result<&Context<'source>> { + match self.contexts.last() { + Some(ctx) => Ok(ctx), + _ => bail!("internal error: no active context found"), + } + } + + fn get_exprs_from_context( + &self, + ) -> Result<( + Option<&'source Expr<'source>>, + Option<&'source Expr<'source>>, + )> { + let ctx = self.get_current_context()?; + Ok((ctx.key_expr, ctx.output_expr)) + } + + fn eval_output_expr(&mut self) -> Result { + // Evaluate output expression after all the statements have been executed. + + let (key_expr, output_expr) = self.get_exprs_from_context()?; + let mut loops = vec![]; + + if let Some(ke) = &key_expr { + self.hoist_loops_impl(ke, &mut loops); + } + if let Some(oe) = &output_expr { + self.hoist_loops_impl(oe, &mut loops); + } + + self.eval_output_expr_in_loop(&loops[..])?; + + let ctx = self.get_current_context()?; + if let Some(_oe) = ctx.output_expr { + // Ensure that at least one output was generated. + Ok(ctx.value != Value::Undefined) + } else { + Ok(true) + } + } + + fn eval_stmts(&mut self, stmts: &'source [LiteralStmt<'source>]) -> Result { + let mut result = true; + + for (idx, stmt) in stmts.iter().enumerate() { + if !result { + break; + } + + let loop_exprs = self.hoist_loops(&stmt.literal); + if !loop_exprs.is_empty() { + // If there are hoisted loop expressions, execute subsequent statements + // within loops. + return self.eval_stmts_in_loop(&stmts[idx..], &loop_exprs[..]); + } + result = self.eval_stmt(stmt)?; + } + + if result { + result = self.eval_output_expr()?; + } + Ok(result) + } + + fn eval_query(&mut self, query: &'source Query<'source>) -> Result { + // Execute the query in a new scope + self.scopes.push(Scope::new()); + let r = self.eval_stmts(&query.stmts); + self.scopes.pop(); + r + } + + fn eval_array(&mut self, items: &'source Vec>) -> Result { + let mut array = Vec::new(); + + for item in items { + let term = self.eval_expr(item)?; + if term == Value::Undefined { + return Ok(Value::Undefined); + } + + array.push(term); + } + + Ok(Value::from_array(array)) + } + + fn eval_object(&mut self, fields: &'source Vec<(Span, Expr, Expr)>) -> Result { + let mut object = BTreeMap::new(); + + for (_, key, value) in fields { + // TODO: check this + // While the grammar defines a object-item as + // ( scalar | ref | var ) ":" term, the OPA + // implementation is more like expr ":" expr + let key = self.eval_expr(key)?; + let value = self.eval_expr(value)?; + object.insert(key, value); + } + + Ok(Value::from_map(object)) + } + + fn eval_set(&mut self, items: &'source Vec>) -> Result { + let mut set = BTreeSet::new(); + + for item in items { + let term = self.eval_expr(item)?; + if term == Value::Undefined { + return Ok(Value::Undefined); + } + set.insert(term); + } + + Ok(Value::from_set(set)) + } + + fn eval_membership( + &mut self, + key: &'source Expr<'source>, + value: &'source Option>, + collection: &'source Expr<'source>, + ) -> Result { + let key = self.eval_expr(key)?; + + let collection = self.eval_expr(collection)?; + + let result = match &collection { + Value::Array(array) => { + if let Some(value) = value { + let value = self.eval_expr(value)?; + collection[&key] == value + } else { + array.iter().any(|item| *item == key) + } + } + Value::Object(object) => { + if let Some(value) = value { + let value = self.eval_expr(value)?; + collection[&key] == value + } else { + object.values().into_iter().any(|item| *item == key) + } + } + Value::Set(set) => { + if value.is_some() { + false + //return Err(anyhow!("key-value pair is not supported for set")); + } else { + set.contains(&key) + } + } + _ => { + return Err(anyhow!("\"{}\" must be array, object, or set", collection)); + } + }; + + Ok(Value::Bool(result)) + } + + fn eval_array_compr( + &mut self, + term: &'source Expr<'source>, + query: &'source Query<'source>, + ) -> Result { + // Push new context + self.contexts.push(Context { + key_expr: None, + output_expr: Some(term), + value: Value::new_array(), + }); + + // Evaluate body first. + self.eval_query(query)?; + + match self.contexts.pop() { + Some(ctx) => Ok(ctx.value), + None => bail!("internal error: context already popped"), + } + } + + fn eval_set_compr( + &mut self, + term: &'source Expr<'source>, + query: &'source Query<'source>, + ) -> Result { + // Push new context + self.contexts.push(Context { + key_expr: None, + output_expr: Some(term), + value: Value::new_set(), + }); + + self.eval_query(query)?; + + match self.contexts.pop() { + Some(ctx) => Ok(ctx.value), + None => bail!("internal error: context already popped"), + } + } + + fn eval_object_compr( + &mut self, + key: &'source Expr<'source>, + value: &'source Expr<'source>, + query: &'source Query<'source>, + ) -> Result { + // Push new context + self.contexts.push(Context { + key_expr: Some(key), + output_expr: Some(value), + value: Value::new_object(), + }); + + self.eval_query(query)?; + + match self.contexts.pop() { + Some(ctx) => Ok(ctx.value), + None => bail!("internal error: context already popped"), + } + } + + fn lookup_function(&self, fcn: &'source Expr<'source>) -> Result<&'source Rule<'source>> { + let mut path = Self::get_path_string(fcn, None)?; + if !path.starts_with("data.") { + path = self.current_module_path.clone() + "." + &path; + } + + match self.functions.get(&path) { + Some(r) => Ok(r), + _ => { + bail!("function not found") + } + } + } + + fn eval_call( + &mut self, + span: &'source Span<'source>, + fcn: &'source Expr<'source>, + params: &'source Vec>, + ) -> Result { + let fcn_rule = match self.lookup_function(fcn) { + Ok(r) => r, + _ => { + return Err(span + .source + .error(span.line, span.col, "could not find function")) + } + }; + + let (args, output_expr, bodies) = match fcn_rule { + Rule::Spec { + head: RuleHead::Func { args, assign, .. }, + bodies, + .. + } => (args, assign.as_ref().map(|a| &a.value), bodies), + _ => bail!("internal error not a function"), + }; + + if args.len() != params.len() { + return Err(span.source.error( + span.line, + span.col, + format!( + "mismatch in number of arguments. supplied {}, expected {}", + params.len(), + args.len() + ) + .as_str(), + )); + } + + let mut args_scope = Scope::new(); + for (idx, a) in args.iter().enumerate() { + let a = match a { + Expr::Var(s) => s.text(), + _ => unimplemented!("destructuring function arguments"), + }; + //TODO: check call in params + args_scope.insert( + a.to_string(), + Variable { + value: self.eval_expr(¶ms[idx])?, + partial: false, + _has_default: false, + }, + ); + } + + let ctx = Context { + key_expr: None, + output_expr, + value: Value::new_set(), + }; + + // Back up local variables of current function and empty + // the local variables of callee function. + let scopes = std::mem::take(&mut self.scopes); + + // Set the arguments scope. + self.scopes.push(args_scope); + let value = self.eval_rule_bodies(ctx, span, bodies)?; + let result = match &value { + Value::Set(s) if s.len() == 1 => Ok(s.iter().next().unwrap().clone()), + Value::Set(s) if !s.is_empty() => Err(span.source.error( + span.line, + span.col, + format!("function produced multiple outputs {:?}", value).as_str(), + )), + // If the function successfully executed, but did not return any value, then return true. + Value::Set(s) if s.is_empty() && output_expr.is_none() => Ok(Value::Bool(true)), + + // If the function execution resulted in undefined, then propagate it. + Value::Undefined => Ok(Value::Undefined), + _ => bail!("internal error: function did not return set {value:?}"), + }; + + // Restore local variables for current context. + self.scopes = scopes; + result + } + + fn get_var_value(&self, name: &str) -> Option { + // Lookup local variables and arguments. + for scope in self.scopes.iter().rev() { + if let Some(v) = scope.get(name) { + return Some(v.value.clone()); + } + } + None + } + + fn ensure_rule_evaluated(&mut self, path: String) -> Result<()> { + if let Some(rules) = self.rules.get(&path) { + for r in rules.clone() { + if !self.processed.contains(r) { + let module = self.get_rule_module(r)?; + self.eval_rule(module, r)?; + } + } + } + // Evaluate the associated default rules after non-default rules + if let Some(rules) = self.default_rules.get(&path) { + for (r, _) in rules.clone() { + if !self.processed.contains(r) { + let module = self.get_rule_module(r)?; + let prev_module = self.set_current_module(Some(module))?; + self.eval_default_rule(r)?; + self.set_current_module(prev_module)?; + } + } + } + Ok(()) + } + + fn lookup_var(&mut self, name: &str, fields: &[&str]) -> Result { + // Return local variable/argument. + if let Some(v) = self.get_var_value(name) { + return Ok(Self::get_value_chained(v, fields)); + } + + // Handle input. + if name == "input" { + return Ok(Self::get_value_chained(self.input.clone(), fields)); + } + + // Ensure that rules are evaluated + if name == "data" { + // Evaluate rule corresponding to longest matching path. + for i in (1..fields.len() + 1).rev() { + let path = "data.".to_owned() + &fields[0..i].join("."); + if self.rules.get(&path).is_some() || self.default_rules.get(&path).is_some() { + self.ensure_rule_evaluated(path)?; + break; + } + } + Ok(Self::get_value_chained(self.data.clone(), fields)) + } else { + // Add module prefix and ensure that any matching rule is evaluated. + let module_path = + Self::get_path_string(&self.current_module()?.package.refr, Some("data"))?; + let path = module_path + "." + name; + self.ensure_rule_evaluated(path)?; + + let mut path: Vec<&str> = + Parser::get_path_ref_components(&self.module.unwrap().package.refr)? + .iter() + .map(|s| s.text()) + .collect(); + path.push(name); + + let value = Self::get_value_chained(self.data.clone(), &path[..]); + Ok(Self::get_value_chained(value, fields)) + } + } + + fn eval_expr(&mut self, expr: &'source Expr<'source>) -> Result { + match expr { + Expr::Null(_) => Ok(Value::Null), + Expr::True(_) => Ok(Value::Bool(true)), + Expr::False(_) => Ok(Value::Bool(false)), + Expr::Number(span) => match serde_json::from_str::(span.text()) { + Ok(v) => Ok(v), + Err(e) => Err(span.source.error( + span.line, + span.col, + format!("could not parse number. {}", e).as_str(), + )), + }, + // TODO: Handle string vs rawstring + Expr::String(span) => Ok(Value::String(span.text().to_string())), + Expr::RawString(span) => Ok(Value::String(span.text().to_string())), + + // TODO: Handle undefined variables + Expr::Var(_) => self.eval_chained_ref_dot_or_brack(expr), + Expr::RefDot { .. } => self.eval_chained_ref_dot_or_brack(expr), + Expr::RefBrack { .. } => match self.loop_var_values.get(expr) { + Some(v) => Ok(v.clone()), + _ => self.eval_chained_ref_dot_or_brack(expr), + }, + + // Expressions with operators + Expr::ArithExpr { op, lhs, rhs, .. } => self.eval_arith_expr(op, lhs, rhs), + Expr::AssignExpr { op, lhs, rhs, .. } => self.eval_assign_expr(op, lhs, rhs), + Expr::BinExpr { op, lhs, rhs, .. } => self.eval_bin_expr(op, lhs, rhs), + Expr::BoolExpr { op, lhs, rhs, .. } => self.eval_bool_expr(op, lhs, rhs), + Expr::Membership { + key, + value, + collection, + .. + } => self.eval_membership(key, value, collection), + + // Creation expression + Expr::Array { items, .. } => self.eval_array(items), + Expr::Object { fields, .. } => self.eval_object(fields), + Expr::Set { items, .. } => self.eval_set(items), + + // Comprehensions + Expr::ArrayCompr { term, query, .. } => self.eval_array_compr(term, query), + Expr::ObjectCompr { + key, value, query, .. + } => self.eval_object_compr(key, value, query), + Expr::SetCompr { term, query, .. } => self.eval_set_compr(term, query), + Expr::UnaryExpr { .. } => unimplemented!("unar expr is umplemented"), + Expr::Call { span, fcn, params } => self.eval_call(span, fcn, params), + } + } + + fn make_rule_context( + &self, + head: &'source RuleHead<'source>, + ) -> Result<(Context<'source>, Vec>)> { + //TODO: include "data" ? + let mut path = Parser::get_path_ref_components(&self.module.unwrap().package.refr)?; + + match head { + RuleHead::Compr { refr, assign, .. } => { + let output_expr = assign.as_ref().map(|assign| &assign.value); + let (refr, key_expr, value) = match refr { + Expr::RefBrack { refr, index, .. } => { + (refr.as_ref(), Some(index.as_ref()), Value::new_object()) + } + _ => (refr, None, Value::new_array()), + }; + + Parser::get_path_ref_components_into(refr, &mut path)?; + + Ok(( + Context { + key_expr, + output_expr, + value, + }, + path, + )) + } + RuleHead::Set { refr, key, .. } => { + Parser::get_path_ref_components_into(refr, &mut path)?; + Ok(( + Context { + key_expr: None, + output_expr: key.as_ref(), + value: Value::new_set(), + }, + path, + )) + } + _ => unimplemented!("unhandled rule ref type"), + } + } + + fn get_rule_module(&self, rule: &'source Rule<'source>) -> Result<&'source Module<'source>> { + for m in &self.modules { + if m.policy.contains(rule) { + return Ok(m); + } + } + bail!("internal error: could not find module for rule"); + } + + fn eval_rule_bodies( + &mut self, + ctx: Context<'source>, + span: &'source Span<'source>, + bodies: &'source Vec>, + ) -> Result { + let mut result = true; + self.scopes.push(Scope::new()); + + if bodies.is_empty() { + self.contexts.push(ctx.clone()); + result = self.eval_output_expr()?; + } else { + for body in bodies { + self.contexts.push(ctx.clone()); + result = self.eval_query(&body.query)?; + + // The body evaluated successfully. + if result { + break; + } + + if bodies.len() > 1 { + unimplemented!("else bodies"); + } + } + } + + let ctx = match self.contexts.pop() { + Some(ctx) => ctx, + _ => bail!("internal error: rule's context already popped"), + }; + + // Drop local variables and leave the local scope + self.scopes.pop(); + + Ok(match result { + true => match &ctx.value { + Value::Object(_) => ctx.value, + Value::Array(a) if a.len() == 1 => a[0].clone(), + Value::Array(a) if a.is_empty() => Value::Bool(true), + Value::Array(_) => { + return Err(span.source.error( + span.line, + span.col, + "complete rules should not produce multiple outputs", + )) + } + Value::Set(_) => ctx.value, + _ => unimplemented!("todo fix this"), + }, + false => Value::Undefined, + }) + } + + fn get_value_chained(mut obj: Value, path: &[&str]) -> Value { + for p in path { + obj = obj[&Value::String(p.to_string())].clone(); + } + obj + } + + #[inline] + fn make_or_get_value_mut<'a>(obj: &'a mut Value, paths: &[&str]) -> Result<&'a mut Value> { + if paths.is_empty() { + return Ok(obj); + } + + let key = Value::String(paths[0].to_owned()); + if obj == &Value::Undefined { + *obj = Value::new_object(); + } + if let Value::Object(map) = obj { + if map.get(&key).is_none() { + Rc::make_mut(map).insert(key.clone(), Value::Undefined); + } + } + + match obj { + Value::Object(map) => match Rc::make_mut(map).get_mut(&key) { + Some(v) if paths.len() == 1 => Ok(v), + Some(v) => Self::make_or_get_value_mut(v, &paths[1..]), + _ => bail!("internal error: unexpected"), + }, + Value::Undefined if paths.len() > 1 => { + *obj = Value::new_object(); + Self::make_or_get_value_mut(obj, paths) + } + Value::Undefined => Ok(obj), + _ => bail!("make: not an object {obj:?}"), + } + } + + pub fn merge_value(span: &Span<'source>, value: &mut Value, mut new: Value) -> Result<()> { + match (value, &mut new) { + (v @ Value::Undefined, _) => *v = new, + (Value::Set(ref mut set), Value::Set(new)) => { + Rc::make_mut(set).append(Rc::make_mut(new)) + } + (Value::Object(map), Value::Object(new)) => { + for (k, v) in new.iter() { + match map.get(k) { + Some(pv) if *pv != *v => { + return Err(span.source.error( + span.line, + span.col, + format!( + "value for key `{}` generated multiple times: `{}` and `{}`", + serde_json::to_string_pretty(&k)?, + serde_json::to_string_pretty(&pv)?, + serde_json::to_string_pretty(&v)?, + ) + .as_str(), + )); + } + _ => Rc::make_mut(map).insert(k.clone(), v.clone()), + }; + } + } + _ => bail!("could not merge value"), + }; + Ok(()) + } + + pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result { + let mut comps = vec![]; + let mut expr = Some(refr); + while expr.is_some() { + match expr { + Some(Expr::RefDot { refr, field, .. }) => { + comps.push(field.text()); + expr = Some(refr); + } + Some(Expr::RefBrack { refr, index, .. }) + if matches!(index.as_ref(), Expr::String(_)) => + { + if let Expr::String(s) = index.as_ref() { + comps.push(s.text()); + expr = Some(refr); + } + } + Some(Expr::Var(v)) => { + comps.push(v.text()); + expr = None; + } + _ => bail!("not a simple ref"), + } + } + if let Some(d) = document { + comps.push(d); + }; + comps.reverse(); + Ok(comps.join(".")) + } + + fn set_current_module( + &mut self, + module: Option<&'source Module<'source>>, + ) -> Result>> { + let m = self.module; + if let Some(m) = module { + self.current_module_path = Self::get_path_string(&m.package.refr, Some("data"))?; + } + self.module = module; + Ok(m) + } + + pub fn update_function_table(&mut self) -> Result<()> { + for module in self.modules.clone() { + let prev_module = self.set_current_module(Some(module))?; + let module_path = + Self::get_path_string(&self.current_module()?.package.refr, Some("data"))?; + for rule in &module.policy { + if let Rule::Spec { + head: RuleHead::Func { refr, .. }, + .. + } = rule + { + let mut path = + Parser::get_path_ref_components(&self.current_module()?.package.refr)?; + + Parser::get_path_ref_components_into(refr, &mut path)?; + let path: Vec<&str> = path.iter().map(|s| s.text()).collect(); + + if path.len() > 1 { + let value = + Self::make_or_get_value_mut(&mut self.data, &path[0..path.len() - 1])?; + if value == &Value::Undefined { + *value = Value::new_object(); + } + } + + let full_path = Self::get_path_string(refr, Some(module_path.as_str()))?; + self.functions.insert(full_path, rule); + } + } + self.set_current_module(prev_module)?; + } + Ok(()) + } + + fn get_rule_refr(rule: &'source Rule<'source>) -> &'source Expr<'source> { + match rule { + Rule::Spec { head, .. } => match &head { + RuleHead::Compr { refr, .. } + | RuleHead::Set { refr, .. } + | RuleHead::Func { refr, .. } => refr, + }, + Rule::Default { refr, .. } => refr, + } + } + + fn eval_default_rule(&mut self, rule: &'source Rule<'source>) -> Result<()> { + // Skip reprocessing rule. + if self.processed.contains(rule) { + return Ok(()); + } + + match rule { + Rule::Default { + span, refr, value, .. + } => { + let mut path = Parser::get_path_ref_components(&self.module.unwrap().package.refr)?; + + let (refr, index) = match refr { + Expr::RefBrack { refr, index, .. } => (refr.as_ref(), Some(index.as_ref())), + Expr::Var(_) => (refr, None), + _ => bail!("invalid token {:?} with the default keyword", refr), + }; + + Parser::get_path_ref_components_into(refr, &mut path)?; + let paths: Vec<&str> = path.iter().map(|s| s.text()).collect(); + + if matches!( + value, + Expr::Var(_) | Expr::RefBrack { .. } | Expr::RefDot { .. } + ) { + bail!("illegal default rule (value contains a variable or reference)"); + } + let value = self.eval_expr(value)?; + + // Assume at this point that all the non-default rules have been evaluated. + // Merge the default value only if + // 1. The corresponding variable does not have value yet + // 2. The corresponding index in the object does not have value yet + if let Some(index) = index { + let index = self.eval_expr(index)?; + let mut object = Value::new_object(); + object.as_object_mut()?.insert(index.clone(), value); + + let vref = Self::make_or_get_value_mut(&mut self.data, &paths)?; + + if let Value::Object(btree) = &vref { + if !btree.contains_key(&index) { + Self::merge_value(span, vref, object)?; + } + } else if let Value::Undefined = vref { + Self::merge_value(span, vref, object)?; + } + } else { + let vref = Self::make_or_get_value_mut(&mut self.data, &paths)?; + if let Value::Undefined = &vref { + Self::merge_value(span, vref, value)?; + } + }; + + self.processed.insert(rule); + } + _ => println!("not a default rule"), + } + + Ok(()) + } + + fn eval_rule( + &mut self, + module: &'source Module<'source>, + rule: &'source Rule<'source>, + ) -> Result<()> { + // Skip reprocessing rule + if self.processed.contains(rule) { + return Ok(()); + } + + // Skip default rules + if let Rule::Default { .. } = rule { + return Ok(()); + } + + self.active_rules.push(rule); + if self.active_rules.iter().filter(|&r| r == &rule).count() == 2 { + let mut msg = String::default(); + for r in &self.active_rules { + let refr = Self::get_rule_refr(r); + let span = refr.span(); + msg += span + .source + .message(span.line, span.col, "depends on", "") + .as_str(); + } + msg += "cyclic evaluation"; + let refr = Self::get_rule_refr(rule); + let span = refr.span(); + return Err(span.source.error( + span.line, + span.col, + format!("recursion detected when evaluating rule:{msg}").as_str(), + )); + } + + let prev_module = self.set_current_module(Some(module))?; + match rule { + Rule::Spec { + span, + head: rule_head, + bodies: rule_body, + } => { + if matches!(rule_head, RuleHead::Func { .. }) { + return Ok(()); + } + + let (ctx, mut path) = self.make_rule_context(rule_head)?; + let special_set = matches!((ctx.output_expr, &ctx.value), (None, Value::Set(_))); + let value = match self.eval_rule_bodies(ctx, span, rule_body)? { + Value::Set(_) if special_set => { + let entry = path[path.len() - 1].text(); + let mut s = BTreeSet::new(); + s.insert(Value::String(entry.to_owned())); + path = path[0..path.len() - 1].to_vec(); + Value::from_set(s) + } + v => v, + }; + + if value != Value::Undefined { + let paths: Vec<&str> = path.iter().map(|s| s.text()).collect(); + let vref = Self::make_or_get_value_mut(&mut self.data, &paths[..])?; + Self::merge_value(span, vref, value)?; + } + } + _ => bail!("internal error: unexpected"), + } + self.set_current_module(prev_module)?; + self.processed.insert(rule); + match self.active_rules.pop() { + Some(r) if r == rule => Ok(()), + _ => bail!("internal error: current rule not active"), + } + } + + pub fn eval(&mut self, data: &Option, input: &Option) -> Result { + if let Some(input) = input { + self.input = input.clone(); + + info!("input: {:#?}", self.input); + } + if let Some(data) = data { + self.data = data.clone(); + } + + self.update_function_table()?; + self.gather_rules()?; + + for module in self.modules.clone() { + for rule in &module.policy { + self.eval_rule(module, rule)?; + } + } + + // Defer the evaluation of the default rules to here + for module in self.modules.clone() { + let prev_module = self.set_current_module(Some(module))?; + for rule in &module.policy { + self.eval_default_rule(rule)?; + } + self.set_current_module(prev_module)?; + } + + Ok(self.data.clone()) + } + + pub fn eval_query_snippet(&mut self, snippet: &'source Expr<'source>) -> Result { + // Create a new scope for evaluating the expression. + self.scopes.push(Scope::new()); + let prev_module = self.set_current_module(self.modules.last().copied())?; + let value = self.eval_expr(snippet)?; + // Pop the scope. + let scope = self.scopes.pop(); + let r = match snippet { + Expr::AssignExpr { .. } => { + if let Some(scope) = scope { + let mut r = Value::new_object(); + let map = r.as_object_mut()?; + // Capture each binding. + for (name, v) in scope { + map.insert(Value::String(name), v.value); + } + Ok(r) + } else { + bail!("internal error: expression scope not found"); + } + } + _ => Ok(value), + }; + self.set_current_module(prev_module)?; + r + } + + fn gather_rules(&mut self) -> Result<()> { + for module in self.modules.clone() { + let prev_module = self.set_current_module(Some(module))?; + for rule in &module.policy { + let refr = Self::get_rule_refr(rule); + if let Rule::Spec { .. } = rule { + // Adjust refr to ensure simple ref. + // TODO: refactor. + let refr = match refr { + Expr::RefBrack { index, .. } + if matches!(index.as_ref(), Expr::String(_)) => + { + refr + } + Expr::RefBrack { refr, .. } => refr, + _ => refr, + }; + let path = Self::get_path_string(refr, None)?; + let path = self.current_module_path.clone() + "." + &path; + match self.rules.entry(path) { + Entry::Occupied(o) => { + o.into_mut().push(rule); + } + Entry::Vacant(v) => { + v.insert(vec![rule]); + } + } + } else if let Rule::Default { .. } = rule { + let (refr, index) = match refr { + // TODO: Validate the index + Expr::RefBrack { refr, index, .. } => { + if !matches!( + index.as_ref(), + Expr::True(_) | Expr::False(_) | Expr::Number(_) | Expr::String(_) + ) { + // OPA's behavior is ignoring the non-scalar index + bail!("index is not a scalar value"); + } + + let index = self.eval_expr(index)?; + + (refr.as_ref(), Some(index.to_string())) + } + _ => (refr, None), + }; + + let path = Self::get_path_string(refr, None)?; + let path = self.current_module_path.clone() + "." + &path; + match self.default_rules.entry(path) { + Entry::Occupied(o) => { + for (_, i) in o.get() { + if index.is_some() && i.is_some() { + let old = i.as_ref().unwrap(); + let new = index.as_ref().unwrap(); + if old == new { + bail!("multiple default rules for the variable with the same index"); + } + } else { + bail!("conflict type with the default rules"); + } + } + o.into_mut().push((rule, index)); + } + Entry::Vacant(v) => { + v.insert(vec![(rule, index)]); + } + } + } + } + self.set_current_module(prev_module)?; + } + Ok(()) + } +} diff --git a/src/lexer.rs b/src/lexer.rs new file mode 100644 index 0000000..100e431 --- /dev/null +++ b/src/lexer.rs @@ -0,0 +1,506 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. + +use core::fmt::{Debug, Formatter}; +use core::iter::Peekable; +use core::str::CharIndices; + +use crate::value::Value; +use anyhow::{anyhow, bail, Result}; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct Source<'source> { + pub file: &'source str, + pub contents: &'source str, + pub lines: Vec<&'source str>, +} + +impl<'source> Source<'source> { + pub fn message(&self, line: u16, col: u16, kind: &str, msg: &str) -> String { + if line as usize > self.lines.len() { + return format!("{}: invalid line {} specified", self.file, line); + } + + let line_str = format!("{}", line); + let line_num_width = line_str.len() + 1; + let col_spaces = col as usize - 1; + + format!( + "\n-->{}:{}:{}\n{: anyhow::Error { + anyhow!(self.message(line, col, "error", msg)) + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct Span<'source> { + pub source: &'source Source<'source>, + pub line: u16, + pub col: u16, + pub start: u16, + pub end: u16, +} + +impl<'source> Span<'source> { + pub fn text(&self) -> &'source str { + &self.source.contents[self.start as usize..self.end as usize] + } +} + +impl<'source> Debug for Span<'source> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + let t = self.text().escape_debug().to_string(); + let max = 32; + let (txt, trailer) = if t.len() > max { + (&t[0..max], "...") + } else { + (t.as_str(), "") + }; + + f.write_fmt(format_args!( + "{}:{}:{}:{}, \"{}{}\"", + self.line, self.col, self.start, self.end, txt, trailer + )) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum TokenKind { + Symbol, + String, + RawString, + Number, + Ident, + Eof, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Token<'source>(pub TokenKind, pub Span<'source>); + +#[derive(Clone)] +pub struct Lexer<'source> { + source: &'source Source<'source>, + iter: Peekable>, + line: u16, + col: u16, +} + +impl<'source> Lexer<'source> { + pub fn new(source: &'source Source<'source>) -> Self { + Self { + source, + iter: source.contents.char_indices().peekable(), + line: 1, + col: 1, + } + } + + fn peek(&mut self) -> (usize, char) { + match self.iter.peek() { + Some((index, chr)) => (*index, *chr), + _ => (self.source.contents.len(), '\x00'), + } + } + + fn peekahead(&mut self, n: usize) -> (usize, char) { + match self.iter.clone().nth(n) { + Some((index, chr)) => (index, chr), + _ => (self.source.contents.len(), '\x00'), + } + } + + fn read_ident(&mut self) -> Result> { + let start = self.peek().0; + let col = self.col; + loop { + let ch = self.peek().1; + if ch.is_ascii_alphanumeric() || ch == '_' { + self.iter.next(); + } else { + break; + } + } + let end = self.peek().0; + self.col += (end - start) as u16; + Ok(Token( + TokenKind::Ident, + Span { + source: self.source, + line: self.line, + col, + start: start as u16, + end: end as u16, + }, + )) + } + + fn read_digits(&mut self) { + while self.peek().1.is_ascii_digit() { + self.iter.next(); + } + } + + // See https://www.json.org/json-en.html for number's grammar + fn read_number(&mut self) -> Result> { + let (start, chr) = self.peek(); + let col = self.col; + self.iter.next(); + + // Read integer part. + if chr != '0' { + // Starts with 1.. or 9. Read digits. + self.read_digits(); + } + + // Read fraction part + // . must be followed by at least 1 digit. + if self.peek().1 == '.' && self.peekahead(1).1.is_ascii_digit() { + self.iter.next(); // . + self.read_digits(); + } + + // Read exponent part + let ch = self.peek().1; + if ch == 'e' || ch == 'E' { + self.iter.next(); + // e must be followed by an optional sign and digits + if matches!(self.peek().1, '+' | '-') { + self.iter.next(); + } + // Read digits. Absence of digit will be validated by serde later. + self.read_digits(); + } + + let end = self.peek().0; + self.col += (end - start) as u16; + + // Check for invalid number.Valid number cannot be followed by + // these characters: + let ch = self.peek().1; + if ch == '_' || ch == '.' || ch.is_ascii_alphanumeric() { + return Err(self.source.error(self.line, self.col, "invalid number")); + } + + // Ensure that the number is parsable in Rust. + match serde_json::from_str::<'source, Value>(&self.source.contents[start..end]) { + Ok(_) => (), + Err(e) => { + let serde_msg = &e.to_string(); + let msg = match &serde_msg { + m if m.contains("out of range") => "out of range", + m if m.contains("invalid number") => "invalid number", + m if m.contains("expected value") => "expected value", + m if m.contains("trailing characters") => "trailing characters", + m => m.to_owned(), + }; + + bail!( + "{} {}", + self.source.error( + self.line, + col, + "invalid number. serde_json cannot parse number:" + ), + msg + ) + } + } + + Ok(Token( + TokenKind::Number, + Span { + source: self.source, + line: self.line, + col, + start: start as u16, + end: end as u16, + }, + )) + } + + fn read_raw_string(&mut self) -> Result> { + self.iter.next(); + self.col += 1; + let (start, _) = self.peek(); + let (line, col) = (self.line, self.col); + loop { + let (_, ch) = self.peek(); + self.iter.next(); + match ch { + '`' => { + self.col += 1; + break; + } + '\x00' => { + return Err(self.source.error(line, col, "unmatched `")); + } + '\t' => self.col += 4, + '\n' => { + self.line += 1; + self.col = 1; + } + _ => self.col += 1, + } + } + let end = self.peek().0; + Ok(Token( + TokenKind::RawString, + Span { + source: self.source, + line, + col, + start: start as u16, + end: end as u16 - 1, + }, + )) + } + + fn read_string(&mut self) -> Result> { + let (line, col) = (self.line, self.col); + self.iter.next(); + self.col += 1; + let (start, _) = self.peek(); + loop { + let (offset, ch) = self.peek(); + let col = self.col + (offset - start) as u16; + match ch { + '"' | '#' | '\x00' => { + break; + } + '\\' => { + self.iter.next(); + let (_, ch) = self.peek(); + self.iter.next(); + match ch { + // json escape sequence + '"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't' => (), + 'u' => { + for _i in 0..4 { + let (offset, ch) = self.peek(); + let col = self.col + (offset - start) as u16; + if !ch.is_ascii_hexdigit() { + return Err(self.source.error( + line, + col, + "invalid hex escape sequence", + )); + } + self.iter.next(); + } + } + _ => return Err(self.source.error(line, col, "invalid escape sequence")), + } + } + _ => { + // check for valid json chars + let col = self.col + (offset - start) as u16; + if !('\u{0020}'..='\u{10FFFF}').contains(&ch) { + return Err(self.source.error(line, col, "invalid character in string")); + } + self.iter.next(); + } + } + } + + if self.peek().1 != '"' { + return Err(self.source.error(line, col, "unmatched \"")); + } + + self.iter.next(); + let end = self.peek().0; + self.col += (end - start) as u16; + + // Ensure that the string is parsable in Rust. + match serde_json::from_str::<'source, String>(&self.source.contents[start - 1..end]) { + Ok(_) => (), + Err(e) => { + let serde_msg = &e.to_string(); + let msg = serde_msg; + bail!( + "{} {}", + self.source + .error(self.line, col, "serde_json cannot parse string:"), + msg + ) + } + } + + Ok(Token( + TokenKind::String, + Span { + source: self.source, + line, + col: col + 1, + start: start as u16, + end: end as u16 - 1, + }, + )) + } + + fn skip_ws(&mut self) -> Result<()> { + // Only the 4 json whitespace characters are recognized. + // https://www.crockford.com/mckeeman.html. + // Additionally, comments are also skipped. + // A tab is considered 4 space characters. + 'outer: loop { + match self.peek().1 { + ' ' => self.col += 1, + '\t' => self.col += 4, + '\r' => { + if self.peekahead(1).1 != '\n' { + return Err(self.source.error( + self.line, + self.col, + "\\r must be followed by \\n", + )); + } + } + '\n' => { + self.col = 1; + self.line += 1; + } + '#' => { + self.iter.next(); + loop { + match self.peek().1 { + '\n' | '\x00' => continue 'outer, + _ => self.iter.next(), + }; + } + } + _ => break, + } + self.iter.next(); + } + Ok(()) + } + + pub fn next_token(&mut self) -> Result> { + self.skip_ws()?; + + let (start, chr) = self.peek(); + let col = self.col; + + match chr { + // Special case for - followed by digit which is a + // negative json number. + // . followed by digit is invalid number. + '-' | '.' if self.peekahead(1).1.is_ascii_digit() => { + self.read_number() + } + // grouping characters + '{' | '}' | '[' | ']' | '(' | ')' | + // arith operator + '+' | '-' | '*' | '/' | + // bin operator + '&' | '|' | + // separators + ',' | ';' | '.' => { + self.col += 1; + self.iter.next(); + Ok(Token(TokenKind::Symbol, Span { + source: self.source, + line: self.line, + col, + start: start as u16, + end: start as u16 + 1, + })) + } + ':' => { + self.col += 1; + self.iter.next(); + let mut end = start as u16 + 1; + if self.peek().1 == '=' { + self.col += 1; + self.iter.next(); + end += 1; + } + Ok(Token(TokenKind::Symbol, Span { + source: self.source, + line: self.line, + col, + start: start as u16, + end + })) + } + // < <= > >= = == + '<' | '>' | '=' => { + self.col += 1; + self.iter.next(); + if self.peek().1 == '=' { + self.col += 1; + self.iter.next(); + }; + Ok(Token(TokenKind::Symbol, Span { + source: self.source, + line: self.line, + col, + start: start as u16, + end: self.peek().0 as u16, + })) + } + '!' if self.peekahead(1).1 == '=' => { + self.col += 2; + self.iter.next(); + self.iter.next(); + Ok(Token(TokenKind::Symbol, Span { + source: self.source, + line: self.line, + col, + start: start as u16, + end: self.peek().0 as u16, + })) + } + '"' => self.read_string(), + '`' => self.read_raw_string(), + '\x00' => Ok(Token(TokenKind::Eof, Span { + source: self.source, + line:self.line, + col, + start: start as u16, + end: start as u16 + })), + _ if chr.is_ascii_digit() => self.read_number(), + _ if chr.is_ascii_alphabetic() || chr == '_' => { + let mut ident = self.read_ident()?; + if ident.1.text() == "set" && self.peek().1 == '(' { + // set immediately followed by ( is treated as set( if + // the next token is ). + let state = (self.iter.clone(), self.line, self.col); + self.iter.next(); + + // Check it next token is ). + let next_tok = self.next_token()?; + let is_setp = next_tok.1.text() == ")"; + + // Restore state + (self.iter, self.line, self.col) = state; + + if is_setp { + self.iter.next(); + self.col += 1; + ident.1.end += 1; + } + } + Ok(ident) + } + _ => Err(self.source.error(self.line, self.col, "invalid character")) + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..186d3d3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. + +pub mod ast; +pub mod builtins; +pub mod interpreter; +pub mod lexer; +pub mod parser; +pub mod value; + +pub use ast::*; +pub use interpreter::*; +pub use lexer::*; +pub use parser::*; +pub use value::*; diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..f60f600 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,1572 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. + +use crate::ast::*; +use crate::lexer::*; +use std::collections::BTreeMap; + +use anyhow::{anyhow, bail, Result}; + +#[derive(Clone)] +pub struct Parser<'source> { + source: &'source Source<'source>, + lexer: Lexer<'source>, + tok: Token<'source>, + line: u16, + end: u16, + future_keywords: BTreeMap<&'source str, Span<'source>>, + in_default_value: bool, +} + +const FUTURE_KEYWORDS: [&str; 4] = ["contains", "every", "if", "in"]; + +impl<'source> Parser<'source> { + pub fn new(source: &'source Source<'source>) -> Result { + let mut lexer = Lexer::new(source); + let tok = lexer.next_token()?; + Ok(Self { + source, + lexer, + tok, + line: 0, + end: 0, + future_keywords: BTreeMap::new(), + in_default_value: false, + }) + } + + pub fn next_token(&mut self) -> Result<()> { + self.line = self.tok.1.line; + self.end = self.tok.1.end; + self.tok = self.lexer.next_token()?; + Ok(()) + } + + fn expect(&mut self, text: &str, context: &str) -> Result<()> { + if self.tok.1.text() == text { + self.next_token() + } else { + let msg = format!("expecting `{}` {}", text, context); + Err(self.source.error(self.tok.1.line, self.tok.1.col, &msg)) + } + } + + fn is_imported_future_keyword(&self, kw: &str) -> bool { + self.future_keywords.get(kw).is_some() + } + + pub fn warn_future_keyword(&self) { + let kw = self.tok.1.text(); + let msg = format!( + "`{kw}` will be treated as identifier due to missing `import future.keywords.{kw}`" + ); + println!( + "{}", + self.source + .message(self.tok.1.line, self.tok.1.col, "warning", &msg) + ); + } + + pub fn set_future_keyword(&mut self, kw: &'source str, span: &Span<'source>) -> Result<()> { + match &self.future_keywords.get(kw) { + Some(s) => Err(self.source.error( + span.line, + span.col, + format!( + "this import shadows previous import of `{kw}` defined at:{}", + self.source + .message(s.line, s.col, "", "this import is shadowed.") + ) + .as_str(), + )), + None => { + self.future_keywords.insert(kw, span.clone()); + Ok(()) + } + } + } + + pub fn get_path_ref_components_into( + refr: &Expr<'source>, + comps: &mut Vec>, + ) -> Result<()> { + match refr { + Expr::RefDot { refr, field, .. } => { + Self::get_path_ref_components_into(refr, comps)?; + comps.push(field.clone()); + } + Expr::RefBrack { refr, index, .. } => { + Self::get_path_ref_components_into(refr, comps)?; + Self::get_path_ref_components_into(index, comps)?; + } + Expr::Var(v) => comps.push(v.clone()), + Expr::String(s) => comps.push(s.clone()), + _ => bail!("not a simple ref"), + } + Ok(()) + } + + pub fn get_path_ref_components(refr: &Expr<'source>) -> Result>> { + let mut comps = vec![]; + Self::get_path_ref_components_into(refr, &mut comps)?; + Ok(comps) + } + + fn handle_import_future_keywords(&mut self, comps: &Vec>) -> Result { + if comps.len() >= 2 && comps[0].text() == "future" && comps[1].text() == "keywords" { + match comps.len() - 2 { + 1 => self.set_future_keyword(comps[2].text(), &comps[2])?, + 0 => { + let span = &comps[1]; + for kw in FUTURE_KEYWORDS.iter() { + self.set_future_keyword(kw, span)?; + } + } + _ => { + let s = &comps[3]; + return Err(self + .source + .error(s.line, s.col - 1, "invalid future keyword")); + } + } + Ok(true) + } else if !comps.is_empty() && comps[0].text() == "future" { + let s = &comps[0]; + Err(self + .source + .error(s.line, s.col, "invalid import, must be `future.keywords`")) + } else { + Ok(false) + } + } + + pub fn parse_future_keyword( + &mut self, + kw: &str, + is_optional: bool, + context: &str, + ) -> Result<()> { + if self.tok.1.text() == kw { + match &self.future_keywords.get(kw) { + Some(_) => self.next_token(), + None => { + self.warn_future_keyword(); + Ok(()) + } + } + } else if !is_optional { + // Required future keyword is missing. + self.expect(kw, context) + } else { + // Keyword is optional. + Ok(()) + } + } + + fn is_keyword(&self, ident: &'source str) -> bool { + matches!( + ident, + "as" | "default" + | "else" + | "false" + | "import" + | "package" + | "not" + | "null" + | "some" + | "true" + | "with" + ) + } + + fn parse_ident(&mut self) -> Result> { + let span = self.tok.1.clone(); + match self.tok.0 { + TokenKind::Ident if self.is_keyword(span.text()) => Err(self.source.error( + self.tok.1.line, + self.tok.1.col, + &format!("unexpected keyword `{}`", span.text()), + )), + TokenKind::Ident => { + self.next_token()?; + Ok(span) + } + _ => Err(self + .source + .error(self.tok.1.line, self.tok.1.col, "expecting identifier")), + } + } + + fn parse_var(&mut self) -> Result> { + let span = self.tok.1.clone(); + match self.tok.0 { + TokenKind::Ident + if self.is_keyword(span.text()) || self.is_imported_future_keyword(span.text()) => + { + Err(self.source.error( + self.tok.1.line, + self.tok.1.col, + &format!("unexpected keyword `{}`", span.text()), + )) + } + TokenKind::Ident => { + self.next_token()?; + Ok(span) + } + _ => Err(self + .source + .error(self.tok.1.line, self.tok.1.col, "expecting identifier")), + } + } + + fn parse_scalar_or_var(&mut self) -> Result> { + let span = self.tok.1.clone(); + let node = match &self.tok.0 { + TokenKind::Number => Expr::Number(span), + TokenKind::String => Expr::String(span), + TokenKind::RawString => Expr::RawString(span), + TokenKind::Ident => match self.tok.1.text() { + "null" => Expr::Null(span), + "true" => Expr::True(span), + "false" => Expr::False(span), + _ => return Ok(Expr::Var(self.parse_var()?)), + }, + _ => { + return Err(self.source.error( + self.tok.1.line, + self.tok.1.col, + "expecting expression", + )) + } + }; + self.next_token()?; + Ok(node) + } + + fn parse_compr(&mut self, delim: &str) -> Result<(Expr<'source>, Query<'source>)> { + // Save the state. + let state = self.clone(); + let mut span = self.tok.1.clone(); + + // Parse the first expression as a ref. + let term = match self.parse_ref() { + Ok(e) if self.tok.1.text() == "|" => e, + _ => { + // Not a comprehension. Restore state. + *self = state; + bail!("internal - not a compr"); + } + }; + + let query_span = self.tok.1.clone(); + self.next_token()?; + let pos = self.end; + match self.parse_query(query_span, delim) { + Ok(query) => { + span.end = self.end; + Ok((term, query)) + } + Err(_) if self.end == pos => { + // No progress was made in parsing the query. + // Restore state and try parsing as set, array or object. + *self = state; + bail!("internal - not a compr"); + } + Err(err) => Err(err), + } + } + + fn parse_compr_or_array(&mut self) -> Result> { + // Save the state. + let mut span = self.tok.1.clone(); + self.expect("[", "while parsing array comprehension or array")?; + + let pos = self.end; + match self.parse_compr("]") { + Ok((term, query)) => { + span.end = self.end; + Ok(Expr::ArrayCompr { + span, + term: Box::new(term), + query, + }) + } + Err(_) if self.end == pos => { + // No progress was made in parsing comprehension. + // Parse as array. + let mut items = vec![]; + if self.tok.1.text() != "]" { + items.push(self.parse_in_expr()?); + while self.tok.1.text() == "," { + self.next_token()?; + match self.tok.1.text() { + "]" => break, + "" if self.tok.0 == TokenKind::Eof => break, + _ => items.push(self.parse_in_expr()?), + } + } + } + self.expect("]", "while parsing array")?; + span.end = self.end; + Ok(Expr::Array { span, items }) + } + Err(err) => Err(err), + } + } + + fn parse_compr_set_or_object(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + self.expect("{", "while parsing set, object or comprehension")?; + + let pos = self.end; + match self.parse_compr("}") { + Ok((term, query)) => { + span.end = self.end; + return Ok(Expr::SetCompr { + span, + term: Box::new(term), + query, + }); + } + Err(err) if self.end != pos => { + // Some progress was made parsing the set comprehension. + // Report errors. + return Err(err); + } + _ => (), + } + + // It could be a set, object or object comprehension. + // In all the cases, the first expressoin must parse successfully. + if self.tok.1.text() == "}" { + self.next_token()?; + span.end = self.end; + return Ok(Expr::Object { + span, + fields: vec![], + }); + } + + let mut item_span = self.tok.1.clone(); + let first = self.parse_in_expr()?; + + if self.tok.1.text() != ":" { + // Parse as set. + let mut items = vec![first]; + while self.tok.1.text() == "," { + self.next_token()?; + match self.tok.1.text() { + "}" => break, + "" if self.tok.0 == TokenKind::Eof => break, + _ => items.push(self.parse_in_expr()?), + } + } + self.expect("}", "while parsing set")?; + span.end = self.end; + return Ok(Expr::Set { span, items }); + } + + // Parse as object. + self.next_token()?; + + let pos = self.end; + match self.parse_compr("}") { + Ok((term, query)) => { + span.end = self.end; + return Ok(Expr::ObjectCompr { + span, + key: Box::new(first), + value: Box::new(term), + query, + }); + } + Err(err) if self.end != pos => { + // Some progress was made parsing the object comprehension. + // Report errors. + return Err(err); + } + _ => (), + } + + // Parse object + let mut items = vec![]; + + let value = self.parse_in_expr()?; + item_span.end = self.end; + items.push((item_span, first, value)); + + while self.tok.1.text() == "," { + self.next_token()?; + let item_start = self.tok.1.start; + let key = match self.tok.1.text() { + "}" => break, + "" if self.tok.0 == TokenKind::Eof => break, + _ => self.parse_in_expr()?, + }; + + let mut item_span = self.tok.1.clone(); + span.start = item_start; + self.expect(":", "while parsing object item")?; + let value = self.parse_in_expr()?; + item_span.end = self.end; + + items.push((item_span, key, value)); + } + + self.expect("}", "while parsing object")?; + span.end = self.end; + + Ok(Expr::Object { + span, + fields: items, + }) + } + + fn parse_empty_set(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + self.expect("set(", "while parsing empty set")?; + self.expect(")", "while parsing empty set")?; + span.end = self.tok.1.end; + Ok(Expr::Set { + span, + items: vec![], + }) + } + + fn parse_parens_expr(&mut self) -> Result> { + self.next_token()?; + let expr = self.parse_membership_expr()?; + self.expect(")", "while parsing parenthesized expression")?; + //TODO: if needed introduce a parens-expr node or adjust expr's span. + Ok(expr) + } + + fn parse_unary_expr(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + self.next_token()?; + let expr = self.parse_in_expr()?; + span.end = self.end; + Ok(Expr::UnaryExpr { + span, + expr: Box::new(expr), + }) + } + + fn parse_ref(&mut self) -> Result> { + let start = self.tok.1.start; + let mut term = match self.tok.1.text() { + "[" => self.parse_compr_or_array()?, + "{" => self.parse_compr_set_or_object()?, + "set(" => self.parse_empty_set()?, + "(" => return self.parse_parens_expr(), + "-" => return self.parse_unary_expr(), + _ => self.parse_scalar_or_var()?, + }; + + let mut possible_fcn = matches!(&term, Expr::Var(_)); + + loop { + let mut span = self.tok.1.clone(); + let sep_pos = span.start; + span.start = start; + match self.tok.1.text() { + "." | "[" if self.tok.1.start != self.end => { + if self.line != self.tok.1.line { + // Newline encountered. This could be a separate + // literal. + break; + } + bail!( + "{}", + self.source.error( + self.tok.1.line, + self.tok.1.col, + format!("invalid whitespace before {}", self.tok.1.text()).as_str() + ) + ); + } + "." => { + // Read identifier. + self.next_token()?; + let field = self.parse_var()?; + span.end = self.end; + + // Disallow any whitespace between . and identifier. + if field.start != sep_pos + 1 { + bail!( + "{}", + self.source.error( + field.line, + field.col - 1, + "invalid whitespace between . and identifier" + ) + ); + } + term = Expr::RefDot { + span, + refr: Box::new(term), + field, + }; + } + "[" => { + self.next_token()?; + let index = self.parse_in_expr()?; + + // If the index is a string, the ref could be path to a function. + possible_fcn = possible_fcn && matches!(&index, Expr::String(_)); + + self.expect("]", "while parsing bracketed reference")?; + span.end = self.end; + + term = Expr::RefBrack { + span, + refr: Box::new(term), + index: Box::new(index), + }; + } + "(" if possible_fcn => { + self.next_token()?; + let mut args = vec![self.parse_in_expr()?]; + while self.tok.1.text() == "," { + self.next_token()?; + match self.tok.1.text() { + ")" => break, + "" if self.tok.0 == TokenKind::Eof => break, + _ => args.push(self.parse_in_expr()?), + } + } + self.expect(")", "while parsing call expr")?; + span.end = self.end; + term = Expr::Call { + span, + fcn: Box::new(term), + params: args, + }; + + // The expression can no longer be a function after the call. + possible_fcn = false; + } + _ => break, + } + } + + if self.in_default_value { + if let Some((kind, span)) = match &term { + Expr::Var(v) => Some(("var", v)), + Expr::RefDot { span, .. } => Some(("ref", span)), + Expr::Call { span, .. } => Some(("call", span)), + Expr::RefBrack { span, .. } => Some(("ref", span)), + _ => None, + } { + return Err(self.source.error( + span.line, + span.col, + format!("invalid {kind} in default value").as_str(), + )); + } + } + + Ok(term) + } + + fn parse_term(&mut self) -> Result> { + self.parse_ref() + } + + fn parse_mul_div_expr(&mut self) -> Result> { + let start = self.tok.1.start; + let mut expr = self.parse_term()?; + + loop { + let mut span = self.tok.1.clone(); + span.start = start; + let op = match self.tok.1.text() { + "*" => ArithOp::Mul, + "/" => ArithOp::Div, + _ => return Ok(expr), + }; + self.next_token()?; + let right = self.parse_term()?; + span.end = self.end; + expr = Expr::ArithExpr { + span, + op, + lhs: Box::new(expr), + rhs: Box::new(right), + }; + } + } + + fn parse_arith_expr(&mut self) -> Result> { + let start = self.tok.1.start; + let mut expr = self.parse_mul_div_expr()?; + + loop { + let mut span = self.tok.1.clone(); + span.start = start; + let op = match self.tok.1.text() { + "+" => ArithOp::Add, + "-" => ArithOp::Sub, + _ => return Ok(expr), + }; + self.next_token()?; + let right = self.parse_mul_div_expr()?; + span.end = self.end; + expr = Expr::ArithExpr { + span, + op, + lhs: Box::new(expr), + rhs: Box::new(right), + }; + } + } + + fn parse_and_expr(&mut self) -> Result> { + let start = self.tok.1.start; + let mut expr = self.parse_arith_expr()?; + + while self.tok.1.text() == "&" { + let mut span = self.tok.1.clone(); + span.start = start; + self.next_token()?; + let right = self.parse_arith_expr()?; + span.end = self.end; + expr = Expr::BinExpr { + span, + op: BinOp::And, + lhs: Box::new(expr), + rhs: Box::new(right), + }; + } + Ok(expr) + } + + fn parse_or_expr(&mut self) -> Result> { + let start = self.tok.1.start; + let mut expr = self.parse_and_expr()?; + + while self.tok.1.text() == "|" { + let mut span = self.tok.1.clone(); + span.start = start; + self.next_token()?; + let right = self.parse_and_expr()?; + span.end = self.end; + expr = Expr::BinExpr { + span, + op: BinOp::Or, + lhs: Box::new(expr), + rhs: Box::new(right), + }; + } + Ok(expr) + } + + fn parse_bool_expr(&mut self) -> Result> { + let start = self.tok.1.start; + let mut expr = self.parse_or_expr()?; + loop { + let mut span = self.tok.1.clone(); + span.start = start; + let op = match self.tok.1.text() { + "<" => BoolOp::Lt, + "<=" => BoolOp::Le, + "==" => BoolOp::Eq, + ">=" => BoolOp::Ge, + ">" => BoolOp::Gt, + "!=" => BoolOp::Ne, + _ => break, + }; + self.next_token()?; + let right = self.parse_or_expr()?; + span.end = self.end; + expr = Expr::BoolExpr { + span, + op, + lhs: Box::new(expr), + rhs: Box::new(right), + }; + } + Ok(expr) + } + + fn parse_membership_tail( + &mut self, + start: u16, + mut expr1: Expr<'source>, + mut expr2: Option>, + ) -> Result> { + loop { + let mut span = self.tok.1.clone(); + span.start = start; + self.parse_future_keyword("in", false, "while parsing membership expression")?; + let expr3 = self.parse_bool_expr()?; + span.end = self.end; + expr1 = Expr::Membership { + span, + key: Box::new(expr1), + value: Box::new(expr2), + collection: Box::new(expr3), + }; + expr2 = None; + + if self.tok.1.text() != "in" { + break; + } + } + + Ok(expr1) + } + + fn parse_in_expr(&mut self) -> Result> { + let start = self.tok.1.start; + let mut expr = self.parse_bool_expr()?; + + while self.tok.1.text() == "in" { + expr = self.parse_membership_tail(start, expr, None)?; + } + + Ok(expr) + } + + pub fn parse_membership_expr(&mut self) -> Result> { + let start = self.tok.1.start; + let mut expr = self.parse_bool_expr()?; + + if self.tok.1.text() == "," { + self.next_token()?; + let value = self.parse_bool_expr()?; + expr = self.parse_membership_tail(start, expr, Some(value))?; + } + + while self.tok.1.text() == "in" { + expr = self.parse_membership_tail(start, expr, None)?; + } + + Ok(expr) + } + + fn parse_assign_expr(&mut self) -> Result> { + let state = self.clone(); + let start = self.tok.1.start; + let expr = self.parse_ref()?; + + let mut span = self.tok.1.clone(); + span.start = start; + let op = match self.tok.1.text() { + "=" => AssignOp::Eq, + ":=" => AssignOp::ColEq, + _ => { + *self = state; + return self.parse_membership_expr(); + } + }; + + self.next_token()?; + let right = self.parse_membership_expr()?; + span.end = self.end; + Ok(Expr::AssignExpr { + span, + op, + lhs: Box::new(expr), + rhs: Box::new(right), + }) + } + + fn parse_with_modifiers(&mut self) -> Result>> { + let mut modifiers = vec![]; + while self.tok.1.text() == "with" { + let mut span = self.tok.1.clone(); + self.next_token()?; + let refr = self.parse_path_ref()?; + self.expect("as", "while parsing with-modifier expression")?; + let r#as = self.parse_in_expr()?; + span.end = self.end; + modifiers.push(WithModifier { span, refr, r#as }); + } + Ok(modifiers) + } + + fn parse_every_stmt(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + let context = "Failed to parse `every` statement."; + self.parse_future_keyword("every", false, context)?; + + let key = self.parse_var()?; + let value = match self.tok.1.text() { + "," => { + self.next_token()?; + match self.parse_var() { + Ok(v) => Some(v), + Err(e) => { + return Err(self.source.error( + span.line, + span.col, + format!("Failed to parse `every` statement.\n{}", e).as_str(), + )) + } + } + } + _ => None, + }; + + self.parse_future_keyword("in", false, context)?; + let domain = self.parse_bool_expr()?; + let query_span = self.tok.1.clone(); + self.expect("{", context)?; + let query = self.parse_query(query_span, "}")?; + span.end = self.end; + + Ok(Literal::Every { + span, + key, + value, + domain, + query, + }) + } + + fn parse_some_stmt(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + self.expect("some", "while parsing some-decl")?; + + // parse any vars. + let mut vars = vec![self.tok.1.clone()]; + let mut refs = vec![self.parse_ref()?]; + + while self.tok.1.text() == "," { + self.next_token()?; + let mut span = self.tok.1.clone(); + refs.push(self.parse_ref()?); + span.end = self.end; + vars.push(span); + } + + if self.tok.1.text() != "in" || self.future_keywords.get("in").is_none() { + if self.tok.1.text() == "in" { + self.warn_future_keyword(); + } + // All the refs must be identifiers + for (idx, ref_expr) in refs.iter().enumerate() { + let span = &vars[idx]; + match ref_expr { + Expr::Var(_) => (), + _ => { + return Err(anyhow!( + "{}:{}:{} error: encountered `{}` while expecting identifier", + span.source.file, + span.line, + span.col, + span.text() + )); + } + } + } + + span.end = self.end; + return Ok(Literal::SomeVars { span, vars }); + } + + let (key, value) = match refs.len() { + 2 => (refs[0].clone(), Some(refs[1].clone())), + 1 => (refs[0].clone(), None), + _ => { + let span = &vars[2]; + return Err(anyhow!( + "{}:{}:{} error: encountered `{}` while expecting `in`", + span.source.file, + span.line, + span.col, + span.text() + )); + } + }; + + self.parse_future_keyword("in", false, "while parsing some-decl")?; + let collection = self.parse_bool_expr()?; // TODO: check this + Ok(Literal::SomeIn { + span, + key, + value, + collection, + }) + } + + fn parse_literal(&mut self) -> Result> { + match self.tok.1.text() { + "some" => return self.parse_some_stmt(), + "every" => { + if self.future_keywords.get("every").is_some() { + return self.parse_every_stmt(); + } + self.warn_future_keyword(); + } + _ => (), + } + let mut span = self.tok.1.clone(); + let not_expr = if self.tok.1.text() == "not" { + self.next_token()?; + true + } else { + false + }; + + let expr = self.parse_assign_expr()?; + span.end = self.end; + if not_expr { + Ok(Literal::NotExpr { span, expr }) + } else { + Ok(Literal::Expr { span, expr }) + } + } + + pub fn parse_literal_stmt(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + let literal = self.parse_literal()?; + let with_mods = self.parse_with_modifiers()?; + span.end = self.end; + + Ok(LiteralStmt { + span, + literal, + with_mods, + }) + } + + pub fn parse_query( + &mut self, + mut span: Span<'source>, + end_delim: &str, + ) -> Result> { + let state = self.clone(); + let _is_definite_query = matches!(self.tok.1.text(), "some" | "every"); + + // TODO: empty query? + let mut literals = vec![]; + + let stmt = match self.parse_literal_stmt() { + Ok(stmt) => stmt, + Err(e) if _is_definite_query => return Err(e), + _ => { + // There was error parsing the first literal + // Restore the state and return. + *self = state; + return Err(anyhow!("encountered , when expecting {}", end_delim)); + } + }; + + if self.tok.1.text() == "," { + // This is likely an array or set. + // Restore the state. + *self = state; + return Err(anyhow!("encountered , when expecting {}", end_delim)); + } + + literals.push(stmt); + + loop { + match self.tok.1.text() { + t if t == end_delim => break, + "" if self.tok.0 == TokenKind::Eof => break, + ";" => self.next_token()?, + _ => { + // Next literal must be on a new line. + if self.line == self.tok.1.line { + break; + } + } + } + let stmt = self.parse_literal_stmt()?; + literals.push(stmt); + } + + self.expect(end_delim, "while parsing query")?; + span.end = self.end; + Ok(Query { + span, + stmts: literals, + }) + } + + pub fn parse_rule_assign(&mut self) -> Result>> { + let mut span = self.tok.1.clone(); + + let op = match self.tok.1.text() { + "=" => { + self.next_token()?; + AssignOp::Eq + } + ":=" => { + self.next_token()?; + AssignOp::ColEq + } + _ => return Ok(None), + }; + + let expr = self.parse_membership_expr()?; + span.end = self.end; + Ok(Some(RuleAssign { + span, + op, + value: expr, + })) + } + + fn parse_path_ref(&mut self) -> Result> { + let start = self.tok.1.start; + let var = self.parse_var()?; + + let mut refr = Expr::Var(var); + loop { + let mut span = self.tok.1.clone(); + let sep_pos = span.start; + span.start = start; + match self.tok.1.text() { + "." | "[" if self.tok.1.start != self.end => { + bail!( + "{}", + self.source.error( + self.tok.1.line, + self.tok.1.col - 1, + format!("invalid whitespace before {}", self.tok.1.text()).as_str() + ) + ); + } + "." => { + // Read identifier. + self.next_token()?; + let field = self.parse_ident()?; + span.end = self.end; + + // Disallow any whitespace between . and identifier. + if field.start != sep_pos + 1 { + bail!( + "{}", + self.source.error( + field.line, + field.col - 1, + "invalid whitespace between . and identifier" + ) + ); + } + refr = Expr::RefDot { + span, + refr: Box::new(refr), + field, + }; + } + "[" => { + self.next_token()?; + let index = match &self.tok.0 { + TokenKind::String => Expr::String(self.tok.1.clone()), + _ => { + return Err(self.source.error( + self.tok.1.line, + self.tok.1.col, + "expected string", + )); + } + }; + self.next_token()?; + self.expect("]", "while parsing bracketed reference")?; + span.end = self.end; + refr = Expr::RefBrack { + span, + refr: Box::new(refr), + index: Box::new(index), + }; + } + _ => break, + } + } + + Ok(refr) + } + + fn check_rule_ref(&self, mut refr: &Expr) -> Result<()> { + // Only the last term can be non-string + loop { + refr = match refr { + Expr::RefDot { refr, .. } => refr, + Expr::RefBrack { span, refr, index } => { + if !matches!(index.as_ref(), Expr::String(_)) { + return Err(self.source.error( + span.line, + span.col, + "only the final ref term can be non-string", + )); + } + refr + } + Expr::Var(_) => return Ok(()), + _ => bail!("internal error: not a valid ref"), + }; + } + } + + fn parse_rule_ref(&mut self) -> Result> { + let start = self.tok.1.start; + let span = self.tok.1.clone(); + + let mut term = if self.tok.0 == TokenKind::Ident { + Expr::Var(self.parse_var()?) + } else { + return Err(self.source.error( + span.line, + span.col, + "expecting identifier. Failed to parse rule-ref.", + )); + }; + + loop { + let mut span = self.tok.1.clone(); + span.start = start; + match self.tok.1.text() { + // . and [ must not have any space between the previous token. + "." | "[" if self.tok.1.start != self.end => { + bail!( + "{}", + self.source.error( + self.tok.1.line, + self.tok.1.col - 1, + format!("invalid whitespace before {}", self.tok.1.text()).as_str() + ) + ); + } + "." => { + let sep_pos = self.tok.1.start; + self.next_token()?; + let field = self.parse_var()?; + span.end = self.end; + + // Disallow any whitespace between . and identifier. + if field.start != sep_pos + 1 { + bail!( + "{}", + self.source.error( + field.line, + field.col - 1, + "invalid whitespace between . and identifier" + ) + ); + } + term = Expr::RefDot { + span, + refr: Box::new(term), + field, + }; + } + "[" => { + self.next_token()?; + let index = self.parse_membership_expr()?; + span.end = self.end; + self.expect("]", "while parsing bracketed reference")?; + term = Expr::RefBrack { + span, + refr: Box::new(term), + index: Box::new(index), + }; + } + _ => break, + } + } + + Ok(term) + } + + pub fn parse_rule_head(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + + let rule_ref = self.parse_rule_ref()?; + match self.tok.1.text() { + "(" => { + self.check_rule_ref(&rule_ref)?; + self.next_token()?; + let mut args = vec![self.parse_term()?]; + while self.tok.1.text() == "," { + self.next_token()?; + match self.tok.1.text() { + ")" => break, + "" if self.tok.0 == TokenKind::Eof => break, + _ => args.push(self.parse_term()?), + } + } + self.expect(")", "while parsing function rule args")?; + let assign = self.parse_rule_assign()?; + + span.end = self.end; + Ok(RuleHead::Func { + span, + refr: rule_ref, + args, + assign, + }) + } + "contains" => { + self.check_rule_ref(&rule_ref)?; + self.next_token()?; + let key = self.parse_membership_expr()?; + span.end = self.end; + Ok(RuleHead::Set { + span, + refr: rule_ref, + key: Some(key), + }) + } + _ => { + let assign = self.parse_rule_assign()?; + span.end = self.end; + + // Ensure that only the last term can be non-string. + match &rule_ref { + Expr::RefBrack { refr, .. } => self.check_rule_ref(refr)?, + Expr::RefDot { refr, .. } => self.check_rule_ref(refr)?, + _ => (), + } + + // Determine whether to create a set or a compr + let is_set_follower = !self.is_keyword(self.tok.1.text()) + && !self.is_imported_future_keyword(self.tok.1.text()); + if assign.is_none() && is_set_follower { + match &rule_ref { + Expr::RefBrack { refr, index, .. } + if matches!(refr.as_ref(), Expr::Var(_)) => + { + return Ok(RuleHead::Set { + span, + refr: refr.as_ref().clone(), + key: Some(index.as_ref().clone()), + }); + } + Expr::RefDot { refr, .. } if matches!(refr.as_ref(), Expr::Var(_)) => { + return Ok(RuleHead::Set { + span, + refr: rule_ref, + key: None, + }); + } + _ => (), + } + } + + // Default to a compr rule. + Ok(RuleHead::Compr { + span, + refr: rule_ref, + assign, + }) + } + } + } + + pub fn if_is_keyword(&self) -> bool { + self.future_keywords.get("if").is_some() + } + + pub fn parse_query_or_literal_stmt(&mut self) -> Result> { + let state = self.clone(); + let mut span = self.tok.1.clone(); + + if self.tok.1.text() == "{" { + self.next_token()?; + let pos = self.end; + match self.parse_query(span.clone(), "}") { + Ok(query) => return Ok(query), + Err(e) if pos != self.end => { + // Error encountered while parsing query. + return Err(e); + } + _ => (), + } + } + + // Restore state. + *self = state; + let stmts = vec![self.parse_literal_stmt()?]; + span.end = self.end; + Ok(Query { span, stmts }) + } + + pub fn parse_rule_bodies(&mut self) -> Result>> { + let mut span = self.tok.1.clone(); + let mut bodies = vec![]; + + let assign = None; + let has_query = match self.tok.1.text() { + "if" if self.if_is_keyword() => { + self.next_token()?; + let query = self.parse_query_or_literal_stmt()?; + span.end = self.end; + bodies.push(RuleBody { + span, + assign, + query, + }); + true + } + "if" => { + self.warn_future_keyword(); + false + } + "{" => { + self.next_token()?; + let query = self.parse_query(span.clone(), "}")?; + span.end = self.end; + bodies.push(RuleBody { + span, + assign, + query, + }); + true + } + _ => false, + }; + + match self.tok.1.text() { + "{" if has_query => self.parse_query_blocks(&mut bodies)?, + "else" if has_query => self.parse_else_blocks(&mut bodies)?, + _ => (), + } + + Ok(bodies) + } + + pub fn parse_query_blocks(&mut self, bodies: &mut Vec>) -> Result<()> { + while self.tok.1.text() == "{" { + let mut span = self.tok.1.clone(); + self.next_token()?; + let query = self.parse_query(span.clone(), "}")?; + span.end = self.end; + bodies.push(RuleBody { + span, + assign: None, + query, + }); + } + Ok(()) + } + + pub fn parse_else_blocks(&mut self, bodies: &mut Vec>) -> Result<()> { + loop { + let mut span = self.tok.1.clone(); + + match self.tok.1.text() { + "{" => { + return Err(self.source.error( + self.tok.1.line, + self.tok.1.col, + "expected `else` keyword", + )) + } + "else" => self.next_token()?, + _ => break, + } + + let assign = self.parse_rule_assign()?; + + match self.tok.1.text() { + "if" if self.if_is_keyword() => { + self.next_token()?; + let query = self.parse_query_or_literal_stmt()?; + span.end = self.end; + bodies.push(RuleBody { + span, + assign, + query, + }); + } + "{" => { + self.next_token()?; + let query = self.parse_query(span.clone(), "}")?; + span.end = self.end; + bodies.push(RuleBody { + span, + assign, + query, + }); + } + _ if assign.is_none() => { + if self.tok.1.text() == "if" { + self.warn_future_keyword(); + } + return Err(self.source.error( + self.tok.1.line, + self.tok.1.col, + "expected assignment or query after `else`", + )); + } + _ => break, + } + } + Ok(()) + } + + pub fn parse_default_rule(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + self.expect("default", "while parsing default rule")?; + let rule_ref = self.parse_rule_ref()?; + + let op = match self.tok.1.text() { + "=" => AssignOp::Eq, + ":=" => AssignOp::ColEq, + _ => { + self.expect(":=", "while parsing default rule")?; + // Should never reach here. + AssignOp::Eq + } + }; + self.next_token()?; + + // todo: Rego errors for binary expressions here, but they are + // somehow valid in a comprehension + self.in_default_value = true; + let value = self.parse_term()?; + self.in_default_value = false; + span.end = self.end; + Ok(Rule::Default { + span, + refr: rule_ref, + op, + value, + }) + } + + pub fn parse_rule(&mut self) -> Result> { + let pos = self.end; + match self.parse_default_rule() { + Ok(r) => return Ok(r), + Err(e) if pos != self.end => return Err(e), + _ => (), + } + + let mut span = self.tok.1.clone(); + let head = self.parse_rule_head()?; + let bodies = self.parse_rule_bodies()?; + span.end = self.end; + Ok(Rule::Spec { span, head, bodies }) + } + + fn parse_package(&mut self) -> Result> { + let mut span = self.tok.1.clone(); + self.expect("package", "Missing package declaration.")?; + let name = self.parse_path_ref()?; + span.end = self.end; + Ok(Package { span, refr: name }) + } + + fn check_and_add_import( + &self, + import: Import<'source>, + imports: &mut Vec>, + ) -> Result<()> { + let comps: Vec<&str> = Self::get_path_ref_components(&import.refr)? + .iter() + .map(|s| s.text()) + .collect(); + + for imp in imports.iter() { + let imp_comps: Vec<&str> = Self::get_path_ref_components(&imp.refr)? + .iter() + .map(|s| s.text()) + .collect(); + + let shadow = match (&imp.r#as, &import.r#as) { + (Some(i1), Some(i2)) if i1.text() == i2.text() => true, + (None, None) if imp_comps == comps => true, + _ => false, + }; + + if shadow { + return Err(self.source.error( + import.span.line, + import.span.col, + format!( + "import shadows following import defined earlier:{}", + self.source.message( + imp.span.line, + imp.span.col, + "", + "this import is shadowed" + ) + ) + .as_str(), + )); + } + } + + imports.push(import); + Ok(()) + } + + fn parse_imports(&mut self) -> Result>> { + let mut imports = vec![]; + while self.tok.1.text() == "import" { + let mut span = self.tok.1.clone(); + self.next_token()?; + let refr = self.parse_path_ref()?; + + let comps = Self::get_path_ref_components(&refr)?; + if !matches!(comps[0].text(), "data" | "future" | "input") { + return Err(self.source.error( + comps[0].line, + comps[0].col, + "import path must begin with one of: {data, future, input}", + )); + } + + let is_future_kw = self.handle_import_future_keywords(&comps)?; + + let var = if self.tok.1.text() == "as" { + if is_future_kw { + return Err(self.source.error( + self.tok.1.line, + self.tok.1.col, + "`future` imports cannot be aliased", + )); + } + + self.next_token()?; + let var = self.parse_var()?; + if var.text() == "_" { + return Err(self.source.error( + var.line, + var.col, + "`_` cannot be used as alias", + )); + } + Some(var) + } else { + None + }; + span.end = self.end; + + // TODO: interpreter must check that all the imports are used. + // future.keywords don't have to be used. + self.check_and_add_import( + Import { + span, + refr, + r#as: var, + }, + &mut imports, + )?; + } + + Ok(imports) + } + + pub fn parse(&mut self) -> Result> { + let package = self.parse_package()?; + let imports = self.parse_imports()?; + + let mut policy = vec![]; + while self.tok.0 != TokenKind::Eof { + policy.push(self.parse_rule()?); + } + + Ok(Module { + package, + imports, + policy, + }) + } +} diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..9f82bb9 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,325 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. + +use core::fmt; +use std::collections::{BTreeMap, BTreeSet}; +use std::ops; +use std::rc::Rc; + +use anyhow::{anyhow, Result}; +use ordered_float::OrderedFloat; +use serde::de::{self, Deserializer}; +use serde::ser::{SerializeMap, Serializer}; +use serde::{Deserialize, Serialize}; + +// TODO: rego uses BigNum which has arbitrary precision. But there seems +// to be some bugs with it e.g ((a + b) -a) == b doesn't return true for large +// values of a and b. +// Json doesn't specify a limit on precision, but in practice double (f64) seems +// to be enough to support most use cases and portability too. +// See discussions in jq's repository. +// For now we use OrderedFloat. We can't use f64 directly since it doesn't +// implement Ord trait. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Number(pub OrderedFloat); + +impl Serialize for Number { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let n_f64 = self.0 .0; + let n_i64 = n_f64 as i64; + let n_u64 = n_f64 as u64; + + if n_u64 as f64 == n_f64 { + serializer.serialize_u64(n_u64) + } else if n_i64 as f64 == n_f64 { + serializer.serialize_i64(n_i64) + } else { + serializer.serialize_f64(n_f64) + } + } +} + +struct NumberVisitor; +impl<'de> de::Visitor<'de> for NumberVisitor { + type Value = Number; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a json number") + } + + fn visit_f64(self, v: f64) -> Result { + Ok(Number(OrderedFloat(v))) + } + + fn visit_u64(self, v: u64) -> Result { + Ok(Number(OrderedFloat(v as f64))) + } + + fn visit_i64(self, v: i64) -> Result { + Ok(Number(OrderedFloat(v as f64))) + } +} + +impl<'de> Deserialize<'de> for Number { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_f64(NumberVisitor) + } +} + +impl fmt::Display for Number { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +// We cannot use serde_json::Value because Rego has set type and object's key can be +// other rego values. +// BTree is more efficient that a hast table. Another alternative is a sorted vector. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[serde(untagged)] +pub enum Value { + // Json data types. serde will automatically map json to these variants. + Null, + Bool(bool), + String(String), + Number(Number), + Array(Rc>), + Object(Rc>), + + // Extra rego data type + Set(Rc>), + + // Indicate that a value is undefined + Undefined, +} + +impl Serialize for Value { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::Error; + match self { + Value::Null => serializer.serialize_none(), + Value::Bool(b) => serializer.serialize_bool(*b), + Value::String(s) => serializer.serialize_str(s.as_str()), + Value::Number(n) => n.serialize(serializer), + Value::Array(a) => a.serialize(serializer), + Value::Object(fields) => { + let mut map = serializer.serialize_map(Some(fields.len()))?; + for (k, v) in fields.iter() { + match k { + Value::String(_) => map.serialize_entry(k, v)?, + _ => { + let key_str = serde_json::to_string(k).map_err(Error::custom)?; + map.serialize_entry(&key_str, v)? + } + } + } + map.end() + } + + // display set as an array + Value::Set(s) => s.serialize(serializer), + + // display undefined as a special string + Value::Undefined => serializer.serialize_str(""), + } + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match serde_json::to_string(self) { + Ok(s) => write!(f, "{}", s), + Err(_e) => Err(std::fmt::Error), + } + } +} + +impl Value { + pub fn new_object() -> Value { + Value::from_map(BTreeMap::new()) + } + + pub fn new_set() -> Value { + Value::from_set(BTreeSet::new()) + } + + pub fn new_array() -> Value { + Value::from_array(vec![]) + } + + pub fn from_json_str(json: &str) -> Result { + Ok(serde_json::from_str(json)?) + } + + pub fn to_json_str(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) + } +} + +impl Value { + pub fn from_f64(v: f64) -> Value { + Value::Number(Number(OrderedFloat(v))) + } + + pub fn from_array(a: Vec) -> Value { + Value::Array(Rc::new(a)) + } + + pub fn from_set(s: BTreeSet) -> Value { + Value::Set(Rc::new(s)) + } + + pub fn from_map(m: BTreeMap) -> Value { + Value::Object(Rc::new(m)) + } + + pub fn is_null(&self) -> bool { + matches!(self, Value::Null) + } + + pub fn is_undefined(&self) -> bool { + matches!(self, Value::Null) + } + + pub fn as_bool(&self) -> Result<&bool> { + match self { + Value::Bool(b) => Ok(b), + _ => Err(anyhow!("not a bool")), + } + } + + pub fn as_bool_mut(&mut self) -> Result<&mut bool> { + match self { + Value::Bool(b) => Ok(b), + _ => Err(anyhow!("not a bool")), + } + } + + pub fn as_string(&self) -> Result<&String> { + match self { + Value::String(s) => Ok(s), + _ => Err(anyhow!("not a string")), + } + } + + pub fn as_string_mut(&mut self) -> Result<&mut String> { + match self { + Value::String(s) => Ok(s), + _ => Err(anyhow!("not a string")), + } + } + + pub fn as_number(&self) -> Result<&Number> { + match self { + Value::Number(n) => Ok(n), + _ => Err(anyhow!("not a number")), + } + } + + pub fn as_number_mut(&mut self) -> Result<&mut Number> { + match self { + Value::Number(n) => Ok(n), + _ => Err(anyhow!("not a number")), + } + } + + pub fn as_array(&self) -> Result<&Vec> { + match self { + Value::Array(a) => Ok(a), + _ => Err(anyhow!("not an array")), + } + } + + pub fn as_array_mut(&mut self) -> Result<&mut Vec> { + match self { + Value::Array(a) => Ok(Rc::make_mut(a)), + _ => Err(anyhow!("not an array")), + } + } + + pub fn as_set(&self) -> Result<&BTreeSet> { + match self { + Value::Set(s) => Ok(s), + _ => Err(anyhow!("not a set")), + } + } + + pub fn as_set_mut(&mut self) -> Result<&mut BTreeSet> { + match self { + Value::Set(s) => Ok(Rc::make_mut(s)), + _ => Err(anyhow!("not a set")), + } + } + + pub fn as_object(&self) -> Result<&BTreeMap> { + match self { + Value::Object(m) => Ok(m), + _ => Err(anyhow!("not an object")), + } + } + + pub fn as_object_mut(&mut self) -> Result<&mut BTreeMap> { + match self { + Value::Object(m) => Ok(Rc::make_mut(m)), + _ => Err(anyhow!("not an object")), + } + } +} + +impl ops::Index for Value { + type Output = Value; + + fn index(&self, index: usize) -> &Self::Output { + match self.as_array() { + Ok(a) if index < a.len() => &a[index], + _ => &Value::Undefined, + } + } +} + +impl ops::Index<&str> for Value { + type Output = Value; + + fn index(&self, key: &str) -> &Self::Output { + &self[&Value::String(key.to_owned())] + } +} + +impl ops::Index<&String> for Value { + type Output = Value; + + fn index(&self, key: &String) -> &Self::Output { + &self[&Value::String(key.clone())] + } +} + +impl ops::Index<&Value> for Value { + type Output = Value; + + fn index(&self, key: &Value) -> &Self::Output { + match (self, &key) { + (Value::Object(o), _) => match &o.get(key) { + Some(v) => v, + _ => &Value::Undefined, + }, + (Value::Array(a), Value::Number(n)) => { + let index = n.0 .0 as usize; + if index < a.len() { + &a[index] + } else { + &Value::Undefined + } + } + _ => &Value::Undefined, + } + } +} diff --git a/tests/interpreter/cases/arithmetic/mod.rs b/tests/interpreter/cases/arithmetic/mod.rs new file mode 100644 index 0000000..079652e --- /dev/null +++ b/tests/interpreter/cases/arithmetic/mod.rs @@ -0,0 +1,45 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +#![cfg(test)] + +use crate::interpreter::*; +use anyhow::Result; + +#[test] +fn basic() -> Result<()> { + let rego = r#" + package test + + add { + 1 + 2 == 3 + } + + sub { + 5 - 1 == 4 + } + + mul { + 3 * 4 == 12 + } + + # Lock down float operation. + div { + 21 / 5 == 4.2 + } +"#; + + let expected = Value::from_json_str( + r#" { + "add" : true, + "sub" : true, + "mul" : true, + "div" : true +}"#, + )?; + + assert_eq!( + eval_file(&[rego.to_owned()], None, None, "data.test")?, + expected + ); + Ok(()) +} diff --git a/tests/interpreter/cases/basic_001.yaml b/tests/interpreter/cases/basic_001.yaml new file mode 100644 index 0000000..dd66376 --- /dev/null +++ b/tests/interpreter/cases/basic_001.yaml @@ -0,0 +1,32 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + cases: + - data: {} + modules: + - | + package test + + add { + 1 + 2 == 3 + } + + sub { + 5 - 1 == 4 + } + + mul { + 3 * 4 == 12 + } + + # Lock down float operation. + div { + 21 / 5 == 4.2 + } + note: arithmetic/basic + query: data.test + sort_bindings: true + want_result: + add: true + sub: true + mul: true + div: true diff --git a/tests/interpreter/cases/builtins/compare.yaml b/tests/interpreter/cases/builtins/compare.yaml new file mode 100644 index 0000000..eedb7fd --- /dev/null +++ b/tests/interpreter/cases/builtins/compare.yaml @@ -0,0 +1,234 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: number-equals + data: {} + modules: + - | + package test + + v = 1 + r = [ + # Comparison with number + v == 1, + v == 2, + + # Comparison with primitives + v == null, + v == "hello", + v == true, + v == false, + + # Comparison with arrays + v == [], + v == [v], + + # Comparison with sets + v == set(), + v == { v }, + + # Comparison with objects + v == {}, + v == { "a": v}, + ] + sort_bindings: false + query: data.test + want_result: + v: 1 + r: [ true, false, false, false, false, false, + false, false, false, false, false, false] + + - note: null-equals + data: {} + modules: + - | + package test + + v = null + r = [ + # Comparison with number + v == 1, + v == 2, + + # Comparison with primitives + v == null, + v == "hello", + v == true, + v == false, + + # Comparison with arrays + v == [], + v == [v], + + # Comparison with sets + v == set(), + v == { v }, + + # Comparison with objects + v == {}, + v == { "a": v}, + ] + sort_bindings: false + query: data.test + want_result: + v: null + r: [ false, false, true, false, false, false, + false, false, false, false, false, false] + + - note: string-equals + data: {} + modules: + - | + package test + + v = "hello" + r = [ + # Comparison with number + v == 1, + v == 2, + + # Comparison with primitives + v == null, + v == "hello", + v == true, + v == false, + + # Comparison with arrays + v == [], + v == [v], + + # Comparison with sets + v == set(), + v == { v }, + + # Comparison with objects + v == {}, + v == { "a": v}, + ] + sort_bindings: false + query: data.test + want_result: + v: "hello" + r: [ false, false, false, true, false, false, + false, false, false, false, false, false] + + - note: true-equals + data: {} + modules: + - | + package test + + v = true + r = [ + # Comparison with number + v == 1, + v == 2, + + # Comparison with primitives + v == null, + v == "hello", + v == true, + v == false, + + # Comparison with arrays + v == [], + v == [v], + + # Comparison with sets + v == set(), + v == { v }, + + # Comparison with objects + v == {}, + v == { "a": v}, + ] + sort_bindings: false + query: data.test + want_result: + v: true + r: [ false, false, false, false, true, false, + false, false, false, false, false, false] + + + - note: false-equals + data: {} + modules: + - | + package test + + v = false + r = [ + # Comparison with number + v == 1, # true + v == 1, # false + + # Comparison with primitives + v == null, + v == "hello", + v == true, + v == false, + + # Comparison with arrays + v == [], + v == [v], + + # Comparison with sets + v == set(), + v == { v }, + + # Comparison with objects + v == {}, + v == { "a": v}, + ] + sort_bindings: false + query: data.test + want_result: + v: false + r: [ false, false, false, false, false, true, + false, false, false, false, false, false] + + - note: undefined-equals + data: {} + modules: + - | + package test + import future.keywords + + v = 1 if false + r = [ + # Comparison with number + v == 1, + v == 2, + + # Comparison with primitives + v == null, + v == "hello", + v == true, + v == false, + + # Comparison with arrays + v == [], + v == [v], + + # Comparison with sets + v == set(), + v == { v }, + + # Comparison with objects + v == {}, + v == { "a": v}, + ] + + r2 = 1 if v + + # This variable should appear in output since it is not undefined. + ok = true + + sort_bindings: false + query: data.test + want_result: + ok: true + + + diff --git a/tests/interpreter/cases/call/basic.yaml b/tests/interpreter/cases/call/basic.yaml new file mode 100644 index 0000000..0c7f8b9 --- /dev/null +++ b/tests/interpreter/cases/call/basic.yaml @@ -0,0 +1,44 @@ +cases: + - note: basic + data: {} + modules: + - | + package test + + inc(x) = x + 1 + + a1 = inc(5) + query: data.test + want_result: + a1: 6 + + - note: call-in-arg + data: {} + modules: + - | + package test + + inc(x) = x + 1 + + a1 = inc(inc(5)) + query: data.test + want_result: + a1: 7 + + - note: call-nested + data: {} + modules: + - | + package test + + sub(a, b) := a - b + + foo(a, b) := r { + r =(a + b) * sub(a, b) + } + + a = foo(5, 6) + query: data.test + want_result: + a: -11 + diff --git a/tests/interpreter/cases/compr/array-vs-compr-tricky.yaml b/tests/interpreter/cases/compr/array-vs-compr-tricky.yaml new file mode 100644 index 0000000..af4b197 --- /dev/null +++ b/tests/interpreter/cases/compr/array-vs-compr-tricky.yaml @@ -0,0 +1,132 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +# Captures tricky cases where similar looking expressions will be +# treated as arrays or array-comprs. +cases: + - note: case1-compr + data: {} + modules: + - | + package test + x = {1} + y = {2} + # If there is one term and the term has an | operator, it will be parsed + # as a compr. + z = [ x | y ] #, 5] # in {5}, 6] + query: data.test + sort_bindings: true + want_result: + x: + set!: [1] + y: + set!: [2] + z: + - set!: [1] + + - note: case2-array + data: {} + modules: + - | + package test + x = {1} + y = {2} + # If there is a comma following the expression to the right of | + # ie "y ," then enclosing expression will be parsed as array. + z = [ x | y, 5] # in {5}, 6] + query: data.test + sort_bindings: true + want_result: + x: + set!: [1] + y: + set!: [2] + z: + - set!: [2, 1] + - 5 + + - note: case3-compr + data: {} + modules: + - | + package test + import future.keywords.in + x = {1} + y = {2} + # In the following case, the "y, 5 in {5}" which is to the right of | + # will be parsed as a membership expression. Since this expression is not + # followed by a ",", the enclosing expression will be parsed as a compr. + z = [ x | y, 5 in {5}] #, 6] + query: data.test + sort_bindings: true + want_result: + x: + set!: [1] + y: + set!: [2] + z: [] + + - note: case4-array + data: {} + modules: + - | + package test + import future.keywords.in + x = {1} + y = {2} + # Unlike the previous case, the "y, 5 in {5}" is followed by a ",". + # Therefore, the enclosing expression will be parsed as an array. + z = [ x | y, 5 in {5}, 6] + query: data.test + sort_bindings: true + want_result: + x: + set!: [1] + y: + set!: [2] + z: + - set!: [2, 1] + - true + - 6 + + - note: case5-array + data: {} + modules: + - | + package test + import future.keywords.in + x = {1} + y = {2} + # If the first expression "x - {10}"is something higher than an in-expr + # (e.g arithexpr) then the enclosing expression will be parsed as an array. + z = [ x - {10} | y, 5 in {5}] #, 6] + query: data.test + sort_bindings: true + want_result: + x: + set!: [1] + y: + set!: [2] + z: + - set!: [2, 1] + - true + + - note: case6-compr + data: {} + modules: + - | + package test + import future.keywords.in + x = {1} + y = {2} + # The way to get the previous case parsed as a compr is to enclose the first + # expression in parentheses. + z = [ (x - {10}) | y, 5 in {5}] #, 6] + query: data.test + sort_bindings: true + want_result: + x: + set!: [1] + y: + set!: [2] + z: [] diff --git a/tests/interpreter/cases/compr/mod.rs b/tests/interpreter/cases/compr/mod.rs new file mode 100644 index 0000000..669dfda --- /dev/null +++ b/tests/interpreter/cases/compr/mod.rs @@ -0,0 +1,123 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +#![cfg(test)] + +use crate::interpreter::*; + +#[test] +fn basic_array() -> Result<()> { + let rego = r#" + package test + + array = [1, 2, 3] + + array_compr_0 = [ x | x = 1 ] + + array_compr_1 = [ array | true ] + + array_compr_2 = [ x | x = array[_] ] + + array_compr_3 = [ x | x = array[_]; x != 2 ] + + # This produces 3 values + array_compr_4 = [ 1 | [1, 2, 3][_] ] + + # This produces 6 values. + array_compr_5 = [ 1 | [1, 2, 3][_]; {1, 2}[_] ] + + # This also produces 6 values. + array_compr_6 = [ 1 | [1, 2, 3][_]; {"a":1, "b":2}[_] ] + + # This produces 3 values. + array_compr_7 = [ 1 | [1, 2, 3][_]; [1, 2][_] >= 2 ] +"#; + + let expected = Value::from_json_str( + r#" { + "array": [1, 2, 3], + "array_compr_0": [1], + "array_compr_1": [[1, 2, 3]], + "array_compr_2": [1, 2, 3], + "array_compr_3": [1, 3], + "array_compr_4": [1, 1, 1], + "array_compr_5": [1, 1, 1, 1, 1, 1], + "array_compr_6": [1, 1, 1, 1, 1, 1], + "array_compr_7": [1, 1, 1] +}"#, + )?; + + assert_match( + eval_file(&[rego.to_owned()], None, None, "data.test")?, + expected, + ); + Ok(()) +} + +#[test] +fn basic_set() -> Result<()> { + let rego = r#" + package test + + set = { 1, "string", 1, [2, 3, 4], 567, false, 1 } + + set_compr_0 = { x | x = 1 } + + set_compr_1 = { set | true } + + set_compr_2 = { x | x = set[_] } + + set_compr_3 = { x | x = set[_]; x != [2, 3, 4] } + + # This produces 1 value + set_compr_4 = { 1 | [1, 2, 3][_] } + + # This produces 4 values. + set_compr_5 = { (a+b) | a=[1, 2, 3][_]; b={1, 2}[_] } + + # This also produces 2 values. + set_compr_6 = { a | [1, 2, 3][_]; a={"a":1, "b":2}[_] } + + # This produces 3 values. + set_compr_7 = { a | a = [1, 2, 3][_]; [1, 2][_] >= 2 } +"#; + + let expected = Value::from_json_str( + r#" { + "set": { + "set!": [1, "string", [2, 3, 4], 567, false] + }, + "set_compr_0": { + "set!": [1] + }, + "set_compr_1": { + "set!" : [{ + "set!": [1, "string", [2, 3, 4], 567, false] + }] + }, + "set_compr_2": { + "set!": [1, "string", [2, 3, 4], 567, false] + }, + "set_compr_3": { + "set!": [1, "string", 567, false] + }, + "set_compr_4": { + "set!": [1] + }, + "set_compr_5": { + "set!": [2, 3, 4, 5] + }, + "set_compr_6": { + "set!": [1, 2] + }, + "set_compr_7": { + "set!": [1, 2, 3] + } + }"#, + )?; + + assert_match( + eval_file(&[rego.to_owned()], None, None, "data.test")?, + expected, + ); + Ok(()) +} diff --git a/tests/interpreter/cases/compr/object.yaml b/tests/interpreter/cases/compr/object.yaml new file mode 100644 index 0000000..52506cd --- /dev/null +++ b/tests/interpreter/cases/compr/object.yaml @@ -0,0 +1,67 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: simple + data: {} + modules: + - | + package test + + x = { k:1 | k = ["Hello", "world", 1][_] } + query: data.test + want_result: + x: + object!: + - key: "Hello" + value: 1 + - key: "world" + value: 1 + - key: 1 + value: 1 + + + - note: key-loop + data: {} + modules: + - | + package test + + x = { ["Hello", "world", 1][_]:1 | true } + query: data.test + want_result: + x: + object!: + - key: "Hello" + value: 1 + - key: "world" + value: 1 + - key: 1 + value: 1 + + - note: multiple-occurance-of-same-key-value-pair + data: {} + modules: + - | + package test + x = { k:v | k = ["Hello", "world", 1][_]; v = [1, 1][_] } + query: data.test + want_result: + x: + object!: + - key: "Hello" + value: 1 + - key: "world" + value: 1 + - key: 1 + value: 1 + + - note: different-values-for-same-key + data: {} + modules: + - | + package test + x = { k:v | k = ["Hello", "world", 1][_]; v = [1, 2][_] } + query: data.test + error: "value for key `\"Hello\"` generated multiple times: `1` and `2`" + want_result: diff --git a/tests/interpreter/cases/default/basic.yaml b/tests/interpreter/cases/default/basic.yaml new file mode 100644 index 0000000..227fe61 --- /dev/null +++ b/tests/interpreter/cases/default/basic.yaml @@ -0,0 +1,56 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + data: {} + modules: + - | + package test + + default x = 5 + + a = b + + default b = 6 + + c = d + + d { + x == 3 + } + + d { + x == 4 + } + + default d = "has_default" + + default object["key"] = "string" + + default complex[true] = "bool_true" + + default complex[false] = "bool_false" + + complex["hello"] = "world" + + query: data.test + want_result: + x: 5 + a: 6 + b: 6 + c: "has_default" + d: "has_default" + object: + object!: + - key: "key" + value: "string" + complex: + object!: + - key: true + value: "bool_true" + - key: false + value: "bool_false" + - key: "hello" + value: "world" + diff --git a/tests/interpreter/cases/in/mod.rs b/tests/interpreter/cases/in/mod.rs new file mode 100644 index 0000000..f624959 --- /dev/null +++ b/tests/interpreter/cases/in/mod.rs @@ -0,0 +1,126 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +#![cfg(test)] + +use crate::interpreter::*; + +#[test] +fn basic() -> Result<()> { + let rego = r#" + package test + + import future.keywords.in + + array = [1, 2, 3] + + in_array_key_value { + 0, 1 in array + } + + in_array_key_value_negative { + 0, 2 in array + } + + some_decl_array_key_value { + some 0, 1 in array + } + + some_decl_array_key_value_negative { + some 0, 2 in array + } + + some_decl_array_value { + some 1 in array + } + + some_decl_array_value_negative { + some 4 in array + } + + in_array_value { + 1 in array + } + + in_array_value_negative { + 4 in array + } + + object = { "number": 1, "array": [2, 3], "string": "test", "bool": true } + + in_object_key_value { + "number", 1 in object + } + + in_object_key_value_negative { + "non-exist", 1 in object + } + + some_decl_object_key_value { + some "number", 1 in object + } + + some_decl_object_key_value_negative { + some "non-exist", 1 in object + } + + in_object_value { + [2, 3] in object + } + + in_object_value_negative { + false in object + } + + some_decl_object_value { + some [2, 3] in object + } + + some_decl_object_value_negative { + some false in object + } + + set = { "string", [2, 3, 4], 567, false } + + in_set_value { + "string" in set + } + + some_decl_set_value { + some "string" in set + } + + in_set_value_negative { + "non-exist" in set + } + + some_decl_set_value_negative { + some "non-exist" in set + } +"#; + + let expected = Value::from_json_str( + r#" { + "array": [1, 2, 3], + "in_array_key_value": true, + "some_decl_array_key_value": true, + "in_array_value": true, + "some_decl_array_value": true, + "object": { "number": 1, "array": [2, 3], "string": "test", "bool": true}, + "in_object_key_value": true, + "some_decl_object_key_value": true, + "in_object_value": true, + "some_decl_object_value": true, + "set": { + "set!": ["string", [2, 3, 4], 567, false] + }, + "in_set_value": true, + "some_decl_set_value": true +}"#, + )?; + + assert_match( + eval_file(&[rego.to_owned()], None, None, "data.test")?, + expected, + ); + Ok(()) +} diff --git a/tests/interpreter/cases/mod.rs b/tests/interpreter/cases/mod.rs new file mode 100644 index 0000000..c035f6d --- /dev/null +++ b/tests/interpreter/cases/mod.rs @@ -0,0 +1,7 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. + +mod arithmetic; +mod compr; +mod r#in; +mod variables; diff --git a/tests/interpreter/cases/multi/basic.yaml b/tests/interpreter/cases/multi/basic.yaml new file mode 100644 index 0000000..27aa16b --- /dev/null +++ b/tests/interpreter/cases/multi/basic.yaml @@ -0,0 +1,32 @@ +cases: + - note: simple-no-cross-ref + data: {} + modules: + - | + package a + b = 1 + - | + package b + c = 1 + query: data + want_result: + a: + b: 1 + b: + c: 1 + + - note: simple-cross-ref-in-order + data: {} + modules: + - | + package a + b = 1 + - | + package b + c = data.a.b + query: data + want_result: + a: + b: 1 + b: + c: 1 diff --git a/tests/interpreter/cases/rule/contains.yaml b/tests/interpreter/cases/rule/contains.yaml new file mode 100644 index 0000000..3471080 --- /dev/null +++ b/tests/interpreter/cases/rule/contains.yaml @@ -0,0 +1,32 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: simple + data: {} + modules: + - | + package test + import future.keywords + tbl contains name if { + name = ["a", "b", "c"][_] + } + query: data.test + want_result: + tbl: + set!: ["c", "b", "a"] + + - note: arithmetic + data: {} + modules: + - | + package test + import future.keywords + tbl contains x + 10 if { + x = [1, 2, 3][_] + } + query: data.test + want_result: + tbl: + set!: [11, 12, 13] + diff --git a/tests/interpreter/cases/rule/dependency.yaml b/tests/interpreter/cases/rule/dependency.yaml new file mode 100644 index 0000000..0b54926 --- /dev/null +++ b/tests/interpreter/cases/rule/dependency.yaml @@ -0,0 +1,41 @@ +cases: + - note: forward-ref + data: {} + modules: + - | + package test + a = b + b = 5 + query: data.test + want_result: + a: 5 + b: 5 + + - note: recursive + data: {} + modules: + - | + package test + a = b + b = c + c = a + query: data.test + error: recursion detected + + - note: cross-module + data: {} + modules: + - | + package a + x = data.b.y * 2 + - | + package b + y = 10 + z = data.a.x + 5 + query: data + want_result: + a: + x: 20 + b: + y: 10 + z: 25 diff --git a/tests/interpreter/cases/rule/object.yaml b/tests/interpreter/cases/rule/object.yaml new file mode 100644 index 0000000..46edbfe --- /dev/null +++ b/tests/interpreter/cases/rule/object.yaml @@ -0,0 +1,35 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: simple + data: {} + modules: + - | + package test + + x[a] = b { + a = "hello" + b = "world" + } + query: data.test + want_result: + x: + "hello": "world" + + - note: loop + data: {} + modules: + - | + package test + + x[a] = b { + a = ["hello", "world"][_] + b = 1 + } + query: data.test + want_result: + x: + "hello": 1 + "world": 1 + diff --git a/tests/interpreter/cases/rule/old_set.yaml b/tests/interpreter/cases/rule/old_set.yaml new file mode 100644 index 0000000..e747999 --- /dev/null +++ b/tests/interpreter/cases/rule/old_set.yaml @@ -0,0 +1,31 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: index-syntax + data: {} + modules: + - | + package test + x[a] { + a = ["hello", "world"][_] + } + query: data.test + want_result: + x: + set!: ["hello", "world"] + + - note: special-case-field-syntax + data: {} + modules: + - | + package test + x.a { + a = ["hello", "world"][_] + true + } + query: data.test + want_result: + x: + set!: ["a"] + diff --git a/tests/interpreter/cases/snippets/snippets.yaml b/tests/interpreter/cases/snippets/snippets.yaml new file mode 100644 index 0000000..2d5dccc --- /dev/null +++ b/tests/interpreter/cases/snippets/snippets.yaml @@ -0,0 +1,249 @@ +cases: + - note: snippet1 + data: {} + modules: + - | + package test + + a := [ 1 | {3} ] + + q:= {4} + + e1 := q & {1} | {true} + e2 := (q & {1}) | {true} + + e3 := [ q & {1} | {true} ] + e4 := [ (q & {1}) | {true} ] + query: data.test + want_result: + a: [1] + e1: + set!: [true] + e2: + set!: [true] + e3: + - set!: [true] + e4: + - set!: [] + q: + set!: [4] + + - note: snippet2.1 + data: {} + modules: + - | + package test + + import future.keywords + + b = 15 + + get_b(a) := v { v := b } + + x = a { + a = b with data.test.b as 10 + } + query: data.test + want_result: + b: 15 + x: 10 + + - note: snippet2 + data: {} + skip: true + modules: + - | + package test + + a := {4} + + mydoc(x) := path { + path := "data.test.a" + } + + x := [ y | + y := data.test.a | data.test.b with data.test.a as {5} with data.test.b as {6} + ] + + r := [ m | m := data.test.p with data.test.p as 5 + 6; true ] + + + allow { + input.x + == 5 + + input.y == 5 + input.y + == 5 + } + query: data.test + want_result: + a: + set!: [4] + r: [11] + x: + - set!: [5, 6] + + + - note: snippet3 + data: {} + modules: + - | + package test + + a(b) { true } + b(a) { false } + + p := 5 + + allow { + # TODO: ude data.test + q := 5 #data.test["p"] + y := data.test.a( + 5) + q == 5 + y + } + + r := allow + x = a(5) + y = b(5) + + query: data.test + want_result: + allow: true + p: 5 + r: true + x: true + + - note: snippet4 + data: {} + modules: + - | + package test + + sum (a,b) := c { + c := a + b + } + + obj(v) := o { + o := { "a" : 5 + v } + } + p := sum(5, 6) # There must not be space between sum and ( + #q := { "a" : "sum"}[a](5, 6) + q := obj(1).a + query: data.test + want_result: + p: 11 + q: 6 + + - note: snippet5 + data: {} + modules: + - | + package test + + import future.keywords.if + import future.keywords.in + + b := {2, 3} + + double(x) := y { y := [x, x] } + + # Membership needs to be enclosed in parens + x := [ (y in {2,3}) | + y := 5 in { "z" : 5} + ] + query: data.test + want_result: + b: + set!: [2, 3] + x: [false] + + - note: snippet6 + data: {} + modules: + - | + package test + + import future.keywords.in + import future.keywords.every + import future.keywords.if + import future.keywords.contains + + # expr top; membership + x := false in {a, b} { + a := 1 + b := 0, 2 in [5, 6] + 0, 2 in [5, 6] + } + + a.y(a) := b { b := a } + + p["q"](a1) := b { b:= a1 } + + c := a.y(5) + d := p.q(5) + + r["s"] p1 { + s1 := "1" + p1 := s1 + } + query: data.test + want_result: + a: {} + c: 5 + d: 5 + p: {} + p1: true + r: + set!: ["s"] + + - note: snippet8 + data: {} + modules: + - | + package test + + import future.keywords.if + import future.keywords.in + import future.keywords.contains + + # Try removing parenthesis below + ref["p"] contains [(x | {5})] # [a] + # Uncomment [a]. Why doesn't rego complain? + # Then add a := 5 to the body. + { + x:= {6} + } + + arg := 5 + ref1[arg] { + arg := {5, 6} + } + + ref2.arg := {5, 6} + + a[b] { + b := {5,6}[_] + } + + #x["b"] := b { + # b := {5,6}[_] + #} + query: data.test + want_result: + a: + set!: [5, 6] + arg: 5 + ref: + p: + set!: + - + - set!: [5, 6] + ref1: + set!: + - set!: [5, 6] + ref2: + arg: + set!: [5, 6] diff --git a/tests/interpreter/cases/variables/basic.yaml b/tests/interpreter/cases/variables/basic.yaml new file mode 100644 index 0000000..5d8c849 --- /dev/null +++ b/tests/interpreter/cases/variables/basic.yaml @@ -0,0 +1,75 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + data: {} + modules: + - | + package test + + array = [1, 2, 3] + + nested_array = [1, [2, 3, 4], 5, 6] + + object = { "key0": "value0" } + + key = "key" + + object_var = { key: array } + + local_0 = x { + x = 10 + } + + local_1 = x { + x = "test_local" + } + + # Set with nested object, array and set. + set = { 1, 2, + {"a": 3, "b" : 4}, [5, 6], {7, 8}} + + # Object with non-string as keys + complex_object = { + {1, 2, 3} : [4, 5, 6], + true: false, + [1, 3] : {"hello", "world"} + } + + query: data.test + want_result: + array: [1, 2, 3] + nested_array: [1, [2, 3, 4], 5, 6] + object: + key0: value0 + key: key + object_var: + key: [1, 2, 3] + local_0: 10 + local_1: test_local + set: + # Specify set using special encoding. + # Order of elements shouldn't matter for set. + set!: + - 2 + - 1 + - a : 3 + b : 4 + - [5, 6] + - set!: [8, 7] + complex_object: + # Specify object using special encoding. + object!: + - key: + set!: [3, 2, 1] + value: [4, 5, 6] + - key: true + value: false + - key: [1, 3] + value: + set!: + - "hello" + - "world" + + diff --git a/tests/interpreter/cases/variables/mod.rs b/tests/interpreter/cases/variables/mod.rs new file mode 100644 index 0000000..5cfcd5f --- /dev/null +++ b/tests/interpreter/cases/variables/mod.rs @@ -0,0 +1,54 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +#![cfg(test)] + +use crate::interpreter::*; + +#[test] +fn basic() -> Result<()> { + let rego = r#" + package test + + array = [1, 2, 3] + + nested_array = [1, [2, 3, 4], 5, 6] + + object = { "key0": "value0" } + + key = "key" + + object_var = { key: array } + + local_0 = x { + x = 10 + } + + local_1 = x { + some x + x = "test_local" + } + + set = {1, 2, 3} +"#; + + let expected = Value::from_json_str( + r#" { + "array": [1, 2, 3], + "nested_array": [1, [2, 3, 4], 5, 6], + "object": { "key0": "value0" }, + "key": "key", + "object_var": { "key": [1, 2, 3] }, + "set" : { + "set!" : [3, 2, 1] + }, + "local_0": 10, + "local_1": "test_local" +}"#, + )?; + + assert_match( + eval_file(&[rego.to_owned()], None, None, "data.test")?, + expected, + ); + Ok(()) +} diff --git a/tests/interpreter/mod.rs b/tests/interpreter/mod.rs new file mode 100644 index 0000000..e07cecd --- /dev/null +++ b/tests/interpreter/mod.rs @@ -0,0 +1,386 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +#![cfg(test)] + +use std::env; + +use anyhow::{bail, Result}; +use rego_rs::*; +use serde::{Deserialize, Serialize}; +use test_generator::test_resources; +//use walkdir::WalkDir; + +mod cases; + +// Process test value specified in json/yaml to interpret special encodings. +pub fn process_value(v: &Value) -> Result { + match v { + // Handle Undefined encoded as a string "#undefined" + Value::String(s) if s == "#undefined" => Ok(Value::Undefined), + + // Handle set encoded as an object + // set! : + // - item1 + // - item2 + // ... + Value::Object(ref fields) if fields.len() == 1 && matches!(&v["set!"], Value::Array(_)) => { + let mut set_value = Value::new_set(); + let set = set_value.as_set_mut()?; + for item in v["set!"].as_array()? { + set.insert(process_value(item)?); + } + Ok(set_value) + } + + // Handle complex object specified explicitly: + // object! : + // - key: ... + // value: ... + Value::Object(fields) if fields.len() == 1 && matches!(&v["object!"], Value::Array(_)) => { + let mut object_value = Value::new_object(); + let object = object_value.as_object_mut()?; + for item in v["object!"].as_array()? { + object.insert(process_value(&item["key"])?, process_value(&item["value"])?); + } + Ok(object_value) + } + + // Recursively process arrays + Value::Array(items) => { + let mut array_value = Value::new_array(); + let array = array_value.as_array_mut()?; + for item in items.iter() { + array.push(process_value(item)?); + } + Ok(array_value) + } + + // Recursively process objects + Value::Object(fields) => { + let mut object_value = Value::new_object(); + let object = object_value.as_object_mut()?; + for (key, value) in fields.iter() { + object.insert(process_value(key)?, process_value(value)?); + } + Ok(object_value) + } + + Value::Set(_) => bail!("unexpected set in value read from json/yaml"), + + // Simple variants + _ => Ok(v.clone()), + } +} + +fn display_values(c: &Value, e: &Value) -> Result { + Ok(format!( + "\nleft = {}\nright = {}\n", + serde_json::to_string_pretty(c)?, + serde_json::to_string_pretty(e)? + )) +} + +// Helper function to match computed and expecte values. +// On mismatch, prints the failing sub-value instead of the whole value. +fn match_values_impl(computed: &Value, expected: &Value) -> Result<()> { + match (&computed, &expected) { + (Value::Array(a1), Value::Array(a2)) => { + if a1.len() != a2.len() { + bail!( + "array length mismatch: {} != {}{}", + a1.len(), + a2.len(), + display_values(computed, expected)? + ); + } + + for (idx, v1) in a1.iter().enumerate() { + match_values_impl(v1, &a2[idx])?; + } + Ok(()) + } + + (Value::Set(s1), Value::Set(s2)) => { + if s1.len() != s2.len() { + bail!( + "set length mismatch: {} != {}{}", + s1.len(), + s2.len(), + display_values(computed, expected)? + ); + } + + let mut itr2 = s2.iter(); + for v1 in s1.iter() { + match_values_impl(v1, itr2.next().unwrap())?; + } + Ok(()) + } + + (Value::Object(o1), Value::Object(o2)) => { + if o1.len() != o2.len() { + bail!( + "object length mismatch: {} != {}{}", + o1.len(), + o2.len(), + display_values(computed, expected)? + ); + } + + let mut itr2 = o2.iter(); + for (k1, v1) in o1.iter() { + let (k2, v2) = itr2.next().unwrap(); + match_values_impl(k1, k2)?; + match_values_impl(v1, v2)?; + } + Ok(()) + } + + (Value::Number(n1), Value::Number(n2)) if n1 == n2 => Ok(()), + (Value::String(s1), Value::String(s2)) if s1 == s2 => Ok(()), + (Value::Bool(b1), Value::Bool(b2)) if b1 == b2 => Ok(()), + (Value::Null, Value::Null) => Ok(()), + (Value::Undefined, Value::Undefined) => Ok(()), + + _ => bail!("value mismatch: {}", display_values(computed, expected)?), + } +} + +fn match_values(computed: &Value, expected: &Value) -> Result<()> { + match match_values_impl(computed, expected) { + Ok(()) => Ok(()), + Err(e) => bail!("\nmismatch in {}{}", display_values(computed, expected)?, e), + } +} + +pub fn assert_match(computed: Value, expected: Value) { + let expected = match process_value(&expected) { + Ok(e) => e, + _ => panic!("unable to process value :\n {expected:?}"), + }; + match match_values(&computed, &expected) { + Ok(()) => (), + Err(e) => panic!("{}", e), + } +} + +pub fn eval_file( + regos: &[String], + data: Option, + input: Option, + query: &str, +) -> Result { + let mut files = vec![]; + let mut sources = vec![]; + let mut modules = vec![]; + let mut modules_ref = vec![]; + for (idx, _) in regos.iter().enumerate() { + files.push(format!("rego_{idx}")); + } + + for (idx, file) in files.iter().enumerate() { + let contents = regos[idx].as_str(); + sources.push(Source { + file, + contents, + lines: contents.split('\n').collect(), + }); + } + + for source in &sources { + let mut parser = Parser::new(source)?; + modules.push(parser.parse()?); + } + + for m in &modules { + modules_ref.push(m); + } + + // First eval the modules. + let mut interpreter = interpreter::Interpreter::new(modules_ref)?; + interpreter.eval(&data, &input)?; + + // Now eval the query. + let source = Source { + file: "", + contents: query, + lines: query.split('\n').collect(), + }; + let mut parser = Parser::new(&source)?; + let expr = parser.parse_membership_expr()?; + interpreter.eval_query_snippet(&expr) +} + +#[test] +#[ignore = "intended for use by scripts/rego-eval"] +fn one_file() -> Result<()> { + env_logger::init(); + + let mut file = String::default(); + let mut input = None; + for a in env::args() { + if a.ends_with(".rego") { + file = a; + } else if a.ends_with(".json") { + let input_json = std::fs::read_to_string(&a)?; + let value = Value::from_json_str(input_json.as_str())?; + input = Some(value); + } + } + + if file.is_empty() { + bail!("missing "); + } + + let contents = std::fs::read_to_string(&file)?; + + let source = Source { + file: file.as_str(), + contents: contents.as_str(), + lines: contents.split('\n').collect(), + }; + let mut parser = Parser::new(&source)?; + let tree = parser.parse()?; + let mut interpreter = interpreter::Interpreter::new(vec![&tree])?; + let results = interpreter.eval(&None, &input)?; + println!("eval results:\n{}", serde_json::to_string_pretty(&results)?); + Ok(()) +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct TestCase { + data: Value, + input: Option, + modules: Vec, + note: String, + query: String, + sort_bindings: Option, + want_result: Option, + skip: Option, + error: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct YamlTest { + cases: Vec, +} + +fn yaml_test_impl(file: &str) -> Result<()> { + let yaml_str = std::fs::read_to_string(file)?; + let test: YamlTest = serde_yaml::from_str(&yaml_str)?; + + println!("running {}", file); + for case in test.cases { + print!("case {} ", case.note); + if case.skip == Some(true) { + println!("skipped"); + continue; + } + + match (&case.want_result, &case.error) { + (Some(_), None) | (None, Some(_)) => (), + _ => panic!("either want_result or error must be specified in test case."), + } + + // First eval the modules. + match eval_file( + &case.modules, + Some(case.data), + case.input, + case.query.as_str(), + ) { + Ok(results) => match case.want_result { + Some(want_result) => assert_match(results, want_result), + _ => panic!("eval succeeded and did not produce any errors"), + }, + Err(actual) => match &case.error { + Some(expected) => { + let actual = actual.to_string(); + if !actual.contains(expected) { + bail!( + "Error message\n`{}\n`\ndoes not contain `{}`", + actual, + expected + ); + } + println!("{actual}"); + } + _ => return Err(actual), + }, + } + + println!("passed"); + } + + Ok(()) +} + +fn yaml_test(file: &str) -> Result<()> { + match yaml_test_impl(file) { + Ok(_) => Ok(()), + Err(e) => { + // If Err is returned, it doesn't always get printed by cargo test. + // Therefore, panic with the error. + panic!("{}", e); + } + } +} + +#[test] +fn yaml_test_basic() -> Result<()> { + yaml_test("tests/interpreter/cases/basic_001.yaml") +} + +#[test] +#[ignore = "intended for use by scripts/yaml-test-eval"] +fn one_yaml() -> Result<()> { + let mut file = String::default(); + for a in env::args() { + if a.ends_with(".yaml") { + file = a; + break; + } + } + + if file.is_empty() { + bail!("missing "); + } + + yaml_test(file.as_str()) +} + +/* +fn run_yaml_tests_in(folder: &str) -> Result<()> { + let mut total = 0; + + for entry in WalkDir::new(folder) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry + .path() + .to_str() + .ok_or_else(|| anyhow!("failed to convert path to utf8 {:?}", entry.path()))?; + if !path.ends_with(".yaml") { + continue; + } + + total += 1; + yaml_test(path)?; + } + + println!("{} yaml tests passed.", total); + Ok(()) +} + +#[test] +fn run_yaml_tests() -> Result<()> { + run_yaml_tests_in("tests/interpreter") +} +*/ + +#[test_resources("tests/interpreter/**/*.yaml")] +fn run(path: &str) { + yaml_test(path).unwrap() +} diff --git a/tests/lexer/cases/all.yaml b/tests/lexer/cases/all.yaml new file mode 100644 index 0000000..670160b --- /dev/null +++ b/tests/lexer/cases/all.yaml @@ -0,0 +1,34 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - rego: | + + - * # This is a comment + / + [ ] {} ( ) & | + , . + < <= = == > >= + tIS+t[# + #[ ] {} ( ) & | + #, . + #< <= = == > >=*/ + a] + 1 0 1230 0.1 0.123 123.0 123.12345 1.023e308 + 1 . # + "Hello" "Hello\tWorld" "" + "abc" + `This is + a raw string` + . 5 set ( ) set( ) set( 5 ) + # The following will be lexed as 'set' '(' since there is no matching ')'. + set( + note: all-tokens + tokens: + [ "+", "-", "*", "/", "[", "]", "{", "}", "(", ")", "&", "|", ",", + ".", "<", "<=", "=", "==", ">", ">=", + "tIS", "+", "t", "[", "a", "]", "1", "0", "1230", "0.1", + "0.123", "123.0", "123.12345", "1.023e308", "1", ".", + "Hello", "Hello\\tWorld", "", "abc", "This is\na raw string", + ".", "5", "set", "(", ")", "set(", ")", "set", "(", "5", ")", + "set", "(", + ""] diff --git a/tests/lexer/cases/boolean.yaml b/tests/lexer/cases/boolean.yaml new file mode 100644 index 0000000..51a5553 --- /dev/null +++ b/tests/lexer/cases/boolean.yaml @@ -0,0 +1,10 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + # To Booleans are parsed as identifiers. + # It is up to the parser to handle them as appropriate. + - note: boolean/all + rego: true false + tokens: [ "true", "false", "" ] + kinds: [Ident, Ident, Eof] diff --git a/tests/lexer/cases/comment.yaml b/tests/lexer/cases/comment.yaml new file mode 100644 index 0000000..7c8dda4 --- /dev/null +++ b/tests/lexer/cases/comment.yaml @@ -0,0 +1,71 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: comments-immediately-following/1 + rego: | + #This is a comment #Nested comment + ident#a + 123.4e5#c + 6#c + +#c + -#c + *#c + /#c + &#c + |#c + [#c + ]#c + (#c + )#c + {#c + }#c + ,#c + ;#c + .#c + :#c + <#c + <=#c + >#c + >=#c + =#c + ==#c + # Ensure that <, =, > are read separately. + <#c + =#c + >#c + =#c + =#c + tokens: ["ident", "123.4e5", "6", "+", "-", "*", "/", "&", "|", + "[", "]", "(", ")", "{", "}", ",", ";", ".", ":", + "<", "<=", ">", ">=", "=", "==", + "<", "=", ">", "=", "=", + ""] + + # Non-ascii chars can appear in comments. + - note: comments-non-ascii + rego: # சிக்கி + tokens: [""] + + - note: comments-within-raw-string + rego: | + #Comments aren't lexed within raw strings. + `raw#This is not a comment + string`#c + tokens: ["raw#This is not a comment\nstring", ""] + + - note: comments-within-rstring + rego: | + #Comments aren't allowed within strings, + "#This is not a comment + "#c + tokens: + error: unmatched " + + - note: comment-integer-break + rego: | + 12# + .3 + tokens: + error: invalid number + diff --git a/tests/lexer/cases/eof.yaml b/tests/lexer/cases/eof.yaml new file mode 100644 index 0000000..b0240e8 --- /dev/null +++ b/tests/lexer/cases/eof.yaml @@ -0,0 +1,35 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + # All other yamls lock down eof. + # Specific eof tests: + # Empty file. + - note: empty-file + rego: + tokens: [""] + kinds: [Eof] + + # File with comments. + - note: only-comments + rego: | + # This is a comment. + # This is another comment. + tokens: [""] + kinds: [Eof] + + # File with eof and trailing chars. + # Trailing chars are ignored. + - note: eof-trailing-chars + rego: "true (\x0000false + hello" + tokens: ["true", "(", ""] + kinds: [Ident, Symbol, Eof] + + # Empty string must be distinguishable from Eof eventhough + # both result in an empty string as the span. + - note: empty-string-eof + rego: "\"\"" + tokens: ["", ""] + kinds: [String, Eof] + + diff --git a/tests/lexer/cases/identifier.yaml b/tests/lexer/cases/identifier.yaml new file mode 100644 index 0000000..138227a --- /dev/null +++ b/tests/lexer/cases/identifier.yaml @@ -0,0 +1,38 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: all + rego: a ab abc a1 a1b1 a1b1c _ _a _ab _1 _abc1 + tokens: ["a", "ab", "abc", "a1", "a1b1", "a1b1c", + "_", "_a", "_ab", "_1", "_abc1", + ""] + + - note: placeholder + rego: _ + tokens: ["_", ""] + + # Identifiers can only be accii. + - note: invalid-char/1 + rego: சிக்கி + tokens: + error: invalid character + - note: invalid-char/2 + rego: aசி_க்கி + tokens: + error: invalid character + + # set( is a special case. + - note: set( + rego: | + # This is a function call. + set () + # This is lexed as "set(" ")" + set() + # This is lexed as a function call. + set(5) + tokens: [ + "set", "(", ")", + "set(", ")", + "set", "(", "5", ")", + ""] diff --git a/tests/lexer/cases/keyword.yaml b/tests/lexer/cases/keyword.yaml new file mode 100644 index 0000000..be24bc4 --- /dev/null +++ b/tests/lexer/cases/keyword.yaml @@ -0,0 +1,10 @@ +cases: + # Keywords are parsed as identifiers. + # It is up to the parser to handle them as appropriate. + - note: keywords/all + rego: as default else false import package not null some true with + tokens: [ "as", "default", "else", "false", "import", + "package", "not", "null", "some", "true", "with", + "" ] + kinds: [Ident, Ident, Ident, Ident, Ident, Ident, Ident, Ident, + Ident, Ident, Ident, Eof] diff --git a/tests/lexer/cases/newline.yaml b/tests/lexer/cases/newline.yaml new file mode 100644 index 0000000..6a2709f --- /dev/null +++ b/tests/lexer/cases/newline.yaml @@ -0,0 +1,13 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: carriage-return/ok + rego: "\n\n\r\n\n\n " + tokens: [""] + + - note: carriage-return/ok + rego: "\r \n" + tokens: [""] + error: \r must be followed by \n + diff --git a/tests/lexer/cases/number.yaml b/tests/lexer/cases/number.yaml new file mode 100644 index 0000000..23ed8f5 --- /dev/null +++ b/tests/lexer/cases/number.yaml @@ -0,0 +1,184 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + # Valid integers + - note: integers + rego: | + # Integers + 0 1 11 22 12345678 + 0 -1 -11 -22 -12345678 + tokens: + [ "0", "1", "11", "22", "12345678", + "0", "-1", "-11", "-22", "-12345678", + ""] + + # Large integers are supported. + # Note, currently integers that can't fit in an f64 may nto evaluate correctly. + - note: large-integers + rego: | + 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + -12345678901234567890123456789012345678901234567890123456789012345678901234567890 + tokens: + ["12345678901234567890123456789012345678901234567890123456789012345678901234567890", + "-12345678901234567890123456789012345678901234567890123456789012345678901234567890", + ""] + + # Negative zero seems to be supported in json. + # - followed by a space and digit will be parsed as two tokens. + - note: negative-zero + rego: -0 - 0 + tokens: [ "-0", "-", "0", "" ] + + # 0 cannot be followed by 0 or other digits. + - note: invalid-leading-0/a + rego: 00 + tokens: + error: invalid number + - note: invalid-leading-0/b + rego: 01 + tokens: + error: invalid number + + # Number cannot be followed by . _ or alphanumeric + - note: invalid-suffix/0. + rego: 0. + tokens: + error: invalid number + - note: invalid-suffix/1_ + rego: 1_ + tokens: + error: invalid number + - note: invalid-suffix/9a + rego: 9a + tokens: + error: invalid number + - note: invalid-suffix/0.1. + rego: 0.1. + tokens: + error: invalid number + + # Floats + - note: floats + rego: | + 0.1 1.09 11.000000001 22.333 12345678.6789 + 0.1 -1.09 -11.000000001 -22.3333 -12345678.6789 + tokens: ["0.1", "1.09", "11.000000001", "22.333", 12345678.6789, + "0.1", "-1.09", "-11.000000001", "-22.3333", "-12345678.6789", + ""] + + # Large integers are supported. + # Note, currently integers that can't fit in an f64 may not evaluate correctly. + - note: large-floats + rego: | + 12345678901234567890123456789012345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890123456789012345678901234567890 + -12345678901234567890123456789012345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890123456789012345678901234567890 + tokens: + ["12345678901234567890123456789012345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890123456789012345678901234567890", + "-12345678901234567890123456789012345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890123456789012345678901234567890", + ""] + + # Specific floats + - note: specific-floats + rego: | + 123.456e-789 0.4e0066 -1e+308 -123456e303 123456e303 + 123456e-10000000 -123456789123456789123456789123 + 900000000000000000000 + -237462374673276894279832749832423479823246327846 + 123e65 0e9 -0 -1 2E34 2E-3 2E+3 + -0.000000000000000000000000000000000000000000000000000000000000000000000000000009 + tokens: ["123.456e-789", "0.4e0066", "-1e+308", + "-123456e303", "123456e303", "123456e-10000000", + "-123456789123456789123456789123", + "900000000000000000000", + "-237462374673276894279832749832423479823246327846", + "123e65", "0e9", "-0", "-1", "2E34", "2E-3", "2E+3", + "-0.000000000000000000000000000000000000000000000000000000000000000000000000000009", + ""] + + # + sign is parsed as separate token + - note: plus-number + rego: +1 +1.2 +1e2 +1E2 + tokens: [ "+", "1", "+", "1.2", "+", "1e2", "+", "1E2", "" ] + + # Bunch of invalid numbers + - note: invalid-number/0.1.1 + rego: 0.1.1 + tokens: + error: invalid number + - note: invalid-number/0e1.1 + rego: 0e1.1 + tokens: + error: invalid number + - note: invalid-number/01 + rego: 01 + tokens: + error: invalid number + - note: invalid-number/0.4e + rego: 0.4e + tokens: + error: invalid number + - note: invalid-number/0.4E + rego: 0.4E + tokens: + error: invalid number + - note: invalid-number/0.4e- + rego: 0.4e- + tokens: + error: invalid number + - note: invalid-number/0.Ee- + rego: 0.4E- + tokens: + error: invalid number + - note: invalid-number/0.4e+ + rego: 0.4e+ + tokens: + error: invalid number + - note: invalid-number/0.Ee+ + rego: 0.4E+ + tokens: + error: invalid number + - note: invalid-number/8a + rego: 8a + tokens: + error: invalid number + - note: invalid-number/8eA + rego: 8eA + tokens: + error: invalid number + - note: invalid-hex-number/0x1 + rego: 0x1 + tokens: + error: invalid number + - note: invalid-octal-number/01 + rego: 01 + tokens: + error: invalid number + - note: invalid-number/8_ + rego: 8_ + tokens: + error: invalid number + - note: invalid-number/8.1_ + rego: 8.1_ + tokens: + error: invalid number + - note: invalid-number/.8 + rego: .8 + tokens: + error: invalid number + - note: invalid-number/8. + rego: 8. + tokens: + error: invalid number + - note: invalid-number/2.e3 + rego: 2.e3 + tokens: + error: invalid number + - note: invalid-number/2.e+3 + rego: 2.e3 + tokens: + error: invalid number + - note: invalid-number/2.e-3 + rego: 2.e3 + tokens: + error: invalid number diff --git a/tests/lexer/cases/rawstring.yaml b/tests/lexer/cases/rawstring.yaml new file mode 100644 index 0000000..2f227a3 --- /dev/null +++ b/tests/lexer/cases/rawstring.yaml @@ -0,0 +1,37 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: positive + rego: | + #Empty + `` + # Multiline + ` This is m + u + lti + line` + + `"String within rawstring"` + + # Comment retained + ` + Hello #retained comment! + ` + # Utf-8 + `Hello, சிக்கி + ` + tokens: [ "", " This is m\n u\n lti\nline", + "\"String within rawstring\"", + "\nHello #retained comment!\n", + "Hello, சிக்கி\n", + ""] + + kinds: [RawString, RawString, RawString, RawString, RawString, Eof] + + - note: unclosed + rego: | + ` + Peekoo Maharaaj ki jai ho + tokens: [] + error: unmatched ` diff --git a/tests/lexer/cases/string.yaml b/tests/lexer/cases/string.yaml new file mode 100644 index 0000000..f87d649 --- /dev/null +++ b/tests/lexer/cases/string.yaml @@ -0,0 +1,114 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: positive + rego: | + "Hello, World\n" + + # Correctly parse double-quote + "\"" + + #Escape sequences + "\"\\\/\b\f\n\r\t" + + # UTF-8 codes + "\u0060\u012a\u12AB" + + # Surrogate pairs + "\uD801\udc37\ud83d\ude39\ud83d\udc8d\uDBFF\uDFFF" + + # This shouldn't be parsed as an invalid escape. + "\\upqrs" + + # Control char + "\u0012" + + # Lowest possible code followed by chars. + "\u0000 a" + + # Largest possible code + "\uFFFF" "\uffff" + + # Unicode escape + "\u0061\u30af\u30EA\u30b9" + + "சிக்கி π, €𝄞㈴ é" + tokens: [ + "Hello, World\\n", + "\\\"", + "\\\"\\\\\\/\\b\\f\\n\\r\\t", + "\\u0060\\u012a\\u12AB", + "\\uD801\\udc37\\ud83d\\ude39\\ud83d\\udc8d\\uDBFF\\uDFFF", + "\\\\upqrs", + "\\u0012", + "\\u0000 a", + "\\uFFFF", "\\uffff", + "\\u0061\\u30af\\u30EA\\u30b9", + #"\\uDADA", + "சிக்கி π, €𝄞㈴ é", + ""] + + - note: invalid-char + rego: | + "world\n\r\b " + tokens: [""] + error: invalid character in string + + - note: unmatched + rego: "\"Chikki Kuttiiii" + tokens: [] + error: unmatched " + + - note: newline + rego: "\"Chikki Kuttiiii\n" + tokens: [] + error: invalid character in string + + - note: invalid/capital-U + rego: | + "\U1234" + tokens: [] + error: invalid escape sequence + + - note: invalid/unclosed-escape + rego: "\"\\" + tokens: [] + error: invalid escape sequence + + - note: invalid/solo-double-quote + rego: "\"" + tokens: [] + error: unmatched " + + - note: invalid/unicode-escape + rego: | + "\uabcg" + tokens: [] + error: invalid hex escape sequence + + - note: invalid/ascii-escape + rego: | + "\a" + tokens: [] + error: invalid escape sequence + + - note: invalid/hex-escape + rego: | + "\uabc" + tokens: [] + error: invalid hex escape sequence + + - note: invalid/escape-slash + rego: | + "\\\ a" + tokens: [] + error: invalid escape sequence + + - note: unparsable + rego: | + "\uD805" + tokens: [""] + error: serde_json cannot parse string + + diff --git a/tests/lexer/cases/symbol.yaml b/tests/lexer/cases/symbol.yaml new file mode 100644 index 0000000..93b9751 --- /dev/null +++ b/tests/lexer/cases/symbol.yaml @@ -0,0 +1,23 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + # Two single char symbols in a row must be parsed correctly. + - note: single-char-two-in-a-row + rego: | + ++--**//&&||{{}}(())[[]];;,,..::<<>> + tokens: [ + "+", "+", "-", "-", "*", "*", "/", "/", "&", "&", + "|", "|", "{", "{", "}", "}", "(", "(", ")", ")", + "[", "[", "]", "]", ";", ";", ",", ",", ".", ".", + ":", ":", "<", "<", ">", ">", + ""] + + # Multi char operators must be correctly parsed. + - note: multi-char-in-a-row + rego: ">====<== ===" + tokens: [ + ">=", "==", "=", "<=", "=", "==", "=", + ""] + + diff --git a/tests/lexer/cases/whitespace.yaml b/tests/lexer/cases/whitespace.yaml new file mode 100644 index 0000000..12a1059 --- /dev/null +++ b/tests/lexer/cases/whitespace.yaml @@ -0,0 +1,96 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: valid + rego: "\n\t\r\n " + tokens: [""] + kinds: [Eof] + + # Lock down unicode whitespace chars other than above. + # https://en.wikipedia.org/wiki/Whitespace_character#Unicode + - note: invalid/\u000b + rego: "\u000b" + tokens: [] + error: invalid character + - note: invalid/\u000c + rego: "\u000c" + tokens: [] + error: invalid character + - note: invalid/\u0085 + rego: "\u0085" + tokens: [] + error: invalid character + - note: invalid/\u00a0 + rego: "\u00a0" + tokens: [] + error: invalid character + - note: invalid/\u1680 + rego: "\u1680" + tokens: [] + error: invalid character + - note: invalid/\u2000 + rego: "\u2000" + tokens: [] + error: invalid character + - note: invalid/\u2001 + rego: "\u2001" + tokens: [] + error: invalid character + - note: invalid/\u2002 + rego: "\u2002" + tokens: [] + error: invalid character + - note: invalid/\u2003 + rego: "\u2003" + tokens: [] + error: invalid character + - note: invalid/\u2004 + rego: "\u2004" + tokens: [] + error: invalid character + - note: invalid/\u2005 + rego: "\u2005" + tokens: [] + error: invalid character + - note: invalid/\u2006 + rego: "\u2006" + tokens: [] + error: invalid character + - note: invalid/\u2007 + rego: "\u2007" + tokens: [] + error: invalid character + - note: invalid/\u2008 + rego: "\u2008" + tokens: [] + error: invalid character + - note: invalid/\u2009 + rego: "\u2009" + tokens: [] + error: invalid character + - note: invalid/\u200a + rego: "\u200a" + tokens: [] + error: invalid character + - note: invalid/\u2028 + rego: "\u2028" + tokens: [] + error: invalid character + - note: invalid/\u2029 + rego: "\u2029" + tokens: [] + error: invalid character + - note: invalid/\u202f + rego: "\u202f" + tokens: [] + error: invalid character + - note: invalid/\u205f + rego: "\u205f" + tokens: [] + error: invalid character + - note: invalid/\u3000 + rego: "\u3000" + tokens: [] + error: invalid character + diff --git a/tests/lexer/mod.rs b/tests/lexer/mod.rs new file mode 100644 index 0000000..63ead06 --- /dev/null +++ b/tests/lexer/mod.rs @@ -0,0 +1,335 @@ +#![cfg(test)] + +use anyhow::{bail, Result}; +use rego_rs::*; +use serde::{Deserialize, Serialize}; +use std::env; +use test_generator::test_resources; +//use walkdir::WalkDir; + +fn get_tokens<'source>(source: &'source Source<'source>) -> Result>> { + let mut tokens = vec![]; + let mut lex = Lexer::new(source); + loop { + let tok = lex.next_token()?; + tokens.push(tok.clone()); + if tok.0 == TokenKind::Eof { + break; + } + } + + Ok(tokens) +} + +fn check_loc(tok: &Token) -> Result<()> { + let msg = tok.1.source.message(tok.1.line, tok.1.col, "", ""); + let lines: Vec<&str> = msg.split('\n').collect(); + let source_line = lines[3]; + let caret_line = lines[4]; + let mut idx = 0usize; + let mut source_idx = idx; + loop { + match source_idx < source_line.len() && idx < caret_line.len() { + true => (), + // Handle Eof + false if tok.0 == TokenKind::Eof && source_idx >= source_line.len() => return Ok(()), + // Handle case where a raw string's first char is a newline. + false if tok.0 == TokenKind::RawString && &tok.1.text()[0..1] == "\n" => return Ok(()), + _ => bail!("could not find caret for {tok:#?} {msg}"), + } + match &caret_line[idx..idx + 1] { + "^" => { + let span_str = tok.1.text(); + let span_str = span_str.split('\n').collect::>()[0]; + let source_str = &source_line[source_idx..]; + assert!( + source_str.starts_with(span_str) || span_str.starts_with(source_str), + "location mismatch for {tok:#?} {msg}\n{span_str}\n{source_str}" + ); + return Ok(()); + } + _ if &source_line[source_idx..source_idx + 1] == "\t" => idx += 4, + _ => idx += 1, + } + source_idx += 1; + } +} + +#[test] +#[ignore = "intended for use by scripts/lex-file"] +fn one_file() -> Result<()> { + let mut file = String::default(); + let mut verbose = false; + for a in env::args() { + if a.ends_with(".rego") { + file = a.clone(); + } + if matches!(a.as_str(), "verbose") { + verbose = true; + } + } + + if file.is_empty() { + bail!("missing ") + } + + let contents = std::fs::read_to_string(&file)?; + + let source = Source { + file: file.as_str(), + contents: contents.as_str(), + lines: contents.split('\n').collect(), + }; + + for tok in &get_tokens(&source)? { + if tok.0 == TokenKind::Eof { + break; + } + check_loc(tok)?; + if verbose { + println!("{}", tok.1.source.message(tok.1.line, tok.1.col, "", "")); + } + println!("{:?}", tok); + } + + Ok(()) +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct Case { + pub rego: String, + pub note: String, + pub tokens: Vec, + pub kinds: Option>, + pub error: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct Test { + cases: Vec, +} + +fn yaml_test_impl(file: &str) -> Result<()> { + println!("\nrunning {}", file); + + let yaml = std::fs::read_to_string(file)?; + let test: Test = serde_yaml::from_str(&yaml)?; + + for case in &test.cases { + let source = Source { + file: "case.rego", + contents: case.rego.as_str(), + lines: case.rego.as_str().split('\n').collect(), + }; + + print!("case {} ", &case.note); + + match get_tokens(&source) { + Ok(tokens) => { + for (idx, tok) in tokens.iter().enumerate() { + if idx >= case.tokens.len() { + break; + } + assert_eq!( + tok.1.text(), + case.tokens[idx], + "{} Expected token `{}` not found", + source.message(tok.1.line, tok.1.col, "mismatch-error", &case.tokens[idx]), + &case.tokens[idx] + ); + + if let Some(k) = &case.kinds { + if idx >= k.len() { + break; + } + assert_eq!( + format!("{:?}", tok.0), + k[idx], + "{}", + source.message( + tok.1.line, + tok.1.col, + "mismatch-error", + "token kind mismatch" + ) + ); + } + + check_loc(tok)?; + } + assert_eq!( + tokens.len(), + case.tokens.len(), + "\n. Token count mismatch.\nLexed tokens:{:?}", + tokens + ); + if let Some(k) = &case.kinds { + assert_eq!( + tokens.len(), + k.len(), + "\n. Kind count mismatch.\nLexed tokens:{:?}", + tokens + ); + } + } + Err(actual) => match &case.error { + Some(expected) => { + let actual = actual.to_string(); + if !actual.contains(expected) { + bail!( + "Error message\n`{}\n`\ndoes not contain `{}`", + actual, + expected + ); + } + } + _ => return Err(actual), + }, + } + + println!("passed"); + } + println!("{} cases passed.", test.cases.len()); + Ok(()) +} + +fn yaml_test(file: &str) -> Result<()> { + match yaml_test_impl(file) { + Ok(_) => Ok(()), + Err(e) => { + // If Err is returned, it doesn't always get printed by cargo test. + // Therefore, panic with the error. + panic!("{}", e); + } + } +} + +#[test] +#[ignore = "intended for use by scripts/yaml-test-lex"] +fn one_yaml() -> Result<()> { + let mut file = String::default(); + for a in env::args() { + if a.ends_with(".yaml") { + file = a; + break; + } + } + + if file.is_empty() { + bail!("missing yaml test file"); + } + + yaml_test(file.as_str()) +} + +/* +fn run_yaml_tests_in(folder: &str) -> Result<()> { + let mut total = 0; + + for entry in WalkDir::new(folder) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry + .path() + .to_str() + .ok_or_else(|| anyhow!("failed to convert path to utf8 {:?}", entry.path()))?; + if !path.ends_with(".yaml") { + continue; + } + + total += 1; + yaml_test(path)?; + } + + println!("{} lexer yaml tests passed.", total); + Ok(()) +} + +#[test] +fn lexer_yaml_tests() -> Result<()> { + run_yaml_tests_in("tests/lexer") +}*/ + +#[test_resources("tests/lexer/**/*.yaml")] +fn run(path: &str) { + yaml_test(path).unwrap() +} + +#[test] +fn debug() -> Result<()> { + let rego = "\"This string is 35 characters long.\"\"short string\""; + let source = Source { + file: "case.rego", + contents: rego, + lines: rego.split('\n').collect(), + }; + + let mut lexer = Lexer::new(&source); + let tok = lexer.next_token()?; + check_loc(&tok)?; + + assert_eq!( + format!("{:?}", tok.1), + "1:2:1:35, \"This string is 35 characters lon...\"", + "long span not truncated correctly" + ); + + let tok = lexer.next_token()?; + check_loc(&tok)?; + assert_eq!(format!("{:?}", tok.1), "1:38:37:49, \"short string\""); + + Ok(()) +} + +#[test] +fn tab() -> Result<()> { + let rego = r#" "This string is 35 characters long."`raw string`p"#; + let source = Source { + file: "case.rego", + contents: rego, + lines: rego.split('\n').collect(), + }; + + let mut lexer = Lexer::new(&source); + + // read first tab and string. + let tok = lexer.next_token()?; + check_loc(&tok)?; + assert_eq!(tok.1.col, 6, "tab not accounted correctly."); + + // read raw string which contains tab. + let tok = lexer.next_token()?; + check_loc(&tok)?; + assert_eq!(tok.1.col, 42, "raw string not positioned correctly"); + + // read next token (ident) + let tok = lexer.next_token()?; + check_loc(&tok)?; + println!("{:?}", &tok); + println!("{}", source.message(tok.1.line, tok.1.col, "", "")); + assert_eq!( + tok.1.col, 56, + "tab within rawstring not accounted correctly" + ); + + Ok(()) +} + +#[test] +fn invalid_line() -> Result<()> { + let rego = ""; + let source = Source { + file: "case.rego", + contents: rego, + lines: rego.split('\n').collect(), + }; + + assert_eq!( + source.message(2, 0, "", ""), + "case.rego: invalid line 2 specified" + ); + + Ok(()) +} diff --git a/tests/parser/cases/every/every.yaml b/tests/parser/cases/every/every.yaml new file mode 100644 index 0000000..d252e2f --- /dev/null +++ b/tests/parser/cases/every/every.yaml @@ -0,0 +1,60 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + rego: | + package test + import future.keywords + + y = 8 { + every x in [2] { + x > 0 + every a, b in vals { check(a) } + } + } + policy: + - spec: + head: + compr: + refr: + var: y + assign: + op: "=" + value: + number: 8 + bodies: + - query: + stmts: + - literal: + every: + key: x + domain: + array: + - number: 2 + query: + stmts: + - literal: + expr: + boolexpr: + op: ">" + lhs: + var: x + rhs: + number: 0 + - literal: + every: + key: a + value: b + domain: + var: vals + query: + stmts: + - literal: + expr: + call: + fcn: + var: check + params: + - var: a + diff --git a/tests/parser/cases/expressions/arithmetic.yaml b/tests/parser/cases/expressions/arithmetic.yaml new file mode 100644 index 0000000..d8b7a17 --- /dev/null +++ b/tests/parser/cases/expressions/arithmetic.yaml @@ -0,0 +1,40 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + rego: | + package test + + x = 1 - 2 * 3 / 4 + 2 + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: = + value: + arithexpr: + op: + + lhs: + arithexpr: + op: "-" + lhs: + number: 1 + rhs: + arithexpr: + op: "/" + lhs: + arithexpr: + op: "*" + lhs: + number: 2 + rhs: + number: 3 + rhs: + number: 4 + rhs: + number: 2 + bodies: [] diff --git a/tests/parser/cases/expressions/array-compr.yaml b/tests/parser/cases/expressions/array-compr.yaml new file mode 100644 index 0000000..637bf28 --- /dev/null +++ b/tests/parser/cases/expressions/array-compr.yaml @@ -0,0 +1,209 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: case1-compr + rego: | + package test + x = {1} + y = {2} + z = [ x | y ] #, 5] # in {5}, 6] + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + arraycompr: + term: + var: x + query: + stmts: + - span: y + literal: + expr: + var: y + bodies: [] + + - note: case2-array + rego: | + package test + x = {1} + y = {2} + z = [ x | y, 5] # in {5}, 6] + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + array: + - binexpr: + op: "|" + lhs: + var: x + rhs: + var: y + - number: 5 + bodies: [] + + - note: case3-compr + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = [ x | y, 5 in {5}] #, 6] + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + arraycompr: + term: + var: x + query: + stmts: + - span: y, 5 in {5} + literal: + expr: + inexpr: + key: + var: y + value: + number: 5 + collection: + set: + - number: 5 + bodies: [] + + - note: case4-array + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = [ x | y, 5 in {5}, 6] + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + array: + - binexpr: + op: "|" + lhs: + var: x + rhs: + var: y + - inexpr: + key: + number: 5 + collection: + set: + - number: 5 + - number: 6 + bodies: [] + + - note: case5-array + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = [ x - {10} | y, 5 in {5}] #, 6] + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + array: + - binexpr: + op: "|" + lhs: + arithexpr: + op: "-" + lhs: + var: x + rhs: + set: + - number: 10 + rhs: + var: y + - inexpr: + key: + number: 5 + collection: + set: + - number: 5 + bodies: [] + + - note: case6-compr + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = [ (x - {10}) | y, 5 in {5}] #, 6] + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + arraycompr: + term: + arithexpr: + op: "-" + lhs: + var: x + rhs: + set: + - number: 10 + query: + stmts: + - literal: + expr: + inexpr: + key: + var: y + value: + number: 5 + collection: + set: + - number: 5 + bodies: [] + diff --git a/tests/parser/cases/expressions/array.yaml b/tests/parser/cases/expressions/array.yaml new file mode 100644 index 0000000..ae9260f --- /dev/null +++ b/tests/parser/cases/expressions/array.yaml @@ -0,0 +1,108 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: all + rego: | + package test + # One item + x = {1} + + # Multiple items + y = [ + 2.5, "abc", [ 4, `raw`, # Trailing comma + ], + # Empty array + [], + # Nested empty + [[[[[]]]]] + ] + + policy: + - spec: + head: + compr: + refr: + var: x + assign: + span: = {1} + op: "=" + value: + set: + - number: 1 + bodies: --skip-- + + - spec: + head: + compr: + refr: + var: y + assign: + op: "=" + value: + array: + - number: 2.5 + - string: abc + - array: + - number: 4 + - rawstring: raw + - array: [] + - array: + - array: + - array: + - array: + - array: [] + bodies: [] + + - note: trailing-comma + rego: | + package test + x = [1,] + y = [1,2 + ,] + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + array: + span: "[1,]" + values: + - number: 1 + bodies: [] + - spec: + head: + compr: + refr: + var: y + assign: + op: "=" + value: + array: + span: "[1,2\n,]" + values: + - number: 1 + - number: 2 + bodies: [] + + - note: no-comma + rego: | + package test + x = [1 2] + error: expecting `]` while parsing array + + - note: two-trailing-commas + rego: | + package test + x = [1,2,,] + error: expecting expression + + - note: unclosed + rego: | + package test + x = [ 1 + error: expecting `]` while parsing array diff --git a/tests/parser/cases/expressions/bin.yaml b/tests/parser/cases/expressions/bin.yaml new file mode 100644 index 0000000..08fe6a2 --- /dev/null +++ b/tests/parser/cases/expressions/bin.yaml @@ -0,0 +1,64 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + rego: | + package test + + # & has higher precedence than |. + # If not the following expressions would evaluate to empty set. + x = {1, 2, 3} | {2} & {4} + y = {2} & {4} | {1, 2, 3} + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: = + value: + binexpr: + op: "|" + lhs: + set: + - number: 1 + - number: 2 + - number: 3 + rhs: + binexpr: + op: "&" + lhs: + set: + - number: 2 + rhs: + set: + - number: 4 + bodies: [] + - spec: + head: + compr: + refr: + var: y + assign: + op: = + value: + binexpr: + op: "|" + rhs: + set: + - number: 1 + - number: 2 + - number: 3 + lhs: + binexpr: + op: "&" + lhs: + set: + - number: 2 + rhs: + set: + - number: 4 + bodies: [] + diff --git a/tests/parser/cases/expressions/bool.yaml b/tests/parser/cases/expressions/bool.yaml new file mode 100644 index 0000000..2b56f67 --- /dev/null +++ b/tests/parser/cases/expressions/bool.yaml @@ -0,0 +1,43 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + rego: | + package test + + # bool ops have lower precedence than arithmetic operators + x = 1 + 2 > 3 - 2 + + # TODO: lock down + # different types against object + # different types against set + # strings etc + + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: = + value: + boolexpr: + op: ">" + lhs: + arithexpr: + op: "+" + lhs: + number: 1 + rhs: + number: 2 + rhs: + arithexpr: + op: "-" + lhs: + number: 3 + rhs: + number: 2 + + bodies: [] diff --git a/tests/parser/cases/expressions/call.yaml b/tests/parser/cases/expressions/call.yaml new file mode 100644 index 0000000..112db6c --- /dev/null +++ b/tests/parser/cases/expressions/call.yaml @@ -0,0 +1,48 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + rego: | + package test + + # A call in rule-ref. This creates an object. + deny[sprintf("Hello %v", ["world"])] = 1 + + # Trailing comma + x = inc(5,) + policy: + - spec: + head: + compr: + refr: + refbrack: + refr: + var: deny + index: + call: + fcn: + var: sprintf + params: + - string: "Hello %v" + - array: + - string: "world" + assign: + op: = + value: + number: 1 + bodies: [] + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + call: + fcn: + var: inc + params: + - number: 5 + bodies: [] diff --git a/tests/parser/cases/expressions/in.yaml b/tests/parser/cases/expressions/in.yaml new file mode 100644 index 0000000..82b9b71 --- /dev/null +++ b/tests/parser/cases/expressions/in.yaml @@ -0,0 +1,59 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + rego: | + package test + import future.keywords.in + + x = 5 in [4, 5] + + # in-exprs are left associative and ahve lower-precedence than bin-expr. + # The following will be parsed as + # (5 in [4, 5]) in (set() | {true}) + z = 5 in [4, 5] in set() | {true} + + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + inexpr: + key: + number: 5 + collection: + array: + - number: 4 + - number: 5 + bodies: [] + - spec: + head: + compr: + refr: + var: z + assign: + op: "=" + value: + inexpr: + key: + inexpr: + key: + number: 5 + collection: + array: + - number: 4 + - number: 5 + collection: + binexpr: + op: "|" + lhs: + set: [] + rhs: + set: + - true + bodies: [] diff --git a/tests/parser/cases/expressions/membership.yaml b/tests/parser/cases/expressions/membership.yaml new file mode 100644 index 0000000..24666ba --- /dev/null +++ b/tests/parser/cases/expressions/membership.yaml @@ -0,0 +1,53 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: basic + rego: | + package test + import future.keywords.in + + # key, value in collection + x = 0, 5 in [5] + + # Chained in-exprs + y = 0, 5 in c in d + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + inexpr: + key: + number: 0 + value: + number: 5 + collection: + array: + - number: 5 + bodies: [] + - spec: + head: + compr: + refr: + var: y + assign: + op: "=" + value: + inexpr: + key: + inexpr: + key: + number: 0 + value: + number: 5 + collection: + var: c + collection: + var: d + bodies: [] + diff --git a/tests/parser/cases/expressions/object.yaml b/tests/parser/cases/expressions/object.yaml new file mode 100644 index 0000000..bb99ea6 --- /dev/null +++ b/tests/parser/cases/expressions/object.yaml @@ -0,0 +1,163 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: all + rego: | + package test + import future.keywords.in + + # Empty object + x = {} + + # Single field + y = { + "a" : 5 + } + + # Multiple fields + z = { + "a" : 5, + "b" : [ 1, 2, 3 ], + "c" : { 4, 5, 6 }, + "d" : { + "a" : 5, + "b" : set() + } + # array as key + , [1, + 2, + 3] : 4, + + # set as key + { 1 } : 2, + + # Object as key + { + "a" : 1, + "b" : 2, # Trailing comma + } : `hello, + world` + + # Null, boolean + , null: false, + true : true, + + # Only single var in is supported as value + "p" : "q", "r" in "d" : "e" + + } + policy: + - spec: + span: x = {} + head: + compr: + span: x = {} + refr: + var: x + assign: + span: = {} + op: = + value: + object: + fields: [] + bodies: [] + - spec: + head: + compr: + refr: + var: y + assign: + op: = + value: + object: + fields: + - key: + string: a + value: + number: 5 + bodies: [] + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + object: + fields: + - key: + string: a + value: + number: 5 + - key: + string: b + value: + array: + - number: 1 + - number: 2 + - number: 3 + - key: + string: c + value: + set: + - number: 4 + - number: 5 + - number : 6 + - key: + string: d + value: + object: + fields: + - key: + string: a + value: + number: 5 + - key: + string: b + value: + set: [] + - key: + array: + - number: 1 + - number: 2 + - number: 3 + value: + number: 4 + - key: + set: + - number: 1 + value: + number: 2 + - key: + object: + fields: + - key: + string: a + value: + number: 1 + - key: + string: b + value: + number: 2 + value: + rawstring: "hello,\n world" + - key: null + value: false + - key: true + value: true + - key: + string: p + value: + string: q + - key: + inexpr: + key: + string: r + collection: + string: d + value: + string: e + + bodies: [] diff --git a/tests/parser/cases/expressions/set-compr.yaml b/tests/parser/cases/expressions/set-compr.yaml new file mode 100644 index 0000000..bb24bd5 --- /dev/null +++ b/tests/parser/cases/expressions/set-compr.yaml @@ -0,0 +1,209 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: case1-compr + rego: | + package test + x = {1} + y = {2} + z = { x | y } #, 5} # in {5}, 6} + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + setcompr: + term: + var: x + query: + stmts: + - span: y + literal: + expr: + var: y + bodies: [] + + - note: case2-set + rego: | + package test + x = {1} + y = {2} + z = { x | y, 5} # in {5}, 6} + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + set: + - binexpr: + op: "|" + lhs: + var: x + rhs: + var: y + - number: 5 + bodies: [] + + - note: case3-compr + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = { x | y, 5 in {5}} #, 6} + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + setcompr: + term: + var: x + query: + stmts: + - span: y, 5 in {5} + literal: + expr: + inexpr: + key: + var: y + value: + number: 5 + collection: + set: + - number: 5 + bodies: [] + + - note: case4-set + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = { x | y, 5 in {5}, 6} + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + set: + - binexpr: + op: "|" + lhs: + var: x + rhs: + var: y + - inexpr: + key: + number: 5 + collection: + set: + - number: 5 + - number: 6 + bodies: [] + + - note: case5-set + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = { x - {10} | y, 5 in {5}} #, 6} + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + set: + - binexpr: + op: "|" + lhs: + arithexpr: + op: "-" + lhs: + var: x + rhs: + set: + - number: 10 + rhs: + var: y + - inexpr: + key: + number: 5 + collection: + set: + - number: 5 + bodies: [] + + - note: case6-compr + rego: | + package test + import future.keywords.in + x = {1} + y = {2} + z = { (x - {10}) | y, 5 in {5}} #, 6} + policy: + - --skip-- + - --skip-- + - spec: + head: + compr: + refr: + var: z + assign: + op: = + value: + setcompr: + term: + arithexpr: + op: "-" + lhs: + var: x + rhs: + set: + - number: 10 + query: + stmts: + - literal: + expr: + inexpr: + key: + var: y + value: + number: 5 + collection: + set: + - number: 5 + bodies: [] + diff --git a/tests/parser/cases/expressions/set.yaml b/tests/parser/cases/expressions/set.yaml new file mode 100644 index 0000000..f2fa2a3 --- /dev/null +++ b/tests/parser/cases/expressions/set.yaml @@ -0,0 +1,112 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: all + rego: | + package test + # One item + x = {1} + + # Multiple items + y = { + 2.5, "abc", { 4, `raw` + }, + # Empty set has a special syntax + set( ), + # Nested empty + {{{{{}}}}} + } + + policy: + - spec: + head: + compr: + refr: + var: x + assign: + span: = {1} + op: "=" + value: + set: + - number: 1 + bodies: --skip-- + + - spec: + head: + compr: + refr: + var: y + assign: + op: "=" + value: + set: + - number: 2.5 + - string: abc + - set: + - number: 4 + - rawstring: raw + - set: [] + - set: + - set: + - set: + - set: + - object: + span: "{}" + fields: [] + bodies: [] + + - note: trailing-comma + rego: | + package test + x = {1,} + y = {1,2 + ,} + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + set: + span: "{1,}" + values: + - number: 1 + bodies: [] + - spec: + head: + compr: + refr: + var: y + assign: + op: "=" + value: + set: + span: "{1,2\n,}" + values: + - number: 1 + - number: 2 + bodies: [] + + + - note: no-comma + rego: | + package test + x = {1 2} + error: expecting `}` while parsing set + + - note: two-trailing-commas + rego: | + package test + x = {1,2,,} + error: expecting expression + + - note: unclosed + rego: | + package test + x = { 1 + error: expecting `}` while parsing set + diff --git a/tests/parser/cases/import/future.yaml b/tests/parser/cases/import/future.yaml new file mode 100644 index 0000000..ab798a1 --- /dev/null +++ b/tests/parser/cases/import/future.yaml @@ -0,0 +1,154 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: namespace + rego: | + package test + import future.keywords + imports: + - span: import future.keywords + refr: + refdot: + span: future.keywords + refr: + var: future + field: keywords + + - note: all + rego: | + package test + import future.keywords.contains import future.keywords.every + import future.keywords.if + import + future.keywords.in + imports: + - span: import future.keywords.contains + refr: + refdot: + span: future.keywords.contains + refr: + refdot: + span: future.keywords + refr: + var: future + field: keywords + field: contains + - span: import future.keywords.every + refr: + refdot: + span: future.keywords.every + refr: + refdot: + span: future.keywords + refr: + var: future + field: keywords + field: every + - span: import future.keywords.if + refr: + refdot: + span: future.keywords.if + refr: + refdot: + span: future.keywords + refr: + var: future + field: keywords + field: if + - span: "import\nfuture.keywords.in" + refr: + refdot: + span: future.keywords.in + refr: + refdot: + span: future.keywords + refr: + var: future + field: keywords + field: in + + - note: bracket + rego: | + package test + import future["keywords"]["contains"] import future.keywords["every"] + import + future["keywords"].if + imports: + - span: import future["keywords"]["contains"] + refr: + refbrack: + span: future["keywords"]["contains"] + refr: + refbrack: + span: future["keywords"] + refr: + var: future + index: + string: keywords + index: + string: contains + - span: import future.keywords["every"] + refr: + refbrack: + span: future.keywords["every"] + refr: + refdot: + span: future.keywords + refr: + var: future + field: keywords + index: + string: every + - span: "import\nfuture[\"keywords\"].if" + refr: + refdot: + span: future["keywords"].if + refr: + refbrack: + span: future["keywords"] + refr: + var: future + index: + string: keywords + field: if + + - note: as + rego: | + package test + import future.keywords.in as on + error: "`future` imports cannot be aliased" + + - note: shadow + rego: | + package test + import future.keywords + import future["keywords"] + error: "this import shadows previous import of `contains`" + + - note: shadow/1 + rego: | + package test + import future.keywords + import future.keywords.if + error: "this import shadows previous import of `if`" + + - note: shadow/2 + rego: | + package test + import future.keywords.if + import future.keywords + error: "this import shadows previous import of `if`" + + - note: in-as-var + rego: | + package test + import future.keywords.if + in = 5 + + - note: in-as-var-imported + rego: | + package test + import future.keywords.in + in = 5 + error: unexpected keyword `in` diff --git a/tests/parser/cases/import/import.yaml b/tests/parser/cases/import/import.yaml new file mode 100644 index 0000000..091de25 --- /dev/null +++ b/tests/parser/cases/import/import.yaml @@ -0,0 +1,308 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: data + rego: | + package test + import data + import input + imports: + - span: import data + refr: + var: data + - span: import input + refr: + var: input + + - note: input + rego: | + package test + import input + imports: + - span: import input + refr: + var: input + + - note: dot + rego: | + package test + import input.a + import data.b + imports: + - span: import input.a + refr: + refdot: + refr: + var: input + field: a + - span: import data.b + refr: + refdot: + refr: + var: data + field: b + + - note: bracket + rego: | + package test + import input["a"] + import data["b"] + imports: + - span: import input["a"] + refr: + refbrack: + refr: + var: input + index: + string: a + - span: import data["b"] + refr: + refbrack: + refr: + var: data + index: + string: b + + - note: multi-dot + rego: | + package test + import input.a.b + import data.c.d + imports: + - span: import input.a.b + refr: + refdot: + span: input.a.b + refr: + refdot: + span: input.a + refr: + var: input + field: a + field: b + - span: import data.c.d + refr: + refdot: + span: data.c.d + refr: + refdot: + span: data.c + refr: + var: data + field: c + field: d + policy: [] + + + - note: complex + rego: | + package test + import input["b.c"].d["e.f"].g + import data.a["b.c"].d["e.f"] + package: --skip-- + imports: + - span: import input["b.c"].d["e.f"].g + refr: + refdot: + span: input["b.c"].d["e.f"].g + refr: + refbrack: + span: input["b.c"].d["e.f"] + refr: + refdot: + span: input["b.c"].d + refr: + refbrack: + span: input["b.c"] + refr: + var: input + index: + string: b.c + field: d + index: + string: e.f + field: g + - span: import data.a["b.c"].d["e.f"] + refr: + refbrack: + span: data.a["b.c"].d["e.f"] + refr: + refdot: + span: data.a["b.c"].d + refr: + refbrack: + span: data.a["b.c"] + refr: + refdot: + span: data.a + refr: + var: data + field: a + index: + string: b.c + field: d + index: + string: e.f + + - note: same-line + rego: package test import input.a["b"] import data["c"].d + package: + span: package test + refr: + var: test + imports: + - span: import input.a["b"] + refr: + refbrack: + span: input.a["b"] + refr: + refdot: + span: input.a + refr: + var: input + field: a + index: + string: b + - span: import data["c"].d + refr: + refdot: + span: data["c"].d + refr: + refbrack: + span: data["c"] + refr: + var: data + index: + string: c + field: d + + - note: as + rego: | + package test + import input.x as y + imports: + - span: import input.x as y + refr: + refdot: + span: input.x + refr: + var: input + field: x + as: y + + - note: as/newline + rego: | + package test + import + input.x + as + y + imports: + - span: "import\ninput.x\nas\ny" + refr: + refdot: + span: input.x + refr: + var: input + field: x + as: y + + - note: missing-ref + rego: | + package test + import ( a) + error: expecting identifier + + - note: missing-ref-1 + rego: | + package test + import ["a"] + error: expecting identifier + + - note: invalid-beginning + rego: | + package test + import foo + error: "import path must begin with one of: {data, future, input}" + + - note: invalid-beginning-1 + rego: | + package test + import foo.bar + error: "import path must begin with one of: {data, future, input}" + + - note: missing-field-1 + rego: | + package test + import data.a. + error: expecting identifier + + - note: missing-field-2 + rego: | + package test + import data.a.b. + error: expecting identifier + + - note: space-after-dot + rego: | + package test + import input. a + error: invalid whitespace between . and identifier + + - note: space-before-dot + rego: | + package test + import input .a.b + error: invalid whitespace before . + + - note: space-after-lbracket + rego: package test import a ["b"] + error: invalid whitespace before [ + + - note: non-string-index + rego: package test import a[1] + error: expected string + + - note: shadow + rego: + package test + import data.x import data.y import data["x"] + error: import shadows following import defined earlier + + - note: keyword/else + rego: + package test + import data.x as else + error: "unexpected keyword `else`" + + - note: keyword/as + rego: + package test + import data.x as as + error: "unexpected keyword `as`" + + - note: as/_ + rego: + package test + import data.x as _ + error: "`_` cannot be used as alias" + + - note: as/_ + rego: + package test + import data.x as 1 + error: expecting identifier + + - note: as/eof + rego: + package test + import data.x as + error: expecting identifier + + - note: as/multiple + rego: + package test + import data.x as y + import data.y as y + error: import shadows following import defined earlier diff --git a/tests/parser/cases/package/package.yaml b/tests/parser/cases/package/package.yaml new file mode 100644 index 0000000..c857c68 --- /dev/null +++ b/tests/parser/cases/package/package.yaml @@ -0,0 +1,129 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: single-char + rego: package a + package: + span: package a + refr: + var: a + + - note: simple + rego: package test + package: + span: package test + refr: + var: test + + - note: dot + rego: package a.b + package: + span: package a.b + refr: + refdot: + span: a.b + refr: + var: a + field: b + + - note: multi-dot + rego: package a.b.c + package: + span: package a.b.c + refr: + refdot: + span: a.b.c + refr: + refdot: + span: a.b + refr: + var: a + field: b + field: c + + - note: bracket + rego: package a["b"] + package: + span: package a["b"] + refr: + refbrack: + span: a["b"] + refr: + var: a + index: + string: b + + - note: multi-bracket + rego: package a["b"]["c.d"] + package: + span: package a["b"]["c.d"] + refr: + refbrack: + span: a["b"]["c.d"] + refr: + refbrack: + span: a["b"] + refr: + var: a + index: + string: b + index: + string: c.d + + - note: complex + rego: package a["b.c"].d["e.f"].g + package: + span: package a["b.c"].d["e.f"].g + refr: + refdot: + span: a["b.c"].d["e.f"].g + refr: + refbrack: + span: a["b.c"].d["e.f"] + refr: + refdot: + span: a["b.c"].d + refr: + refbrack: + span: a["b.c"] + refr: + var: a + index: + string: "b.c" + field: d + index: + string: e.f + field: g + + - note: missing-package-keyword + rego: packge a + error: expecting `package` + + - note: missing-var + rego: package 5 + error: expecting identifier + + - note: missing-var-1 + rego: package ( + error: expecting identifier + + - note: missing-field + rego: package a.b. + error: expecting identifier + + - note: space-after-dot + rego: package a. b + error: invalid whitespace between . and identifier + + - note: space-before-dot + rego: package a .b + error: invalid whitespace before . + + - note: space-after-lbracket + rego: package a ["b"] + error: invalid whitespace before [ + + - note: non-string-index + rego: package a[1] + error: expected string diff --git a/tests/parser/cases/rules/basic.yaml b/tests/parser/cases/rules/basic.yaml new file mode 100644 index 0000000..728daca --- /dev/null +++ b/tests/parser/cases/rules/basic.yaml @@ -0,0 +1,44 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: same-line-no-body + rego: | + package test + add(x, y) := 5 sub(x, y) = 5 + package: --skip-- + imports: + policy: + - spec: + span: add(x, y) := 5 + head: + func: + span: add(x, y) := 5 + refr: + var: add + args: + - var: x + - var: y + assign: + span: := 5 + op: := + value: + number: 5 + bodies: [] + - spec: + span: sub(x, y) = 5 + head: + func: + span: sub(x, y) = 5 + refr: + var: sub + args: + - var: x + - var: y + assign: + span: = 5 + op: = + value: + number: 5 + bodies: [] + diff --git a/tests/parser/cases/rules/else.yaml b/tests/parser/cases/rules/else.yaml new file mode 100644 index 0000000..5001e37 --- /dev/null +++ b/tests/parser/cases/rules/else.yaml @@ -0,0 +1,250 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: no-query + rego: | + package test + + x = 10 else { + true + } + error: unexpected keyword `else` + + - note: no-query + rego: | + package test + + x = 10 { + false + } { + true + } + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + number: 10 + bodies: + - query: + stmts: + - literal: + expr: + false + - query: + stmts: + - literal: + expr: + true + + - note: no-else + rego: | + package test + + x = 10 { + false + } { + true + } + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + number: 10 + bodies: + - query: + stmts: + - literal: + expr: + false + - query: + stmts: + - literal: + expr: + true + + - note: if-no-else + rego: | + package test + import future.keywords.if + + x = 10 if { + false + } { + true + } + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + number: 10 + bodies: + - query: + stmts: + - literal: + expr: + false + - query: + stmts: + - literal: + expr: + true + + - note: rule-named-if + rego: | + package test + + x = 10 if { + false + } { + true + } + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + number: 10 + bodies: [] + - spec: + head: + compr: + refr: + var: if + bodies: + - query: + stmts: + - literal: + expr: + false + - query: + stmts: + - literal: + expr: + true + + - note: query-else + rego: | + package test + + x = 10 { + false + } else { + true + } + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + number: 10 + bodies: + - query: + stmts: + - literal: + expr: + false + - query: + stmts: + - literal: + expr: + true + + - note: if-literal-else + rego: | + package test + import future.keywords.if + + x = 10 if 1 < 0 else { + true + } + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + number: 10 + bodies: + - query: + stmts: + - literal: + expr: + boolexpr: + op: "<" + lhs: + number: 1 + rhs: + number: 0 + - query: + stmts: + - literal: + expr: + true + + - note: if-literal-else-assign + rego: | + package test + import future.keywords.if + + # This will evaluate to 10 + x = 10 if 1 < 0 else := 20 { + true + } + policy: + - spec: + head: + compr: + refr: + var: x + assign: + op: "=" + value: + number: 10 + bodies: + - query: + stmts: + - literal: + expr: + boolexpr: + op: "<" + lhs: + number: 1 + rhs: + number: 0 + - assign: + op: ":=" + value: + number: 20 + query: + stmts: + - literal: + expr: + true + diff --git a/tests/parser/cases/rules/set.yaml b/tests/parser/cases/rules/set.yaml new file mode 100644 index 0000000..f3c230b --- /dev/null +++ b/tests/parser/cases/rules/set.yaml @@ -0,0 +1,135 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: contains + rego: | + package test + import future.keywords + + deny.a contains 0, "bar" in ["bar1"] if true + policy: + - spec: + head: + set: + refr: + refdot: + refr: + var: deny + field: a + key: + inexpr: + key: + number: 0 + value: + string: bar + collection: + array: + - string: bar1 + bodies: + - query: + stmts: + - literal: + expr: + true + - note: old-syntax + rego: | + package test + import future.keywords.if + + # The following are not sets + x1 if true + x2 { true } + + # The following are sets + y.a { true } + y["b"] { true } + + # The following are not sets + z.a if { true } + z["b"] if { true } + + policy: + - spec: + head: + compr: + refr: + var: x1 + bodies: + - query: + stmts: + - literal: + expr: + true + - spec: + head: + compr: + refr: + var: x2 + bodies: + - query: + stmts: + - literal: + expr: + true + + - spec: + head: + set: + refr: + refdot: + refr: + var: y + field: a + bodies: + - query: + stmts: + - literal: + expr: + true + + - spec: + head: + set: + refr: + var: y + key: + string: b + bodies: + - query: + stmts: + - literal: + expr: + true + + - spec: + head: + compr: + refr: + refdot: + refr: + var: z + field: a + bodies: + - query: + stmts: + - literal: + expr: + true + + - spec: + head: + compr: + refr: + refbrack: + refr: + var: z + index: + string: b + bodies: + - query: + stmts: + - literal: + expr: + true + diff --git a/tests/parser/cases/some/some.in.yaml b/tests/parser/cases/some/some.in.yaml new file mode 100644 index 0000000..02f74ba --- /dev/null +++ b/tests/parser/cases/some/some.in.yaml @@ -0,0 +1,226 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: all + rego: | + package test + import future.keywords.in + + x = y { + # Empty set. This evaluates to false. + some a in {1} + + # Key, value combo + some a + , b in r + + # Key, value combo can be non-vars + some 5, x in array + + some "hello", "world" in map + + some p, q in { r | + some a, b in d + } + } + + policy: + - spec: + head: --skip-- + bodies: + - query: + stmts: + - span: some a in {1} + literal: + some-decl: + key: + var: a + collection: + set: + - number: 1 + - span: "some a\n , b in r" + literal: + some-decl: + key: + var: a + value: + var: b + collection: + var: r + - span: "some 5, x in array" + literal: + some-decl: + key: + number: 5 + value: + var: x + collection: + var: array + - span: "some \"hello\", \"world\" in map" + literal: + some-decl: + key: + string: hello + value: + string: world + collection: + var: map + - literal: + some-decl: + key: + var: p + value: + var: q + collection: + setcompr: + term: + var: r + query: + stmts: + - literal: + some-decl: + key: + var: a + value: + var: b + collection: + var: d + + + - note: unimported-in + rego: | + package test + x = y { + some a in b + } + error: expecting `}` while parsing query + + - note: more-refs + rego: | + package test + import future.keywords.in + x = y { + some a, b, c in d + } + error: encountered `c` while expecting `in` + + - note: eof + rego: | + package test + import future.keywords.in + x = y { + some a, b in + error: expecting expression + + - note: missing-expr + rego: | + package test + import future.keywords.in + x = y { + some a, b in + } + error: expecting expression + + - note: missing-comma + rego: | + package test + import future.keywords.in + x = y { + some a b in c + } + error: expecting `}` while parsing query + + - note: same-line + rego: | + package test + x = y{ + some a b in {4, 5} + [1, 2, 3][a] == 3 + y = a + } + error: expecting `}` while parsing query + + - note: multi-line-parsed as membership + rego: | + package test + import future.keywords.in + b := 5 + x = y{ + some a + b in {4, 5} + # The following [ starting a line ought to get + # parsed as a literal statement and not raise errors + # regarding gap from previous refr. + [1, 2, 3][a] == 3 + y = a + } + policy: + - spec: + head: + compr: + span: b := 5 + refr: + var: b + assign: + op: := + value: + number: 5 + bodies: [] + - spec: + head: + compr: + span: x = y + refr: + var: x + assign: + op: = + value: + var: y + bodies: + - query: + stmts: + - literal: + some-vars: + span: some a + vars: + - a + - literal: + expr: + inexpr: + span: b in {4, 5} + key: + var: b + collection: + set: + - number: 4 + - number: 5 + - literal: + expr: + boolexpr: + span: "[1, 2, 3][a] == 3" + op: == + lhs: + refbrack: + span: "[1, 2, 3][a]" + refr: + array: + - number: 1 + - number: 2 + - number: 3 + index: + var: a + rhs: + number: 3 + - literal: + expr: + assignexpr: + span: y = a + op: = + lhs: + var: y + rhs: + var: a + + + diff --git a/tests/parser/cases/some/some.vars.yaml b/tests/parser/cases/some/some.vars.yaml new file mode 100644 index 0000000..cbd6374 --- /dev/null +++ b/tests/parser/cases/some/some.vars.yaml @@ -0,0 +1,74 @@ +# Copyright (c) Rego-Rs Authors. +# Licensed under the Apache 2.0 license. + +cases: + - note: single-multi-vars + rego: | + package test + x = y { + # Single var + some a + # Multiple var. With and without spaces. + some + d,e, + f + } + policy: + - spec: + head: --skip-- + bodies: + - query: + stmts: + - span: some a + literal: + some-vars: + span: some a + vars: + - a + - span: "some\n d,e,\n f" + literal: + some-vars: + span: "some\n d,e,\n f" + vars: [d, e, f] + + - note: same-line-error + rego: | + package test + x = y { + some a some b + } + error: expecting `}` while parsing query + + - note: keyword + rego: | + package test + x = y { + some as + } + error: unexpected keyword `as` + + - note: top-level + rego: | + package test + x = some y + error: unexpected keyword `some` + + - note: top-level1 + rego: | + package test + some y + error: unexpected keyword `some` + + - note: some-in-set + rego: | + package test + x = { some y } + error: unexpected keyword `some` + + - note: non-var + rego: | + package test + x = y { + some a, 5 + } + error: encountered `5` while expecting identifier diff --git a/tests/parser/mod.rs b/tests/parser/mod.rs new file mode 100644 index 0000000..3e005a9 --- /dev/null +++ b/tests/parser/mod.rs @@ -0,0 +1,795 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +#![cfg(test)] + +use anyhow::{anyhow, bail, Result}; +use rego_rs::*; +use serde::{Deserialize, Serialize}; +use std::env; +use test_generator::test_resources; +//use walkdir::WalkDir; + +macro_rules! my_assert_eq { + ($left:expr, $right:expr, $($arg:tt)+) => { + match (&($left), &($right)) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + return Err(anyhow!("mismatch:\nleft = {}\nright = {}\n{}", + &$left, &$right, format_args!($($arg)+))); + } + } + } + } +} + +#[test] +#[ignore = "intended for use by scripts/rego-parse"] +fn one_file() -> Result<()> { + let mut file = String::default(); + for a in env::args() { + if a.ends_with(".rego") { + file = a; + break; + } + } + + if file.is_empty() { + bail!("missing "); + } + + let contents = std::fs::read_to_string(&file)?; + + let source = Source { + file: file.as_str(), + contents: contents.as_str(), + lines: contents.split('\n').collect(), + }; + let mut parser = Parser::new(&source)?; + let ast = parser.parse()?; + println!("{:#?}", ast); + Ok(()) +} + +fn skip_value(v: &Value) -> bool { + matches!(v, Value::String(s) if s == "--skip--") +} + +fn match_span(s: &Span, v: &Value) -> Result<()> { + match &v { + Value::String(vs) => { + my_assert_eq!( + s.text(), + vs, + "{}", + s.source + .message(s.line, s.col, "match-error", "mismatch happened here.") + ); + } + _ => { + my_assert_eq!( + s.text(), + serde_json::to_string_pretty(v)?, + "{}", + s.source + .message(s.line, s.col, "match-error", "mismatch happened here.") + ) + } + } + + Ok(()) +} + +fn match_span_opt(s: &Span, v: &Value) -> Result<()> { + if *v != Value::Undefined { + match_span(s, v) + } else { + Ok(()) + } +} + +fn match_vec(s: &Span, vec: &Vec, v: &Value) -> Result<()> { + if v.as_object().is_ok() { + match_span_opt(s, &v["span"])?; + return match_vec(s, vec, &v["values"]); + } + let v = v.as_array()?; + my_assert_eq!( + vec.len(), + v.len(), + "{}", + s.source.message( + s.line, + s.col, + "match-error", + "mismatch in number of elements in sequence following this location" + ) + ); + for i in 0..vec.len() { + match_expr(&vec[i], &v[i])?; + } + Ok(()) +} + +fn match_object(s: &Span, fields: &Vec<(Span, Expr, Expr)>, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match_span_opt(s, &v["span"])?; + match &v["fields"].as_array() { + Ok(a) => { + my_assert_eq!(fields.len(), a.len(), "field length mismatch"); + for (idx, (_, k, v)) in fields.iter().enumerate() { + match_expr(k, &a[idx]["key"])?; + match_expr(v, &a[idx]["value"])?; + } + Ok(()) + } + _ => bail!("incorrect field specification in yaml. Must be array."), + } +} + +fn match_expr_impl(e: &Expr, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match e { + Expr::String(s) => match_span(s, &v["string"]), + Expr::RawString(s) => match_span(s, &v["rawstring"]), + Expr::Number(s) => match_span(s, &v["number"]), + Expr::True(s) => match_span(s, v), + Expr::False(s) => match_span(s, v), + Expr::Null(s) => match_span(s, v), + Expr::Var(s) => match_span(s, &v["var"]), + Expr::Array { span, items } => match_vec(span, items, &v["array"]), + Expr::Set { span, items } => match_vec(span, items, &v["set"]), + Expr::Object { span, fields } => match_object(span, fields, &v["object"]), + Expr::ArrayCompr { span, term, query } => { + match_span_opt(span, &v["arraycompr"]["span"])?; + match_expr(term, &v["arraycompr"]["term"])?; + match_query(query, &v["arraycompr"]["query"]) + } + Expr::SetCompr { span, term, query } => { + match_span_opt(span, &v["setcompr"]["span"])?; + match_expr(term, &v["setcompr"]["term"])?; + match_query(query, &v["setcompr"]["query"]) + } + Expr::ObjectCompr { + span, + key, + value, + query, + } => { + match_span_opt(span, &v["objectcompr"]["span"])?; + match_expr(key, &v["objectcompr"]["key"])?; + match_expr(value, &v["objectcompr"]["value"])?; + match_query(query, &v["objectcompr"]["query"]) + } + Expr::Call { span, fcn, params } => { + match_span_opt(span, &v["call"]["span"])?; + match_expr(fcn, &v["call"]["fcn"])?; + match_vec(span /*dummy*/, params, &v["call"]["params"]) + } + Expr::RefDot { span, refr, field } => { + match_span_opt(span, &v["refdot"]["span"])?; + match_expr(refr, &v["refdot"]["refr"])?; + match_span(field, &v["refdot"]["field"]) + } + Expr::RefBrack { span, refr, index } => { + match_span_opt(span, &v["refbrack"]["span"])?; + match_expr(refr, &v["refbrack"]["refr"])?; + match_expr(index, &v["refbrack"]["index"]) + } + Expr::UnaryExpr { span, expr } => { + match_span_opt(span, &v["span"])?; + my_assert_eq!( + &Value::String("-".to_owned()), + &v["op"], + "{}", + span.source.message( + span.line, + span.col, + "mismatch-error", + "could not match `-` operator", + ), + ); + match_expr(expr, &v["expr"]) + } + Expr::BinExpr { span, op, lhs, rhs } => { + match_span_opt(span, &v["binexpr"]["span"])?; + match_bin_op(span, op, &v["binexpr"]["op"])?; + match_expr(lhs, &v["binexpr"]["lhs"])?; + match_expr(rhs, &v["binexpr"]["rhs"]) + } + Expr::ArithExpr { span, op, lhs, rhs } => { + match_span_opt(span, &v["arithexpr"]["span"])?; + match_arith_op(span, op, &v["arithexpr"]["op"])?; + match_expr(lhs, &v["arithexpr"]["lhs"])?; + match_expr(rhs, &v["arithexpr"]["rhs"]) + } + Expr::BoolExpr { span, op, lhs, rhs } => { + match_span_opt(span, &v["boolexpr"]["span"])?; + match_bool_op(span, op, &v["boolexpr"]["op"])?; + match_expr(lhs, &v["boolexpr"]["lhs"])?; + match_expr(rhs, &v["boolexpr"]["rhs"]) + } + Expr::AssignExpr { span, op, lhs, rhs } => { + match_span_opt(span, &v["assignexpr"]["span"])?; + match_assign_op(span, op, &v["assignexpr"]["op"])?; + match_expr(lhs, &v["assignexpr"]["lhs"])?; + match_expr(rhs, &v["assignexpr"]["rhs"]) + } + Expr::Membership { + span, + key, + value, + collection, + } => { + match_span_opt(span, &v["inexpr"]["span"])?; + match_expr(key, &v["inexpr"]["key"])?; + match_expr_opt(span, value, &v["inexpr"]["value"])?; + match_expr(collection, &v["inexpr"]["collection"]) + } + } +} + +fn match_expr(expr: &Expr, v: &Value) -> Result<()> { + match match_expr_impl(expr, v) { + Ok(()) => Ok(()), + Err(e) => bail!( + "{e}\nexpr = {expr:#?}\nv={}\n-----------------------\n", + serde_json::to_string_pretty(v)? + ), + } +} + +fn match_with_mod(m: &WithModifier, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match_span_opt(&m.span, &v["span"])?; + match_expr(&m.refr, &v["refr"])?; + match_expr(&m.r#as, &v["as"]) +} + +fn match_literal_stmt(ls: &LiteralStmt, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match_span_opt(&ls.span, &v["span"])?; + match_literal(&ls.literal, &v["literal"])?; + + let with_mods = &v["with-mods"]; + if skip_value(with_mods) { + return Ok(()); + } + + match with_mods.as_array() { + Ok(a) => { + my_assert_eq!( + ls.with_mods.len(), + a.len(), + "{}", + ls.span.source.message( + ls.span.line, + ls.span.col, + "mismatch-error", + "with-modifier count mismatch" + ) + ); + for (idx, with_mod) in a.iter().enumerate() { + match_with_mod(&ls.with_mods[idx], with_mod)?; + } + } + _ if ls.with_mods.is_empty() => (), + _ => { + bail!( + "{}", + ls.span.source.message( + ls.span.line, + ls.span.col, + "mismatch-error", + "failed to match with-modifiers" + ) + ) + } + } + Ok(()) +} + +fn match_query(q: &Query, v: &Value) -> Result<()> { + match_span_opt(&q.span, &v["span"])?; + let stmts = &v["stmts"].as_array(); + let stmts = match &stmts { + Ok(s) => s, + _ => { + bail!( + "{}", + q.span.source.message( + q.span.line, + q.span.col, + "mismatch-error", + "empty statements list in query specified" + ) + ) + } + }; + my_assert_eq!( + q.stmts.len(), + stmts.len(), + "{}", + q.span.source.message( + q.span.line, + q.span.col, + "mismatch-error", + "mismatch in statement count" + ) + ); + for (idx, stmt) in stmts.iter().enumerate() { + match_literal_stmt(&q.stmts[idx], stmt)?; + } + Ok(()) +} + +fn match_expr_opt(s: &Span, e: &Option, v: &Value) -> Result<()> { + match (e, v) { + (Some(e), v) => match_expr(e, v), + (None, Value::Undefined) => Ok(()), + _ => { + bail!( + "{}", + s.source.message( + s.line, + s.col, + "mismatch-error", + format!( + "failed to match {:#?} and {}", + e, + serde_json::to_string_pretty(&v)? + ) + .as_str() + ) + ) + } + } +} + +fn match_bin_op(s: &Span, op: &BinOp, v: &Value) -> Result<()> { + match (op, v) { + (BinOp::And, Value::String(s)) if s == "&" => Ok(()), + (BinOp::Or, Value::String(s)) if s == "|" => Ok(()), + _ => bail!( + "{}", + s.source.message( + s.line, + s.col, + "mismatch-error", + format!("left = {:?}\nright = {:?}\n", op, v).as_str() + ) + ), + } +} + +fn match_arith_op(s: &Span, op: &ArithOp, v: &Value) -> Result<()> { + match (op, v) { + (ArithOp::Add, Value::String(s)) if s == "+" => Ok(()), + (ArithOp::Sub, Value::String(s)) if s == "-" => Ok(()), + (ArithOp::Mul, Value::String(s)) if s == "*" => Ok(()), + (ArithOp::Div, Value::String(s)) if s == "/" => Ok(()), + _ => bail!( + "{}", + s.source.message( + s.line, + s.col, + "mismatch-error", + format!("left = {:?}\nright = {:?}\n", op, v).as_str() + ) + ), + } +} + +fn match_bool_op(s: &Span, op: &BoolOp, v: &Value) -> Result<()> { + match (op, v) { + (BoolOp::Lt, Value::String(s)) if s == "<" => Ok(()), + (BoolOp::Le, Value::String(s)) if s == "<=" => Ok(()), + (BoolOp::Eq, Value::String(s)) if s == "==" => Ok(()), + (BoolOp::Ge, Value::String(s)) if s == ">=" => Ok(()), + (BoolOp::Gt, Value::String(s)) if s == ">" => Ok(()), + _ => bail!( + "{}", + s.source.message( + s.line, + s.col, + "mismatch-error", + format!("left = {:?}\nright = {:?}\n", op, v).as_str() + ) + ), + } +} + +fn match_assign_op(s: &Span, op: &AssignOp, v: &Value) -> Result<()> { + match (op, v) { + (AssignOp::Eq, Value::String(s)) if s == "=" => Ok(()), + (AssignOp::ColEq, Value::String(s)) if s == ":=" => Ok(()), + _ => bail!( + "{}", + s.source.message( + s.line, + s.col, + "mismatch-error", + format!("left = {:?}\nright = {:?}\n", op, v).as_str() + ) + ), + } +} + +fn match_rule_assign(a: &RuleAssign, v: &Value) -> Result<()> { + match_span_opt(&a.span, &v["span"])?; + match_assign_op(&a.span, &a.op, &v["op"])?; + match_expr(&a.value, &v["value"]) +} + +fn match_rule_assign_opt(a: &Option, v: &Value) -> Result<()> { + match a { + Some(a) => match_rule_assign(a, v), + None => { + my_assert_eq!(*v, Value::Undefined, "mismatch in null assign"); + Ok(()) + } + } +} + +fn match_rule_head(h: &RuleHead, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match h { + RuleHead::Compr { span, refr, assign } => { + match_span_opt(span, &v["compr"]["span"])?; + match_expr(refr, &v["compr"]["refr"])?; + match_rule_assign_opt(assign, &v["compr"]["assign"]) + } + RuleHead::Set { span, refr, key } => { + match_span_opt(span, &v["set"]["span"])?; + match_expr(refr, &v["set"]["refr"])?; + match_expr_opt(span, key, &v["set"]["key"]) + } + RuleHead::Func { + span, + refr, + args, + assign, + } => { + match_span_opt(span, &v["func"]["span"])?; + match_expr(refr, &v["func"]["refr"])?; + match_vec(span /*dummy*/, args, &v["func"]["args"])?; + match_rule_assign_opt(assign, &v["func"]["assign"]) + } + } +} + +fn match_literal(l: &Literal, v: &Value) -> Result<()> { + match l { + Literal::SomeVars { span, vars } => { + let v = &v["some-vars"]; + match_span_opt(span, &v["span"])?; + let values = &v["vars"].as_array()?; + my_assert_eq!( + vars.len(), + values.len(), + "some-vars mismatch {:#?} {}", + vars, + serde_json::to_string_pretty(&values)? + ); + for idx in 0..vars.len() { + match_span(&vars[idx], &values[idx])? + } + Ok(()) + } + Literal::SomeIn { + span, + key, + value, + collection, + } => { + let v = &v["some-decl"]; + match_span_opt(span, &v["span"])?; + match_expr(key, &v["key"])?; + match_expr_opt(span, value, &v["value"])?; + match_expr(collection, &v["collection"]) + } + Literal::Expr { expr, .. } => match_expr(expr, &v["expr"]), + Literal::NotExpr { expr, span } => { + let v = &v["notexpr"]; + match &v["op"] { + Value::String(s) if s == "not" => (), + _ => { + bail!( + "{}", + span.source.message( + span.line, + span.col, + "mismatch-error", + "`op: -` not found in value`" + ) + ) + } + } + match_expr(expr, v) + } + Literal::Every { + span, + key, + value, + domain, + query, + } => { + match_span_opt(span, &v["every"]["span"])?; + match_span(key, &v["every"]["key"])?; + match value { + Some(s) => match_span(s, &v["every"]["value"])?, + None => { + my_assert_eq!( + &Value::Undefined, + &v["value"], + "{}", + span.source.message( + span.line, + span.col, + "mismatch-error", + "could not match `value``" + ) + ); + } + } + match_expr(domain, &v["every"]["domain"])?; + match_query(query, &v["every"]["query"]) + } + } +} + +fn match_rule_body(b: &RuleBody, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match_span_opt(&b.span, &v["span"])?; + match_rule_assign_opt(&b.assign, &v["assign"])?; + match_query(&b.query, &v["query"]) +} + +fn match_rule_bodies(span: &Span, bodies: &Vec, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + let v = &v.as_array(); + let v = match &v { + Ok(v) => v, + _ => { + bail!( + "incorrect yaml. bodies is not an array. Corresponding rego: {}", + span.source.message(span.line, span.col, "invalid-yaml", "") + ); + } + }; + my_assert_eq!( + bodies.len(), + v.len(), + "{}", + span.source.message( + span.line, + span.col, + "mismatch-error", + "mismatch in body count", + ), + ); + + for idx in 0..bodies.len() { + match_rule_body(&bodies[idx], &v[idx])?; + } + + Ok(()) +} + +fn match_rule(r: &Rule, v: &Value) -> Result<()> { + match r { + Rule::Spec { span, head, bodies } => { + let obj = &v["spec"]; + match_span_opt(span, &obj["span"])?; + match_rule_head(head, &obj["head"])?; + match_rule_bodies(span, bodies, &obj["bodies"]) + } + Rule::Default { + span, + refr, + op, + value, + } => { + let obj = &v["default"]; + match_span_opt(span, &obj["span"])?; + match_expr(refr, &obj["refr"])?; + match_assign_op(span, op, &obj["op"])?; + match_expr(value, &obj["value"]) + } + } +} + +fn match_package(p: &Package, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match_span_opt(&p.span, &v["span"])?; + match_expr(&p.refr, &v["refr"]) +} + +fn match_import(i: &Import, v: &Value) -> Result<()> { + if skip_value(v) { + return Ok(()); + } + match_span_opt(&i.span, &v["span"])?; + match_expr(&i.refr, &v["refr"])?; + match (&i.r#as, &v["as"]) { + (Some(a), v) => match_span(a, v), + (None, Value::Undefined) => Ok(()), + _ => Err(i + .span + .source + .error(i.span.line, i.span.col, "import does not have `as` binding")), + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct TestCase { + rego: String, + note: String, + package: Option, + imports: Option>, + policy: Option>, + error: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct YamlTest { + cases: Vec, +} + +fn yaml_test_impl(file: &str) -> Result<()> { + println!("\nrunning {}", file); + + let yaml_str = std::fs::read_to_string(file)?; + let test: YamlTest = serde_yaml::from_str(&yaml_str)?; + + for case in &test.cases { + print!("\ncase {} ", case.note); + let source = Source { + file: "case.rego", + contents: case.rego.as_str(), + lines: case.rego.split('\n').collect(), + }; + let mut parser = Parser::new(&source)?; + match parser.parse() { + Ok(module) => { + if let Some(e) = &case.error { + bail!("error `{}` not raised by parser.", e); + } + if let Some(p) = &case.package { + match_package(&module.package, p)?; + } + + if let Some(imports) = &case.imports { + my_assert_eq!( + module.imports.len(), + imports.len(), + "mismatch in number of imports" + ); + + for (idx, import) in imports.iter().enumerate().take(module.imports.len()) { + match_import(&module.imports[idx], import)?; + } + } + + if let Some(policy) = &case.policy { + my_assert_eq!( + module.policy.len(), + policy.len(), + "mismatch in policy length" + ); + for (idx, policy) in policy.iter().enumerate().take(module.policy.len()) { + if skip_value(policy) { + continue; + } + match_rule(&module.policy[idx], policy)?; + } + } + } + Err(actual) => match &case.error { + Some(expected) => { + let actual = actual.to_string(); + if !actual.contains(expected) { + bail!( + "Error message\n`{}\n`\ndoes not contain `{}`", + actual, + expected + ); + } + println!("{actual}"); + } + _ => return Err(actual), + }, + } + + println!("passed"); + } + + println!("{} cases passed.", test.cases.len()); + Ok(()) +} + +fn yaml_test(file: &str) -> Result<()> { + match yaml_test_impl(file) { + Ok(_) => Ok(()), + Err(e) => { + // If Err is returned, it doesn't always get printed by cargo test. + // Therefore, panic with the error. + panic!("{}", e); + } + } +} + +#[test] +#[ignore = "intended for use by scripts/yaml-test-parse"] +fn one_yaml() -> Result<()> { + let mut file = String::default(); + for a in env::args() { + if a.ends_with(".yaml") { + file = a; + break; + } + } + + if file.is_empty() { + bail!("missing "); + } + + yaml_test(file.as_str()) +} + +/* +fn run_yaml_tests_in(folder: &str) -> Result<()> { + let mut total = 0; + + for entry in WalkDir::new(folder) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry + .path() + .to_str() + .ok_or_else(|| anyhow!("failed to convert path to utf8 {:?}", entry.path()))?; + if !path.ends_with(".yaml") { + continue; + } + + total += 1; + match yaml_test(path) { + Ok(_) => (), + Err(e) => { + bail!("test failed."); + } + } + } + + println!("{} parser yaml tests passed.", total); + Ok(()) +} + + +#[test] +fn parser_yaml_tests() -> Result<()> { + run_yaml_tests_in("tests/parser") +} +*/ + +#[test_resources("tests/parser/**/*.yaml")] +fn run(path: &str) { + yaml_test(path).unwrap() +} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..3c9218a --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,7 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. + +mod interpreter; +mod lexer; +mod parser; +mod value; diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/arith_0.rego b/tests/tmp-files-to-be-added-as-yaml-tests/arith_0.rego new file mode 100644 index 0000000..243d949 --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/arith_0.rego @@ -0,0 +1,18 @@ +package test + +add { + 1 + 2 == 3 +} + +sub { + 5 - 1 == 4 +} + +mul { + 3 * 4 == 12 +} + +# Lock down float operation. +div { + 21 / 5 == 4.2 +} diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/basic.rego b/tests/tmp-files-to-be-added-as-yaml-tests/basic.rego new file mode 100644 index 0000000..248badf --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/basic.rego @@ -0,0 +1,20 @@ +package basic + +import future.keywords.if +import future.keywords.in + +default hello := false + +hello if input.message == "world" + + +default foo := false + +foo { + # some i, {j:5} in {1,2,3} & {5, 6} + true +} + +bar { + 1 in {1, 2} +} \ No newline at end of file diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/input_0.json b/tests/tmp-files-to-be-added-as-yaml-tests/input_0.json new file mode 100644 index 0000000..ab96b3c --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/input_0.json @@ -0,0 +1,4 @@ +{ + "age" : 20, + "date" : "12/1/2022" +} diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/parse_0.rego b/tests/tmp-files-to-be-added-as-yaml-tests/parse_0.rego new file mode 100644 index 0000000..c536a23 --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/parse_0.rego @@ -0,0 +1,37 @@ +package acc.rego + +import future.keywords.in + +[ 1, null, 5, "Hello", [ [ 1, 2, 3, set( ), { set( +), 5, `ab +`}] ], + set().a.b.c[a.b], + 1 >= 2 <= 1 + (- 8 * 44), + 6, (7, 8 in [ 5, 6] & 7), + { + a : 56, + b : 0, + 5 : 6 + }, + a.foo(5+6, 8, {9}), + [ (x + 5) | + foo(x) + true + not foo(x) + { (x + 5) | + foo(x) + true + not foo(x) + } + + ], + { + a : { 5, 6, 7 }, + b : 6, + c : { p: q | + p + 5 with data.p as 86 + q * 6 + 5 in {5, 6} + } + } +] \ No newline at end of file diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/query.rego b/tests/tmp-files-to-be-added-as-yaml-tests/query.rego new file mode 100644 index 0000000..66b0950 --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/query.rego @@ -0,0 +1,3 @@ +x := 5, +y := x + 5 * 3; x & y +] \ No newline at end of file diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/rego_0.rego b/tests/tmp-files-to-be-added-as-yaml-tests/rego_0.rego new file mode 100644 index 0000000..d3728d6 --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/rego_0.rego @@ -0,0 +1,5 @@ +package test + +accept { + input.date == "12/1/2022" +} diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/rule_assign_0.rego b/tests/tmp-files-to-be-added-as-yaml-tests/rule_assign_0.rego new file mode 100644 index 0000000..845e3cf --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/rule_assign_0.rego @@ -0,0 +1,25 @@ +package test + +no_assign { + input.date == "12/1/2022" +} + +assign_null = null { + input.date == "12/1/2022" +} + +assign_bool = false { + input.date == "12/1/2022" +} + +assign_int = 101 { + input.date == "12/1/2022" +} + +assign_float = 3.14 { + input.date == "12/1/2022" +} + +assign_string = "test_string" { + input.date == "12/1/2022" +} diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/tmp.rego b/tests/tmp-files-to-be-added-as-yaml-tests/tmp.rego new file mode 100644 index 0000000..2172419 --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/tmp.rego @@ -0,0 +1,12 @@ +package test + +# This is a comment + +x := 5 +oct { + z := -1 + true + } +y := 1234 +z := "abc\n\u0000 +" \ No newline at end of file diff --git a/tests/tmp-files-to-be-added-as-yaml-tests/variables_0.rego b/tests/tmp-files-to-be-added-as-yaml-tests/variables_0.rego new file mode 100644 index 0000000..fca298e --- /dev/null +++ b/tests/tmp-files-to-be-added-as-yaml-tests/variables_0.rego @@ -0,0 +1,6 @@ +package test + +local = x { + some x + x = 10 +} diff --git a/tests/value/mod.rs b/tests/value/mod.rs new file mode 100644 index 0000000..58718d6 --- /dev/null +++ b/tests/value/mod.rs @@ -0,0 +1,176 @@ +// Copyright (c) Rego-Rs Authors. +// Licensed under the Apache 2.0 license. +#![cfg(test)] + +use anyhow::Result; +use rego_rs::*; + +#[test] +fn non_string_key() -> Result<()> { + let mut obj = Value::new_object(); + + obj.as_object_mut()?.insert(Value::Null, Value::Null); + obj.as_object_mut()?.insert(Value::Bool(false), Value::Null); + obj.as_object_mut()? + .insert(Value::from_f64(std::f64::consts::PI), Value::Null); + obj.as_object_mut()?.insert( + Value::from_array(vec![ + Value::Bool(true), + Value::Null, + Value::from_f64(std::f64::consts::PI), + ]), + Value::Null, + ); + + let mut set = Value::new_set(); + set.as_set_mut()?.insert(Value::Bool(true)); + set.as_set_mut()?.insert(Value::Bool(false)); + set.as_set_mut()?.insert(Value::Bool(true)); + set.as_set_mut()? + .insert(Value::from_f64(std::f64::consts::PI)); + obj.as_object_mut()?.insert(set, Value::Null); + + obj.as_object_mut()?.insert(Value::Undefined, Value::Null); + + let key_obj = obj.clone(); + obj.as_object_mut()?.insert(key_obj, Value::Null); + + let json = serde_json::to_string_pretty(&obj)?; + println!("{}", json); + + let expected = r#"{ + "null": null, + "false": null, + "3.141592653589793": null, + "[true,null,3.141592653589793]": null, + "{\"null\":null,\"false\":null,\"3.141592653589793\":null,\"[true,null,3.141592653589793]\":null,\"[false,true,3.141592653589793]\":null,\"\\\"\\\"\":null}": null, + "[false,true,3.141592653589793]": null, + "\"\"": null +}"#; + + assert_eq!(json, expected); + + Ok(()) +} + +#[test] +fn serialize_number() -> Result<()> { + // Check that integer values are serialized without fractional part + assert_eq!(serde_json::to_string_pretty(&Value::from_f64(1.0))?, "1"); + assert_eq!(serde_json::to_string_pretty(&Value::from_f64(-1.0))?, "-1"); + + // Ensure that fractional parts are also serialized. + assert_eq!(serde_json::to_string_pretty(&Value::from_f64(1.1))?, "1.1"); + assert_eq!( + serde_json::to_string_pretty(&Value::from_f64(-1.1))?, + "-1.1" + ); + + Ok(()) +} + +#[test] +fn display_number() { + use ordered_float::OrderedFloat; + let n = Number(OrderedFloat(123456f64)); + assert_eq!(format!("{}", &n), "123456"); +} + +#[test] +fn serialize_string() -> Result<()> { + assert_eq!( + Value::String("Hello, World\n".to_owned()).to_json_str()?, + "\"Hello, World\\n\"" + ); + Ok(()) +} + +#[test] +fn constructors() -> Result<()> { + assert_eq!(Value::new_object(), Value::from_json_str("{}")?); + assert!(Value::new_set().as_set()?.is_empty()); + Ok(()) +} + +#[test] +fn value_as_index() -> Result<()> { + let idx = Value::from_f64(2.0); + + let mut item = Value::new_array(); + item.as_array_mut()?.push(Value::from_f64(3.0)); + item.as_array_mut()?.push(Value::from_f64(4.0)); + item.as_array_mut()?.push(Value::from_f64(5.0)); + + // Check case of item present. + assert_eq!(&Value::from_json_str("[1, 2, [3, 4, 5]]")?[&idx], &item); + + // Check case of item not present. + let idx = Value::from_f64(5.0); + assert_eq!( + &Value::from_json_str("[1, 2, [3, 4, 5]]")?[&idx], + &Value::Undefined + ); + + // Check case of non indexable item. + assert_eq!(&Value::Undefined[&idx], &Value::Undefined); + assert_eq!(&Value::Null[&idx], &Value::Undefined); + assert_eq!(&Value::Bool(true)[&idx], &Value::Undefined); + assert_eq!(&Value::String("Hello".to_owned())[&idx], &Value::Undefined); + assert_eq!(&Value::new_set()[&idx], &Value::Undefined); + + Ok(()) +} + +#[test] +fn string_as_index() -> Result<()> { + let obj = Value::from_json_str(r#"{ "a" : 5, "b" : 6 }"#)?; + assert_eq!(&obj["a"], &Value::from_f64(5.0)); + assert_eq!(&obj[&"b".to_owned()], &Value::from_f64(6.0)); + Ok(()) +} + +#[test] +fn usize_as_index() -> Result<()> { + assert_eq!( + &Value::from_json_str("[1, 2, 3]")?[0], + &Value::from_f64(1.0) + ); + assert_eq!(&Value::from_json_str("[1, 2, 3]")?[5], &Value::Undefined); + Ok(()) +} + +#[test] +fn api() -> Result<()> { + assert!(&Value::from_json_str("{}")?.as_object()?.is_empty()); + let mut v = Value::new_object(); + v.as_object_mut()? + .insert(Value::String("a".to_owned()), Value::from_f64(3.145)); + assert_eq!(v["a"], Value::from_f64(3.145)); + assert_eq!(v.as_object()?.len(), 1); + + // Null + assert!(Value::Null.is_null()); + + let v = Value::new_set(); + assert_eq!(v.as_set()?.len(), 0); + + // Check invalid api calls. + assert!(matches!(Value::Undefined.as_object(), Err(_))); + assert!(matches!(Value::Undefined.as_object_mut(), Err(_))); + + assert!(matches!(Value::Null.as_set(), Err(_))); + assert!(matches!(Value::Null.as_set_mut(), Err(_))); + + assert!(matches!(Value::String("anc".to_owned()).as_array(), Err(_))); + assert!(matches!( + Value::String("anc".to_owned()).as_array_mut(), + Err(_) + )); + + assert!(matches!(Value::new_object().as_number(), Err(_))); + assert!(matches!(Value::new_object().as_number_mut(), Err(_))); + + assert!(matches!(Value::from_f64(5.6).as_bool(), Err(_))); + assert!(matches!(Value::from_f64(5.6).as_bool_mut(), Err(_))); + Ok(()) +}