Podlog language v1 (#225)
* Initial commit for Podlog language * Spell-checker thinks that 'lits' is a bad abbreviation for 'literals' * Enable SetContains/SetNotContains * Update language based on review feedback * Typo/comment fix * Make native predicates case-sensitive * Enforce max batch size in CustomPredicateBatchBuilder * Remove some unnecessary checks for things handled by the grammar * Clean up more unnecessary error-checking * Typo * Simplify hex processing * Replace various errors with unreachable!() * Translate from big-endian hex string to little-endian RawValue * Update hex en/decoding functions
This commit is contained in:
parent
e8edbbc1c5
commit
541c264586
11 changed files with 2259 additions and 29 deletions
|
|
@ -25,6 +25,8 @@ serde_json = "1.0.140"
|
|||
base64 = "0.22.1"
|
||||
schemars = "0.8.22"
|
||||
hashbrown = { version = "0.14.3", default-features = false, features = ["serde"] }
|
||||
pest = "2.8.0"
|
||||
pest_derive = "2.8.0"
|
||||
|
||||
# Uncomment for debugging with https://github.com/ed255/plonky2/ at branch `feat/debug`. The repo directory needs to be checked out next to the pod2 repo directory.
|
||||
# [patch."https://github.com/0xPolygonZero/plonky2"]
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub fn key(s: &str) -> KeyOrWildcardStr {
|
|||
}
|
||||
|
||||
/// Builder Argument for the StatementTmplBuilder
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BuilderArg {
|
||||
Literal(Value),
|
||||
/// Key: (origin, key), where origin is SELF or Wildcard and key is Key or Wildcard
|
||||
|
|
@ -79,8 +79,8 @@ pub fn literal(v: impl Into<Value>) -> BuilderArg {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct StatementTmplBuilder {
|
||||
predicate: Predicate,
|
||||
args: Vec<BuilderArg>,
|
||||
pub(crate) predicate: Predicate,
|
||||
pub(crate) args: Vec<BuilderArg>,
|
||||
}
|
||||
|
||||
impl StatementTmplBuilder {
|
||||
|
|
@ -98,7 +98,7 @@ impl StatementTmplBuilder {
|
|||
|
||||
/// Desugar the predicate to a simpler form
|
||||
/// Should mirror the logic in `MainPodBuilder::lower_op`
|
||||
fn desugar(self) -> StatementTmplBuilder {
|
||||
pub(crate) fn desugar(self) -> StatementTmplBuilder {
|
||||
match self.predicate {
|
||||
Predicate::Native(NativePredicate::Gt) => {
|
||||
let mut stb = StatementTmplBuilder {
|
||||
|
|
@ -184,6 +184,14 @@ impl CustomPredicateBatchBuilder {
|
|||
priv_args: &[&str],
|
||||
sts: &[StatementTmplBuilder],
|
||||
) -> Result<Predicate> {
|
||||
if self.predicates.len() >= self.params.max_custom_batch_size {
|
||||
return Err(Error::max_length(
|
||||
"self.predicates.len".to_string(),
|
||||
self.predicates.len(),
|
||||
self.params.max_custom_batch_size,
|
||||
));
|
||||
}
|
||||
|
||||
if args.len() > self.params.max_statement_args {
|
||||
return Err(Error::max_length(
|
||||
"args.len".to_string(),
|
||||
|
|
|
|||
94
src/lang/error.rs
Normal file
94
src/lang/error.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use thiserror::Error;
|
||||
|
||||
use crate::{frontend, lang::parser::ParseError, middleware};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LangError {
|
||||
#[error("Parsing failed: {0}")]
|
||||
Parse(Box<ParseError>),
|
||||
|
||||
#[error("AST processing error: {0}")]
|
||||
Processor(Box<ProcessorError>),
|
||||
|
||||
#[error("Middleware error during processing: {0}")]
|
||||
Middleware(Box<middleware::Error>),
|
||||
|
||||
#[error("Frontend error: {0}")]
|
||||
Frontend(Box<frontend::Error>),
|
||||
}
|
||||
|
||||
/// Errors that can occur during the processing of Podlog Pest tree into middleware structures.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ProcessorError {
|
||||
#[error("Undefined identifier: '{name}' at {span:?}")]
|
||||
UndefinedIdentifier {
|
||||
name: String,
|
||||
span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error("Duplicate definition: '{name}' at {span:?}")]
|
||||
DuplicateDefinition {
|
||||
name: String,
|
||||
span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error("Duplicate wildcard: ?{name} in scope at {span:?}")]
|
||||
DuplicateWildcard {
|
||||
name: String,
|
||||
span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error("Type error: expected {expected}, found {found} for '{item}' at {span:?}")]
|
||||
TypeError {
|
||||
expected: String,
|
||||
found: String,
|
||||
item: String,
|
||||
span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error(
|
||||
"Invalid argument count for '{predicate}': expected {expected}, found {found} at {span:?}"
|
||||
)]
|
||||
ArgumentCountMismatch {
|
||||
predicate: String,
|
||||
expected: usize,
|
||||
found: usize,
|
||||
span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error("Multiple REQUEST definitions found. Only one is allowed. First at {first_span:?}, second at {second_span:?}")]
|
||||
MultipleRequestDefinitions {
|
||||
first_span: Option<(usize, usize)>,
|
||||
second_span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error("Internal processing error: {0}")]
|
||||
Internal(String),
|
||||
#[error("Middleware error: {0}")]
|
||||
Middleware(middleware::Error),
|
||||
#[error("Undefined wildcard: '?{name}' at {span:?}")]
|
||||
UndefinedWildcard {
|
||||
name: String,
|
||||
span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error("Invalid literal format for {kind}: '{value}' at {span:?}")]
|
||||
InvalidLiteralFormat {
|
||||
kind: String,
|
||||
value: String,
|
||||
span: Option<(usize, usize)>,
|
||||
},
|
||||
#[error("Frontend error: {0}")]
|
||||
Frontend(#[from] frontend::Error),
|
||||
}
|
||||
|
||||
impl From<ParseError> for LangError {
|
||||
fn from(err: ParseError) -> Self {
|
||||
LangError::Parse(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProcessorError> for LangError {
|
||||
fn from(err: ProcessorError) -> Self {
|
||||
LangError::Processor(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<middleware::Error> for LangError {
|
||||
fn from(err: middleware::Error) -> Self {
|
||||
LangError::Middleware(Box::new(err))
|
||||
}
|
||||
}
|
||||
96
src/lang/grammar.pest
Normal file
96
src/lang/grammar.pest
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Grammar for the "Podlog" language. Used for describing POD2 Custom
|
||||
// Predicates and Proof Requests.
|
||||
|
||||
// Silent rules (`_`) are automatically handled by Pest between other rules.
|
||||
// WHITESPACE matches one or more spaces, tabs, or newlines.
|
||||
WHITESPACE = _{ (" " | "\t" | NEWLINE)+ }
|
||||
|
||||
// COMMENT matches '//' followed by any characters until the end of the line.
|
||||
// Also silent.
|
||||
COMMENT = _{ "//" ~ (!NEWLINE ~ ANY)* }
|
||||
|
||||
// Define rules for identifiers (predicate names, variable names without '?')
|
||||
// Must start with alpha or _, followed by alpha, numeric, or _
|
||||
identifier = @{ !("private") ~ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||
|
||||
private_kw = { "private:" }
|
||||
|
||||
self_keyword = @{ "SELF" }
|
||||
|
||||
// Define wildcard names (start with '?')
|
||||
wildcard = @{ "?" ~ identifier }
|
||||
|
||||
arg_section = {
|
||||
public_arg_list ~ ("," ~ private_kw ~ private_arg_list)?
|
||||
}
|
||||
|
||||
public_arg_list = { identifier ~ ("," ~ identifier)* }
|
||||
private_arg_list = { identifier ~ ("," ~ identifier)* }
|
||||
|
||||
document = { SOI ~ (custom_predicate_def | request_def)* ~ EOI }
|
||||
|
||||
request_def = { "REQUEST" ~ "(" ~ statement_list? ~ ")" }
|
||||
|
||||
// Define conjunction type explicitly
|
||||
conjunction_type = { "AND" | "OR" }
|
||||
|
||||
custom_predicate_def = {
|
||||
identifier
|
||||
~ "(" ~ arg_section ~ ")"
|
||||
~ "="
|
||||
~ conjunction_type
|
||||
~ "(" ~ statement_list ~ ")"
|
||||
}
|
||||
|
||||
statement_list = { statement+ }
|
||||
|
||||
statement_arg = { anchored_key | wildcard | literal_value }
|
||||
statement_arg_list = { statement_arg ~ ("," ~ statement_arg)* }
|
||||
|
||||
statement = { identifier ~ "(" ~ statement_arg_list? ~ ")" }
|
||||
|
||||
// Anchored Key: (SELF | ?Var)["key_literal" | ?KeyVar]
|
||||
anchored_key = { ( self_keyword | wildcard ) ~ "[" ~ (wildcard | literal_string) ~ "]" }
|
||||
|
||||
// Literal Values (ordered to avoid ambiguity, e.g., string before int)
|
||||
literal_value = {
|
||||
literal_dict |
|
||||
literal_set |
|
||||
literal_array |
|
||||
literal_bool |
|
||||
literal_raw |
|
||||
literal_string |
|
||||
literal_int
|
||||
}
|
||||
|
||||
// Primitive literal types
|
||||
literal_int = @{ "-"? ~ ASCII_DIGIT+ }
|
||||
literal_bool = @{ "true" | "false" }
|
||||
|
||||
// literal_raw: 0x followed by exactly 32 PAIRS of hex digits (64 hex characters)
|
||||
// representing a 32-byte value in big-endian order
|
||||
literal_raw = @{ "0x" ~ (ASCII_HEX_DIGIT ~ ASCII_HEX_DIGIT){32} }
|
||||
|
||||
// String literal parsing based on https://pest.rs/book/examples/json.html
|
||||
literal_string = ${ "\"" ~ inner ~ "\"" } // Compound atomic string rule
|
||||
inner = @{ char* } // Atomic rule for the raw inner content
|
||||
char = { // Rule for a single logical character (unescaped or escaped)
|
||||
!("\"" | "\\") ~ ANY // Any char except quote or backslash
|
||||
| "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") // Simple escape sequences
|
||||
| "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) // Unicode escape sequence
|
||||
}
|
||||
|
||||
// Container Literals (recursive definition using literal_value)
|
||||
literal_array = { "[" ~ (literal_value ~ ("," ~ literal_value)*)? ~ "]" }
|
||||
literal_set = { "#[" ~ (literal_value ~ ("," ~ literal_value)*)? ~ "]" }
|
||||
literal_dict = { "{" ~ (dict_pair ~ ("," ~ dict_pair)*)? ~ "}" }
|
||||
dict_pair = { literal_string ~ ":" ~ literal_value }
|
||||
|
||||
// --- Rules for testing full input matching ---
|
||||
test_identifier = { SOI ~ identifier ~ EOI }
|
||||
test_wildcard = { SOI ~ wildcard ~ EOI }
|
||||
test_literal_int = { SOI ~ literal_int ~ EOI }
|
||||
test_literal_raw = { SOI ~ literal_raw ~ EOI }
|
||||
test_literal_value = { SOI ~ literal_value ~ EOI }
|
||||
test_statement = { SOI ~ statement ~ EOI }
|
||||
test_custom_predicate_def = { SOI ~ custom_predicate_def ~ EOI }
|
||||
682
src/lang/mod.rs
Normal file
682
src/lang/mod.rs
Normal file
|
|
@ -0,0 +1,682 @@
|
|||
pub mod error;
|
||||
pub mod parser;
|
||||
pub mod processor;
|
||||
|
||||
pub use error::LangError;
|
||||
pub use parser::{parse_podlog, Pairs, ParseError, Rule};
|
||||
pub use processor::process_pest_tree;
|
||||
use processor::ProcessedOutput;
|
||||
|
||||
use crate::middleware::Params;
|
||||
|
||||
pub fn parse(input: &str, params: &Params) -> Result<ProcessedOutput, LangError> {
|
||||
let pairs = parse_podlog(input)?;
|
||||
processor::process_pest_tree(pairs, params).map_err(LangError::from)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::middleware::{
|
||||
CustomPredicate, CustomPredicateBatch, CustomPredicateRef, Key, KeyOrWildcard,
|
||||
NativePredicate, Params, PodType, Predicate, SelfOrWildcard, StatementTmpl,
|
||||
StatementTmplArg, Value, Wildcard, SELF_ID_HASH,
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
fn wc(name: &str, index: usize) -> Wildcard {
|
||||
Wildcard::new(name.to_string(), index)
|
||||
}
|
||||
|
||||
fn k(name: &str) -> KeyOrWildcard {
|
||||
KeyOrWildcard::Key(Key::new(name.to_string()))
|
||||
}
|
||||
|
||||
fn ko_wc(name: &str, index: usize) -> KeyOrWildcard {
|
||||
KeyOrWildcard::Wildcard(Wildcard::new(name.to_string(), index))
|
||||
}
|
||||
|
||||
fn sta_ak(pod_var: (&str, usize), key_or_wc: KeyOrWildcard) -> StatementTmplArg {
|
||||
StatementTmplArg::AnchoredKey(
|
||||
SelfOrWildcard::Wildcard(wc(pod_var.0, pod_var.1)),
|
||||
key_or_wc,
|
||||
)
|
||||
}
|
||||
|
||||
fn sta_ak_self(key_or_wc: KeyOrWildcard) -> StatementTmplArg {
|
||||
StatementTmplArg::AnchoredKey(SelfOrWildcard::SELF, key_or_wc)
|
||||
}
|
||||
|
||||
fn sta_lit(value: impl Into<Value>) -> StatementTmplArg {
|
||||
StatementTmplArg::Literal(value.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_simple_predicate() -> Result<(), LangError> {
|
||||
let input = r#"
|
||||
is_equal(PodA, PodB) = AND(
|
||||
Equal(?PodA["the_key"], ?PodB["the_key"])
|
||||
)
|
||||
"#;
|
||||
|
||||
let params = Params::default();
|
||||
let pairs = parse_podlog(input)?;
|
||||
let processed = process_pest_tree(pairs, ¶ms)?;
|
||||
let batch_result = processed.custom_batch;
|
||||
let request_result = processed.request_templates;
|
||||
|
||||
assert_eq!(request_result.len(), 0);
|
||||
assert_eq!(batch_result.predicates.len(), 1);
|
||||
|
||||
let batch = batch_result;
|
||||
|
||||
// Expected structure
|
||||
let expected_statements = vec![StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak(("PodA", 0), k("the_key")), // ?PodA["the_key"] -> Wildcard(0), Key("the_key")
|
||||
sta_ak(("PodB", 1), k("the_key")), // ?PodB["the_key"] -> Wildcard(1), Key("the_key")
|
||||
],
|
||||
}];
|
||||
let expected_predicate = CustomPredicate::and(
|
||||
¶ms,
|
||||
"is_equal".to_string(),
|
||||
expected_statements,
|
||||
2, // args_len (PodA, PodB)
|
||||
)?;
|
||||
let expected_batch =
|
||||
CustomPredicateBatch::new(¶ms, "PodlogBatch".to_string(), vec![expected_predicate]);
|
||||
|
||||
assert_eq!(batch, expected_batch);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_simple_request() -> Result<(), LangError> {
|
||||
let input = r#"
|
||||
REQUEST(
|
||||
ValueOf(?ConstPod["my_val"], 0x0000000000000000000000000000000000000000000000000000000000000001)
|
||||
Lt(?GovPod["dob"], ?ConstPod["my_val"])
|
||||
)
|
||||
"#;
|
||||
|
||||
let params = Params::default();
|
||||
let pairs = parse_podlog(input)?;
|
||||
let processed = process_pest_tree(pairs, ¶ms)?;
|
||||
let batch_result = processed.custom_batch;
|
||||
let request_templates = processed.request_templates;
|
||||
|
||||
assert_eq!(batch_result.predicates.len(), 0);
|
||||
assert!(!request_templates.is_empty());
|
||||
|
||||
let request_templates = request_templates;
|
||||
|
||||
// Expected structure
|
||||
let expected_templates = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::ValueOf),
|
||||
args: vec![
|
||||
sta_ak(("ConstPod", 0), k("my_val")), // ?ConstPod["my_val"] -> Wildcard(0), Key("my_val")
|
||||
sta_lit(SELF_ID_HASH),
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Lt),
|
||||
args: vec![
|
||||
sta_ak(("GovPod", 1), k("dob")), // ?GovPod["dob"] -> Wildcard(1), Key("dob")
|
||||
sta_ak(("ConstPod", 0), k("my_val")), // ?ConstPod["my_val"] -> Wildcard(0), Key("my_val")
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(request_templates, expected_templates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_predicate_with_private_var() -> Result<(), LangError> {
|
||||
let input = r#"
|
||||
uses_private(A, private: Temp) = AND(
|
||||
Equal(?A["input_key"], ?Temp["const_key"])
|
||||
ValueOf(?Temp["const_key"], "some_value")
|
||||
)
|
||||
"#;
|
||||
|
||||
let params = Params::default();
|
||||
let pairs = parse_podlog(input)?;
|
||||
let processed = process_pest_tree(pairs, ¶ms)?;
|
||||
let batch_result = processed.custom_batch;
|
||||
let request_result = processed.request_templates;
|
||||
|
||||
assert_eq!(request_result.len(), 0);
|
||||
assert_eq!(batch_result.predicates.len(), 1);
|
||||
|
||||
let batch = batch_result;
|
||||
|
||||
// Expected structure: Public args: A (index 0). Private args: Temp (index 1)
|
||||
let expected_statements = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak(("A", 0), k("input_key")), // ?A["input_key"] -> Wildcard(0), Key("input_key")
|
||||
sta_ak(("Temp", 1), k("const_key")), // ?Temp["const_key"] -> Wildcard(1), Key("const_key")
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::ValueOf),
|
||||
args: vec![
|
||||
sta_ak(("Temp", 1), k("const_key")), // ?Temp["const_key"] -> Wildcard(1), Key("const_key")
|
||||
sta_lit("some_value"), // Literal("some_value")
|
||||
],
|
||||
},
|
||||
];
|
||||
let expected_predicate = CustomPredicate::and(
|
||||
¶ms,
|
||||
"uses_private".to_string(),
|
||||
expected_statements,
|
||||
1, // args_len (A)
|
||||
)?;
|
||||
let expected_batch =
|
||||
CustomPredicateBatch::new(¶ms, "PodlogBatch".to_string(), vec![expected_predicate]);
|
||||
|
||||
assert_eq!(batch, expected_batch);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_request_with_custom_call() -> Result<(), LangError> {
|
||||
let input = r#"
|
||||
my_pred(X, Y) = AND(
|
||||
Equal(?X["val"], ?Y["val"])
|
||||
)
|
||||
|
||||
REQUEST(
|
||||
my_pred(?Pod1, ?Pod2)
|
||||
)
|
||||
"#;
|
||||
|
||||
let params = Params::default();
|
||||
let pairs = parse_podlog(input)?;
|
||||
let processed = process_pest_tree(pairs, ¶ms)?;
|
||||
let batch_result = processed.custom_batch;
|
||||
let request_templates = processed.request_templates;
|
||||
|
||||
assert_eq!(batch_result.predicates.len(), 1);
|
||||
assert!(!request_templates.is_empty());
|
||||
|
||||
let batch = batch_result;
|
||||
let request_templates = request_templates;
|
||||
|
||||
// Expected Batch structure
|
||||
let expected_pred_statements = vec![StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak(("X", 0), k("val")), // ?X["val"] -> Wildcard(0), Key("val")
|
||||
sta_ak(("Y", 1), k("val")), // ?Y["val"] -> Wildcard(1), Key("val")
|
||||
],
|
||||
}];
|
||||
let expected_predicate = CustomPredicate::and(
|
||||
¶ms,
|
||||
"my_pred".to_string(),
|
||||
expected_pred_statements,
|
||||
2, // args_len (X, Y)
|
||||
)?;
|
||||
let expected_batch =
|
||||
CustomPredicateBatch::new(¶ms, "PodlogBatch".to_string(), vec![expected_predicate]);
|
||||
|
||||
assert_eq!(batch, expected_batch);
|
||||
|
||||
// Expected Request structure
|
||||
// Pod1 -> Wildcard 0, Pod2 -> Wildcard 1
|
||||
let expected_request_templates = vec![StatementTmpl {
|
||||
pred: Predicate::Custom(CustomPredicateRef::new(expected_batch, 0)),
|
||||
args: vec![
|
||||
StatementTmplArg::WildcardLiteral(wc("Pod1", 0)),
|
||||
StatementTmplArg::WildcardLiteral(wc("Pod2", 1)),
|
||||
],
|
||||
}];
|
||||
|
||||
assert_eq!(request_templates, expected_request_templates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_request_with_various_args() -> Result<(), LangError> {
|
||||
let input = r#"
|
||||
some_pred(A, B, C) = AND( Equal(?A["foo"], ?B["bar"]) )
|
||||
|
||||
REQUEST(
|
||||
some_pred(
|
||||
?Var1, // Wildcard
|
||||
12345, // Int Literal
|
||||
"hello_string" // String Literal (Removed invalid AK args)
|
||||
)
|
||||
Equal(?AnotherPod["another_key"], ?Var1["some_field"])
|
||||
)
|
||||
"#;
|
||||
|
||||
let params = Params::default();
|
||||
let pairs = parse_podlog(input)?;
|
||||
let processed = process_pest_tree(pairs, ¶ms)?;
|
||||
let batch_result = processed.custom_batch;
|
||||
let request_templates = processed.request_templates;
|
||||
|
||||
assert_eq!(batch_result.predicates.len(), 1); // some_pred is defined
|
||||
assert!(!request_templates.is_empty());
|
||||
|
||||
let request_templates = request_templates;
|
||||
|
||||
// Expected Wildcard Indices in Request Scope:
|
||||
// ?Var1 -> 0
|
||||
// ?AnotherPod -> 1
|
||||
|
||||
// Expected structure
|
||||
let expected_templates = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::Custom(CustomPredicateRef::new(batch_result, 0)), // Refers to some_pred
|
||||
args: vec![
|
||||
StatementTmplArg::WildcardLiteral(wc("Var1", 0)), // ?Var1
|
||||
StatementTmplArg::Literal(Value::from(12345i64)), // 12345
|
||||
StatementTmplArg::Literal(Value::from("hello_string")), // "hello_string"
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
// ?AnotherPod["another_key"] -> Wildcard(1), Key("another_key")
|
||||
sta_ak(("AnotherPod", 1), k("another_key")),
|
||||
// ?Var1["some_field"] -> Wildcard(0), Key("some_field")
|
||||
sta_ak(("Var1", 0), k("some_field")),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(request_templates, expected_templates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_syntactic_sugar_predicates() -> Result<(), LangError> {
|
||||
let input = r#"
|
||||
REQUEST(
|
||||
GtEq(?A["foo"], ?B["bar"])
|
||||
Gt(?C["baz"], ?D["qux"])
|
||||
DictContains(?A["foo"], ?B["bar"], ?C["baz"])
|
||||
DictNotContains(?A["foo"], ?B["bar"])
|
||||
ArrayContains(?A["foo"], ?B["bar"], ?C["baz"])
|
||||
)
|
||||
"#;
|
||||
|
||||
let params = Params::default();
|
||||
let pairs = parse_podlog(input)?;
|
||||
let processed = process_pest_tree(pairs, ¶ms)?;
|
||||
let batch_result = processed.custom_batch;
|
||||
let request_templates = processed.request_templates;
|
||||
|
||||
assert_eq!(batch_result.predicates.len(), 0);
|
||||
assert!(!request_templates.is_empty());
|
||||
|
||||
let request_templates = request_templates;
|
||||
|
||||
let expected_templates = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::LtEq),
|
||||
args: vec![sta_ak(("B", 1), k("bar")), sta_ak(("A", 0), k("foo"))],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Lt),
|
||||
args: vec![sta_ak(("D", 3), k("qux")), sta_ak(("C", 2), k("baz"))],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Contains),
|
||||
args: vec![
|
||||
sta_ak(("A", 0), k("foo")),
|
||||
sta_ak(("B", 1), k("bar")),
|
||||
sta_ak(("C", 2), k("baz")),
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::NotContains),
|
||||
args: vec![sta_ak(("A", 0), k("foo")), sta_ak(("B", 1), k("bar"))],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Contains),
|
||||
args: vec![
|
||||
sta_ak(("A", 0), k("foo")),
|
||||
sta_ak(("B", 1), k("bar")),
|
||||
sta_ak(("C", 2), k("baz")),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(request_templates, expected_templates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_zukyc_request_parsing() -> Result<(), LangError> {
|
||||
let input = r#"
|
||||
REQUEST(
|
||||
// Order matters for comparison with the hardcoded templates
|
||||
SetNotContains(?sanctions["sanctionList"], ?gov["idNumber"])
|
||||
Lt(?gov["dateOfBirth"], ?SELF_HOLDER_18Y["const_18y"])
|
||||
Equal(?pay["startDate"], ?SELF_HOLDER_1Y["const_1y"])
|
||||
Equal(?gov["socialSecurityNumber"], ?pay["socialSecurityNumber"])
|
||||
ValueOf(?SELF_HOLDER_18Y["const_18y"], 1169909388)
|
||||
ValueOf(?SELF_HOLDER_1Y["const_1y"], 1706367566)
|
||||
)
|
||||
"#;
|
||||
|
||||
// Parse the input string
|
||||
let processed = super::parse(input, &Params::default())?;
|
||||
let parsed_templates = processed.request_templates;
|
||||
|
||||
// Define Expected Templates (Copied from prover/mod.rs)
|
||||
let now_minus_18y_val = Value::from(1169909388_i64);
|
||||
let now_minus_1y_val = Value::from(1706367566_i64);
|
||||
|
||||
// Define wildcards and keys for the request
|
||||
// Note: Indices must match the order of appearance in the *parsed* request
|
||||
// Order: sanctions, gov, SELF_HOLDER_18Y, pay, SELF_HOLDER_1Y
|
||||
let wc_sanctions = wc("sanctions", 0);
|
||||
let wc_gov = wc("gov", 1);
|
||||
let wc_self_18y = wc("SELF_HOLDER_18Y", 2);
|
||||
let wc_pay = wc("pay", 3);
|
||||
let wc_self_1y = wc("SELF_HOLDER_1Y", 4);
|
||||
|
||||
let id_num_key = k("idNumber");
|
||||
let dob_key = k("dateOfBirth");
|
||||
let const_18y_key = k("const_18y");
|
||||
let start_date_key = k("startDate");
|
||||
let const_1y_key = k("const_1y");
|
||||
let ssn_key = k("socialSecurityNumber");
|
||||
let sanction_list_key = k("sanctionList");
|
||||
|
||||
// Define the request templates using wildcards for constants
|
||||
let expected_templates = vec![
|
||||
// 1. NotContains(?sanctions["sanctionList"], ?gov["idNumber"])
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::NotContains),
|
||||
args: vec![
|
||||
sta_ak(
|
||||
(wc_sanctions.name.as_str(), wc_sanctions.index),
|
||||
sanction_list_key.clone(),
|
||||
),
|
||||
sta_ak((wc_gov.name.as_str(), wc_gov.index), id_num_key.clone()),
|
||||
],
|
||||
},
|
||||
// 2. Lt(?gov["dateOfBirth"], ?SELF_HOLDER_18Y["const_18y"])
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Lt),
|
||||
args: vec![
|
||||
sta_ak((wc_gov.name.as_str(), wc_gov.index), dob_key.clone()),
|
||||
sta_ak(
|
||||
(wc_self_18y.name.as_str(), wc_self_18y.index),
|
||||
const_18y_key.clone(),
|
||||
),
|
||||
],
|
||||
},
|
||||
// 3. Equal(?pay["startDate"], ?SELF_HOLDER_1Y["const_1y"])
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak((wc_pay.name.as_str(), wc_pay.index), start_date_key.clone()),
|
||||
sta_ak(
|
||||
(wc_self_1y.name.as_str(), wc_self_1y.index),
|
||||
const_1y_key.clone(),
|
||||
),
|
||||
],
|
||||
},
|
||||
// 4. Equal(?gov["socialSecurityNumber"], ?pay["socialSecurityNumber"])
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak((wc_gov.name.as_str(), wc_gov.index), ssn_key.clone()),
|
||||
sta_ak((wc_pay.name.as_str(), wc_pay.index), ssn_key.clone()),
|
||||
],
|
||||
},
|
||||
// 5. ValueOf(?SELF_HOLDER_18Y["const_18y"], 1169909388)
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::ValueOf),
|
||||
args: vec![
|
||||
sta_ak(
|
||||
(wc_self_18y.name.as_str(), wc_self_18y.index),
|
||||
const_18y_key.clone(),
|
||||
),
|
||||
sta_lit(now_minus_18y_val.clone()),
|
||||
],
|
||||
},
|
||||
// 6. ValueOf(?SELF_HOLDER_1Y["const_1y"], 1706367566)
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::ValueOf),
|
||||
args: vec![
|
||||
sta_ak(
|
||||
(wc_self_1y.name.as_str(), wc_self_1y.index),
|
||||
const_1y_key.clone(),
|
||||
),
|
||||
sta_lit(now_minus_1y_val.clone()),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
parsed_templates, expected_templates,
|
||||
"Parsed ZuKYC request templates do not match the expected hard-coded version"
|
||||
);
|
||||
|
||||
assert!(
|
||||
processed.custom_batch.predicates.is_empty(),
|
||||
"Expected no custom predicates for a REQUEST only input"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e2e_ethdos_predicates() -> Result<(), LangError> {
|
||||
let params = Params {
|
||||
max_input_signed_pods: 3,
|
||||
max_input_recursive_pods: 3,
|
||||
max_statements: 31,
|
||||
max_signed_pod_values: 8,
|
||||
max_public_statements: 10,
|
||||
max_statement_args: 6,
|
||||
max_operation_args: 5,
|
||||
max_custom_predicate_arity: 5,
|
||||
max_custom_batch_size: 5,
|
||||
max_custom_predicate_wildcards: 12,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let input = r#"
|
||||
eth_friend(src_key, dst_key, private: attestation_pod) = AND(
|
||||
ValueOf(?attestation_pod["_type"], 1)
|
||||
Equal(?attestation_pod["_signer"], SELF[?src_key])
|
||||
Equal(?attestation_pod["attestation"], SELF[?dst_key])
|
||||
)
|
||||
|
||||
eth_dos_distance_base(src_key, dst_key, distance_key) = AND(
|
||||
Equal(SELF[?src_key], SELF[?dst_key])
|
||||
ValueOf(SELF[?distance_key], 0)
|
||||
)
|
||||
|
||||
eth_dos_distance_ind(src_key, dst_key, distance_key, private: one_key, shorter_distance_key, intermed_key) = AND(
|
||||
eth_dos_distance(?src_key, ?dst_key, ?distance_key)
|
||||
ValueOf(SELF[?one_key], 1)
|
||||
SumOf(SELF[?distance_key], SELF[?shorter_distance_key], SELF[?one_key])
|
||||
eth_friend(?intermed_key, ?dst_key)
|
||||
)
|
||||
|
||||
eth_dos_distance(src_key, dst_key, distance_key, private: intermed_key, shorter_distance_key) = OR(
|
||||
eth_dos_distance_base(?src_key, ?dst_key, ?distance_key)
|
||||
eth_dos_distance_ind(?src_key, ?dst_key, ?distance_key)
|
||||
)
|
||||
"#;
|
||||
|
||||
let processed = super::parse(input, ¶ms)?;
|
||||
|
||||
assert!(
|
||||
processed.request_templates.is_empty(),
|
||||
"Expected no request templates"
|
||||
);
|
||||
assert_eq!(
|
||||
processed.custom_batch.predicates.len(),
|
||||
4,
|
||||
"Expected 4 custom predicates"
|
||||
);
|
||||
|
||||
// Predicate Order: eth_friend (0), base (1), ind (2), distance (3)
|
||||
|
||||
// eth_friend (Index 0)
|
||||
let expected_friend_stmts = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::ValueOf),
|
||||
args: vec![
|
||||
sta_ak(("attestation_pod", 2), k("_type")), // Pub(0-1), Priv(2)
|
||||
sta_lit(PodType::MockSigned),
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak(("attestation_pod", 2), k("_signer")),
|
||||
sta_ak_self(ko_wc("src_key", 0)), // Pub arg 0
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak(("attestation_pod", 2), k("attestation")),
|
||||
sta_ak_self(ko_wc("dst_key", 1)), // Pub arg 1
|
||||
],
|
||||
},
|
||||
];
|
||||
let expected_friend_pred = CustomPredicate::new(
|
||||
¶ms,
|
||||
"eth_friend".to_string(),
|
||||
true, // AND
|
||||
expected_friend_stmts,
|
||||
2, // public_args_len: src_key, dst_key
|
||||
)?;
|
||||
|
||||
// eth_dos_distance_base (Index 1)
|
||||
let expected_base_stmts = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::Equal),
|
||||
args: vec![
|
||||
sta_ak_self(ko_wc("src_key", 0)),
|
||||
sta_ak_self(ko_wc("dst_key", 1)),
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::ValueOf),
|
||||
args: vec![sta_ak_self(ko_wc("distance_key", 2)), sta_lit(0i64)],
|
||||
},
|
||||
];
|
||||
let expected_base_pred = CustomPredicate::new(
|
||||
¶ms,
|
||||
"eth_dos_distance_base".to_string(),
|
||||
true, // AND
|
||||
expected_base_stmts,
|
||||
3, // public_args_len
|
||||
)?;
|
||||
|
||||
// eth_dos_distance_ind (Index 2)
|
||||
// Public args indices: 0-2
|
||||
// Private args indices: 3-5 (one_key, shorter_distance_key, intermed_key)
|
||||
let expected_ind_stmts = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::BatchSelf(3), // Calls eth_dos_distance (index 3)
|
||||
args: vec![
|
||||
// WildcardLiteral args
|
||||
StatementTmplArg::WildcardLiteral(wc("src_key", 0)),
|
||||
StatementTmplArg::WildcardLiteral(wc("dst_key", 1)), // private arg
|
||||
StatementTmplArg::WildcardLiteral(wc("distance_key", 2)), // private arg
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::ValueOf),
|
||||
args: vec![sta_ak_self(ko_wc("one_key", 3)), sta_lit(1i64)], // private arg
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::Native(NativePredicate::SumOf),
|
||||
args: vec![
|
||||
sta_ak_self(ko_wc("distance_key", 2)), // public arg
|
||||
sta_ak_self(ko_wc("shorter_distance_key", 4)), // private arg
|
||||
sta_ak_self(ko_wc("one_key", 3)), // private arg
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::BatchSelf(0), // Calls eth_friend (index 0)
|
||||
args: vec![
|
||||
// WildcardLiteral args
|
||||
StatementTmplArg::WildcardLiteral(wc("intermed_key", 5)), // private arg
|
||||
StatementTmplArg::WildcardLiteral(wc("dst_key", 1)), // public arg
|
||||
],
|
||||
},
|
||||
];
|
||||
let expected_ind_pred = CustomPredicate::new(
|
||||
¶ms,
|
||||
"eth_dos_distance_ind".to_string(),
|
||||
true, // AND
|
||||
expected_ind_stmts,
|
||||
3, // public_args_len
|
||||
)?;
|
||||
|
||||
// eth_dos_distance (Index 3)
|
||||
let expected_dist_stmts = vec![
|
||||
StatementTmpl {
|
||||
pred: Predicate::BatchSelf(1), // Calls eth_dos_distance_base (index 1)
|
||||
args: vec![
|
||||
// WildcardLiteral args
|
||||
StatementTmplArg::WildcardLiteral(wc("src_key", 0)),
|
||||
StatementTmplArg::WildcardLiteral(wc("dst_key", 1)),
|
||||
StatementTmplArg::WildcardLiteral(wc("distance_key", 2)),
|
||||
],
|
||||
},
|
||||
StatementTmpl {
|
||||
pred: Predicate::BatchSelf(2), // Calls eth_dos_distance_ind (index 2)
|
||||
args: vec![
|
||||
// WildcardLiteral args
|
||||
StatementTmplArg::WildcardLiteral(wc("src_key", 0)),
|
||||
StatementTmplArg::WildcardLiteral(wc("dst_key", 1)),
|
||||
StatementTmplArg::WildcardLiteral(wc("distance_key", 2)),
|
||||
],
|
||||
},
|
||||
];
|
||||
let expected_dist_pred = CustomPredicate::new(
|
||||
¶ms,
|
||||
"eth_dos_distance".to_string(),
|
||||
false, // OR
|
||||
expected_dist_stmts,
|
||||
3, // public_args_len
|
||||
)?;
|
||||
|
||||
let expected_batch = CustomPredicateBatch::new(
|
||||
¶ms,
|
||||
"PodlogBatch".to_string(),
|
||||
vec![
|
||||
expected_friend_pred,
|
||||
expected_base_pred,
|
||||
expected_ind_pred,
|
||||
expected_dist_pred,
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
processed.custom_batch, expected_batch,
|
||||
"Processed ETHDoS predicates do not match expected structure"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
210
src/lang/parser.rs
Normal file
210
src/lang/parser.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use pest::{iterators::Pairs as PestPairs, Parser};
|
||||
use pest_derive::Parser;
|
||||
|
||||
// Derive the parser from the grammar file
|
||||
// The Rust analyzer will only reload the grammar file when *this* file is recompiled,
|
||||
// and changes to the grammar file will not automatically trigger a recompile.
|
||||
#[derive(Parser)]
|
||||
#[grammar = "lang/grammar.pest"]
|
||||
pub struct PodlogParser;
|
||||
|
||||
pub type Pairs<'a, R> = PestPairs<'a, R>;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ParseError {
|
||||
#[error("Pest parsing error: {0}")]
|
||||
Pest(Box<pest::error::Error<Rule>>),
|
||||
}
|
||||
|
||||
impl From<pest::error::Error<Rule>> for ParseError {
|
||||
fn from(err: pest::error::Error<Rule>) -> Self {
|
||||
ParseError::Pest(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a Podlog input string according to the grammar rules.
|
||||
pub fn parse_podlog(input: &str) -> Result<Pairs<'_, Rule>, ParseError> {
|
||||
let pairs = PodlogParser::parse(Rule::document, input)?;
|
||||
Ok(pairs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_parses(rule: Rule, input: &str) {
|
||||
match PodlogParser::parse(rule, input) {
|
||||
Ok(_) => (), // Successfully parsed
|
||||
Err(e) => panic!("Failed to parse input:\n{}\nError: {}", input, e),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_fails(rule: Rule, input: &str) {
|
||||
match PodlogParser::parse(rule, input) {
|
||||
Ok(pairs) => panic!(
|
||||
"Expected parse to fail, but it succeeded. Parsed:\n{:#?}",
|
||||
pairs
|
||||
),
|
||||
Err(_) => (), // Failed as expected
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty() {
|
||||
assert_parses(Rule::document, "");
|
||||
assert_parses(Rule::document, " ");
|
||||
assert_parses(Rule::document, "\n\n");
|
||||
assert_parses(Rule::document, "// comment only");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_comment() {
|
||||
assert_parses(Rule::document, "// This is a comment\n");
|
||||
assert_parses(Rule::document, " // Indented comment");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_identifier() {
|
||||
assert_parses(Rule::identifier, "my_pred");
|
||||
assert_parses(Rule::identifier, "_internal");
|
||||
assert_parses(Rule::identifier, "ValidName123");
|
||||
assert_fails(Rule::test_identifier, "?invalid"); // Use test rule
|
||||
assert_fails(Rule::test_identifier, "1_invalid_start"); // Use test rule
|
||||
assert_fails(Rule::test_identifier, "invalid-char"); // Use test rule
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_wildcard() {
|
||||
assert_parses(Rule::wildcard, "?Var");
|
||||
assert_parses(Rule::wildcard, "?_Internal");
|
||||
assert_parses(Rule::wildcard, "?X1");
|
||||
assert_fails(Rule::test_wildcard, "NotAVar"); // Use test rule
|
||||
assert_fails(Rule::test_wildcard, "?"); // Use test rule
|
||||
assert_fails(Rule::test_wildcard, "?invalid-char"); // Use test rule
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_anchored_key() {
|
||||
assert_parses(Rule::anchored_key, "?PodVar[\"literal_key\"]");
|
||||
assert_parses(Rule::anchored_key, "?PodVar[?KeyVar]");
|
||||
assert_parses(Rule::anchored_key, "SELF[?KeyVar]");
|
||||
assert_parses(Rule::anchored_key, "SELF[\"literal_key\"]");
|
||||
assert_fails(Rule::anchored_key, "PodVar[\"key\"]"); // Needs wildcard for pod
|
||||
assert_fails(Rule::anchored_key, "?PodVar[invalid_key]"); // Key must be literal string or wildcard
|
||||
assert_fails(Rule::anchored_key, "?PodVar[]"); // Key cannot be empty
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_literals() {
|
||||
// Int
|
||||
assert_parses(Rule::literal_int, "123");
|
||||
assert_parses(Rule::literal_int, "-45");
|
||||
assert_parses(Rule::literal_int, "0");
|
||||
assert_fails(Rule::test_literal_int, "1.23"); // Use test_literal_int rule
|
||||
// Bool
|
||||
assert_parses(Rule::literal_bool, "true");
|
||||
assert_parses(Rule::literal_bool, "false");
|
||||
|
||||
// Raw - Require 64 hex digits (32 bytes, equal to 4 * 64-bit field elements)
|
||||
assert_parses(
|
||||
Rule::literal_raw,
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
);
|
||||
assert_parses(
|
||||
Rule::literal_raw,
|
||||
"0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
|
||||
);
|
||||
let long_valid_raw = format!("0x{}", "a".repeat(64));
|
||||
assert_parses(Rule::literal_raw, &long_valid_raw);
|
||||
|
||||
// Use anchored rule for failure cases
|
||||
assert_fails(Rule::test_literal_raw, "0xabc"); // Fails (string is too short)
|
||||
assert_fails(Rule::test_literal_raw, "0x"); // Fails (needs at least one pair)
|
||||
assert_fails(Rule::test_literal_raw, &format!("0x{}", "a".repeat(66))); // Fails (string is too long)
|
||||
|
||||
// String
|
||||
assert_parses(Rule::literal_string, "\"hello\"");
|
||||
assert_parses(Rule::literal_string, "\"escaped \\\" quote\"");
|
||||
assert_parses(Rule::literal_string, "\"\\\\ backslash\"");
|
||||
assert_parses(Rule::literal_string, "\"\\uABCD\"");
|
||||
assert_fails(Rule::literal_string, "\"unterminated");
|
||||
// Array
|
||||
assert_parses(Rule::literal_array, "[]");
|
||||
assert_parses(Rule::literal_array, "[1, \"two\", true]");
|
||||
assert_parses(Rule::literal_array, "[ [1], #[2] ]");
|
||||
// Set
|
||||
assert_parses(Rule::literal_set, "#[]");
|
||||
assert_parses(Rule::literal_set, "#[1, 2, 3]");
|
||||
assert_parses(
|
||||
Rule::literal_set,
|
||||
"#[ \"a\", 0x0000000000000000000000000000000000000000000000000000000000000000 ]",
|
||||
);
|
||||
// Dict
|
||||
assert_parses(Rule::literal_dict, "{}");
|
||||
assert_parses(Rule::literal_dict, "{ \"name\": \"Alice\", \"age\": 30 }");
|
||||
assert_parses(Rule::literal_dict, "{ \"nested\": { \"key\": 1 } }");
|
||||
assert_parses(
|
||||
Rule::literal_dict,
|
||||
"{ \"raw_val\": 0x0000000000000000000000000000000000000000000000000000000000000000 } ",
|
||||
);
|
||||
assert_fails(Rule::literal_dict, "{ name: \"Alice\" }"); // Key must be string literal with quotes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_request() {
|
||||
assert_parses(Rule::request_def, "REQUEST()");
|
||||
assert_parses(
|
||||
Rule::request_def,
|
||||
// Trimmed leading/trailing whitespace
|
||||
r#"REQUEST(
|
||||
// Check equality
|
||||
Equal(?gov["socialSecurityNumber"], ?pay["socialSecurityNumber"])
|
||||
// Check age > 18
|
||||
ValueOf(?const_holder["const_18y"], 1169909388)
|
||||
Lt(?gov["dateOfBirth"], ?const_holder["const_18y"])
|
||||
)"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_custom_def() {
|
||||
assert_parses(
|
||||
Rule::test_custom_predicate_def,
|
||||
// Trimmed leading/trailing whitespace
|
||||
r#"my_pred(A, B) = AND(
|
||||
Equal(?A["foo"], ?B["bar"])
|
||||
)"#,
|
||||
);
|
||||
assert_parses(
|
||||
Rule::test_custom_predicate_def,
|
||||
// Trimmed leading/trailing whitespace
|
||||
r#"pred_with_private(X, private: TempKey) = OR(
|
||||
Equal(?X[?TempKey], ?X["other"])
|
||||
)"#,
|
||||
);
|
||||
assert_fails(
|
||||
Rule::test_custom_predicate_def,
|
||||
r#"pred_no_stmts(A,B) = AND()"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_document() {
|
||||
assert_parses(
|
||||
Rule::document,
|
||||
r#"// File defining one predicate and one request
|
||||
is_valid_user(UserPod, private: ConstVal) = AND(
|
||||
// User age must be > 18 (using a constant value)
|
||||
ValueOf(?ConstVal["min_age"], 18)
|
||||
Gt(?UserPod["age"], ?ConstVal["min_age"])
|
||||
// User must not be banned
|
||||
NotContains(?_BANNED_USERS["list"], ?UserPod["userId"])
|
||||
)
|
||||
|
||||
REQUEST(
|
||||
is_valid_user(?SomeUser)
|
||||
Equal(?SomeUser["country"], ?Other["country"])
|
||||
)"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
1120
src/lang/processor.rs
Normal file
1120
src/lang/processor.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,7 @@
|
|||
pub mod backends;
|
||||
pub mod constants;
|
||||
pub mod frontend;
|
||||
pub mod lang;
|
||||
pub mod middleware;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -190,15 +190,22 @@ impl fmt::Display for Hash {
|
|||
impl FromHex for Hash {
|
||||
type Error = FromHexError;
|
||||
|
||||
// TODO make it dependant on backend::Value len
|
||||
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
|
||||
// In little endian
|
||||
// The input `hex` is a big-endian hex string.
|
||||
let bytes = <[u8; 32]>::from_hex(hex)?;
|
||||
let mut buf: [u8; 8] = [0; 8];
|
||||
let mut inner = [F::ZERO; HASH_SIZE];
|
||||
|
||||
for i in 0..HASH_SIZE {
|
||||
buf.copy_from_slice(&bytes[8 * i..8 * (i + 1)]);
|
||||
inner[i] = F::from_canonical_u64(u64::from_le_bytes(buf));
|
||||
let start = i * 8;
|
||||
let end = start + 8;
|
||||
let chunk: [u8; 8] = bytes[start..end]
|
||||
.try_into()
|
||||
.expect("slice with incorrect length");
|
||||
|
||||
// We read big-endian chunks from a big-endian string,
|
||||
// and place them into a little-endian limb array.
|
||||
let u64_val = u64::from_be_bytes(chunk);
|
||||
inner[HASH_SIZE - 1 - i] = F::from_canonical_u64(u64_val);
|
||||
}
|
||||
Ok(Self(inner))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -373,20 +373,12 @@ impl fmt::Display for CustomPredicate {
|
|||
pub struct CustomPredicateBatch {
|
||||
id: Hash,
|
||||
pub name: String,
|
||||
predicates: Vec<CustomPredicate>,
|
||||
pub(crate) predicates: Vec<CustomPredicate>,
|
||||
}
|
||||
|
||||
impl ToFields for CustomPredicateBatch {
|
||||
fn to_fields(&self, params: &Params) -> Vec<F> {
|
||||
// all the custom predicates in order
|
||||
|
||||
// TODO think if this check should go into the StatementTmpl creation,
|
||||
// instead of at the `to_fields` method, where we should assume that the
|
||||
// values are already valid
|
||||
if self.predicates.len() > params.max_custom_batch_size {
|
||||
panic!("Predicate batch exceeds maximum size");
|
||||
}
|
||||
|
||||
let pad_pred = CustomPredicate::empty();
|
||||
let fields: Vec<F> = self
|
||||
.predicates
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Write,
|
||||
};
|
||||
|
||||
use plonky2::field::types::Field;
|
||||
use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer};
|
||||
|
|
@ -13,10 +16,16 @@ fn serialize_field_tuple<S, const N: usize>(
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!(
|
||||
"{:016x}{:016x}{:016x}{:016x}",
|
||||
value[0].0, value[1].0, value[2].0, value[3].0
|
||||
))
|
||||
// `value` is little-endian in memory. We serialize it as a big-endian hex string
|
||||
// for human readability.
|
||||
let s = value
|
||||
.iter()
|
||||
.rev()
|
||||
.fold(String::with_capacity(N * 16), |mut s, limb| {
|
||||
write!(s, "{:016x}", limb.0).unwrap();
|
||||
s
|
||||
});
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
|
||||
fn deserialize_field_tuple<'de, D, const N: usize>(deserializer: D) -> Result<[F; N], D::Error>
|
||||
|
|
@ -25,20 +34,29 @@ where
|
|||
{
|
||||
let hex_str = String::deserialize(deserializer)?;
|
||||
|
||||
if !hex_str.chars().count() == 64 || !hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
let expected_len = N * 16;
|
||||
if hex_str.len() != expected_len {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"Invalid hex string length: expected {} characters, found {}",
|
||||
expected_len,
|
||||
hex_str.len()
|
||||
)));
|
||||
}
|
||||
if !hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(serde::de::Error::custom(
|
||||
"Invalid hex string format - expected 64 hexadecimal characters",
|
||||
"Invalid hex string format: contains non-hexadecimal characters",
|
||||
));
|
||||
}
|
||||
|
||||
let mut v = [F::ZERO; N];
|
||||
for (i, v_i) in v.iter_mut().enumerate() {
|
||||
for i in 0..N {
|
||||
let start = i * 16;
|
||||
let end = start + 16;
|
||||
let hex_part = &hex_str[start..end];
|
||||
*v_i = F::from_canonical_u64(
|
||||
u64::from_str_radix(hex_part, 16).map_err(serde::de::Error::custom)?,
|
||||
);
|
||||
let u64_val = u64::from_str_radix(hex_part, 16).map_err(serde::de::Error::custom)?;
|
||||
// The hex string is big-endian, so the first chunk (i=0) is the most significant.
|
||||
// We store it in the last position of our little-endian array `v`.
|
||||
v[N - 1 - i] = F::from_canonical_u64(u64_val);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue