git.delta.rocks / jrsonnet / refs/commits / b89fdd32bd3a

difftreelog

refactor split literals and trivials

nyvtyzzzYaroslav Bolyukin2026-05-08parent: #d543e94.patch.diff
in: master

39 files changed

modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
after · crates/jrsonnet-ir-parser/src/lib.rs
1use jrsonnet_gcmodule::Acyclic;2use jrsonnet_ir::{3	ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BinaryOpType, BindSpec, CompSpec, Destruct, Expr,4	ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IdentityKind, IfElse,5	IfSpecData, ImportKind, IndexPart, Member, NumValue, ObjBody, ObjComp, ObjMembers, Slice,6	SliceDesc, Source, Span, Spanned, TrivialVal, UnaryOpType, Visibility, unescape,7};8use jrsonnet_lexer::{Lexeme, Lexer, Span as LexSpan, SyntaxKind, T, collect_lexed_str_block};910pub struct ParserSettings {11	pub source: Source,12}1314#[derive(Debug, Clone)]15pub struct ParseError {16	pub message: String,17	pub location: Span,18}1920impl std::fmt::Display for ParseError {21	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {22		write!(f, "{}", self.message)23	}24}2526type Result<T> = std::result::Result<T, ParseError>;2728struct Parser<'a> {29	lexemes: Vec<Lexeme<'a>>,30	offset: usize,31	source: Source,32}3334impl<'a> Parser<'a> {35	fn new(code: &'a str, source: Source) -> Self {36		Self {37			lexemes: Lexer::new(code)38				.filter(|l| {39					!matches!(40						l.kind,41						SyntaxKind::WHITESPACE42							| SyntaxKind::SINGLE_LINE_SLASH_COMMENT43							| SyntaxKind::SINGLE_LINE_HASH_COMMENT44							| SyntaxKind::MULTI_LINE_COMMENT45					)46				})47				.collect(),48			offset: 0,49			source,50		}51	}5253	fn peek(&self) -> SyntaxKind {54		if self.at_eof() {55			SyntaxKind::EOF56		} else {57			self.lexemes[self.offset].kind58		}59	}6061	fn text(&self) -> &'a str {62		self.lexemes[self.offset].text63	}6465	fn at(&self, kind: SyntaxKind) -> bool {66		!self.at_eof() && self.peek() == kind67	}6869	#[allow(dead_code)]70	fn nth(&self, n: usize) -> SyntaxKind {71		self.lexemes72			.get(self.offset + n)73			.map_or(SyntaxKind::EOF, |l| l.kind)74	}7576	fn eat_any(&mut self) {77		self.offset += 1;78	}7980	fn eat_any_spanned(&mut self) -> Span {81		let start = self.span_start();82		self.eat_any();83		let end = self.span_end();84		Span(self.source.clone(), start, end)85	}8687	fn at_eof(&self) -> bool {88		self.offset >= self.lexemes.len()89	}9091	fn try_eat(&mut self, t: SyntaxKind) -> bool {92		if self.at(t) {93			self.eat_any();94			return true;95		}96		false97	}9899	fn current_desc(&self) -> String {100		if self.at_eof() {101			return "end of file".to_owned();102		}103		let kind = self.peek();104		let text = self.text();105		let name = kind.display_name();106		if matches!(kind, SyntaxKind::IDENT | SyntaxKind::FLOAT) {107			format!("{name} \"{text}\"")108		} else {109			name.to_owned()110		}111	}112113	fn eat(&mut self, t: SyntaxKind) -> Result<()> {114		if !self.at(t) {115			return Err(self.error(format!(116				"expected {}, got {}",117				t.display_name(),118				self.current_desc(),119			)));120		}121		self.eat_any();122		Ok(())123	}124	fn eat_spanned(&mut self, t: SyntaxKind) -> Result<Span> {125		let start = self.span_start();126		self.eat(t)?;127		let end = self.span_end();128		Ok(Span(self.source.clone(), start, end))129	}130131	fn span_start(&self) -> u32 {132		if self.at_eof() {133			if let Some(last) = self.lexemes.last() {134				return last.range.1;135			}136			return 0;137		}138		self.lexemes[self.offset].range.0139	}140141	fn span_end(&self) -> u32 {142		self.lexemes[self.offset - 1].range.1143	}144145	fn error(&self, message: String) -> ParseError {146		if self.offset == self.lexemes.len() {147			let pos = self.lexemes.last().map_or(0, |v| v.range.1);148			return ParseError {149				location: Span(self.source.clone(), pos, pos),150				message,151			};152		}153		let LexSpan(start, end) = self.lexemes[self.offset].range;154		ParseError {155			location: Span(self.source.clone(), start, end),156			message,157		}158	}159}160161fn spanned<T: Acyclic>(162	p: &mut Parser<'_>,163	cb: impl FnOnce(&mut Parser<'_>) -> Result<T>,164) -> Result<Spanned<T>> {165	let start = p.span_start();166	let v = cb(p)?;167	let end = p.span_end();168	Ok(Spanned::new(v, Span(p.source.clone(), start, end)))169}170171fn parse_string_content(p: &mut Parser<'_>) -> Result<IStr> {172	let kind = p.peek();173	let text = p.text();174	let s = match kind {175		SyntaxKind::STRING_DOUBLE => {176			let inner = &text[1..text.len() - 1];177			unescape::unescape(inner).ok_or_else(|| p.error("invalid string escape".into()))?178		}179		SyntaxKind::STRING_SINGLE => {180			let inner = &text[1..text.len() - 1];181			unescape::unescape(inner).ok_or_else(|| p.error("invalid string escape".into()))?182		}183		SyntaxKind::STRING_DOUBLE_VERBATIM => {184			let inner = &text[2..text.len() - 1];185			inner.replace("\"\"", "\"")186		}187		SyntaxKind::STRING_SINGLE_VERBATIM => {188			let inner = &text[2..text.len() - 1];189			inner.replace("''", "'")190		}191		SyntaxKind::STRING_BLOCK => {192			let inner = &text[3..];193			let collected = collect_lexed_str_block(inner)194				.map_err(|_| p.error("invalid string block".into()))?;195			let mut result = String::new();196			for (i, line) in collected.lines.iter().enumerate() {197				if i > 0 {198					result.push('\n');199				}200				result.push_str(line);201			}202			if !collected.truncate {203				result.push('\n');204			}205			result206		}207		_ => return Err(p.error(format!("expected string, got {}", p.current_desc()))),208	};209	p.eat_any();210	Ok(s.into())211}212213fn is_string_token(kind: SyntaxKind) -> bool {214	matches!(215		kind,216		SyntaxKind::STRING_DOUBLE217			| SyntaxKind::STRING_SINGLE218			| SyntaxKind::STRING_DOUBLE_VERBATIM219			| SyntaxKind::STRING_SINGLE_VERBATIM220			| SyntaxKind::STRING_BLOCK221	)222}223224fn parse_number(p: &mut Parser<'_>) -> Result<NumValue> {225	let text = p.text();226	let n: f64 = text227		.replace('_', "")228		.parse()229		.map_err(|_| p.error(format!("invalid number literal: {text}")))?;230231	let v = match NumValue::try_from(n) {232		Ok(v) => v,233		Err(e) => return Err(p.error(format!("invalid number value: {e}"))),234	};235236	p.eat_any();237238	Ok(v)239}240241fn ident(p: &mut Parser<'_>) -> Result<IStr> {242	if !p.at(SyntaxKind::IDENT) {243		return Err(p.error(format!("expected identifier, got {}", p.current_desc())));244	}245	let text = p.text();246	p.eat_any();247	Ok(IStr::from(text))248}249250fn assert_stmt(p: &mut Parser<'_>) -> Result<AssertStmt> {251	p.eat(T![assert])?;252	let assertion = spanned(p, expr)?;253	let message = if p.try_eat(T![:]) {254		Some(expr(p)?)255	} else {256		None257	};258	Ok(AssertStmt { assertion, message })259}260261fn if_spec_data(p: &mut Parser<'_>) -> Result<IfSpecData> {262	let v = spanned(p, |p| p.eat(T![if]))?;263	let cond = expr(p)?;264	Ok(IfSpecData { span: v.span, cond })265}266267fn if_else(p: &mut Parser<'_>) -> Result<IfElse> {268	let cond = if_spec_data(p)?;269	p.eat(T![then])?;270	let cond_then = expr(p)?;271	let cond_else = if p.try_eat(T![else]) {272		Some(expr(p)?)273	} else {274		None275	};276	Ok(IfElse {277		cond,278		cond_then,279		cond_else,280	})281}282283fn slice_desc(p: &mut Parser<'_>, start: Option<Spanned<Expr>>) -> Result<SliceDesc> {284	p.eat(T![:])?;285	let end = if !p.at(T![:]) && !p.at(T![']']) {286		Some(spanned(p, expr)?)287	} else {288		None289	};290	let step = if p.try_eat(T![:]) {291		if p.at(T![']']) {292			None293		} else {294			Some(spanned(p, expr)?)295		}296	} else {297		None298	};299	Ok(SliceDesc { start, end, step })300}301302fn destruct(p: &mut Parser<'_>) -> Result<Destruct> {303	if p.at(SyntaxKind::IDENT) {304		return Ok(Destruct::Full(spanned(p, ident)?));305	}306	#[cfg(not(feature = "exp-destruct"))]307	return Err(p.error(format!("expected identifier, got {}", p.current_desc())));308	#[cfg(feature = "exp-destruct")]309	{310		if p.try_eat(T![?]) {311			return Ok(Destruct::Skip);312		}313		if p.at(T!['[']) {314			return destruct_array(p);315		}316		if p.at(T!['{']) {317			return destruct_object(p);318		}319		Err(p.error(format!(320			"expected destructure pattern, got {}",321			p.current_desc()322		)))323	}324}325326#[cfg(feature = "exp-destruct")]327fn destruct_rest(p: &mut Parser<'_>) -> Result<jrsonnet_ir::DestructRest> {328	p.eat(T![...])?;329	if p.at(SyntaxKind::IDENT) {330		Ok(jrsonnet_ir::DestructRest::Keep(ident(p)?))331	} else {332		Ok(jrsonnet_ir::DestructRest::Drop)333	}334}335336#[cfg(feature = "exp-destruct")]337fn destruct_array(p: &mut Parser<'_>) -> Result<Destruct> {338	p.eat(T!['['])?;339	let mut start = Vec::new();340	let mut rest = None;341	let mut end = Vec::new();342	if !p.at(T![']']) {343		loop {344			if p.at(T![...]) {345				rest = Some(destruct_rest(p)?);346				if p.try_eat(T![,]) {347					if !p.at(T![']']) {348						loop {349							end.push(destruct(p)?);350							if !p.try_eat(T![,]) {351								break;352							}353							if p.at(T![']']) {354								break;355							}356						}357					}358				}359				break;360			}361			start.push(destruct(p)?);362			if !p.try_eat(T![,]) {363				break;364			}365			if p.at(T![']']) {366				break;367			}368		}369	}370	p.eat(T![']'])?;371	Ok(Destruct::Array { start, rest, end })372}373374#[cfg(feature = "exp-destruct")]375fn destruct_object(p: &mut Parser<'_>) -> Result<Destruct> {376	p.eat(T!['{'])?;377	let mut fields = Vec::new();378	let mut rest = None;379	if !p.at(T!['}']) {380		loop {381			if p.at(T![...]) {382				rest = Some(destruct_rest(p)?);383				p.try_eat(T![,]);384				break;385			}386			let name = ident(p)?;387			let into = if p.try_eat(T![:]) {388				Some(destruct(p)?)389			} else {390				None391			};392			let default = if p.try_eat(T![=]) {393				Some(spanned(p, expr)?)394			} else {395				None396			};397			fields.push((name, into, default));398			if !p.try_eat(T![,]) {399				break;400			}401			if p.at(T!['}']) {402				break;403			}404		}405	}406	p.eat(T!['}'])?;407	Ok(Destruct::Object { fields, rest })408}409410fn params(p: &mut Parser<'_>) -> Result<ExprParams> {411	if p.at(T![')']) {412		return Ok(ExprParams::new(Vec::new()));413	}414	let mut result = Vec::new();415	loop {416		let d = destruct(p)?;417		let default = if p.try_eat(T![=]) {418			Some(expr(p)?)419		} else {420			None421		};422		result.push(ExprParam {423			destruct: d,424			default,425		});426		if !p.try_eat(T![,]) {427			break;428		}429		if p.at(T![')']) {430			break;431		}432	}433	Ok(ExprParams::new(result))434}435436fn args(p: &mut Parser<'_>) -> Result<ArgsDesc> {437	if p.at(T![')']) {438		return Ok(ArgsDesc::new(Vec::new(), Vec::new(), Vec::new()));439	}440	let mut unnamed = Vec::new();441	let mut names = Vec::new();442	let mut values = Vec::new();443	let mut named_started = false;444	loop {445		let is_named = p.at(SyntaxKind::IDENT) && {446			let next_offset = p.offset + 1;447			next_offset < p.lexemes.len() && p.lexemes[next_offset].kind == T![=]448		};449		if is_named {450			let name: IStr = ident(p)?;451			p.eat(T![=])?;452453			names.push(name);454			values.push(expr(p)?);455			named_started = true;456		} else {457			if named_started {458				return Err(p.error("positional argument after named argument".into()));459			}460			unnamed.push(expr(p)?);461		}462		if !p.try_eat(T![,]) {463			break;464		}465		if p.at(T![')']) {466			break;467		}468	}469	Ok(ArgsDesc::new(unnamed, names, values))470}471472fn bind(p: &mut Parser<'_>) -> Result<BindSpec> {473	#[cfg(feature = "exp-destruct")]474	{475		if !p.at(SyntaxKind::IDENT) {476			let d = destruct(p)?;477			p.eat(T![=])?;478			return Ok(BindSpec::Field {479				into: d,480				value: expr(p)?,481			});482		}483	}484	let name = spanned(p, ident)?;485	if p.try_eat(T!['(']) {486		let ps = params(p)?;487		p.eat(T![')'])?;488		p.eat(T![=])?;489		Ok(BindSpec::Function {490			name,491			params: ps,492			value: expr(p)?,493		})494	} else {495		p.eat(T![=])?;496		Ok(BindSpec::Field {497			into: Destruct::Full(name),498			value: expr(p)?,499		})500	}501}502503fn visibility(p: &mut Parser<'_>) -> Result<Visibility> {504	p.eat(T![:])?;505	if p.try_eat(T![:]) {506		if p.try_eat(T![:]) {507			Ok(Visibility::Unhide)508		} else {509			Ok(Visibility::Hidden)510		}511	} else {512		Ok(Visibility::Normal)513	}514}515516fn field_name(p: &mut Parser<'_>) -> Result<FieldName> {517	if p.at(SyntaxKind::IDENT) {518		Ok(FieldName::Fixed(ident(p)?))519	} else if is_string_token(p.peek()) {520		Ok(FieldName::Fixed(parse_string_content(p)?))521	} else if p.at(T!['[']) {522		p.eat(T!['['])?;523		let e = expr(p)?;524		p.eat(T![']'])?;525		Ok(FieldName::Dyn(e))526	} else {527		Err(p.error(format!("expected field name, got {}", p.current_desc())))528	}529}530531fn field(p: &mut Parser<'_>) -> Result<FieldMember> {532	let name = spanned(p, field_name)?;533534	if p.at(T!['(']) {535		p.eat(T!['('])?;536		let ps = params(p)?;537		p.eat(T![')'])?;538		let vis = visibility(p)?;539		Ok(FieldMember {540			name,541			plus: false,542			params: Some(ps),543			visibility: vis,544			value: expr(p)?,545		})546	} else {547		let plus = p.try_eat(T![+]);548		let vis = visibility(p)?;549		Ok(FieldMember {550			name,551			plus,552			params: None,553			visibility: vis,554			value: expr(p)?,555		})556	}557}558559fn member(p: &mut Parser<'_>) -> Result<Member> {560	if p.at(T![local]) {561		p.eat(T![local])?;562		Ok(Member::BindStmt(bind(p)?))563	} else if p.at(T![assert]) {564		Ok(Member::AssertStmt(assert_stmt(p)?))565	} else {566		Ok(Member::Field(field(p)?))567	}568}569570fn for_spec(p: &mut Parser<'_>) -> Result<CompSpec> {571	p.eat(T![for])?;572	#[cfg(feature = "exp-object-iteration")]573	if p.at(T!['[']) && p.nth(1) == SyntaxKind::IDENT && p.nth(2) == T![']'] && p.nth(3) == T![:] {574		p.eat(T!['['])?;575		let key = ident(p)?;576		p.eat(T![']'])?;577		let visibility = visibility(p)?;578		let value = destruct(p)?;579		p.eat(T![in])?;580		let over = expr(p)?;581		return Ok(CompSpec::ForObjSpec(jrsonnet_ir::ForObjSpecData {582			key,583			visibility,584			value,585			over,586		}));587	}588	let d = destruct(p)?;589	p.eat(T![in])?;590	let over = expr(p)?;591	Ok(CompSpec::ForSpec(ForSpecData { destruct: d, over }))592}593594fn compspecs(p: &mut Parser<'_>) -> Result<Vec<CompSpec>> {595	let mut specs = Vec::new();596	specs.push(for_spec(p)?);597	loop {598		if p.at(T![for]) {599			specs.push(for_spec(p)?);600		} else if p.at(T![if]) {601			let isd = if_spec_data(p)?;602			specs.push(CompSpec::IfSpec(isd));603		} else {604			break;605		}606	}607	Ok(specs)608}609610fn objinside(p: &mut Parser<'_>) -> Result<ObjBody> {611	if p.at(T!['}']) {612		return Ok(ObjBody::MemberList(ObjMembers {613			locals: Vec::new(),614			asserts: Vec::new(),615			fields: Vec::new(),616		}));617	}618619	let mut members = Vec::new();620	loop {621		members.push(member(p)?);622		if !p.try_eat(T![,]) {623			break;624		}625		if p.at(T!['}']) || p.at(T![for]) {626			break;627		}628	}629630	if p.at(T![for]) {631		let specs = compspecs(p)?;632		let mut locals = Vec::new();633		let mut field_member = None;634		for m in members {635			match m {636				Member::Field(f) => {637					if field_member.is_some() {638						return Err(639							p.error("object comprehension can only contain one field".into())640						);641					}642					field_member = Some(f);643				}644				Member::BindStmt(b) => locals.push(b),645				Member::AssertStmt(_) => {646					return Err(p.error("asserts are unsupported in object comprehension".into()));647				}648			}649		}650		Ok(ObjBody::ObjComp(ObjComp {651			locals,652			field: Box::new(653				field_member.ok_or_else(|| p.error("missing object comprehension field".into()))?,654			),655			compspecs: specs,656		}))657	} else {658		let mut locals = Vec::new();659		let mut asserts = Vec::new();660		let mut fields = Vec::new();661		for m in members {662			match m {663				Member::Field(f) => fields.push(f),664				Member::BindStmt(b) => locals.push(b),665				Member::AssertStmt(a) => asserts.push(a),666			}667		}668		Ok(ObjBody::MemberList(ObjMembers {669			locals,670			asserts,671			fields,672		}))673	}674}675676#[allow(clippy::too_many_lines)]677fn expr_basic(p: &mut Parser<'_>) -> Result<Expr> {678	match p.peek() {679		T![self] => Ok(Expr::Identity(p.eat_any_spanned(), IdentityKind::This)),680		T![super] => Ok(Expr::Identity(p.eat_any_spanned(), IdentityKind::Super)),681		T!['$'] => Ok(Expr::Identity(p.eat_any_spanned(), IdentityKind::Dollar)),682		T![null] => {683			p.eat_any();684			Ok(Expr::Trivial(TrivialVal::Null))685		}686		T![true] => {687			p.eat_any();688			Ok(Expr::Trivial(TrivialVal::Bool(true)))689		}690		T![false] => {691			p.eat_any();692			Ok(Expr::Trivial(TrivialVal::Bool(false)))693		}694695		SyntaxKind::STRING_DOUBLE696		| SyntaxKind::STRING_SINGLE697		| SyntaxKind::STRING_DOUBLE_VERBATIM698		| SyntaxKind::STRING_SINGLE_VERBATIM699		| SyntaxKind::STRING_BLOCK => Ok(Expr::Trivial(TrivialVal::Str(parse_string_content(p)?))),700701		SyntaxKind::FLOAT => Ok(Expr::Trivial(TrivialVal::Num(parse_number(p)?))),702703		T!['('] => {704			p.eat(T!['('])?;705			let e = expr(p)?;706			p.eat(T![')'])?;707			Ok(e)708		}709710		T!['['] => {711			p.eat(T!['['])?;712			if p.at(T![']']) {713				p.eat(T![']'])?;714				return Ok(Expr::Arr(Vec::new()));715			}716			let first = expr(p)?;717			if p.at(T![for]) {718				let specs = compspecs(p)?;719				p.eat(T![']'])?;720				Ok(Expr::ArrComp(Box::new(first), specs))721			} else if p.at(T![,]) && {722				let next = p.offset + 1;723				next < p.lexemes.len() && p.lexemes[next].kind == T![for]724			} {725				p.eat(T![,])?;726				let specs = compspecs(p)?;727				p.eat(T![']'])?;728				Ok(Expr::ArrComp(Box::new(first), specs))729			} else {730				let mut elems = vec![first];731				while p.try_eat(T![,]) {732					if p.at(T![']']) {733						break;734					}735					elems.push(expr(p)?);736				}737				p.eat(T![']'])?;738				Ok(Expr::Arr(elems))739			}740		}741742		T!['{'] => {743			p.eat(T!['{'])?;744			let body = objinside(p)?;745			p.eat(T!['}'])?;746			Ok(Expr::Obj(body))747		}748749		T![local] => {750			p.eat(T![local])?;751			let mut binds = Vec::new();752			loop {753				if p.at(T![;]) {754					break;755				}756				binds.push(bind(p)?);757				if !p.try_eat(T![,]) {758					break;759				}760			}761			p.eat(T![;])?;762			let body = expr(p)?;763			Ok(Expr::LocalExpr(binds, Box::new(body)))764		}765766		T![if] => Ok(Expr::IfElse(Box::new(if_else(p)?))),767768		T![function] => {769			let span = p.eat_spanned(T![function])?;770			p.eat(T!['('])?;771			let ps = params(p)?;772			p.eat(T![')'])?;773			let body = expr(p)?;774			Ok(Expr::Function(span, ps, Box::new(body)))775		}776777		T![assert] => {778			let a = assert_stmt(p)?;779			p.eat(T![;])?;780			let rest = expr(p)?;781			Ok(Expr::AssertExpr(Box::new(AssertExpr { assert: a, rest })))782		}783784		T![error] => {785			let span = spanned(p, |p| p.eat(T![error]))?;786			let e = expr(p)?;787			Ok(Expr::ErrorStmt(span.span, Box::new(e)))788		}789790		T![importstr] => {791			let kind = spanned(p, |p| {792				p.eat(T![importstr])?;793				Ok(ImportKind::Str)794			})?;795			let path = expr(p)?;796			Ok(Expr::Import(kind, Box::new(path)))797		}798799		T![importbin] => {800			let kind = spanned(p, |p| {801				p.eat(T![importbin])?;802				Ok(ImportKind::Bin)803			})?;804			let path = expr(p)?;805			Ok(Expr::Import(kind, Box::new(path)))806		}807808		T![import] => {809			let kind = spanned(p, |p| {810				p.eat(T![import])?;811				Ok(ImportKind::Normal)812			})?;813			let path = expr(p)?;814			Ok(Expr::Import(kind, Box::new(path)))815		}816817		SyntaxKind::IDENT => {818			let n = spanned(p, |p| {819				let s: IStr = p.text().into();820				p.eat_any();821				Ok(s)822			})?;823			Ok(Expr::Var(n))824		}825826		_ => Err(p.error(format!("unexpected {}", p.current_desc()))),827	}828}829830fn flush_index_parts(e: &mut Expr, parts: &mut Vec<IndexPart>) {831	if parts.is_empty() {832		return;833	}834	let old = std::mem::replace(e, Expr::Trivial(TrivialVal::Null));835	*e = Expr::Index {836		indexable: Box::new(old),837		parts: std::mem::take(parts),838	};839}840841fn expr_suffix(p: &mut Parser<'_>) -> Result<Expr> {842	let mut e = expr_basic(p)?;843	// Accumulate consecutive index parts (.field, [expr], ?.field, ?.[expr])844	// into a single Expr::Index. This is critical for null-coalesce semantics:845	// a?.b.c needs all parts in one Index so the evaluator can skip .c when .b is null.846	let mut parts: Vec<IndexPart> = Vec::new();847848	loop {849		#[cfg(feature = "exp-null-coaelse")]850		if p.at(T![?]) {851			p.eat_any();852			if p.try_eat(T![.]) {853				if p.at(T!['[']) {854					// ?.[expr]855					p.eat(T!['['])?;856					let idx = spanned(p, expr)?;857					p.eat(T![']'])?;858					parts.push(IndexPart {859						span: idx.span,860						value: idx.value,861						null_coaelse: true,862					});863				} else {864					// ?.field865					let id_spanned = spanned(p, |p| Ok(Expr::Trivial(TrivialVal::Str(ident(p)?))))?;866					parts.push(IndexPart {867						span: id_spanned.span,868						value: id_spanned.value,869						null_coaelse: true,870					});871				}872			} else {873				return Err(p.error("expected '.' after '?'".into()));874			}875			continue;876		}877878		if p.at(T![.]) {879			p.eat(T![.])?;880			let id_spanned = spanned(p, |p| Ok(Expr::Trivial(TrivialVal::Str(ident(p)?))))?;881			parts.push(IndexPart {882				span: id_spanned.span,883				value: id_spanned.value,884				#[cfg(feature = "exp-null-coaelse")]885				null_coaelse: false,886			});887		} else if p.at(T!['[']) {888			p.eat(T!['['])?;889890			if p.at(T![:]) {891				// Slice: flush index parts first, then handle slice892				flush_index_parts(&mut e, &mut parts);893				let slice = slice_desc(p, None)?;894				p.eat(T![']'])?;895				e = Expr::Slice(Box::new(Slice { value: e, slice }));896			} else {897				let idx = spanned(p, expr)?;898				if p.at(T![:]) {899					// Slice with start: flush index parts first900					flush_index_parts(&mut e, &mut parts);901					let slice = slice_desc(p, Some(idx))?;902					p.eat(T![']'])?;903					e = Expr::Slice(Box::new(Slice { value: e, slice }));904				} else {905					// Bracket index: add to parts906					p.eat(T![']'])?;907					parts.push(IndexPart {908						span: idx.span,909						value: idx.value,910						#[cfg(feature = "exp-null-coaelse")]911						null_coaelse: false,912					});913				}914			}915		} else if p.at(T!['(']) {916			flush_index_parts(&mut e, &mut parts);917			let args_spanned = spanned(p, |p| {918				p.eat(T!['('])?;919				let a = args(p)?;920				p.eat(T![')'])?;921				Ok(a)922			})?;923			let tailstrict = p.try_eat(T![tailstrict]);924			e = Expr::Apply(Box::new(e), args_spanned, tailstrict);925		} else if p.at(T!['{']) {926			flush_index_parts(&mut e, &mut parts);927			p.eat(T!['{'])?;928			let body = objinside(p)?;929			p.eat(T!['}'])?;930			e = Expr::ObjExtend(Box::new(e), body);931		} else {932			break;933		}934	}935936	flush_index_parts(&mut e, &mut parts);937	Ok(e)938}939940fn prefix_binding_power(op: UnaryOpType) -> u8 {941	match op {942		UnaryOpType::Plus | UnaryOpType::Minus | UnaryOpType::Not | UnaryOpType::BitNot => 20,943	}944}945946fn infix_binding_power(op: BinaryOpType) -> (u8, u8) {947	match op {948		BinaryOpType::Or => (2, 3),949		#[cfg(feature = "exp-null-coaelse")]950		BinaryOpType::NullCoaelse => (2, 3),951		BinaryOpType::And => (4, 5),952		BinaryOpType::BitOr => (6, 7),953		BinaryOpType::BitXor => (8, 9),954		BinaryOpType::BitAnd => (10, 11),955		BinaryOpType::Eq | BinaryOpType::Neq => (12, 13),956		BinaryOpType::Lt957		| BinaryOpType::Gt958		| BinaryOpType::Lte959		| BinaryOpType::Gte960		| BinaryOpType::In => (14, 15),961		BinaryOpType::Lhs | BinaryOpType::Rhs => (16, 17),962		BinaryOpType::Add | BinaryOpType::Sub => (18, 19),963		BinaryOpType::Mul | BinaryOpType::Div | BinaryOpType::Mod => (20, 21),964	}965}966967fn unary_op(kind: SyntaxKind) -> Option<UnaryOpType> {968	match kind {969		T![+] => Some(UnaryOpType::Plus),970		T![-] => Some(UnaryOpType::Minus),971		T![!] => Some(UnaryOpType::Not),972		T![~] => Some(UnaryOpType::BitNot),973		_ => None,974	}975}976977fn binary_op(p: &Parser<'_>) -> Option<BinaryOpType> {978	match p.peek() {979		T![||] => Some(BinaryOpType::Or),980		T![&&] => Some(BinaryOpType::And),981		T![|] => Some(BinaryOpType::BitOr),982		T![^] => Some(BinaryOpType::BitXor),983		T![&] => Some(BinaryOpType::BitAnd),984		T![==] => Some(BinaryOpType::Eq),985		T![!=] => Some(BinaryOpType::Neq),986		T![<] => Some(BinaryOpType::Lt),987		T![>] => Some(BinaryOpType::Gt),988		T![<=] => Some(BinaryOpType::Lte),989		T![>=] => Some(BinaryOpType::Gte),990		T![<<] => Some(BinaryOpType::Lhs),991		T![>>] => Some(BinaryOpType::Rhs),992		T![+] => Some(BinaryOpType::Add),993		T![-] => Some(BinaryOpType::Sub),994		T![*] => Some(BinaryOpType::Mul),995		T![/] => Some(BinaryOpType::Div),996		T![%] => Some(BinaryOpType::Mod),997		T![in] => Some(BinaryOpType::In),998		#[cfg(feature = "exp-null-coaelse")]999		T![??] => Some(BinaryOpType::NullCoaelse),1000		_ => None,1001	}1002}10031004fn expr_bp(p: &mut Parser<'_>, min_bp: u8) -> Result<Expr> {1005	let mut lhs = if let Some(op) = unary_op(p.peek()) {1006		p.eat_any();1007		let rbp = prefix_binding_power(op);1008		let rhs = expr_bp(p, rbp)?;1009		Expr::UnaryOp(op, Box::new(rhs))1010	} else {1011		expr_suffix(p)?1012	};10131014	loop {1015		if p.at_eof() {1016			break;1017		}10181019		let Some(op) = binary_op(p) else {1020			break;1021		};10221023		let (lbp, rbp) = infix_binding_power(op);1024		if lbp < min_bp {1025			break;1026		}10271028		p.eat_any();1029		let rhs = expr_bp(p, rbp)?;1030		lhs = Expr::BinaryOp(Box::new(BinaryOp { lhs, op, rhs }));1031	}10321033	Ok(lhs)1034}10351036fn expr(p: &mut Parser<'_>) -> Result<Expr> {1037	expr_bp(p, 0)1038}10391040pub fn parse(str: &str, settings: &ParserSettings) -> Result<Expr> {1041	let mut p = Parser::new(str, settings.source.clone());1042	for lexeme in &p.lexemes {1043		if let Some(desc) = lexeme.kind.error_description() {1044			return Err(ParseError {1045				message: desc.to_owned(),1046				location: Span(p.source.clone(), lexeme.range.0, lexeme.range.1),1047			});1048		}1049	}1050	let e = expr(&mut p)?;1051	if !p.at_eof() {1052		return Err(p.error(format!("expected end of file, got {}", p.current_desc())));1053	}1054	Ok(e)1055}10561057pub fn string_to_expr(s: IStr, settings: &ParserSettings) -> Spanned<Expr> {1058	let len = u32::try_from(s.len()).expect("code size is limited by 4gb");10591060	Spanned::new(1061		Expr::Trivial(TrivialVal::Str(s)),1062		Span(settings.source.clone(), 0, len),1063	)1064}10651066#[cfg(test)]1067mod tests {1068	use insta::assert_snapshot;10691070	use super::*;10711072	fn parse_str(input: &str) -> Expr {1073		let source = Source::new_virtual("<test>".into(), input.into());1074		let settings = ParserSettings { source };1075		parse(input, &settings).unwrap()1076	}10771078	#[test]1079	#[cfg(not(feature = "exp-null-coaelse"))]1080	fn basic_test() {1081		let v = parse_str("assert true[false] : false ; true");1082		assert_snapshot!(format!("{v:#?}"));1083	}10841085	#[test]1086	fn literals() {1087		let v = parse_str("[null, true, false, self, super, $]");1088		assert_snapshot!(format!("{v:#?}"));1089	}10901091	#[test]1092	fn basic_math() {1093		let v = parse_str("2+2*2");1094		assert_snapshot!(format!("{v:#?}"));1095	}10961097	#[test]1098	fn underscore_numbers() {1099		let v = parse_str("[1_000, 1_000.000_1, 1_0e1_0]");1100		assert_snapshot!(format!("{v:#?}"));1101	}11021103	#[test]1104	fn strings() {1105		let v = parse_str(r#"["hello", 'world', @"raw""str", @'raw''str']"#);1106		assert_snapshot!(format!("{v:#?}"));1107	}11081109	#[test]1110	fn object() {1111		let v = parse_str("{a: 1, b:: 2, c::: 3}");1112		assert_snapshot!(format!("{v:#?}"));1113	}11141115	#[test]1116	fn function_and_call() {1117		let v = parse_str("local f(x, y=1) = x + y; f(2, y=3)");1118		assert_snapshot!(format!("{v:#?}"));1119	}11201121	#[test]1122	fn if_then_else() {1123		let v = parse_str("if true then 1 else 2");1124		assert_snapshot!(format!("{v:#?}"));1125	}11261127	#[test]1128	fn imports() {1129		let v = parse_str(r#"[import "a", importstr "b", importbin "c"]"#);1130		assert_snapshot!(format!("{v:#?}"));1131	}11321133	#[test]1134	fn array_comp() {1135		let v = parse_str("[x for x in arr]");1136		assert_snapshot!(format!("{v:#?}"));1137	}11381139	#[test]1140	#[cfg(not(feature = "exp-null-coaelse"))]1141	fn index_and_suffix() {1142		let v = parse_str("std.test(2).field[0]");1143		assert_snapshot!(format!("{v:#?}"));1144	}11451146	#[test]1147	fn obj_extend() {1148		let v = parse_str("{} { x: 1 }");1149		assert_snapshot!(format!("{v:#?}"));1150	}11511152	#[test]1153	fn unary_ops() {1154		let v = parse_str("!a && !b");1155		assert_snapshot!(format!("{v:#?}"));1156	}11571158	#[test]1159	fn error_expr() {1160		let v = parse_str("error \"bad\"");1161		assert_snapshot!(format!("{v:#?}"));1162	}11631164	#[test]1165	fn slice() {1166		let v = parse_str("[a[1:], a[1::], a[:1:], a[::1]]");1167		assert_snapshot!(format!("{v:#?}"));1168	}11691170	#[test]1171	#[cfg(not(feature = "exp-null-coaelse"))]1172	fn peg_snapshots() {1173		use std::fs;11741175		use insta::glob;1176		use jrsonnet_ir::{IStr, Source};11771178		glob!("../../jrsonnet-peg-parser/src", "tests/*.jsonnet", |path| {1179			let input = fs::read_to_string(path).expect("read test file");1180			let source = Source::new_virtual("<test>".into(), IStr::empty());1181			let settings = ParserSettings { source };1182			let v = parse(&input, &settings).unwrap();1183			let v = format!("{v:#?}");1184			assert_snapshot!(v);1185		});1186	}1187}
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_math.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_math.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_math.snap
@@ -1,21 +1,28 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1094
 expression: "format!(\"{v:#?}\")"
 ---
 BinaryOp(
     BinaryOp {
-        lhs: Num(
-            2.0,
+        lhs: Trivial(
+            Num(
+                2.0,
+            ),
         ),
         op: Add,
         rhs: BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Mul,
-                rhs: Num(
-                    2.0,
+                rhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
             },
         ),
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_test.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_test.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_test.snap
@@ -1,35 +1,40 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1082
 expression: "format!(\"{v:#?}\")"
 ---
 AssertExpr(
     AssertExpr {
         assert: AssertStmt {
             assertion: Index {
-                indexable: Literal(
-                    virtual:<test>:7-11,
-                    True,
+                indexable: Trivial(
+                    Bool(
+                        true,
+                    ),
                 ),
                 parts: [
                     IndexPart {
                         span: virtual:<test>:12-17,
-                        value: Literal(
-                            virtual:<test>:12-17,
-                            False,
+                        value: Trivial(
+                            Bool(
+                                false,
+                            ),
                         ),
                     },
                 ],
             } from virtual:<test>:7-18,
             message: Some(
-                Literal(
-                    virtual:<test>:21-26,
-                    False,
+                Trivial(
+                    Bool(
+                        false,
+                    ),
                 ),
             ),
         },
-        rest: Literal(
-            virtual:<test>:29-33,
-            True,
+        rest: Trivial(
+            Bool(
+                true,
+            ),
         ),
     },
 )
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__error_expr.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__error_expr.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__error_expr.snap
@@ -1,10 +1,13 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1161
 expression: "format!(\"{v:#?}\")"
 ---
 ErrorStmt(
     virtual:<test>:0-5,
-    Str(
-        "bad",
+    Trivial(
+        Str(
+            "bad",
+        ),
     ),
 )
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1118
 expression: "format!(\"{v:#?}\")"
 ---
 LocalExpr(
@@ -19,8 +20,10 @@
                             "y" from virtual:<test>:11-12,
                         ),
                         default: Some(
-                            Num(
-                                1.0,
+                            Trivial(
+                                Num(
+                                    1.0,
+                                ),
                             ),
                         ),
                     },
@@ -62,16 +65,20 @@
         ),
         ArgsDesc {
             unnamed: [
-                Num(
-                    2.0,
+                Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
             ],
             names: [
                 "y",
             ],
             values: [
-                Num(
-                    3.0,
+                Trivial(
+                    Num(
+                        3.0,
+                    ),
                 ),
             ],
         } from virtual:<test>:26-34,
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__if_then_else.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__if_then_else.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__if_then_else.snap
@@ -1,22 +1,28 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1124
 expression: "format!(\"{v:#?}\")"
 ---
 IfElse(
     IfElse {
         cond: IfSpecData {
             span: virtual:<test>:0-2,
-            cond: Literal(
-                virtual:<test>:3-7,
-                True,
+            cond: Trivial(
+                Bool(
+                    true,
+                ),
             ),
         },
-        cond_then: Num(
-            1.0,
+        cond_then: Trivial(
+            Num(
+                1.0,
+            ),
         ),
         cond_else: Some(
-            Num(
-                2.0,
+            Trivial(
+                Num(
+                    2.0,
+                ),
             ),
         ),
     },
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__imports.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__imports.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__imports.snap
@@ -1,25 +1,32 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1130
 expression: "format!(\"{v:#?}\")"
 ---
 Arr(
     [
         Import(
             Normal from virtual:<test>:1-7,
-            Str(
-                "a",
+            Trivial(
+                Str(
+                    "a",
+                ),
             ),
         ),
         Import(
             Str from virtual:<test>:13-22,
-            Str(
-                "b",
+            Trivial(
+                Str(
+                    "b",
+                ),
             ),
         ),
         Import(
             Bin from virtual:<test>:28-37,
-            Str(
-                "c",
+            Trivial(
+                Str(
+                    "c",
+                ),
             ),
         ),
     ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__index_and_suffix.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__index_and_suffix.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__index_and_suffix.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1143
 expression: "format!(\"{v:#?}\")"
 ---
 Index {
@@ -11,16 +12,20 @@
             parts: [
                 IndexPart {
                     span: virtual:<test>:4-8,
-                    value: Str(
-                        "test",
+                    value: Trivial(
+                        Str(
+                            "test",
+                        ),
                     ),
                 },
             ],
         },
         ArgsDesc {
             unnamed: [
-                Num(
-                    2.0,
+                Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
             ],
             names: [],
@@ -31,14 +36,18 @@
     parts: [
         IndexPart {
             span: virtual:<test>:12-17,
-            value: Str(
-                "field",
+            value: Trivial(
+                Str(
+                    "field",
+                ),
             ),
         },
         IndexPart {
             span: virtual:<test>:18-19,
-            value: Num(
-                0.0,
+            value: Trivial(
+                Num(
+                    0.0,
+                ),
             ),
         },
     ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__literals.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__literals.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__literals.snap
@@ -1,30 +1,32 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1088
 expression: "format!(\"{v:#?}\")"
 ---
 Arr(
     [
-        Literal(
-            virtual:<test>:1-5,
+        Trivial(
             Null,
         ),
-        Literal(
-            virtual:<test>:7-11,
-            True,
+        Trivial(
+            Bool(
+                true,
+            ),
         ),
-        Literal(
-            virtual:<test>:13-18,
-            False,
+        Trivial(
+            Bool(
+                false,
+            ),
         ),
-        Literal(
+        Identity(
             virtual:<test>:20-24,
             This,
         ),
-        Literal(
+        Identity(
             virtual:<test>:26-31,
             Super,
         ),
-        Literal(
+        Identity(
             virtual:<test>:33-34,
             Dollar,
         ),
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__obj_extend.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__obj_extend.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__obj_extend.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1149
 expression: "format!(\"{v:#?}\")"
 ---
 ObjExtend(
@@ -24,8 +25,10 @@
                     plus: false,
                     params: None,
                     visibility: Normal,
-                    value: Num(
-                        1.0,
+                    value: Trivial(
+                        Num(
+                            1.0,
+                        ),
                     ),
                 },
             ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__object.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__object.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__object.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1112
 expression: "format!(\"{v:#?}\")"
 ---
 Obj(
@@ -15,8 +16,10 @@
                     plus: false,
                     params: None,
                     visibility: Normal,
-                    value: Num(
-                        1.0,
+                    value: Trivial(
+                        Num(
+                            1.0,
+                        ),
                     ),
                 },
                 FieldMember {
@@ -26,8 +29,10 @@
                     plus: false,
                     params: None,
                     visibility: Hidden,
-                    value: Num(
-                        2.0,
+                    value: Trivial(
+                        Num(
+                            2.0,
+                        ),
                     ),
                 },
                 FieldMember {
@@ -37,8 +42,10 @@
                     plus: false,
                     params: None,
                     visibility: Unhide,
-                    value: Num(
-                        3.0,
+                    value: Trivial(
+                        Num(
+                            3.0,
+                        ),
                     ),
                 },
             ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@array_comp.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@array_comp.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@array_comp.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/array_comp.jsonnet
 ---
@@ -14,8 +15,10 @@
                     parts: [
                         IndexPart {
                             span: virtual:<test>:7-15,
-                            value: Str(
-                                "deepJoin",
+                            value: Trivial(
+                                Str(
+                                    "deepJoin",
+                                ),
                             ),
                         },
                     ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@basic_math.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@basic_math.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@basic_math.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/basic_math.jsonnet
 ---
@@ -7,18 +8,24 @@
     [
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
@@ -26,18 +33,24 @@
         ),
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
@@ -47,24 +60,32 @@
             BinaryOp {
                 lhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Add,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
@@ -72,24 +93,32 @@
         ),
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Add,
                         rhs: BinaryOp(
                             BinaryOp {
-                                lhs: Num(
-                                    2.0,
+                                lhs: Trivial(
+                                    Num(
+                                        2.0,
+                                    ),
                                 ),
                                 op: Mul,
-                                rhs: Num(
-                                    2.0,
+                                rhs: Trivial(
+                                    Num(
+                                        2.0,
+                                    ),
                                 ),
                             },
                         ),
@@ -99,18 +128,24 @@
         ),
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            3.0,
+                        lhs: Trivial(
+                            Num(
+                                3.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            4.0,
+                        rhs: Trivial(
+                            Num(
+                                4.0,
+                            ),
                         ),
                     },
                 ),
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@comment_eof.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@comment_eof.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@comment_eof.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/comment_eof.jsonnet
 ---
@@ -16,8 +17,10 @@
                     plus: false,
                     params: None,
                     visibility: Normal,
-                    value: Num(
-                        1.0,
+                    value: Trivial(
+                        Num(
+                            1.0,
+                        ),
                     ),
                 },
             ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@default_nondefault.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@default_nondefault.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@default_nondefault.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/default_nondefault.jsonnet
 ---
@@ -14,8 +15,10 @@
                             "foo" from virtual:<test>:8-11,
                         ),
                         default: Some(
-                            Str(
-                                "foo",
+                            Trivial(
+                                Str(
+                                    "foo",
+                                ),
                             ),
                         ),
                     },
@@ -44,14 +47,12 @@
                 ),
                 binds_len: 2,
             },
-            value: Literal(
-                virtual:<test>:28-32,
+            value: Trivial(
                 Null,
             ),
         },
     ],
-    Literal(
-        virtual:<test>:34-38,
+    Trivial(
         Null,
     ),
 )
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@imports.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@imports.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@imports.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/imports.jsonnet
 ---
@@ -7,20 +8,26 @@
     [
         Import(
             Normal from virtual:<test>:2-8,
-            Str(
-                "hello",
+            Trivial(
+                Str(
+                    "hello",
+                ),
             ),
         ),
         Import(
             Str from virtual:<test>:18-27,
-            Str(
-                "garnish.txt",
+            Trivial(
+                Str(
+                    "garnish.txt",
+                ),
             ),
         ),
         Import(
             Bin from virtual:<test>:43-52,
-            Str(
-                "garnish.bin",
+            Trivial(
+                Str(
+                    "garnish.bin",
+                ),
             ),
         ),
     ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@multiline.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@multiline.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@multiline.jsonnet.snap
@@ -1,21 +1,30 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/multiline.jsonnet
 ---
 Arr(
     [
-        Str(
-            "Hello world!\na\n",
+        Trivial(
+            Str(
+                "Hello world!\na\n",
+            ),
         ),
-        Str(
-            "Hello world!\na\n",
+        Trivial(
+            Str(
+                "Hello world!\na\n",
+            ),
         ),
-        Str(
-            "Hello world!\n\ta\n",
+        Trivial(
+            Str(
+                "Hello world!\n\ta\n",
+            ),
         ),
-        Str(
-            "Hello world!\n a\n",
+        Trivial(
+            Str(
+                "Hello world!\n a\n",
+            ),
         ),
     ],
 )
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@reserved.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@reserved.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@reserved.jsonnet.snap
@@ -1,12 +1,12 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/reserved.jsonnet
 ---
 Arr(
     [
-        Literal(
-            virtual:<test>:2-6,
+        Trivial(
             Null,
         ),
         Var(
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@slice.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@slice.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@slice.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/slice.jsonnet
 ---
@@ -12,8 +13,10 @@
                 ),
                 slice: SliceDesc {
                     start: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:4-5,
                     ),
                     end: None,
@@ -28,8 +31,10 @@
                 ),
                 slice: SliceDesc {
                     start: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:11-12,
                     ),
                     end: None,
@@ -45,8 +50,10 @@
                 slice: SliceDesc {
                     start: None,
                     end: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:20-21,
                     ),
                     step: None,
@@ -62,8 +69,10 @@
                     start: None,
                     end: None,
                     step: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:29-30,
                     ),
                 },
@@ -83,8 +92,10 @@
                                     "len" from virtual:<test>:38-41,
                                 ),
                                 op: Sub,
-                                rhs: Num(
-                                    1.0,
+                                rhs: Trivial(
+                                    Num(
+                                        1.0,
+                                    ),
                                 ),
                             },
                         ) from virtual:<test>:38-45,
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@string_escaping.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@string_escaping.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@string_escaping.jsonnet.snap
@@ -1,24 +1,35 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/string_escaping.jsonnet
 ---
 Arr(
     [
-        Str(
-            "Hello, \"world\"!",
+        Trivial(
+            Str(
+                "Hello, \"world\"!",
+            ),
         ),
-        Str(
-            "Hello 'world'!",
+        Trivial(
+            Str(
+                "Hello 'world'!",
+            ),
         ),
-        Str(
-            "\\\\",
+        Trivial(
+            Str(
+                "\\\\",
+            ),
         ),
-        Str(
-            "Hello\nWorld",
+        Trivial(
+            Str(
+                "Hello\nWorld",
+            ),
         ),
-        Str(
-            "Hello\\n\"World\"",
+        Trivial(
+            Str(
+                "Hello\\n\"World\"",
+            ),
         ),
     ],
 )
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@subexp.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@subexp.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@subexp.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/subexp.jsonnet
 ---
@@ -22,8 +23,10 @@
                             into: Full(
                                 "x" from virtual:<test>:11-12,
                             ),
-                            value: Num(
-                                1.0,
+                            value: Trivial(
+                                Num(
+                                    1.0,
+                                ),
                             ),
                         },
                     ],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@suffix.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@suffix.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@suffix.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1184
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/suffix.jsonnet
 ---
@@ -12,8 +13,10 @@
             parts: [
                 IndexPart {
                     span: virtual:<test>:6-10,
-                    value: Str(
-                        "test",
+                    value: Trivial(
+                        Str(
+                            "test",
+                        ),
                     ),
                 },
             ],
@@ -24,8 +27,10 @@
             ),
             ArgsDesc {
                 unnamed: [
-                    Num(
-                        2.0,
+                    Trivial(
+                        Num(
+                            2.0,
+                        ),
                     ),
                 ],
                 names: [],
@@ -41,16 +46,20 @@
                 parts: [
                     IndexPart {
                         span: virtual:<test>:24-28,
-                        value: Str(
-                            "test",
+                        value: Trivial(
+                            Str(
+                                "test",
+                            ),
                         ),
                     },
                 ],
             },
             ArgsDesc {
                 unnamed: [
-                    Num(
-                        2.0,
+                    Trivial(
+                        Num(
+                            2.0,
+                        ),
                     ),
                 ],
                 names: [],
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__slice.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__slice.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__slice.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1167
 expression: "format!(\"{v:#?}\")"
 ---
 Arr(
@@ -11,8 +12,10 @@
                 ),
                 slice: SliceDesc {
                     start: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:3-4,
                     ),
                     end: None,
@@ -27,8 +30,10 @@
                 ),
                 slice: SliceDesc {
                     start: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:10-11,
                     ),
                     end: None,
@@ -44,8 +49,10 @@
                 slice: SliceDesc {
                     start: None,
                     end: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:19-20,
                     ),
                     step: None,
@@ -61,8 +68,10 @@
                     start: None,
                     end: None,
                     step: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:28-29,
                     ),
                 },
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__strings.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__strings.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__strings.snap
@@ -1,20 +1,29 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1106
 expression: "format!(\"{v:#?}\")"
 ---
 Arr(
     [
-        Str(
-            "hello",
+        Trivial(
+            Str(
+                "hello",
+            ),
         ),
-        Str(
-            "world",
+        Trivial(
+            Str(
+                "world",
+            ),
         ),
-        Str(
-            "raw\"str",
+        Trivial(
+            Str(
+                "raw\"str",
+            ),
         ),
-        Str(
-            "raw'str",
+        Trivial(
+            Str(
+                "raw'str",
+            ),
         ),
     ],
 )
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__underscore_numbers.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__underscore_numbers.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__underscore_numbers.snap
@@ -1,17 +1,24 @@
 ---
 source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1100
 expression: "format!(\"{v:#?}\")"
 ---
 Arr(
     [
-        Num(
-            1000.0,
+        Trivial(
+            Num(
+                1000.0,
+            ),
         ),
-        Num(
-            1000.0001,
+        Trivial(
+            Num(
+                1000.0001,
+            ),
         ),
-        Num(
-            100000000000.0,
+        Trivial(
+            Num(
+                100000000000.0,
+            ),
         ),
     ],
 )
modifiedcrates/jrsonnet-ir/src/expr.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/expr.rs
+++ b/crates/jrsonnet-ir/src/expr.rs
@@ -31,6 +31,14 @@
 	Unhide,
 }
 
+#[derive(Debug, Clone, PartialEq, Acyclic)]
+pub enum TrivialVal {
+	Null,
+	Bool(bool),
+	Num(NumValue),
+	Str(IStr),
+}
+
 impl Visibility {
 	pub fn is_visible(&self) -> bool {
 		matches!(self, Self::Normal | Self::Unhide)
@@ -351,14 +359,12 @@
 	ObjComp(ObjComp),
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Acyclic)]
-pub enum LiteralType {
+/// Object identity reference: `self`, `super`, or `$`.#[derive(Debug, PartialEq, Eq, Clone, Copy, Acyclic)]
+#[derive(Debug, PartialEq, Acyclic)]
+pub enum IdentityKind {
 	This,
 	Super,
 	Dollar,
-	Null,
-	True,
-	False,
 }
 
 #[derive(Debug, PartialEq, Acyclic)]
@@ -404,12 +410,12 @@
 /// Syntax base
 #[derive(Debug, PartialEq, Acyclic)]
 pub enum Expr {
-	Literal(Span, LiteralType),
+	/// Object-identity reference: `self`, `super`, `$`.
+	Identity(Span, IdentityKind),
+
+	/// Trivial value literal
+	Trivial(TrivialVal),
 
-	/// String value: "hello"
-	Str(IStr),
-	/// Number: 1, 2.0, 2e+20
-	Num(NumValue),
 	/// Variable name: test
 	Var(Spanned<IStr>),
 
modifiedcrates/jrsonnet-ir/src/visit.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/visit.rs
+++ b/crates/jrsonnet-ir/src/visit.rs
@@ -5,7 +5,7 @@
 use crate::{
 	ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BindSpec, CompSpec, Destruct, Expr, ExprParam,
 	ExprParams, FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, IndexPart,
-	ObjBody, ObjComp, ObjMembers, Slice, SliceDesc,
+	ObjBody, ObjComp, ObjMembers, Slice, SliceDesc, TrivialVal,
 };
 
 pub trait Visitor: Sized {
@@ -178,9 +178,8 @@
 }
 pub fn visit_expr<V: Visitor>(v: &mut V, e: &Expr) {
 	match e {
-		Expr::Literal(_span, _literal_type) => {}
-		Expr::Str(_istr) => {}
-		Expr::Num(_num) => {}
+		Expr::Identity(_span, _identity_kind) => {}
+		Expr::Trivial(_) => {}
 		Expr::Var(_spanned) => {}
 		Expr::Arr(exprs) => {
 			for e in &**exprs {
@@ -220,7 +219,7 @@
 		Expr::Import(kind, expr) => {
 			v.visit_expr(expr);
 
-			if let Expr::Str(expr) = &**expr {
+			if let Expr::Trivial(TrivialVal::Str(expr)) = &**expr {
 				v.visit_import(matches!(**kind, ImportKind::Normal), expr.clone());
 			}
 		}
modifiedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -1,9 +1,9 @@
 use jrsonnet_gcmodule::Acyclic;
 use jrsonnet_ir::{
 	ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BindSpec, CompSpec, Destruct, DestructRest, Expr,
-	ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IfElse, IfSpecData,
-	ImportKind, IndexPart, LiteralType, Member, NumValue, ObjBody, ObjComp, ObjMembers, Slice,
-	SliceDesc, Source, Span, Spanned, Visibility, unescape,
+	ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IdentityKind, IfElse,
+	IfSpecData, ImportKind, IndexPart, Member, NumValue, ObjBody, ObjComp, ObjMembers, Slice,
+	SliceDesc, Source, Span, Spanned, TrivialVal, Visibility, unescape,
 };
 use peg::parser;
 
@@ -263,7 +263,7 @@
 		pub rule local_expr(s: &ParserSettings) -> Expr
 			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, Box::new(expr)) }
 		pub rule string_expr(s: &ParserSettings) -> Expr
-			= s:string() {Expr::Str(s.into())}
+			= s:string() {Expr::Trivial(TrivialVal::Str(s.into()))}
 		pub rule obj_expr(s: &ParserSettings) -> Expr
 			= "{" _ body:objinside(s) _ "}" {Expr::Obj(body)}
 		pub rule array_expr(s: &ParserSettings) -> Expr
@@ -273,7 +273,7 @@
 				Expr::ArrComp(Box::new(expr), specs)
 			}
 		pub rule number_expr(s: &ParserSettings) -> Expr
-			= n:number() {? NumValue::new(n).map_or_else(|| Err("!!!numbers are finite"), |n| Ok(Expr::Num(n)))}
+			= n:number() {? NumValue::new(n).map_or_else(|| Err("!!!numbers are finite"), |n| Ok(Expr::Trivial(TrivialVal::Num(n))))}
 
 		rule spanned<T: Acyclic>(x: rule<T>, s: &ParserSettings) -> Spanned<T>
 			= a:position!() n:x() b:position!() { Spanned::new(n, Span(s.source.clone(), codeidx(a), codeidx(b))) }
@@ -281,7 +281,7 @@
 		pub rule var_expr(s: &ParserSettings) -> Expr
 			= n:spanned(<id()>, s) { Expr::Var(n) }
 		pub rule id_loc(s: &ParserSettings) -> Spanned<Expr>
-			= a:position!() n:id() b:position!() { Spanned::new(Expr::Str(n), Span(s.source.clone(), codeidx(a), codeidx(b))) }
+			= a:position!() n:id() b:position!() { Spanned::new(Expr::Trivial(TrivialVal::Str(n)), Span(s.source.clone(), codeidx(a), codeidx(b))) }
 		pub rule if_then_else_expr(s: &ParserSettings) -> Expr
 			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse(Box::new(IfElse{
 				cond,
@@ -290,14 +290,14 @@
 			}))}
 
 		pub rule literal(s: &ParserSettings) -> Expr
-			= a:position!() v:(
-				keyword("null") {LiteralType::Null}
-				/ keyword("true") {LiteralType::True}
-				/ keyword("false") {LiteralType::False}
-				/ keyword("self") {LiteralType::This}
-				/ keyword("$") {LiteralType::Dollar}
-				/ keyword("super") {LiteralType::Super}
-			) b:position!() {Expr::Literal(Span(s.source.clone(), codeidx(a), codeidx(b)), v)}
+			= keyword("null") { Expr::Trivial(TrivialVal::Null) }
+			/ keyword("true") { Expr::Trivial(TrivialVal::Bool(true)) }
+			/ keyword("false") { Expr::Trivial(TrivialVal::Bool(false)) }
+			/ a:position!() v:(
+				keyword("self") {IdentityKind::This}
+				/ keyword("$") {IdentityKind::Dollar}
+				/ keyword("super") {IdentityKind::Super}
+			) b:position!() { Expr::Identity(Span(s.source.clone(), codeidx(a), codeidx(b)), v) }
 
 		rule import_kind() -> ImportKind
 			= keyword("importstr") { ImportKind::Str }
@@ -429,7 +429,7 @@
 pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> Spanned<Expr> {
 	let len = str.len();
 	Spanned::new(
-		Expr::Str(str),
+		Expr::Trivial(TrivialVal::Str(str)),
 		Span(settings.source.clone(), 0, codeidx(len)),
 	)
 }
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@array_comp.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@array_comp.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@array_comp.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/array_comp.jsonnet
 ---
@@ -14,8 +15,10 @@
                     parts: [
                         IndexPart {
                             span: virtual:<test>:7-15,
-                            value: Str(
-                                "deepJoin",
+                            value: Trivial(
+                                Str(
+                                    "deepJoin",
+                                ),
                             ),
                         },
                     ],
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@basic_math.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@basic_math.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@basic_math.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/basic_math.jsonnet
 ---
@@ -7,18 +8,24 @@
     [
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
@@ -26,18 +33,24 @@
         ),
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
@@ -47,24 +60,32 @@
             BinaryOp {
                 lhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Add,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            2.0,
+                        rhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                     },
                 ),
@@ -72,24 +93,32 @@
         ),
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            2.0,
+                        lhs: Trivial(
+                            Num(
+                                2.0,
+                            ),
                         ),
                         op: Add,
                         rhs: BinaryOp(
                             BinaryOp {
-                                lhs: Num(
-                                    2.0,
+                                lhs: Trivial(
+                                    Num(
+                                        2.0,
+                                    ),
                                 ),
                                 op: Mul,
-                                rhs: Num(
-                                    2.0,
+                                rhs: Trivial(
+                                    Num(
+                                        2.0,
+                                    ),
                                 ),
                             },
                         ),
@@ -99,18 +128,24 @@
         ),
         BinaryOp(
             BinaryOp {
-                lhs: Num(
-                    2.0,
+                lhs: Trivial(
+                    Num(
+                        2.0,
+                    ),
                 ),
                 op: Add,
                 rhs: BinaryOp(
                     BinaryOp {
-                        lhs: Num(
-                            3.0,
+                        lhs: Trivial(
+                            Num(
+                                3.0,
+                            ),
                         ),
                         op: Mul,
-                        rhs: Num(
-                            4.0,
+                        rhs: Trivial(
+                            Num(
+                                4.0,
+                            ),
                         ),
                     },
                 ),
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@comment_eof.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@comment_eof.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@comment_eof.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/comment_eof.jsonnet
 ---
@@ -16,8 +17,10 @@
                     plus: false,
                     params: None,
                     visibility: Normal,
-                    value: Num(
-                        1.0,
+                    value: Trivial(
+                        Num(
+                            1.0,
+                        ),
                     ),
                 },
             ],
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@default_nondefault.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@default_nondefault.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@default_nondefault.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/default_nondefault.jsonnet
 ---
@@ -14,8 +15,10 @@
                             "foo" from virtual:<test>:8-11,
                         ),
                         default: Some(
-                            Str(
-                                "foo",
+                            Trivial(
+                                Str(
+                                    "foo",
+                                ),
                             ),
                         ),
                     },
@@ -44,14 +47,12 @@
                 ),
                 binds_len: 2,
             },
-            value: Literal(
-                virtual:<test>:28-32,
+            value: Trivial(
                 Null,
             ),
         },
     ],
-    Literal(
-        virtual:<test>:34-38,
+    Trivial(
         Null,
     ),
 )
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@imports.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@imports.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@imports.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/imports.jsonnet
 ---
@@ -7,20 +8,26 @@
     [
         Import(
             Normal from virtual:<test>:2-8,
-            Str(
-                "hello",
+            Trivial(
+                Str(
+                    "hello",
+                ),
             ),
         ),
         Import(
             Str from virtual:<test>:18-27,
-            Str(
-                "garnish.txt",
+            Trivial(
+                Str(
+                    "garnish.txt",
+                ),
             ),
         ),
         Import(
             Bin from virtual:<test>:43-52,
-            Str(
-                "garnish.bin",
+            Trivial(
+                Str(
+                    "garnish.bin",
+                ),
             ),
         ),
     ],
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@multiline.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@multiline.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@multiline.jsonnet.snap
@@ -1,21 +1,30 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/multiline.jsonnet
 ---
 Arr(
     [
-        Str(
-            "Hello world!\na\n",
+        Trivial(
+            Str(
+                "Hello world!\na\n",
+            ),
         ),
-        Str(
-            "Hello world!\na\n",
+        Trivial(
+            Str(
+                "Hello world!\na\n",
+            ),
         ),
-        Str(
-            "Hello world!\n\ta\n",
+        Trivial(
+            Str(
+                "Hello world!\n\ta\n",
+            ),
         ),
-        Str(
-            "Hello world!\n a\n",
+        Trivial(
+            Str(
+                "Hello world!\n a\n",
+            ),
         ),
     ],
 )
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@reserved.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@reserved.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@reserved.jsonnet.snap
@@ -1,12 +1,12 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/reserved.jsonnet
 ---
 Arr(
     [
-        Literal(
-            virtual:<test>:2-6,
+        Trivial(
             Null,
         ),
         Var(
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@slice.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@slice.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@slice.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/slice.jsonnet
 ---
@@ -12,8 +13,10 @@
                 ),
                 slice: SliceDesc {
                     start: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:4-5,
                     ),
                     end: None,
@@ -28,8 +31,10 @@
                 ),
                 slice: SliceDesc {
                     start: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:11-12,
                     ),
                     end: None,
@@ -45,8 +50,10 @@
                 slice: SliceDesc {
                     start: None,
                     end: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:20-21,
                     ),
                     step: None,
@@ -62,8 +69,10 @@
                     start: None,
                     end: None,
                     step: Some(
-                        Num(
-                            1.0,
+                        Trivial(
+                            Num(
+                                1.0,
+                            ),
                         ) from virtual:<test>:29-30,
                     ),
                 },
@@ -83,8 +92,10 @@
                                     "len" from virtual:<test>:38-41,
                                 ),
                                 op: Sub,
-                                rhs: Num(
-                                    1.0,
+                                rhs: Trivial(
+                                    Num(
+                                        1.0,
+                                    ),
                                 ),
                             },
                         ) from virtual:<test>:38-45,
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@string_escaping.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@string_escaping.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@string_escaping.jsonnet.snap
@@ -1,24 +1,35 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/string_escaping.jsonnet
 ---
 Arr(
     [
-        Str(
-            "Hello, \"world\"!",
+        Trivial(
+            Str(
+                "Hello, \"world\"!",
+            ),
         ),
-        Str(
-            "Hello 'world'!",
+        Trivial(
+            Str(
+                "Hello 'world'!",
+            ),
         ),
-        Str(
-            "\\\\",
+        Trivial(
+            Str(
+                "\\\\",
+            ),
         ),
-        Str(
-            "Hello\nWorld",
+        Trivial(
+            Str(
+                "Hello\nWorld",
+            ),
         ),
-        Str(
-            "Hello\\n\"World\"",
+        Trivial(
+            Str(
+                "Hello\\n\"World\"",
+            ),
         ),
     ],
 )
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@subexp.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@subexp.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@subexp.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/subexp.jsonnet
 ---
@@ -22,8 +23,10 @@
                             into: Full(
                                 "x" from virtual:<test>:11-12,
                             ),
-                            value: Num(
-                                1.0,
+                            value: Trivial(
+                                Num(
+                                    1.0,
+                                ),
                             ),
                         },
                     ],
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@suffix.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@suffix.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@suffix.jsonnet.snap
@@ -1,5 +1,6 @@
 ---
 source: crates/jrsonnet-peg-parser/src/lib.rs
+assertion_line: 459
 expression: v
 input_file: crates/jrsonnet-peg-parser/src/tests/suffix.jsonnet
 ---
@@ -12,8 +13,10 @@
             parts: [
                 IndexPart {
                     span: virtual:<test>:6-10,
-                    value: Str(
-                        "test",
+                    value: Trivial(
+                        Str(
+                            "test",
+                        ),
                     ),
                 },
             ],
@@ -24,8 +27,10 @@
             ),
             ArgsDesc {
                 unnamed: [
-                    Num(
-                        2.0,
+                    Trivial(
+                        Num(
+                            2.0,
+                        ),
                     ),
                 ],
                 names: [],
@@ -41,16 +46,20 @@
                 parts: [
                     IndexPart {
                         span: virtual:<test>:24-28,
-                        value: Str(
-                            "test",
+                        value: Trivial(
+                            Str(
+                                "test",
+                            ),
                         ),
                     },
                 ],
             },
             ArgsDesc {
                 unnamed: [
-                    Num(
-                        2.0,
+                    Trivial(
+                        Num(
+                            2.0,
+                        ),
                     ),
                 ],
                 names: [],