/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{ borrow::Borrow, collections::HashMap, hash::Hash, io::Result, iter::Peekable, str::Chars, }; use super::{ ast::{Arg, Constant, Literal, Statement, StatementType, Variable, Witness}, constants::{ALLOWED_FIELDS, MAX_K, MAX_NS_LEN}, error::ErrorEmitter, lexer::{Token, TokenType}, LitType, Opcode, VarType, }; /// zkas language builtin keywords. /// These can not be used anywhere except where they are expected. const KEYWORDS: [&str; 5] = ["k", "field", "constant", "witness", "circuit"]; /// Forbidden namespaces const NOPE_NS: [&str; 4] = [".constant", ".literal", ".witness", ".circuit"]; /// Valid constant types and their allowed names. const CONSTANT_TYPES: &[(&str, VarType, &[&str])] = &[ ("EcFixedPoint", VarType::EcFixedPoint, &["VALUE_COMMIT_RANDOM"]), ("EcFixedPointShort", VarType::EcFixedPointShort, &["VALUE_COMMIT_VALUE"]), ("EcFixedPointBase", VarType::EcFixedPointBase, &["VALUE_COMMIT_RANDOM_BASE", "NULLIFIER_K"]), ]; #[derive(Clone)] struct IndexMap { pub order: Vec, pub map: HashMap, } impl IndexMap { fn new() -> Self { Self { order: vec![], map: HashMap::new() } } } impl IndexMap where K: Eq + Hash + Send + Sync + Clone + 'static, V: Send + Sync + Clone + 'static, { fn contains_key(&self, k: &Q) -> bool where K: Borrow, { self.map.contains_key(k) } fn get(&self, k: &Q) -> Option<&V> where K: Borrow, { self.map.get(k) } fn insert(&mut self, k: K, v: V) -> Option { self.order.push(k.clone()); self.map.insert(k, v) } fn scam_iter(&self) -> Vec<(K, V)> { self.order.iter().map(|k| (k.clone(), self.get(k).unwrap().clone())).collect() } } // Valid witness types impl TryFrom<&Token> for VarType { type Error = String; fn try_from(token: &Token) -> std::result::Result { match token.token.as_str() { "EcPoint" => Ok(Self::EcPoint), "EcNiPoint" => Ok(Self::EcNiPoint), "Base" => Ok(Self::Base), "Scalar" => Ok(Self::Scalar), "MerklePath" => Ok(Self::MerklePath), "SparseMerklePath" => Ok(Self::SparseMerklePath), "Uint32" => Ok(Self::Uint32), "Uint64" => Ok(Self::Uint64), x => Err(format!("{x} is an unsupported witness type")), } } } pub struct Parser { tokens: Vec, error: ErrorEmitter, } type Parsed = (String, u32, Vec, Vec, Vec); /// Intermediate structure to hold parsed section tokens. /// The tokens gathered from each of the sections are stored here /// before being converted into AST nodes. struct SectionTokens { constant: Vec, witness: Vec, circuit: Vec, } impl SectionTokens { fn new() -> Self { Self { constant: vec![], witness: vec![], circuit: vec![] } } } impl Parser { pub fn new(filename: &str, source: Chars, tokens: Vec) -> Self { // For nice error reporting, we'll load everything into a string // vector so we have references to lines. let lines: Vec = source.as_str().lines().map(|x| x.to_string()).collect(); let error = ErrorEmitter::new("Parser", filename, lines); Self { tokens, error } } pub fn parse(&self) -> Result { if self.tokens.is_empty() { return Err(self.error.abort("Source file does not contain any valid tokens.", 0, 0)) } if self.tokens[0].token_type != TokenType::Symbol { return Err(self.error.abort( "Source file does not start with a section. Expected `constant/witness/circuit`.", 0, 0, )) } let mut iter = self.tokens.iter(); // Parse header (k and field declarations) let declared_k = self.parse_header(&mut iter)?; // Parse all sections and collect their tokens let (namespace, section_tokens) = self.parse_sections(&mut iter)?; // Build AST from section tokens let constants = self.build_constants(§ion_tokens.constant)?; let witnesses = self.build_witnesses(§ion_tokens.witness)?; let statements = self.parse_ast_circuit(§ion_tokens.circuit)?; if statements.is_empty() { return Err(self.error.abort("Circuit section is empty.", 0, 0)) } Ok((namespace, declared_k, constants, witnesses, statements)) } /// Parse the file header: k=N; field="..."; /// /// The first thing that has to be declared in the source code is the /// constant "k" which defines 2^k rows that the circuit needs to /// successfully execute. /// /// Then we declare the field we're working in. fn parse_header<'a>(&self, iter: &mut impl Iterator) -> Result { let Some((k, equal, number, semicolon)) = Self::next_tuple4(iter) else { return Err(self.error.abort("Source file does not start with k=n;", 0, 0)) }; self.expect_token_type(k, TokenType::Symbol)?; self.expect_token_type(equal, TokenType::Assign)?; self.expect_token_type(number, TokenType::Number)?; self.expect_token_type(semicolon, TokenType::Semicolon)?; if k.token != "k" { return Err(self.error.abort("Source file does not start with k=n;", k.line, k.column)) } // Ensure that the value for k can be parsed correctly into the token type. let declared_k: u32 = number.token.parse().map_err(|e| { self.error.abort( &format!("k param is invalid, max allowed is {MAX_K}. Error: {e}"), number.line, number.column, ) })?; if declared_k > MAX_K { return Err(self.error.abort( &format!("k param is too high, max allowed is {MAX_K}"), number.line, number.column, )) } // Parse field declaration let Some((field, equal, field_name, semicolon)) = Self::next_tuple4(iter) else { return Err(self.error.abort("Source file does not declare field after k", 0, 0)) }; self.expect_token_type(field, TokenType::Symbol)?; self.expect_token_type(equal, TokenType::Assign)?; self.expect_token_type(field_name, TokenType::String)?; self.expect_token_type(semicolon, TokenType::Semicolon)?; if field.token != "field" { return Err(self.error.abort( "Source file does not declare field after k", field.line, field.column, )) } if !ALLOWED_FIELDS.contains(&field_name.token.as_str()) { return Err(self.error.abort( &format!( "Declared field \"{}\" is not supported. Use any of: {ALLOWED_FIELDS:?}", field_name.token ), field_name.line, field_name.column, )) } Ok(declared_k) } /// Parse all sections (constant, witness, circuit) and return their tokens. /// /// Sections "constant", "witness", and "circuit" are the sections we must /// be declaring in our source code. When we find one, we'll take all the /// tokens found in the section and place them in their respective vec. /// /// NOTE: Currently this logic depends on the fact that the sections are /// closed off with braces. This should be revisited later when we decide /// to add other lang functionality that also depends on using braces. fn parse_sections<'a>( &self, iter: &mut impl Iterator, ) -> Result<(String, SectionTokens)> { let mut sections = SectionTokens::new(); let mut namespace: Option = None; let mut declared = (false, false, false); // constant, witness, circuit while let Some(t) = iter.next() { let section_tokens = match t.token.as_str() { "constant" => { if declared.0 { return Err(self.error.abort( "Duplicate `constant` section found.", t.line, t.column, )) } declared.0 = true; &mut sections.constant } "witness" => { if declared.1 { return Err(self.error.abort( "Duplicate `witness` section found.", t.line, t.column, )) } declared.1 = true; &mut sections.witness } "circuit" => { if declared.2 { return Err(self.error.abort( "Duplicate `circuit` section found.", t.line, t.column, )) } declared.2 = true; &mut sections.circuit } x => { return Err(self.error.abort( &format!("Section `{x}` is not a valid section"), t.line, t.column, )) } }; // Absorb all tokens until closing brace self.absorb_section_tokens(iter, section_tokens)?; // Validate and extract namespace namespace = Some(self.validate_section_namespace(&t.token, section_tokens, namespace)?); } let ns = namespace.ok_or_else(|| self.error.abort("Missing namespace in .zk source.", 0, 0))?; if !declared.0 { return Err(self.error.abort("Missing `constant` section in .zk source.", 0, 0)) } if !declared.1 { return Err(self.error.abort("Missing `witness` section in .zk source.", 0, 0)) } if !declared.2 { return Err(self.error.abort("Missing `circuit` section in .zk source.", 0, 0)) } Ok((ns, sections)) } /// Absorb tokens from iterator until a closing brace is found. /// Validates that no keywords are used in improper places. fn absorb_section_tokens<'a>( &self, iter: &mut impl Iterator, dest: &mut Vec, ) -> Result<()> { for inner in iter { if KEYWORDS.contains(&inner.token.as_str()) && inner.token_type == TokenType::Symbol { return Err(self.error.abort( &format!("Keyword '{}' used in improper place.", inner.token), inner.line, inner.column, )) } dest.push(inner.clone()); if inner.token_type == TokenType::RightBrace { break } } Ok(()) } /// Validate namespace consistency across sections. /// All sections must use the same namespace, and it must not be a reserved name. fn validate_section_namespace( &self, section_name: &str, tokens: &[Token], existing_ns: Option, ) -> Result { if tokens.is_empty() { return Err(self.error.abort(&format!("Section `{section_name}` has no tokens"), 0, 0)) } let ns_token = &tokens[0]; if let Some(ns) = existing_ns { if ns != ns_token.token { return Err(self.error.abort( &format!("Found '{}' namespace, expected '{ns}'.", ns_token.token), ns_token.line, ns_token.column, )) } return Ok(ns) } if NOPE_NS.contains(&ns_token.token.as_str()) { return Err(self.error.abort( &format!("'{}' cannot be a namespace.", ns_token.token), ns_token.line, ns_token.column, )) } if ns_token.token.len() > MAX_NS_LEN { return Err(self.error.abort( &format!("Namespace too long, max {MAX_NS_LEN} bytes"), ns_token.line, ns_token.column, )) } Ok(ns_token.token.clone()) } /// Build constants from section tokens. /// Validates constant types against the CONSTANT_TYPES table. fn build_constants(&self, tokens: &[Token]) -> Result> { self.check_section_structure("constant", tokens)?; let parsed = self.parse_typed_section("constant", tokens)?; let mut ret = vec![]; // name = constant name for (name, (name_token, type_token)) in parsed.scam_iter() { self.validate_section_entry("Constant", &name, &name_token, &type_token)?; // Look up the constant type in our table let type_name = type_token.token.as_str(); let constant_def = CONSTANT_TYPES.iter().find(|(t, _, _)| *t == type_name); match constant_def { Some((_, var_type, valid_names)) => { if !valid_names.contains(&name_token.token.as_str()) { return Err(self.error.abort( &format!( "`{}` is not a valid {type_name} constant. Supported: {valid_names:?}", name_token.token ), name_token.line, name_token.column, )) } ret.push(Constant { name: name.to_string(), typ: *var_type, line: type_token.line, column: type_token.column, }); } None => { return Err(self.error.abort( &format!("`{type_name}` is an unsupported constant type."), type_token.line, type_token.column, )) } } } Ok(ret) } /// Build witnesses from section tokens. fn build_witnesses(&self, tokens: &[Token]) -> Result> { self.check_section_structure("witness", tokens)?; let parsed = self.parse_typed_section("witness", tokens)?; let mut ret = vec![]; // name = witness name for (name, (name_token, type_token)) in parsed.scam_iter() { self.validate_section_entry("Witness", &name, &name_token, &type_token)?; match VarType::try_from(&type_token) { Ok(typ) => { ret.push(Witness { name: name.to_string(), typ, line: name_token.line, column: name_token.column, }); } Err(e) => return Err(self.error.abort(&e, type_token.line, type_token.column)), } } Ok(ret) } /// Parse a typed section (constant or witness) into an IndexMap. /// Both sections have the same structure: pairs of ` ` separated by commas. fn parse_typed_section( &self, section_name: &str, tokens: &[Token], ) -> Result> { let mut result = IndexMap::new(); // Skip namespace and braces: tokens[0] is namespace, tokens[1] is {, last is } // This is everything between the braces: { ... } let inner_tokens = &tokens[2..tokens.len() - 1]; let mut iter = inner_tokens.iter(); while let Some((typ, name, comma)) = Self::next_tuple3(&mut iter) { if comma.token_type != TokenType::Comma { return Err(self.error.abort("Separator is not a comma.", comma.line, comma.column)) } // No variable shadowing if result.contains_key(name.token.as_str()) { return Err(self.error.abort( &format!( "Section `{section_name}` already contains the token `{}`.", &name.token ), name.line, name.column, )) } result.insert(name.token.clone(), (name.clone(), typ.clone())); } if iter.next().is_some() { return Err(self.error.abort( &format!("Internal error, leftovers in '{section_name}' iterator"), 0, 0, )) } Ok(result) } /// Common validation for constant/witness entries. /// Ensures name and type tokens are symbols and match expected values. fn validate_section_entry( &self, section_type: &str, name: &str, name_token: &Token, type_token: &Token, ) -> Result<()> { if name_token.token != name { return Err(self.error.abort( &format!( "{section_type} name `{}` doesn't match token `{name}`.", name_token.token ), name_token.line, name_token.column, )) } if name_token.token_type != TokenType::Symbol { return Err(self.error.abort( &format!("{section_type} name `{}` is not a symbol.", name_token.token), name_token.line, name_token.column, )) } if type_token.token_type != TokenType::Symbol { return Err(self.error.abort( &format!("{section_type} type `{}` is not a symbol.", type_token.token), type_token.line, type_token.column, )) } Ok(()) } /// Routine checks on section structure. /// Validates that sections have proper opening/closing braces and correct element counts. fn check_section_structure(&self, section: &str, tokens: &[Token]) -> Result<()> { // Offsets 0 and 1 are accessed directly below, so we need a length of at // least 2 in order to avoid an index-out-of-bounds panic. if tokens.len() < 2 { return Err(self.error.abort("Insufficient number of tokens in section.", 0, 0)) } if tokens[0].token_type != TokenType::String { return Err(self.error.abort( "Section declaration must start with a naming string.", tokens[0].line, tokens[0].column, )) } if tokens[1].token_type != TokenType::LeftBrace { return Err(self.error.abort( "Section must be opened with a left brace '{'", tokens[0].line, tokens[0].column, )) } if tokens.last().unwrap().token_type != TokenType::RightBrace { return Err(self.error.abort( "Section must be closed with a right brace '}'", tokens[0].line, tokens[0].column, )) } match section { "constant" | "witness" => { if tokens.len() == 3 { self.error.warn(&format!("{section} section is empty."), 0, 0); } if !tokens[2..tokens.len() - 1].len().is_multiple_of(3) { return Err(self.error.abort( &format!("Invalid number of elements in '{section}' section. Must be pairs of ' ' separated with a comma ','."), tokens[0].line, tokens[0].column )) } } "circuit" => { if tokens.len() == 3 { return Err(self.error.abort("circuit section is empty.", 0, 0)) } if tokens[tokens.len() - 2].token_type != TokenType::Semicolon { return Err(self.error.abort( "Circuit section does not end with a semicolon. Would never finish parsing.", tokens[tokens.len()-2].line, tokens[tokens.len()-2].column, )) } } _ => unreachable!(), }; Ok(()) } /// Parse the circuit section into statements. /// /// The statement layouts/syntax in the language are as follows: /// /// ```text /// C = poseidon_hash(pub_x, pub_y, value, token, serial); /// | | | | | /// V V V V V /// variable opcode arg arg /// assign /// /// constrain_instance(C); /// | | /// V V /// opcode arg /// /// inner opcode arg /// | /// constrain_instance(ec_get_x(foo)); /// | | /// V V /// opcode arg as opcode /// ``` /// /// In the latter, we want to support nested function calls, e.g.: /// /// ```text /// constrain_instance(ec_get_x(token_commit)); /// ``` /// /// The inner call's result would still get pushed on the heap, /// but it will not be accessible in any other scope. /// /// In certain opcodes, we also support literal types, and the /// opcodes can return a variable type after running the operation. /// e.g. /// ```text /// one = witness_base(1); /// zero = witness_base(0); /// ``` /// /// The literal type is used only in the function call's scope, but /// the result is then accessible on the heap to be used by further /// computation. /// /// Regarding multiple return values from opcodes, this is perhaps /// not necessary for the current language scope, as this is a low /// level representation. Note that it could be relatively easy to /// modify the parsing logic to support that here. For now we'll /// defer it, and if at some point we decide that the language is /// too expressive and noisy, we'll consider having multiple return /// types. It also very much depends on the type of functions/opcodes /// that we want to support. fn parse_ast_circuit(&self, tokens: &[Token]) -> Result> { self.check_section_structure("circuit", tokens)?; // Split circuit tokens into statements (delimited by semicolons). // Here, our statements tokens have been parsed and delimited by // semicolons (;) in the source file. This iterator contains each // of those statements as an array of tokens we then consume and // build the AST further. let mut circuit_stmts: Vec> = vec![]; let mut current_stmt: Vec = vec![]; for token in tokens[2..tokens.len() - 1].iter() { if token.token_type == TokenType::Semicolon { // Push completed statement to the heap circuit_stmts.push(current_stmt); current_stmt = vec![]; continue } current_stmt.push(token.clone()); } // Vec of statements to return from this entire parsing operation. let mut ret = vec![]; for statement in circuit_stmts { if statement.is_empty() { continue } self.validate_statement_brackets(&statement)?; // Peekable iterator so we can see tokens in advance // without consuming the iterator. let mut iter = statement.iter().peekable(); let stmt = self.parse_statement(&mut iter)?; ret.push(stmt); } Ok(ret) } /// Validate matching brackets in a statement. /// Ensures parentheses and brackets are balanced and properly nested. fn validate_statement_brackets(&self, statement: &[Token]) -> Result<()> { let (mut left_paren, mut right_paren, mut left_bracket, mut right_bracket) = (0, 0, 0, 0); for token in statement { match token.token.as_str() { "(" => left_paren += 1, ")" => right_paren += 1, "[" => left_bracket += 1, "]" => right_bracket += 1, _ => {} } } if (left_paren == 0 && right_paren == 0) && (left_bracket == 0 && right_bracket == 0) { return Err(self.error.abort( "Statement must include a function call or array initialization. No parentheses or square brackets present.", statement[0].line, statement[0].column, )) } if (left_bracket != right_bracket) || (left_paren != right_paren) { return Err(self.error.abort( "Parentheses or brackets are not matched.", statement[0].line, statement[0].column, )) } // Is there a valid use-case for defining nested arrays? For now, // if square brackets are present, raise an error unless there is // exactly one pair. if left_bracket > 1 { return Err(self.error.abort( "Only one pair of brackets allowed for array declaration", statement[0].line, statement[0].column, )) } Ok(()) } /// Parse a single statement from tokens. /// Determines if this is an assignment (var = ...) or a direct call (opcode(...)). fn parse_statement( &self, iter: &mut Peekable>, ) -> Result { // Dummy statement that we'll hopefully fill now. let mut stmt = Statement::default(); let Some(token) = iter.next() else { return Err(self.error.abort("Empty statement", 0, 0)) }; // TODO: MAKE SURE IT'S A SYMBOL // Check if this is an assignment (var = ...) or a direct call (opcode(...)) // This logic must be changed if we want to support multiple return values. if let Some(next_token) = iter.peek() { if next_token.token_type == TokenType::Assign { // Assignment statement stmt.line = token.line; stmt.typ = StatementType::Assign; stmt.lhs = Some(Variable { name: token.token.clone(), typ: VarType::Dummy, line: token.line, column: token.column, }); // Skip over the `=` token. iter.next(); // Get the opcode token let Some(opcode_token) = iter.next() else { return Err(self.error.abort( "Expected opcode after assignment", token.line, token.column, )) }; self.parse_opcode_call(opcode_token, iter, &mut stmt)?; } else if next_token.token_type == TokenType::LeftParen { // Direct call statement stmt.line = token.line; stmt.typ = StatementType::Call; stmt.lhs = None; self.parse_opcode_call(token, iter, &mut stmt)?; } else if next_token.token_type == TokenType::LeftBracket { // Array declaration. // TODO: Support function calls in array declarations. // Currently only literals can be used to construct an array. return Err(self.error.abort( "Arrays are not implemented yet.", token.line, token.column, )) } else { return Err(self.error.abort( &format!("Illegal token `{}`.", next_token.token), next_token.line, next_token.column, )) } } // At this stage of parsing, we should have assigned `stmt` a // StatementType that is not a Noop. If we have failed to do so, we // cannot proceed because Noops must never be passed to the compiler. // This can occur when multiple independent statements are passed on // one line, or if a statement is not terminated by a semicolon. if stmt.typ == StatementType::Noop { return Err(self.error.abort( "Statement is a NOOP; not allowed. (Did you miss a semicolon?)", token.line, token.column, )) } Ok(stmt) } /// Parse an opcode call and fill in the statement. /// The assumption here is that the current token is a function call, /// so we check if it's legit and start digging. fn parse_opcode_call( &self, token: &Token, iter: &mut Peekable>, stmt: &mut Statement, ) -> Result<()> { let func_name = token.token.as_str(); // Ensure the current function is a symbol if token.token_type != TokenType::Symbol { return Err(self.error.abort("This token is not a symbol.", token.line, token.column)) } if let Some(op) = Opcode::from_name(func_name) { let rhs = self.parse_function_call(token, iter)?; stmt.opcode = op; stmt.rhs = rhs; Ok(()) } else { Err(self.error.abort( &format!("Unimplemented opcode `{func_name}`."), token.line, token.column, )) } } /// Parse a function call and its arguments. /// Handles nested function calls recursively, creating intermediate /// variables for inner call results. fn parse_function_call( &self, token: &Token, iter: &mut Peekable>, ) -> Result> { if let Some(next_token) = iter.peek() { if next_token.token_type != TokenType::LeftParen { return Err(self.error.abort( "Invalid function call opening. Must start with a '('.", next_token.line, next_token.column, )) } // Skip the opening parenthesis iter.next(); } else { return Err(self.error.abort("Premature ending of statement.", token.line, token.column)) } let mut ret = vec![]; // The next element in the iter now hopefully contains an opcode // argument. If it's another opcode, we'll recurse into this // function's logic. // Otherwise, we look for variable and literal types. while let Some(arg) = iter.next() { // ============================ // Parse a nested function call // ============================ if let Some(op_inner) = Opcode::from_name(&arg.token) { if let Some(paren) = iter.peek() { if paren.token_type != TokenType::LeftParen { return Err(self.error.abort( "Invalid function call opening. Must start with a '('.", paren.line, paren.column, )) } // Recurse this function to get the params of the nested one. let args = self.parse_function_call(arg, iter)?; // Then we assign a "fake" variable that serves as a heap // reference. let var = Variable { name: format!("_op_inner_{}_{}", arg.line, arg.column), typ: VarType::Dummy, line: arg.line, column: arg.column, }; let arg = Arg::Func(Statement { typ: StatementType::Assign, opcode: op_inner, lhs: Some(var), rhs: args, line: arg.line, }); ret.push(arg); continue } return Err(self.error.abort( "Missing tokens in statement, there's a syntax error here.", arg.line, arg.column, )) } // ========================================== // Parse normal argument, not a function call // ========================================== if let Some(sep) = iter.next() { // See if we have a variable or a literal type. match arg.token_type { TokenType::Symbol => ret.push(Arg::Var(Variable { name: arg.token.clone(), typ: VarType::Dummy, line: arg.line, column: arg.column, })), TokenType::Number => { // Check if we can actually convert this into a number. arg.token.parse::().map_err(|e| { self.error.abort( &format!("Failed to convert literal into u64: {e}"), arg.line, arg.column, ) })?; ret.push(Arg::Lit(Literal { name: arg.token.clone(), typ: LitType::Uint64, line: arg.line, column: arg.column, })) } TokenType::RightParen => { if let Some(comma) = iter.peek() { if comma.token_type == TokenType::Comma { iter.next(); } } break } // Note: Unimplemented symbols throw an error now instead of a panic. // This assists with fuzz testing as existing features can still be tested // without causing the fuzzer to choke due to the panic created // by unimplmented!(). _ => { return Err(self.error.abort( "Character is illegal/unimplemented in this context", arg.line, arg.column, )) } }; if sep.token_type == TokenType::RightParen { if let Some(comma) = iter.peek() { if comma.token_type == TokenType::Comma { iter.next(); } } // Reached end of args break } if sep.token_type != TokenType::Comma { return Err(self.error.abort( "Argument separator is not a comma (`,`)", sep.line, sep.column, )) } } } Ok(ret) } /// Check that a token has the expected type. fn expect_token_type(&self, token: &Token, expected: TokenType) -> Result<()> { if token.token_type != expected { return Err(self.error.abort( &format!("Expected {:?}, got {:?}", expected, token.token_type), token.line, token.column, )) } Ok(()) } /// Get next 3 items from an iterator as a tuple. fn next_tuple3(iter: &mut I) -> Option<(T, T, T)> where I: Iterator, { let a = iter.next()?; let b = iter.next()?; let c = iter.next()?; Some((a, b, c)) } /// Get next 4 items from an iterator as a tuple. fn next_tuple4(iter: &mut I) -> Option<(T, T, T, T)> where I: Iterator, { let a = iter.next()?; let b = iter.next()?; let c = iter.next()?; let d = iter.next()?; Some((a, b, c, d)) } }