--- a/Cargo.lock +++ b/Cargo.lock @@ -638,6 +638,7 @@ "jrsonnet-gcmodule", "jrsonnet-interner", "jrsonnet-ir", + "jrsonnet-ir-parser", "jrsonnet-macros", "jrsonnet-peg-parser", "jrsonnet-types", --- a/crates/jrsonnet-evaluator/Cargo.toml +++ b/crates/jrsonnet-evaluator/Cargo.toml @@ -13,11 +13,13 @@ workspace = true [features] -default = ["explaining-traces"] +default = ["explaining-traces", "ir-parser"] # Rustc-like trace visualization explaining-traces = ["annotate-snippets", "hi-doc"] # Allows library authors to throw custom errors anyhow-error = ["anyhow"] +# Use hand-written recursive descent parser instead of PEG parser +ir-parser = ["dep:jrsonnet-ir-parser"] # Allows to preserve field order in objects exp-preserve-order = [] @@ -28,12 +30,13 @@ # Bigint type exp-bigint = ["num-bigint", "jrsonnet-types/exp-bigint"] # obj?.field, obj?.['field'] -exp-null-coaelse = ["jrsonnet-peg-parser/exp-null-coaelse"] +exp-null-coaelse = ["jrsonnet-peg-parser/exp-null-coaelse", "jrsonnet-ir-parser?/exp-null-coaelse"] [dependencies] jrsonnet-interner.workspace = true jrsonnet-ir.workspace = true jrsonnet-peg-parser.workspace = true +jrsonnet-ir-parser = { workspace = true, optional = true } jrsonnet-types.workspace = true jrsonnet-macros.workspace = true jrsonnet-gcmodule.workspace = true --- a/crates/jrsonnet-evaluator/src/async_import.rs +++ b/crates/jrsonnet-evaluator/src/async_import.rs @@ -7,6 +7,9 @@ FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, ObjBody, Slice, SliceDesc, Source, SourcePath, Spanned, }; +#[cfg(feature = "ir-parser")] +use jrsonnet_ir_parser::ParserSettings; +#[cfg(not(feature = "ir-parser"))] use jrsonnet_peg_parser::ParserSettings; use rustc_hash::FxHashMap; @@ -323,7 +326,7 @@ }; let source = Source::new(path.clone(), code.clone()); // If failed - then skip import - file.parsed = jrsonnet_peg_parser::parse(&code, &ParserSettings { source }) + file.parsed = crate::parse_jsonnet(&code, &ParserSettings { source }) .map(Rc::new) .ok(); if let Some(parsed) = &file.parsed { --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -154,6 +154,7 @@ ImportNotSupported(SourcePath, ResolvePathOwned), #[error("can't import from virtual file")] CantImportFromVirtualFile, + #[cfg(not(feature = "ir-parser"))] #[error( "syntax error: {}", // Peg has no fancier way to handle critical parsing errors https://github.com/kevinmehall/rust-peg/issues/225 @@ -172,6 +173,14 @@ error: Box, }, + #[cfg(feature = "ir-parser")] + #[error("syntax error: {error}")] + ImportSyntaxError { + path: Source, + #[trace(skip)] + error: Box, + }, + #[error("runtime error: {}", format_empty_str(.0))] RuntimeError(IStr), #[error("stack overflow, try to reduce recursion, or set --max-stack to bigger value")] --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -46,6 +46,9 @@ use jrsonnet_ir::{Expr, Source, SourcePath}; #[doc(hidden)] pub use jrsonnet_macros; +#[cfg(feature = "ir-parser")] +use jrsonnet_ir_parser::ParserSettings; +#[cfg(not(feature = "ir-parser"))] use jrsonnet_peg_parser::ParserSettings; pub use obj::*; pub use rustc_hash; @@ -56,6 +59,22 @@ use crate::gc::WithCapacityExt as _; +#[cfg(feature = "ir-parser")] +pub(crate) fn parse_jsonnet( + code: &str, + settings: &ParserSettings, +) -> Result { + jrsonnet_ir_parser::parse(code, settings) +} + +#[cfg(not(feature = "ir-parser"))] +pub(crate) fn parse_jsonnet( + code: &str, + settings: &ParserSettings, +) -> Result { + jrsonnet_peg_parser::parse(code, settings) +} + cc_dyn!( #[derive(Clone)] CcUnbound, @@ -345,7 +364,7 @@ let file_name = Source::new(path.clone(), code.clone()); if file.parsed.is_none() { file.parsed = Some( - jrsonnet_peg_parser::parse( + parse_jsonnet( &code, &ParserSettings { source: file_name.clone(), @@ -461,7 +480,7 @@ pub fn evaluate_snippet(&self, name: impl Into, code: impl Into) -> Result { let code = code.into(); let source = Source::new_virtual(name.into(), code.clone()); - let parsed = jrsonnet_peg_parser::parse( + let parsed = parse_jsonnet( &code, &ParserSettings { source: source.clone(), @@ -482,7 +501,7 @@ ) -> Result { let code = code.into(); let source = Source::new_virtual(name.into(), code.clone()); - let parsed = jrsonnet_peg_parser::parse( + let parsed = parse_jsonnet( &code, &ParserSettings { source: source.clone(), --- a/crates/jrsonnet-ir-parser/Cargo.toml +++ b/crates/jrsonnet-ir-parser/Cargo.toml @@ -6,6 +6,9 @@ repository.workspace = true version.workspace = true +[features] +exp-null-coaelse = ["jrsonnet-ir/exp-null-coaelse"] + [dependencies] insta.workspace = true jrsonnet-gcmodule.workspace = true --- a/crates/jrsonnet-ir-parser/src/lib.rs +++ b/crates/jrsonnet-ir-parser/src/lib.rs @@ -1,13 +1,37 @@ use std::rc::Rc; -use insta::assert_snapshot; use jrsonnet_gcmodule::Acyclic; use jrsonnet_ir::{ - AssertExpr, AssertStmt, Expr, IfElse, IfSpecData, LiteralType, Slice, SliceDesc, Source, - SourceVirtual, Span, Spanned, + unescape, ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BinaryOpType, BindSpec, CompSpec, + Destruct, Expr, ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IfElse, + IfSpecData, ImportKind, IndexPart, LiteralType, Member, ObjBody, ObjComp, ObjMembers, Slice, + SliceDesc, Source, Span, Spanned, UnaryOpType, Visibility, }; -use jrsonnet_lexer::{Lexeme, Lexer, SyntaxKind, T}; +use jrsonnet_lexer::{collect_lexed_str_block, Lexeme, Lexer, SyntaxKind, T}; +pub struct ParserSettings { + pub source: Source, +} + +#[derive(Debug, Clone)] +pub struct ParseErrorLocation { + pub offset: usize, +} + +#[derive(Debug, Clone)] +pub struct ParseError { + pub message: String, + pub location: ParseErrorLocation, +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +type R = Result; + struct Parser<'a> { lexemes: Vec>, offset: usize, @@ -15,30 +39,46 @@ } impl<'a> Parser<'a> { - fn new(s: &'a str) -> Self { + fn new(code: &'a str, source: Source) -> Self { Self { - lexemes: Lexer::new(s) - .filter(|l| l.kind != SyntaxKind::WHITESPACE) + lexemes: Lexer::new(code) + .filter(|l| { + !matches!( + l.kind, + SyntaxKind::WHITESPACE + | SyntaxKind::SINGLE_LINE_SLASH_COMMENT + | SyntaxKind::SINGLE_LINE_HASH_COMMENT + | SyntaxKind::MULTI_LINE_COMMENT + ) + }) .collect(), offset: 0, - source: Source::new_virtual("".into(), s.into()), + source, } } + fn peek(&self) -> SyntaxKind { - self.lexemes[self.offset].kind + if self.at_eof() { + SyntaxKind::EOF + } else { + self.lexemes[self.offset].kind + } } - fn text(&self) -> &str { + + fn text(&self) -> &'a str { self.lexemes[self.offset].text } + fn at(&self, kind: SyntaxKind) -> bool { !self.at_eof() && self.peek() == kind } + fn eat_any(&mut self) { - self.offset += 1 + self.offset += 1; } fn at_eof(&self) -> bool { - self.offset == self.lexemes.len() + self.offset >= self.lexemes.len() } fn try_eat(&mut self, t: SyntaxKind) -> bool { @@ -48,17 +88,165 @@ } false } - fn eat(&mut self, t: SyntaxKind) { - assert_eq!(self.peek(), t); + + fn current_desc(&self) -> String { + if self.at_eof() { + return "end of file".to_owned(); + } + let kind = self.peek(); + let text = self.text(); + let name = kind.display_name(); + if matches!(kind, SyntaxKind::IDENT | SyntaxKind::FLOAT) { + format!("{name} \"{text}\"") + } else { + name.to_owned() + } + } + + fn eat(&mut self, t: SyntaxKind) -> R<()> { + if !self.at(t) { + return Err(self.error(format!( + "expected {}, got {}", + t.display_name(), + self.current_desc(), + ))); + } self.eat_any(); + Ok(()) } fn span_start(&self) -> u32 { + if self.at_eof() { + if let Some(last) = self.lexemes.last() { + return last.range.1; + } + return 0; + } self.lexemes[self.offset].range.0 } + fn span_end(&self) -> u32 { self.lexemes[self.offset - 1].range.1 } + + fn error(&self, message: String) -> ParseError { + ParseError { + location: ParseErrorLocation { + offset: self.span_start() as usize, + }, + message, + } + } + + fn expect_ident(&mut self) -> R { + if !self.at(SyntaxKind::IDENT) { + return Err(self.error(format!("expected identifier, got {}", self.current_desc()))); + } + let text = self.text(); + if is_reserved(text) { + return Err(self.error(format!( + "expected identifier, got reserved word '{text}'" + ))); + } + let s: IStr = text.into(); + self.eat_any(); + Ok(s) + } + + fn at_ident(&self) -> bool { + self.at(SyntaxKind::IDENT) && !is_reserved(self.lexemes[self.offset].text) + } +} + +fn is_reserved(s: &str) -> bool { + matches!( + s, + "assert" + | "else" | "error" + | "false" | "for" + | "function" | "if" + | "import" | "importstr" + | "importbin" | "in" + | "local" | "null" + | "tailstrict" | "then" + | "self" | "super" + | "true" + ) +} + +fn spanned(p: &mut Parser<'_>, cb: impl FnOnce(&mut Parser<'_>) -> R) -> R> { + let start = p.span_start(); + let v = cb(p)?; + let end = p.span_end(); + Ok(Spanned::new(v, Span(p.source.clone(), start, end))) +} + +fn parse_string_content(p: &mut Parser<'_>) -> R { + let kind = p.peek(); + let text = p.text(); + let s = match kind { + SyntaxKind::STRING_DOUBLE => { + let inner = &text[1..text.len() - 1]; + unescape::unescape(inner) + .ok_or_else(|| p.error("invalid string escape".into()))? + } + SyntaxKind::STRING_SINGLE => { + let inner = &text[1..text.len() - 1]; + unescape::unescape(inner) + .ok_or_else(|| p.error("invalid string escape".into()))? + } + SyntaxKind::STRING_DOUBLE_VERBATIM => { + let inner = &text[2..text.len() - 1]; + inner.replace("\"\"", "\"") + } + SyntaxKind::STRING_SINGLE_VERBATIM => { + let inner = &text[2..text.len() - 1]; + inner.replace("''", "'") + } + SyntaxKind::STRING_BLOCK => { + let inner = &text[3..]; + let collected = collect_lexed_str_block(inner) + .map_err(|_| p.error("invalid string block".into()))?; + let mut result = String::new(); + for (i, line) in collected.lines.iter().enumerate() { + if i > 0 { + result.push('\n'); + } + result.push_str(line); + } + if !collected.truncate { + result.push('\n'); + } + result + } + _ => return Err(p.error(format!("expected string, got {}", p.current_desc()))), + }; + p.eat_any(); + Ok(s.into()) +} + +fn is_string_token(kind: SyntaxKind) -> bool { + matches!( + kind, + SyntaxKind::STRING_DOUBLE + | SyntaxKind::STRING_SINGLE + | SyntaxKind::STRING_DOUBLE_VERBATIM + | SyntaxKind::STRING_SINGLE_VERBATIM + | SyntaxKind::STRING_BLOCK + ) +} + +fn parse_number(p: &mut Parser<'_>) -> R { + let text = p.text(); + let n: f64 = text + .replace('_', "") + .parse() + .map_err(|_| p.error(format!("invalid number literal: {text}")))?; + if !n.is_finite() { + return Err(p.error("numbers are finite".into())); + } + p.eat_any(); + Ok(n) } fn literal(p: &mut Parser<'_>) -> Option { @@ -75,109 +263,816 @@ Some(t) } -fn spanned(p: &mut Parser<'_>, cb: impl FnOnce(&mut Parser<'_>) -> T) -> Spanned { - let start = p.span_start(); - let v = cb(p); - let end = p.span_end(); - - Spanned::new(v, Span(p.source.clone(), start, end)) -} - -fn assert_stmt(p: &mut Parser<'_>) -> AssertStmt { - p.eat(T![assert]); - let cond = spanned(p, expr); - dbg!(p.peek()); +fn assert_stmt(p: &mut Parser<'_>) -> R { + p.eat(T![assert])?; + let cond = spanned(p, expr)?; let msg = if p.try_eat(T![:]) { - Some(spanned(p, expr)) + Some(spanned(p, expr)?) } else { None }; - dbg!(AssertStmt(cond, msg)) + Ok(AssertStmt(cond, msg)) } -fn if_spec_data(p: &mut Parser<'_>) -> IfSpecData { - let v = spanned(p, |p| p.eat(T![if])); - let cond = expr(p); - IfSpecData { span: v.span, cond } +fn if_spec_data(p: &mut Parser<'_>) -> R { + let v = spanned(p, |p| p.eat(T![if]))?; + let cond = expr(p)?; + Ok(IfSpecData { span: v.span, cond }) } -fn if_else(p: &mut Parser<'_>) -> IfElse { - let cond = if_spec_data(p); - p.eat(T![then]); - let cond_then = expr(p); - let cond_else = if p.at(T![else]) { Some(expr(p)) } else { None }; - IfElse { +fn if_else(p: &mut Parser<'_>) -> R { + let cond = if_spec_data(p)?; + p.eat(T![then])?; + let cond_then = expr(p)?; + let cond_else = if p.try_eat(T![else]) { + Some(expr(p)?) + } else { + None + }; + Ok(IfElse { cond, cond_then, cond_else, - } + }) } -fn slice_desc(p: &mut Parser<'_>, start: Option>) -> SliceDesc { - // start - p.eat(T![:]); +fn slice_desc(p: &mut Parser<'_>, start: Option>) -> R { + p.eat(T![:])?; let end = if !p.at(T![:]) && !p.at(T![']']) { - Some(spanned(p, expr)) + Some(spanned(p, expr)?) } else { None }; - let step = if p.try_eat(T![:]) && !p.at(T![']']) { - Some(spanned(p, expr)) + let step = if p.try_eat(T![:]) { + if !p.at(T![']']) { + Some(spanned(p, expr)?) + } else { + None + } } else { None }; - SliceDesc { start, end, step } + Ok(SliceDesc { start, end, step }) +} + +fn destruct(p: &mut Parser<'_>) -> R { + Ok(Destruct::Full(p.expect_ident()?)) +} + +fn params(p: &mut Parser<'_>) -> R { + if p.at(T![')']) { + return Ok(ExprParams::new(Vec::new())); + } + let mut result = Vec::new(); + loop { + let d = destruct(p)?; + let default = if p.try_eat(T![=]) { + Some(Rc::new(expr(p)?)) + } else { + None + }; + result.push(ExprParam { + destruct: d, + default, + }); + if !p.try_eat(T![,]) { + break; + } + if p.at(T![')']) { + break; + } + } + Ok(ExprParams::new(result)) } -fn expr_simple(p: &mut Parser<'_>) -> Expr { - let mut e = if let Some(literal) = literal(p) { - Expr::Literal(literal) +fn args(p: &mut Parser<'_>) -> R { + if p.at(T![')']) { + return Ok(ArgsDesc::new(Vec::new(), Vec::new())); + } + let mut unnamed = Vec::new(); + let mut named = Vec::new(); + let mut named_started = false; + loop { + let is_named = p.at_ident() && { + let next_offset = p.offset + 1; + next_offset < p.lexemes.len() && p.lexemes[next_offset].kind == T![=] && { + let after_eq = next_offset + 1; + after_eq >= p.lexemes.len() || p.lexemes[after_eq].kind != T![=] + } + }; + if is_named { + let name: IStr = p.expect_ident()?; + p.eat(T![=])?; + let value = Rc::new(expr(p)?); + named.push((name, value)); + named_started = true; + } else { + if named_started { + return Err(p.error("positional argument after named argument".into())); + } + unnamed.push(Rc::new(expr(p)?)); + } + if !p.try_eat(T![,]) { + break; + } + if p.at(T![')']) { + break; + } + } + Ok(ArgsDesc::new(unnamed, named)) +} + +fn bind(p: &mut Parser<'_>) -> R { + let name = p.expect_ident()?; + if p.try_eat(T!['(']) { + let ps = params(p)?; + p.eat(T![')'])?; + p.eat(T![=])?; + let value = Rc::new(expr(p)?); + Ok(BindSpec::Function { + name, + params: ps, + value, + }) + } else { + p.eat(T![=])?; + let value = Rc::new(expr(p)?); + Ok(BindSpec::Field { + into: Destruct::Full(name), + value, + }) + } +} + +fn visibility(p: &mut Parser<'_>) -> R { + p.eat(T![:])?; + if p.try_eat(T![:]) { + if p.try_eat(T![:]) { + Ok(Visibility::Unhide) + } else { + Ok(Visibility::Hidden) + } + } else { + Ok(Visibility::Normal) + } +} + +fn field_name(p: &mut Parser<'_>) -> R { + if p.at_ident() { + Ok(FieldName::Fixed(p.expect_ident()?)) + } else if is_string_token(p.peek()) { + Ok(FieldName::Fixed(parse_string_content(p)?)) + } else if p.at(T!['[']) { + p.eat(T!['['])?; + let e = expr(p)?; + p.eat(T![']'])?; + Ok(FieldName::Dyn(e)) + } else { + Err(p.error(format!("expected field name, got {}", p.current_desc()))) + } +} + +fn field(p: &mut Parser<'_>) -> R { + let name = spanned(p, field_name)?; + + if p.at(T!['(']) { + p.eat(T!['('])?; + let ps = params(p)?; + p.eat(T![')'])?; + let vis = visibility(p)?; + let value = Rc::new(expr(p)?); + Ok(FieldMember { + name, + plus: false, + params: Some(ps), + visibility: vis, + value, + }) + } else { + let plus = p.try_eat(T![+]); + let vis = visibility(p)?; + let value = Rc::new(expr(p)?); + Ok(FieldMember { + name, + plus, + params: None, + visibility: vis, + value, + }) + } +} + +fn member(p: &mut Parser<'_>) -> R { + if p.at(T![local]) { + p.eat(T![local])?; + Ok(Member::BindStmt(bind(p)?)) } else if p.at(T![assert]) { - let assert = assert_stmt(p); - p.eat(T![;]); - let rest = expr(p); - Expr::AssertExpr(Rc::new(AssertExpr { assert, rest })) - } else if p.at(T![if]) { - Expr::IfElse(Box::new(if_else(p))) + Ok(Member::AssertStmt(assert_stmt(p)?)) + } else { + Ok(Member::Field(field(p)?)) + } +} + +fn for_spec(p: &mut Parser<'_>) -> R { + p.eat(T![for])?; + let d = destruct(p)?; + p.eat(T![in])?; + let over = expr(p)?; + Ok(ForSpecData { destruct: d, over }) +} + +fn compspecs(p: &mut Parser<'_>) -> R> { + let mut specs = Vec::new(); + specs.push(CompSpec::ForSpec(for_spec(p)?)); + loop { + if p.at(T![for]) { + specs.push(CompSpec::ForSpec(for_spec(p)?)); + } else if p.at(T![if]) { + let isd = if_spec_data(p)?; + specs.push(CompSpec::IfSpec(isd)); + } else { + break; + } + } + Ok(specs) +} + +fn objinside(p: &mut Parser<'_>) -> R { + if p.at(T!['}']) { + return Ok(ObjBody::MemberList(ObjMembers { + locals: Rc::new(Vec::new()), + asserts: Rc::new(Vec::new()), + fields: Vec::new(), + })); + } + + let mut members = Vec::new(); + loop { + members.push(member(p)?); + if !p.try_eat(T![,]) { + break; + } + if p.at(T!['}']) || p.at(T![for]) { + break; + } + } + + if p.at(T![for]) { + let specs = compspecs(p)?; + let mut locals = Vec::new(); + let mut field_member = None; + for m in members { + match m { + Member::Field(f) => { + if field_member.is_some() { + return Err(p.error( + "object comprehension can only contain one field".into(), + )); + } + field_member = Some(f); + } + Member::BindStmt(b) => locals.push(b), + Member::AssertStmt(_) => { + return Err(p.error( + "asserts are unsupported in object comprehension".into(), + )); + } + } + } + Ok(ObjBody::ObjComp(ObjComp { + locals: Rc::new(locals), + field: Rc::new( + field_member.ok_or_else(|| { + p.error("missing object comprehension field".into()) + })?, + ), + compspecs: specs, + })) } else { - panic!("unexpected token: {:?}", p.peek()); + let mut locals = Vec::new(); + let mut asserts = Vec::new(); + let mut fields = Vec::new(); + for m in members { + match m { + Member::Field(f) => fields.push(f), + Member::BindStmt(b) => locals.push(b), + Member::AssertStmt(a) => asserts.push(a), + } + } + Ok(ObjBody::MemberList(ObjMembers { + locals: Rc::new(locals), + asserts: Rc::new(asserts), + fields, + })) + } +} + +fn expr_basic(p: &mut Parser<'_>) -> R { + if let Some(lit) = literal(p) { + return Ok(Expr::Literal(lit)); + } + + match p.peek() { + SyntaxKind::STRING_DOUBLE + | SyntaxKind::STRING_SINGLE + | SyntaxKind::STRING_DOUBLE_VERBATIM + | SyntaxKind::STRING_SINGLE_VERBATIM + | SyntaxKind::STRING_BLOCK => Ok(Expr::Str(parse_string_content(p)?)), + + SyntaxKind::FLOAT => Ok(Expr::Num(parse_number(p)?)), + + T!['('] => { + p.eat(T!['('])?; + let e = expr(p)?; + p.eat(T![')'])?; + Ok(e) + } + + T!['['] => { + p.eat(T!['['])?; + if p.at(T![']']) { + p.eat(T![']'])?; + return Ok(Expr::Arr(Rc::new(Vec::new()))); + } + let first = expr(p)?; + if p.at(T![for]) { + let specs = compspecs(p)?; + p.eat(T![']'])?; + Ok(Expr::ArrComp(Rc::new(first), specs)) + } else if p.at(T![,]) && { + let next = p.offset + 1; + next < p.lexemes.len() && p.lexemes[next].kind == T![for] + } { + p.eat(T![,])?; + let specs = compspecs(p)?; + p.eat(T![']'])?; + Ok(Expr::ArrComp(Rc::new(first), specs)) + } else { + let mut elems = vec![first]; + while p.try_eat(T![,]) { + if p.at(T![']']) { + break; + } + elems.push(expr(p)?); + } + p.eat(T![']'])?; + Ok(Expr::Arr(Rc::new(elems))) + } + } + + T!['{'] => { + p.eat(T!['{'])?; + let body = objinside(p)?; + p.eat(T!['}'])?; + Ok(Expr::Obj(body)) + } + + T![local] => { + p.eat(T![local])?; + let mut binds = Vec::new(); + loop { + binds.push(bind(p)?); + if !p.try_eat(T![,]) { + break; + } + } + p.eat(T![;])?; + let body = expr(p)?; + Ok(Expr::LocalExpr(binds, Box::new(body))) + } + + T![if] => Ok(Expr::IfElse(Box::new(if_else(p)?))), + + T![function] => { + p.eat(T![function])?; + p.eat(T!['('])?; + let ps = params(p)?; + p.eat(T![')'])?; + let body = expr(p)?; + Ok(Expr::Function(ps, Rc::new(body))) + } + + T![assert] => { + let a = assert_stmt(p)?; + p.eat(T![;])?; + let rest = expr(p)?; + Ok(Expr::AssertExpr(Rc::new(AssertExpr { assert: a, rest }))) + } + + T![error] => { + let span = spanned(p, |p| p.eat(T![error]))?; + let e = expr(p)?; + Ok(Expr::ErrorStmt(span.span, Box::new(e))) + } + + T![importstr] => { + let kind = spanned(p, |p| { + p.eat(T![importstr])?; + Ok(ImportKind::Str) + })?; + let path = expr(p)?; + Ok(Expr::Import(kind, Box::new(path))) + } + + T![importbin] => { + let kind = spanned(p, |p| { + p.eat(T![importbin])?; + Ok(ImportKind::Bin) + })?; + let path = expr(p)?; + Ok(Expr::Import(kind, Box::new(path))) + } + + T![import] => { + let kind = spanned(p, |p| { + p.eat(T![import])?; + Ok(ImportKind::Normal) + })?; + let path = expr(p)?; + Ok(Expr::Import(kind, Box::new(path))) + } + + SyntaxKind::IDENT => { + let text = p.text(); + if is_reserved(text) { + return Err(p.error(format!("unexpected reserved word '{text}'"))); + } + let n = spanned(p, |p| { + let s: IStr = p.text().into(); + p.eat_any(); + Ok(s) + })?; + Ok(Expr::Var(n)) + } + + _ => Err(p.error(format!("unexpected {}", p.current_desc()))), + } +} + +/// Flush accumulated index parts into an Expr::Index wrapping `e`. +fn flush_index_parts(e: &mut Expr, parts: &mut Vec) { + if parts.is_empty() { + return; + } + let old = std::mem::replace(e, Expr::Literal(LiteralType::Null)); + *e = Expr::Index { + indexable: Box::new(old), + parts: std::mem::take(parts), }; +} - dbg!(&e); +fn expr_suffix(p: &mut Parser<'_>) -> R { + let mut e = expr_basic(p)?; + // Accumulate consecutive index parts (.field, [expr], ?.field, ?.[expr]) + // into a single Expr::Index. This is critical for null-coalesce semantics: + // a?.b.c needs all parts in one Index so the evaluator can skip .c when .b is null. + let mut parts: Vec = Vec::new(); loop { - if p.try_eat(T!['[']) { - if p.at(T![:]) { - let slice = slice_desc(p, None); - e = Expr::Slice(Box::new(Slice { value: e, slice })); - p.eat(T![']']); - continue; + #[cfg(feature = "exp-null-coaelse")] + if p.at(T![?]) { + p.eat_any(); + if p.try_eat(T![.]) { + if p.at(T!['[']) { + // ?.[expr] + p.eat(T!['['])?; + let idx = spanned(p, expr)?; + p.eat(T![']'])?; + parts.push(IndexPart { + span: idx.span, + value: idx.value, + null_coaelse: true, + }); + } else { + // ?.field + let id_spanned = spanned(p, |p| { + let name = p.expect_ident()?; + Ok(Expr::Str(name)) + })?; + parts.push(IndexPart { + span: id_spanned.span, + value: id_spanned.value, + null_coaelse: true, + }); + } + } else { + return Err(p.error("expected '.' after '?'".into())); } + continue; + } - let idx = spanned(p, expr); + if p.at(T![.]) { + p.eat(T![.])?; + let id_spanned = spanned(p, |p| { + let name = p.expect_ident()?; + Ok(Expr::Str(name)) + })?; + parts.push(IndexPart { + span: id_spanned.span, + value: id_spanned.value, + #[cfg(feature = "exp-null-coaelse")] + null_coaelse: false, + }); + } else if p.at(T!['[']) { + p.eat(T!['['])?; + if p.at(T![:]) { - let slice = slice_desc(p, Some(idx)); + // Slice: flush index parts first, then handle slice + flush_index_parts(&mut e, &mut parts); + let slice = slice_desc(p, None)?; + p.eat(T![']'])?; e = Expr::Slice(Box::new(Slice { value: e, slice })); } else { + let idx = spanned(p, expr)?; + if p.at(T![:]) { + // Slice with start: flush index parts first + flush_index_parts(&mut e, &mut parts); + let slice = slice_desc(p, Some(idx))?; + p.eat(T![']'])?; + e = Expr::Slice(Box::new(Slice { value: e, slice })); + } else { + // Bracket index: add to parts + p.eat(T![']'])?; + parts.push(IndexPart { + span: idx.span, + value: idx.value, + #[cfg(feature = "exp-null-coaelse")] + null_coaelse: false, + }); + } } - p.eat(T![']']); + } else if p.at(T!['(']) { + flush_index_parts(&mut e, &mut parts); + let args_spanned = spanned(p, |p| { + p.eat(T!['('])?; + let a = args(p)?; + p.eat(T![')'])?; + Ok(a) + })?; + let tailstrict = p.try_eat(T![tailstrict]); + e = Expr::Apply(Box::new(e), args_spanned, tailstrict); + } else if p.at(T!['{']) { + flush_index_parts(&mut e, &mut parts); + p.eat(T!['{'])?; + let body = objinside(p)?; + p.eat(T!['}'])?; + e = Expr::ObjExtend(Rc::new(e), body); } else { break; } } - dbg!(e) + flush_index_parts(&mut e, &mut parts); + Ok(e) } -fn expr(p: &mut Parser<'_>) -> Expr { - expr_simple(p) +fn prefix_binding_power(op: UnaryOpType) -> u8 { + match op { + UnaryOpType::Plus | UnaryOpType::Minus | UnaryOpType::Not | UnaryOpType::BitNot => 20, + } +} + +fn infix_binding_power(op: BinaryOpType) -> (u8, u8) { + match op { + BinaryOpType::Or => (2, 3), + #[cfg(feature = "exp-null-coaelse")] + BinaryOpType::NullCoaelse => (2, 3), + BinaryOpType::And => (4, 5), + BinaryOpType::BitOr => (6, 7), + BinaryOpType::BitXor => (8, 9), + BinaryOpType::BitAnd => (10, 11), + BinaryOpType::Eq | BinaryOpType::Neq => (12, 13), + BinaryOpType::Lt + | BinaryOpType::Gt + | BinaryOpType::Lte + | BinaryOpType::Gte + | BinaryOpType::In => (14, 15), + BinaryOpType::Lhs | BinaryOpType::Rhs => (16, 17), + BinaryOpType::Add | BinaryOpType::Sub => (18, 19), + BinaryOpType::Mul | BinaryOpType::Div | BinaryOpType::Mod => (20, 21), + } } -#[test] -fn basic_test() { - let mut parser = Parser::new(" assert true[false] : false ; true "); - let e = expr(&mut parser); - let l = &parser.lexemes; +fn unary_op(kind: SyntaxKind) -> Option { + match kind { + T![+] => Some(UnaryOpType::Plus), + T![-] => Some(UnaryOpType::Minus), + T![!] => Some(UnaryOpType::Not), + T![~] => Some(UnaryOpType::BitNot), + _ => None, + } +} - assert_snapshot!(format!("{l:#?}\n\n---\n\n{e:#?}")); +fn binary_op(p: &Parser<'_>) -> Option { + match p.peek() { + T![||] => Some(BinaryOpType::Or), + T![&&] => Some(BinaryOpType::And), + T![|] => Some(BinaryOpType::BitOr), + T![^] => Some(BinaryOpType::BitXor), + T![&] => Some(BinaryOpType::BitAnd), + T![==] => Some(BinaryOpType::Eq), + T![!=] => Some(BinaryOpType::Neq), + T![<] => Some(BinaryOpType::Lt), + T![>] => Some(BinaryOpType::Gt), + T![<=] => Some(BinaryOpType::Lte), + T![>=] => Some(BinaryOpType::Gte), + T![<<] => Some(BinaryOpType::Lhs), + T![>>] => Some(BinaryOpType::Rhs), + T![+] => Some(BinaryOpType::Add), + T![-] => Some(BinaryOpType::Sub), + T![*] => Some(BinaryOpType::Mul), + T![/] => Some(BinaryOpType::Div), + T![%] => Some(BinaryOpType::Mod), + T![in] => Some(BinaryOpType::In), + #[cfg(feature = "exp-null-coaelse")] + T![??] => Some(BinaryOpType::NullCoaelse), + _ => None, + } +} + +fn expr_bp(p: &mut Parser<'_>, min_bp: u8) -> R { + let mut lhs = if let Some(op) = unary_op(p.peek()) { + p.eat_any(); + let rbp = prefix_binding_power(op); + let rhs = expr_bp(p, rbp)?; + Expr::UnaryOp(op, Box::new(rhs)) + } else { + expr_suffix(p)? + }; + + loop { + if p.at_eof() { + break; + } + + let Some(op) = binary_op(p) else { + break; + }; + + let (lbp, rbp) = infix_binding_power(op); + if lbp < min_bp { + break; + } + + p.eat_any(); + let rhs = expr_bp(p, rbp)?; + lhs = Expr::BinaryOp(Box::new(BinaryOp { lhs, op, rhs })); + } + + Ok(lhs) +} + +fn expr(p: &mut Parser<'_>) -> R { + expr_bp(p, 0) +} + +pub fn parse(str: &str, settings: &ParserSettings) -> Result { + let mut p = Parser::new(str, settings.source.clone()); + for lexeme in &p.lexemes { + if let Some(desc) = lexeme.kind.error_description() { + return Err(ParseError { + message: desc.to_owned(), + location: ParseErrorLocation { + offset: lexeme.range.0 as usize, + }, + }); + } + } + let e = expr(&mut p)?; + if !p.at_eof() { + return Err(p.error(format!( + "expected end of file, got {}", + p.current_desc(), + ))); + } + Ok(e) +} + +pub fn string_to_expr(s: IStr, settings: &ParserSettings) -> Spanned { + let len = s.len(); + Spanned::new( + Expr::Str(s), + Span(settings.source.clone(), 0, len as u32), + ) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use insta::{assert_snapshot, glob}; + use jrsonnet_ir::{IStr, Source}; + + use super::*; + + fn parse_str(input: &str) -> Expr { + let source = Source::new_virtual("".into(), input.into()); + let settings = ParserSettings { source }; + parse(input, &settings).unwrap() + } + + #[test] + #[cfg(not(feature = "exp-null-coaelse"))] + fn basic_test() { + let v = parse_str("assert true[false] : false ; true"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn literals() { + let v = parse_str("[null, true, false, self, super, $]"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn basic_math() { + let v = parse_str("2+2*2"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn underscore_numbers() { + let v = parse_str("[1_000, 1_000.000_1, 1_0e1_0]"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn strings() { + let v = parse_str(r#"["hello", 'world', @"raw""str", @'raw''str']"#); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn object() { + let v = parse_str("{a: 1, b:: 2, c::: 3}"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn function_and_call() { + let v = parse_str("local f(x, y=1) = x + y; f(2, y=3)"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn if_then_else() { + let v = parse_str("if true then 1 else 2"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn imports() { + let v = parse_str(r#"[import "a", importstr "b", importbin "c"]"#); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn array_comp() { + let v = parse_str("[x for x in arr]"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + #[cfg(not(feature = "exp-null-coaelse"))] + fn index_and_suffix() { + let v = parse_str("std.test(2).field[0]"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn obj_extend() { + let v = parse_str("{} { x: 1 }"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn unary_ops() { + let v = parse_str("!a && !b"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn error_expr() { + let v = parse_str("error \"bad\""); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + fn slice() { + let v = parse_str("[a[1:], a[1::], a[:1:], a[::1]]"); + assert_snapshot!(format!("{v:#?}")); + } + + #[test] + #[cfg(not(feature = "exp-null-coaelse"))] + fn peg_snapshots() { + glob!("../../jrsonnet-peg-parser/src", "tests/*.jsonnet", |path| { + let input = fs::read_to_string(path).expect("read test file"); + let source = Source::new_virtual("".into(), IStr::empty()); + let settings = ParserSettings { source }; + let v = parse(&input, &settings).unwrap(); + let v = format!("{v:#?}"); + assert_snapshot!(v); + }); + } } --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__array_comp.snap @@ -0,0 +1,21 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +ArrComp( + Var( + "x" from virtual::1-2, + ), + [ + ForSpec( + ForSpecData { + destruct: Full( + "x", + ), + over: Var( + "arr" from virtual::12-15, + ), + }, + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_math.snap @@ -0,0 +1,23 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Add, + rhs: BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Mul, + rhs: Num( + 2.0, + ), + }, + ), + }, +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_test.snap @@ -0,0 +1,31 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +AssertExpr( + AssertExpr { + assert: AssertStmt( + Index { + indexable: Literal( + True, + ), + parts: [ + IndexPart { + span: virtual::12-17, + value: Literal( + False, + ), + }, + ], + } from virtual::7-18, + Some( + Literal( + False, + ) from virtual::21-26, + ), + ), + rest: Literal( + True, + ), + }, +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__error_expr.snap @@ -0,0 +1,10 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +ErrorStmt( + virtual::0-5, + Str( + "bad", + ), +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snap @@ -0,0 +1,80 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +LocalExpr( + [ + Function { + name: "f", + params: ExprParams { + exprs: [ + ExprParam { + destruct: Full( + "x", + ), + default: None, + }, + ExprParam { + destruct: Full( + "y", + ), + default: Some( + Num( + 1.0, + ), + ), + }, + ], + signature: FunctionSignature( + [ + ParamParse { + name: Named( + "x", + ), + default: None, + }, + ParamParse { + name: Named( + "y", + ), + default: Exists, + }, + ], + ), + binds_len: 2, + }, + value: BinaryOp( + BinaryOp { + lhs: Var( + "x" from virtual::18-19, + ), + op: Add, + rhs: Var( + "y" from virtual::22-23, + ), + }, + ), + }, + ], + Apply( + Var( + "f" from virtual::25-26, + ), + ArgsDesc { + unnamed: [ + Num( + 2.0, + ), + ], + named: [ + ( + "y", + Num( + 3.0, + ), + ), + ], + } from virtual::26-34, + false, + ), +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__if_then_else.snap @@ -0,0 +1,22 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +IfElse( + IfElse { + cond: IfSpecData { + span: virtual::0-2, + cond: Literal( + True, + ), + }, + cond_then: Num( + 1.0, + ), + cond_else: Some( + Num( + 2.0, + ), + ), + }, +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__imports.snap @@ -0,0 +1,26 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +Arr( + [ + Import( + Normal from virtual::1-7, + Str( + "a", + ), + ), + Import( + Str from virtual::13-22, + Str( + "b", + ), + ), + Import( + Bin from virtual::28-37, + Str( + "c", + ), + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__index_and_suffix.snap @@ -0,0 +1,44 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +Index { + indexable: Apply( + Index { + indexable: Var( + "std" from virtual::0-3, + ), + parts: [ + IndexPart { + span: virtual::4-8, + value: Str( + "test", + ), + }, + ], + }, + ArgsDesc { + unnamed: [ + Num( + 2.0, + ), + ], + named: [], + } from virtual::8-11, + false, + ), + parts: [ + IndexPart { + span: virtual::12-17, + value: Str( + "field", + ), + }, + IndexPart { + span: virtual::18-19, + value: Num( + 0.0, + ), + }, + ], +} --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__literals.snap @@ -0,0 +1,26 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +Arr( + [ + Literal( + Null, + ), + Literal( + True, + ), + Literal( + False, + ), + Literal( + This, + ), + Literal( + Super, + ), + Literal( + Dollar, + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__obj_extend.snap @@ -0,0 +1,34 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +ObjExtend( + Obj( + MemberList( + ObjMembers { + locals: [], + asserts: [], + fields: [], + }, + ), + ), + MemberList( + ObjMembers { + locals: [], + asserts: [], + fields: [ + FieldMember { + name: Fixed( + "x", + ) from virtual::5-6, + plus: false, + params: None, + visibility: Normal, + value: Num( + 1.0, + ), + }, + ], + }, + ), +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__object.snap @@ -0,0 +1,47 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +Obj( + MemberList( + ObjMembers { + locals: [], + asserts: [], + fields: [ + FieldMember { + name: Fixed( + "a", + ) from virtual::1-2, + plus: false, + params: None, + visibility: Normal, + value: Num( + 1.0, + ), + }, + FieldMember { + name: Fixed( + "b", + ) from virtual::7-8, + plus: false, + params: None, + visibility: Hidden, + value: Num( + 2.0, + ), + }, + FieldMember { + name: Fixed( + "c", + ) from virtual::14-15, + plus: false, + params: None, + visibility: Unhide, + value: Num( + 3.0, + ), + }, + ], + }, + ), +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@array_comp.jsonnet.snap @@ -0,0 +1,82 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/array_comp.jsonnet +--- +Arr( + [ + ArrComp( + Apply( + Index { + indexable: Var( + "std" from virtual::3-6, + ), + parts: [ + IndexPart { + span: virtual::7-15, + value: Str( + "deepJoin", + ), + }, + ], + }, + ArgsDesc { + unnamed: [ + Var( + "x" from virtual::16-17, + ), + ], + named: [], + } from virtual::15-18, + false, + ), + [ + ForSpec( + ForSpecData { + destruct: Full( + "x", + ), + over: Var( + "arr" from virtual::28-31, + ), + }, + ), + ], + ), + ArrComp( + Var( + "a" from virtual::35-36, + ), + [ + ForSpec( + ForSpecData { + destruct: Full( + "a", + ), + over: Var( + "b" from virtual::46-47, + ), + }, + ), + IfSpec( + IfSpecData { + span: virtual::48-50, + cond: Var( + "c" from virtual::51-52, + ), + }, + ), + ForSpec( + ForSpecData { + destruct: Full( + "e", + ), + over: Var( + "f" from virtual::62-63, + ), + }, + ), + ], + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@basic_math.jsonnet.snap @@ -0,0 +1,120 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/basic_math.jsonnet +--- +Arr( + [ + BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Add, + rhs: BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Mul, + rhs: Num( + 2.0, + ), + }, + ), + }, + ), + BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Add, + rhs: BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Mul, + rhs: Num( + 2.0, + ), + }, + ), + }, + ), + BinaryOp( + BinaryOp { + lhs: BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Add, + rhs: Num( + 2.0, + ), + }, + ), + op: Add, + rhs: BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Mul, + rhs: Num( + 2.0, + ), + }, + ), + }, + ), + BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Add, + rhs: BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Add, + rhs: BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Mul, + rhs: Num( + 2.0, + ), + }, + ), + }, + ), + }, + ), + BinaryOp( + BinaryOp { + lhs: Num( + 2.0, + ), + op: Add, + rhs: BinaryOp( + BinaryOp { + lhs: Num( + 3.0, + ), + op: Mul, + rhs: Num( + 4.0, + ), + }, + ), + }, + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@comment_eof.jsonnet.snap @@ -0,0 +1,26 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/comment_eof.jsonnet +--- +Obj( + MemberList( + ObjMembers { + locals: [], + asserts: [], + fields: [ + FieldMember { + name: Fixed( + "a", + ) from virtual::1-2, + plus: false, + params: None, + visibility: Normal, + value: Num( + 1.0, + ), + }, + ], + }, + ), +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@default_nondefault.jsonnet.snap @@ -0,0 +1,55 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/default_nondefault.jsonnet +--- +LocalExpr( + [ + Function { + name: "x", + params: ExprParams { + exprs: [ + ExprParam { + destruct: Full( + "foo", + ), + default: Some( + Str( + "foo", + ), + ), + }, + ExprParam { + destruct: Full( + "bar", + ), + default: None, + }, + ], + signature: FunctionSignature( + [ + ParamParse { + name: Named( + "foo", + ), + default: Exists, + }, + ParamParse { + name: Named( + "bar", + ), + default: None, + }, + ], + ), + binds_len: 2, + }, + value: Literal( + Null, + ), + }, + ], + Literal( + Null, + ), +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@empty_object.jsonnet.snap @@ -0,0 +1,14 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/empty_object.jsonnet +--- +Obj( + MemberList( + ObjMembers { + locals: [], + asserts: [], + fields: [], + }, + ), +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@imports.jsonnet.snap @@ -0,0 +1,27 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/imports.jsonnet +--- +Arr( + [ + Import( + Normal from virtual::2-8, + Str( + "hello", + ), + ), + Import( + Str from virtual::18-27, + Str( + "garnish.txt", + ), + ), + Import( + Bin from virtual::43-52, + Str( + "garnish.bin", + ), + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@infix.jsonnet.snap @@ -0,0 +1,52 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/infix.jsonnet +--- +Arr( + [ + BinaryOp( + BinaryOp { + lhs: UnaryOp( + Not, + Var( + "a" from virtual::3-4, + ), + ), + op: And, + rhs: UnaryOp( + Not, + Var( + "b" from virtual::9-10, + ), + ), + }, + ), + UnaryOp( + Not, + BinaryOp( + BinaryOp { + lhs: Var( + "a" from virtual::13-14, + ), + op: Div, + rhs: UnaryOp( + Not, + Var( + "b" from virtual::18-19, + ), + ), + }, + ), + ), + UnaryOp( + Not, + UnaryOp( + Not, + Var( + "a" from virtual::23-24, + ), + ), + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@multiline.jsonnet.snap @@ -0,0 +1,21 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/multiline.jsonnet +--- +Arr( + [ + Str( + "Hello world!\na\n", + ), + Str( + "Hello world!\na\n", + ), + Str( + "Hello world!\n\ta\n", + ), + Str( + "Hello world!\n a\n", + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@reserved.jsonnet.snap @@ -0,0 +1,32 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/reserved.jsonnet +--- +Arr( + [ + Literal( + Null, + ), + Var( + "nulla" from virtual::8-13, + ), + Apply( + Var( + "a" from virtual::15-16, + ), + ArgsDesc { + unnamed: [ + Var( + "b" from virtual::17-18, + ), + Var( + "null_fields" from virtual::20-31, + ), + ], + named: [], + } from virtual::16-32, + false, + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@slice.jsonnet.snap @@ -0,0 +1,97 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/slice.jsonnet +--- +Arr( + [ + Slice( + Slice { + value: Var( + "a" from virtual::2-3, + ), + slice: SliceDesc { + start: Some( + Num( + 1.0, + ) from virtual::4-5, + ), + end: None, + step: None, + }, + }, + ), + Slice( + Slice { + value: Var( + "a" from virtual::9-10, + ), + slice: SliceDesc { + start: Some( + Num( + 1.0, + ) from virtual::11-12, + ), + end: None, + step: None, + }, + }, + ), + Slice( + Slice { + value: Var( + "a" from virtual::17-18, + ), + slice: SliceDesc { + start: None, + end: Some( + Num( + 1.0, + ) from virtual::20-21, + ), + step: None, + }, + }, + ), + Slice( + Slice { + value: Var( + "a" from virtual::25-26, + ), + slice: SliceDesc { + start: None, + end: None, + step: Some( + Num( + 1.0, + ) from virtual::29-30, + ), + }, + }, + ), + Slice( + Slice { + value: Var( + "str" from virtual::33-36, + ), + slice: SliceDesc { + start: None, + end: Some( + BinaryOp( + BinaryOp { + lhs: Var( + "len" from virtual::38-41, + ), + op: Sub, + rhs: Num( + 1.0, + ), + }, + ) from virtual::38-45, + ), + step: None, + }, + }, + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@string_escaping.jsonnet.snap @@ -0,0 +1,24 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/string_escaping.jsonnet +--- +Arr( + [ + Str( + "Hello, \"world\"!", + ), + Str( + "Hello 'world'!", + ), + Str( + "\\\\", + ), + Str( + "Hello\nWorld", + ), + Str( + "Hello\\n\"World\"", + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@subexp.jsonnet.snap @@ -0,0 +1,58 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/subexp.jsonnet +--- +BinaryOp( + BinaryOp { + lhs: ObjExtend( + Obj( + MemberList( + ObjMembers { + locals: [], + asserts: [], + fields: [], + }, + ), + ), + MemberList( + ObjMembers { + locals: [ + Field { + into: Full( + "x", + ), + value: Num( + 1.0, + ), + }, + ], + asserts: [], + fields: [ + FieldMember { + name: Fixed( + "x", + ) from virtual::18-19, + plus: false, + params: None, + visibility: Normal, + value: Var( + "x" from virtual::21-22, + ), + }, + ], + }, + ), + ), + op: Add, + rhs: Obj( + MemberList( + ObjMembers { + locals: [], + asserts: [], + fields: [], + }, + ), + ), + }, +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@suffix.jsonnet.snap @@ -0,0 +1,73 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: v +input_file: crates/jrsonnet-peg-parser/src/tests/suffix.jsonnet +--- +Arr( + [ + Index { + indexable: Var( + "std" from virtual::2-5, + ), + parts: [ + IndexPart { + span: virtual::6-10, + value: Str( + "test", + ), + }, + ], + }, + Apply( + Var( + "std" from virtual::12-15, + ), + ArgsDesc { + unnamed: [ + Num( + 2.0, + ), + ], + named: [], + } from virtual::15-18, + false, + ), + Apply( + Index { + indexable: Var( + "std" from virtual::20-23, + ), + parts: [ + IndexPart { + span: virtual::24-28, + value: Str( + "test", + ), + }, + ], + }, + ArgsDesc { + unnamed: [ + Num( + 2.0, + ), + ], + named: [], + } from virtual::28-31, + false, + ), + Index { + indexable: Var( + "a" from virtual::33-34, + ), + parts: [ + IndexPart { + span: virtual::35-36, + value: Var( + "b" from virtual::35-36, + ), + }, + ], + }, + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__slice.snap @@ -0,0 +1,72 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +Arr( + [ + Slice( + Slice { + value: Var( + "a" from virtual::1-2, + ), + slice: SliceDesc { + start: Some( + Num( + 1.0, + ) from virtual::3-4, + ), + end: None, + step: None, + }, + }, + ), + Slice( + Slice { + value: Var( + "a" from virtual::8-9, + ), + slice: SliceDesc { + start: Some( + Num( + 1.0, + ) from virtual::10-11, + ), + end: None, + step: None, + }, + }, + ), + Slice( + Slice { + value: Var( + "a" from virtual::16-17, + ), + slice: SliceDesc { + start: None, + end: Some( + Num( + 1.0, + ) from virtual::19-20, + ), + step: None, + }, + }, + ), + Slice( + Slice { + value: Var( + "a" from virtual::24-25, + ), + slice: SliceDesc { + start: None, + end: None, + step: Some( + Num( + 1.0, + ) from virtual::28-29, + ), + }, + }, + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__strings.snap @@ -0,0 +1,20 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +Arr( + [ + Str( + "hello", + ), + Str( + "world", + ), + Str( + "raw\"str", + ), + Str( + "raw'str", + ), + ], +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__unary_ops.snap @@ -0,0 +1,21 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +BinaryOp( + BinaryOp { + lhs: UnaryOp( + Not, + Var( + "a" from virtual::1-2, + ), + ), + op: And, + rhs: UnaryOp( + Not, + Var( + "b" from virtual::7-8, + ), + ), + }, +) --- /dev/null +++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__underscore_numbers.snap @@ -0,0 +1,17 @@ +--- +source: crates/jrsonnet-ir-parser/src/lib.rs +expression: "format!(\"{v:#?}\")" +--- +Arr( + [ + Num( + 1000.0, + ), + Num( + 1000.0001, + ), + Num( + 100000000000.0, + ), + ], +) --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -4,6 +4,11 @@ edition = "2024" publish = false +[features] +default = ["ir-parser"] +ir-parser = ["jrsonnet-evaluator/ir-parser"] +exp-null-coaelse = ["jrsonnet-evaluator/exp-null-coaelse"] + [lints] workspace = true --- a/tests/cpp_test_suite_golden_override/error.parse.array_comma.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.array_comma.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", ".", "?", "[", "]", "{", , , got "3" +syntax error: expected R_BRACK, got "3" error.parse.array_comma.jsonnet:17:7 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.function_arg_positional_after_named.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.function_arg_positional_after_named.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", ".", "?", "[", "{", , , , got ")" - error.parse.function_arg_positional_after_named.jsonnet:19:11 \ No newline at end of file +syntax error: positional argument after named argument + error.parse.function_arg_positional_after_named.jsonnet:19:10 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.index_unterminated.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.index_unterminated.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", ":", "[", "{", , , , , ['"'], ['\''], got "EOF" - error.parse.index_unterminated.jsonnet:17:4 \ No newline at end of file +syntax error: unexpected token in expression: EOF + error.parse.index_unterminated.jsonnet:17:3 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.method_plus.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.method_plus.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of ":", "::", ":::", got "+" +syntax error: expected COLON, got "+" error.parse.method_plus.jsonnet:17:18 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.object_comma.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.object_comma.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", ".", "?", "[", "{", "}", , got "z" +syntax error: expected R_BRACE, got "z" error.parse.object_comma.jsonnet:17:11 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.object_comprehension_local_clash.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.object_comprehension_local_clash.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", ".", "?", "[", "{", "}", , , got ":" +syntax error: expected R_BRACE, got ":" error.parse.object_comprehension_local_clash.jsonnet:17:29 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.self_in_computed_field.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.self_in_computed_field.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "[", "}", , , ['"'], ['\''], got "s" +syntax error: expected field name, got SELF_KW error.parse.self_in_computed_field.jsonnet:17:15 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.static_error_bad_number.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.static_error_bad_number.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", "[", "{", , , , , ['"'], ['\''], got "." +syntax error: unexpected token in expression: DOT error.parse.static_error_bad_number.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected , got "o" - error.parse.string.invalid_escape.jsonnet:17:3 \ No newline at end of file +syntax error: invalid string escape + error.parse.string.invalid_escape.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_non_hex.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_non_hex.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected , got "t" - error.parse.string.invalid_escape_unicode_non_hex.jsonnet:17:7 \ No newline at end of file +syntax error: invalid string escape + error.parse.string.invalid_escape_unicode_non_hex.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected , got "\n" - error.parse.string.invalid_escape_unicode_short.jsonnet:17:7 \ No newline at end of file +syntax error: unexpected token: ERROR_STRING_DOUBLE_UNTERMINATED + error.parse.string.invalid_escape_unicode_short.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short2.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short2.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected , got "\"" - error.parse.string.invalid_escape_unicode_short2.jsonnet:17:7 \ No newline at end of file +syntax error: invalid string escape + error.parse.string.invalid_escape_unicode_short2.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short3.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short3.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected , got "\n" - error.parse.string.invalid_escape_unicode_short3.jsonnet:17:7 \ No newline at end of file +syntax error: unexpected token: ERROR_STRING_DOUBLE_UNTERMINATED + error.parse.string.invalid_escape_unicode_short3.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string.unfinished.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string.unfinished.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "\\\\", "\\u", "\\x", ['"'], ['\\'], [_], got "EOF" - error.parse.string.unfinished.jsonnet:17:3 \ No newline at end of file +syntax error: unexpected token: ERROR_STRING_DOUBLE_UNTERMINATED + error.parse.string.unfinished.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string.unfinished2.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string.unfinished2.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "\\\\", "\\u", "\\x", ['\''], ['\\'], [_], got "EOF" - error.parse.string.unfinished2.jsonnet:17:3 \ No newline at end of file +syntax error: unexpected token: ERROR_STRING_SINGLE_UNTERMINATED + error.parse.string.unfinished2.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.string_multi_no_newline.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.string_multi_no_newline.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", "[", "{", , , , , ['"'], ['\''], got "|" +syntax error: unexpected token: ERROR_STRING_BLOCK_MISSING_NEW_LINE error.parse.string_multi_no_newline.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.text_block_bad_whitespace.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.text_block_bad_whitespace.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", "[", "{", , , , , ['"'], ['\''], got "|" +syntax error: unexpected token: ERROR_STRING_BLOCK_MISSING_TERMINATION error.parse.text_block_bad_whitespace.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.text_block_eof.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.text_block_eof.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", "[", "{", , , , , ['"'], ['\''], got "|" +syntax error: unexpected token: ERROR_STRING_BLOCK_UNEXPECTED_END error.parse.text_block_eof.jsonnet:17:1 \ No newline at end of file --- a/tests/cpp_test_suite_golden_override/error.parse.text_block_not_terminated.jsonnet.golden +++ b/tests/cpp_test_suite_golden_override/error.parse.text_block_not_terminated.jsonnet.golden @@ -1,2 +1,2 @@ -syntax error: expected one of "(", "[", "{", , , , , ['"'], ['\''], got "|" +syntax error: unexpected token: ERROR_STRING_BLOCK_UNEXPECTED_END error.parse.text_block_not_terminated.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.import_syntax-error.jsonnet.golden @@ -0,0 +1,3 @@ +syntax error: unterminated double-quoted string + syntax_error.jsonnet:1:1 + error.import_syntax-error.jsonnet:1:1-8: import "lib/syntax_error.jsonnet" \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.overflow.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: numbers are finite + error.overflow.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.overflow3.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: numbers are finite + error.overflow3.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.array_comma.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected ']', got number "3" + error.parse.array_comma.jsonnet:17:7 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.index_unterminated.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unexpected end of file + error.parse.index_unterminated.jsonnet:17:3 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.method_plus.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected ':', got '+' + error.parse.method_plus.jsonnet:17:18 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.object_comma.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected '}', got identifier "z" + error.parse.object_comma.jsonnet:17:11 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.object_comprehension_local_clash.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected '}', got ':' + error.parse.object_comprehension_local_clash.jsonnet:17:29 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.self_in_computed_field.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected field name, got 'self' + error.parse.self_in_computed_field.jsonnet:17:15 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.static_error_bad_number.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unexpected '.' + error.parse.static_error_bad_number.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.string.invalid_escape_unicode_short.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unterminated double-quoted string + error.parse.string.invalid_escape_unicode_short.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.string.invalid_escape_unicode_short3.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unterminated double-quoted string + error.parse.string.invalid_escape_unicode_short3.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.string.unfinished.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unterminated double-quoted string + error.parse.string.unfinished.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.string.unfinished2.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unterminated single-quoted string + error.parse.string.unfinished2.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.string_multi_no_newline.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: text block requires new line after ||| + error.parse.string_multi_no_newline.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.text_block_bad_whitespace.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unterminated text block + error.parse.text_block_bad_whitespace.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.text_block_eof.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unexpected end of text block + error.parse.text_block_eof.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/cpp_test_suite_golden_override_ir_parser/error.parse.text_block_not_terminated.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unexpected end of text block + error.parse.text_block_not_terminated.jsonnet:17:1 \ No newline at end of file --- /dev/null +++ b/tests/go_testdata_golden_override_ir_parser/error_hexnumber.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected end of file, got identifier "x42" + error_hexnumber.jsonnet:1:2 \ No newline at end of file --- /dev/null +++ b/tests/go_testdata_golden_override_ir_parser/import_syntax_error.jsonnet.golden @@ -0,0 +1,3 @@ +syntax error: unexpected end of file + syntax_error.jsonnet:1:4 + import_syntax_error.jsonnet:1:1-8: import "syntax_error.jsonnet" \ No newline at end of file --- /dev/null +++ b/tests/go_testdata_golden_override_ir_parser/object_comp_assert.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: asserts are unsupported in object comprehension + object_comp_assert.jsonnet:1:46 \ No newline at end of file --- /dev/null +++ b/tests/go_testdata_golden_override_ir_parser/object_comp_illegal.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: missing object comprehension field + object_comp_illegal.jsonnet:1:34 \ No newline at end of file --- /dev/null +++ b/tests/go_testdata_golden_override_ir_parser/static_error_eof.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected ';', got end of file + static_error_eof.jsonnet:1:12 \ No newline at end of file --- /dev/null +++ b/tests/go_testdata_golden_override_ir_parser/syntax_error.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: unexpected end of file + syntax_error.jsonnet:1:4 \ No newline at end of file --- /dev/null +++ b/tests/go_testdata_golden_override_ir_parser/unfinished_args.jsonnet.golden @@ -0,0 +1,2 @@ +syntax error: expected ')', got end of file + unfinished_args.jsonnet:1:17 \ No newline at end of file --- /dev/null +++ b/tests/golden/null_coalesce_chain.jsonnet @@ -0,0 +1,10 @@ +// Regression test: chained index a.b.c.d should produce a single +// Index { a, [b, c, d] } not nested Index nodes. +// This matters for exp-null-coaelse where a?.b.c should skip .c if .b is null. + +local obj = { a: { b: { c: 42 } } }; + +[ + obj.a.b.c, + {a: {b: 1}}.a.b, +] --- /dev/null +++ b/tests/golden_null_coalesce/null_coalesce_access.jsonnet @@ -0,0 +1,18 @@ +// Test null-coalesce chained access: a?.b.c should return null when b is missing, +// not fail with "field c not found on null". + +local obj = { a: { b: { c: 42 } } }; + +[ + // null-coalesce on missing field should return null, not error + obj?.missing.b.c, + + // null-coalesce on present field continues + obj?.a.b.c, + + // null-coalesce with bracket index + obj?.["missing"].b.c, + + // chained null-coalesce + obj?.a?.missing.c, +] --- a/tests/tests/cpp_test_suite.rs +++ b/tests/tests/cpp_test_suite.rs @@ -241,9 +241,26 @@ golden = Some(golden_path); } + // ir-parser has its own override layer + #[cfg(feature = "ir-parser")] + let ir_parser_override_path = { + let p = root_tests + .join(format!("{root_dir}_golden_override_ir_parser")) + .join(golden_path.file_name().expect("file has basename")); + if let Some(golden_path) = read_file(&p)? { + golden = Some(golden_path); + } + p + }; + // Otherwise assume test should just not fail and return true. let golden = golden.unwrap_or_else(|| "true".to_owned()); + #[cfg(feature = "ir-parser")] + let update_golden_path = &ir_parser_override_path; + #[cfg(not(feature = "ir-parser"))] + let update_golden_path = &golden_override; + match (serde_json::from_str(&result), serde_json::from_str(&golden)) { (Err(_), Ok(_)) => panic!( "unexpected error for golden {}:\n\n{result}\n\n\n{golden}\n", @@ -258,7 +275,7 @@ let diff = JsonDiff::diff_string(&golden, &result_v, false); if let Some(diff) = diff { if env::var_os("UPDATE_GOLDEN").is_some() { - fs::write(golden_override, result)?; + fs::write(update_golden_path, result)?; } else { panic!( "Result \n{result_v:#}\n\ @@ -273,7 +290,7 @@ (Err(_), Err(_)) => { if result != golden.trim_end() { if env::var_os("UPDATE_GOLDEN").is_some() { - fs::write(golden_override, result)?; + fs::write(update_golden_path, result)?; } else { panic!( "golden didn't match for {}:\n\n{result}\n\n\n{golden}\n", --- a/tests/tests/golden.rs +++ b/tests/tests/golden.rs @@ -45,3 +45,13 @@ assert_snapshot!(result); }); } + +#[test] +#[cfg(feature = "exp-null-coaelse")] +fn golden_null_coalesce() { + glob!("../", "golden_null_coalesce/*.jsonnet", |path| { + let result = run(path); + + assert_snapshot!(result); + }); +} --- /dev/null +++ b/tests/tests/snapshots/golden__golden@null_coalesce_chain.jsonnet.snap @@ -0,0 +1,9 @@ +--- +source: tests/tests/golden.rs +expression: result +input_file: tests/golden/null_coalesce_chain.jsonnet +--- +[ + 42, + 1 +] --- /dev/null +++ b/tests/tests/snapshots/golden__golden_null_coalesce.snap @@ -0,0 +1,11 @@ +--- +source: tests/tests/golden.rs +expression: result +input_file: tests/golden_null_coalesce/null_coalesce_access.jsonnet +--- +[ + null, + 42, + null, + null +]