git.delta.rocks / jrsonnet / refs/commits / 78d82dfdf2a1

difftreelog

feat derive(Typed) for struct

Yaroslav Bolyukin2022-04-04parent: #d710b4f.patch.diff
in: master

9 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -70,8 +70,9 @@
 
 [[package]]
 name = "clap"
-version = "3.0.0-beta.2"
-source = "git+https://github.com/clap-rs/clap?rev=f0c5ea5e1503de5c8e74d8c047a799cf51498e83#f0c5ea5e1503de5c8e74d8c047a799cf51498e83"
+version = "3.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c"
 dependencies = [
  "atty",
  "bitflags",
@@ -82,27 +83,28 @@
  "strsim",
  "termcolor",
  "textwrap",
- "vec_map",
 ]
 
 [[package]]
+name = "clap_complete"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df6f3613c0a3cddfd78b41b10203eb322cb29b600cbdf808a7d3db95691b8e25"
+dependencies = [
+ "clap",
+]
+
+[[package]]
 name = "clap_derive"
-version = "3.0.0-beta.2"
-source = "git+https://github.com/clap-rs/clap?rev=f0c5ea5e1503de5c8e74d8c047a799cf51498e83#f0c5ea5e1503de5c8e74d8c047a799cf51498e83"
+version = "3.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1"
 dependencies = [
  "heck",
  "proc-macro-error",
  "proc-macro2",
  "quote",
  "syn",
-]
-
-[[package]]
-name = "clap_generate"
-version = "3.0.0-beta.2"
-source = "git+https://github.com/clap-rs/clap?rev=f0c5ea5e1503de5c8e74d8c047a799cf51498e83#f0c5ea5e1503de5c8e74d8c047a799cf51498e83"
-dependencies = [
- "clap",
 ]
 
 [[package]]
@@ -148,12 +150,9 @@
 
 [[package]]
 name = "heck"
-version = "0.3.3"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
 
 [[package]]
 name = "hermit-abi"
@@ -185,7 +184,7 @@
 version = "0.4.2"
 dependencies = [
  "clap",
- "clap_generate",
+ "clap_complete",
  "gcmodule",
  "jrsonnet-cli",
  "jrsonnet-evaluator",
@@ -214,6 +213,7 @@
  "bincode",
  "gcmodule",
  "jrsonnet-interner",
+ "jrsonnet-macros",
  "jrsonnet-parser",
  "jrsonnet-stdlib",
  "jrsonnet-types",
@@ -236,6 +236,15 @@
 ]
 
 [[package]]
+name = "jrsonnet-macros"
+version = "0.4.2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "jrsonnet-parser"
 version = "0.4.2"
 dependencies = [
@@ -301,6 +310,12 @@
 checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
 
 [[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
 name = "mimalloc-sys"
 version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -321,9 +336,12 @@
 
 [[package]]
 name = "os_str_bytes"
-version = "3.1.0"
+version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+dependencies = [
+ "memchr",
+]
 
 [[package]]
 name = "parking_lot"
@@ -357,9 +375,9 @@
 
 [[package]]
 name = "peg"
-version = "0.7.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a"
+checksum = "af728fe826811af3b38c37e93de6d104485953ea373d656eebae53d6987fcd2c"
 dependencies = [
  "peg-macros",
  "peg-runtime",
@@ -367,9 +385,9 @@
 
 [[package]]
 name = "peg-macros"
-version = "0.7.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c"
+checksum = "4536be147b770b824895cbad934fccce8e49f14b4c4946eaa46a6e4a12fcdc16"
 dependencies = [
  "peg-runtime",
  "proc-macro2",
@@ -378,9 +396,9 @@
 
 [[package]]
 name = "peg-runtime"
-version = "0.7.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088"
+checksum = "f9b0efd3ba03c3a409d44d60425f279ec442bcf0b9e63ff4e410da31c8b0f69f"
 
 [[package]]
 name = "proc-macro-error"
@@ -536,12 +554,9 @@
 
 [[package]]
 name = "textwrap"
-version = "0.14.2"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
-dependencies = [
- "unicode-width",
-]
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
 
 [[package]]
 name = "thiserror"
@@ -562,12 +577,6 @@
  "quote",
  "syn",
 ]
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
 
 [[package]]
 name = "unicode-width"
@@ -580,12 +589,6 @@
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
-
-[[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 
 [[package]]
 name = "version_check"
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -179,7 +179,7 @@
 	}
 }
 
-pub type Result<V> = std::result::Result<V, LocError>;
+pub type Result<V, E = LocError> = std::result::Result<V, E>;
 
 #[macro_export]
 macro_rules! throw {
modifiedcrates/jrsonnet-evaluator/src/function.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function.rs
+++ b/crates/jrsonnet-evaluator/src/function.rs
@@ -8,6 +8,7 @@
 };
 use gcmodule::Trace;
 use jrsonnet_interner::IStr;
+pub use jrsonnet_macros::builtin;
 use jrsonnet_parser::{ArgsDesc, ExprLocation, LocExpr, ParamsDesc};
 use std::{borrow::Cow, collections::HashMap, convert::TryFrom};
 
@@ -377,6 +378,7 @@
 	pub has_default: bool,
 }
 
+/// Do not implement it directly, instead use #[builtin] macro
 pub trait Builtin: Trace {
 	fn name(&self) -> &str;
 	fn params(&self) -> &[BuiltinParam];
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -174,7 +174,11 @@
 	pub(crate) static EVAL_STATE: RefCell<Option<EvaluationState>> = RefCell::new(None)
 }
 pub(crate) fn with_state<T>(f: impl FnOnce(&EvaluationState) -> T) -> T {
-	EVAL_STATE.with(|s| f(s.borrow().as_ref().unwrap()))
+	EVAL_STATE.with(|s| {
+		f(s.borrow().as_ref().expect(
+			"missing evaluation state, some functions should be called inside of run_in_state call",
+		))
+	})
 }
 pub fn push_frame<T>(
 	e: Option<&ExprLocation>,
@@ -728,12 +732,15 @@
 	}
 
 	macro_rules! eval {
-		($str: expr) => {
-			EvaluationState::default()
-				.with_stdlib()
-				.evaluate_snippet_raw(PathBuf::from("raw.jsonnet").into(), $str.into())
-				.unwrap()
-		};
+		($str: expr) => {{
+			let evaluator = EvaluationState::default();
+			evaluator.with_stdlib();
+			evaluator.run_in_state(|| {
+				evaluator
+					.evaluate_snippet_raw(PathBuf::from("raw.jsonnet").into(), $str.into())
+					.unwrap()
+			})
+		}};
 	}
 	macro_rules! eval_json {
 		($str: expr) => {{
@@ -1265,4 +1272,47 @@
 		assert_eval!(r#"std.assertEqual(std.count(["a", "b", "a"], "d"), 0)"#);
 		assert_eval!(r#"std.assertEqual(std.count(["a", "b", "a"], "a"), 2)"#);
 	}
+
+	mod derive_typed {
+		use crate::{typed::Typed, EvaluationState};
+		use std::path::PathBuf;
+
+		#[derive(Typed, PartialEq, Debug)]
+		struct MyTyped {
+			a: u32,
+			b: String,
+		}
+
+		#[test]
+		fn test() {
+			let es = EvaluationState::default();
+			let val = eval!("{a: 14, b: 'Hello, world!'}");
+			let typed = es.run_in_state(|| MyTyped::try_from(val).unwrap());
+
+			assert_eq!(
+				typed,
+				MyTyped {
+					a: 14,
+					b: "Hello, world!".to_string()
+				}
+			);
+			es.settings_mut().globals.insert(
+				"mytyped".into(),
+				es.run_in_state(|| typed.try_into()).unwrap(),
+			);
+
+			let v = es
+				.evaluate_snippet_raw(
+					PathBuf::from("raw.jsonnet").into(),
+					"
+				mytyped == {a: 14, b: 'Hello, world!'}
+			"
+					.into(),
+				)
+				.unwrap()
+				.as_bool()
+				.unwrap();
+			assert!(v)
+		}
+	}
 }
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -1,6 +1,7 @@
 use std::convert::{TryFrom, TryInto};
 
 use jrsonnet_interner::IStr;
+pub use jrsonnet_macros::Typed;
 use jrsonnet_types::{ComplexValType, ValType};
 
 use crate::{
modifiedcrates/jrsonnet-evaluator/src/typed/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/typed/mod.rs
1use std::{fmt::Display, rc::Rc};23mod conversions;4pub use conversions::*;56use crate::{7	error::{Error, LocError, Result},8	push_description_frame, Val,9};10use gcmodule::Trace;11use jrsonnet_types::{ComplexValType, ValType};12use thiserror::Error;1314#[macro_export]15macro_rules! unwrap_type {16	($desc:expr, $value:expr, $typ:expr => $match:path) => {{17		use $crate::{push_frame, typed::CheckType};18		push_frame(None, $desc, || Ok($typ.check(&$value)?))?;19		match $value {20			$match(v) => v,21			_ => unreachable!(),22		}23	}};24}2526#[derive(Debug, Error, Clone, Trace)]27pub enum TypeError {28	#[error("expected {0}, got {1}")]29	ExpectedGot(ComplexValType, ValType),30	#[error("missing property {0} from {1:?}")]31	MissingProperty(#[skip_trace] Rc<str>, ComplexValType),32	#[error("every failed from {0}:\n{1}")]33	UnionFailed(ComplexValType, TypeLocErrorList),34	#[error(35		"number out of bounds: {0} not in {}..{}",36		.1.map(|v|v.to_string()).unwrap_or_else(|| "".to_owned()),37		.2.map(|v|v.to_string()).unwrap_or_else(|| "".to_owned()),38	)]39	BoundsFailed(f64, Option<f64>, Option<f64>),40}41impl From<TypeError> for LocError {42	fn from(e: TypeError) -> Self {43		Error::TypeError(e.into()).into()44	}45}4647#[derive(Debug, Clone, Trace)]48pub struct TypeLocError(Box<TypeError>, ValuePathStack);49impl From<TypeError> for TypeLocError {50	fn from(e: TypeError) -> Self {51		Self(Box::new(e), ValuePathStack(Vec::new()))52	}53}54impl From<TypeLocError> for LocError {55	fn from(e: TypeLocError) -> Self {56		Error::TypeError(e).into()57	}58}59impl Display for TypeLocError {60	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {61		write!(f, "{}", self.0)?;62		if !(self.1).0.is_empty() {63			write!(f, " at {}", self.1)?;64		}65		Ok(())66	}67}6869#[derive(Debug, Clone, Trace)]70pub struct TypeLocErrorList(Vec<TypeLocError>);71impl Display for TypeLocErrorList {72	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {73		use std::fmt::Write;74		let mut out = String::new();75		for (i, err) in self.0.iter().enumerate() {76			if i != 0 {77				writeln!(f)?;78			}79			out.clear();80			write!(out, "{}", err)?;8182			for (i, line) in out.lines().enumerate() {83				if line.trim().is_empty() {84					continue;85				}86				if i != 0 {87					writeln!(f)?;88					write!(f, "    ")?;89				} else {90					write!(f, "  - ")?;91				}92				write!(f, "{}", line)?;93			}94		}95		Ok(())96	}97}9899fn push_type_description(100	error_reason: impl Fn() -> String,101	path: impl Fn() -> ValuePathItem,102	item: impl Fn() -> Result<()>,103) -> Result<()> {104	push_description_frame(error_reason, || match item() {105		Ok(_) => Ok(()),106		Err(mut e) => {107			if let Error::TypeError(e) = &mut e.error_mut() {108				(e.1).0.push(path())109			}110			Err(e)111		}112	})113}114115// TODO: check_fast for fast path of union type checking116pub trait CheckType {117	fn check(&self, value: &Val) -> Result<()>;118}119120impl CheckType for ValType {121	fn check(&self, value: &Val) -> Result<()> {122		let got = value.value_type();123		if got != *self {124			let loc_error: TypeLocError = TypeError::ExpectedGot((*self).into(), got).into();125			return Err(loc_error.into());126		}127		Ok(())128	}129}130131#[derive(Clone, Debug, Trace)]132enum ValuePathItem {133	Field(#[skip_trace] Rc<str>),134	Index(u64),135}136impl Display for ValuePathItem {137	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {138		match self {139			Self::Field(name) => write!(f, ".{}", name)?,140			Self::Index(idx) => write!(f, "[{}]", idx)?,141		}142		Ok(())143	}144}145146#[derive(Clone, Debug, Trace)]147struct ValuePathStack(Vec<ValuePathItem>);148impl Display for ValuePathStack {149	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {150		write!(f, "self")?;151		for elem in self.0.iter().rev() {152			write!(f, "{}", elem)?;153		}154		Ok(())155	}156}157158impl CheckType for ComplexValType {159	fn check(&self, value: &Val) -> Result<()> {160		match self {161			Self::Any => Ok(()),162			Self::Simple(s) => s.check(value),163			Self::Char => match value {164				Val::Str(s) if s.len() == 1 || s.chars().count() == 1 => Ok(()),165				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),166			},167			Self::BoundedNumber(from, to) => {168				if let Val::Num(n) = value {169					if from.map(|from| from > *n).unwrap_or(false)170						|| to.map(|to| to < *n).unwrap_or(false)171					{172						return Err(TypeError::BoundsFailed(*n, *from, *to).into());173					}174					Ok(())175				} else {176					Err(TypeError::ExpectedGot(self.clone(), value.value_type()).into())177				}178			}179			Self::Array(elem_type) => match value {180				Val::Arr(a) => {181					for (i, item) in a.iter().enumerate() {182						push_type_description(183							|| format!("array index {}", i),184							|| ValuePathItem::Index(i as u64),185							|| elem_type.check(&item.clone()?),186						)?;187					}188					Ok(())189				}190				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),191			},192			Self::ArrayRef(elem_type) => match value {193				Val::Arr(a) => {194					for (i, item) in a.iter().enumerate() {195						push_type_description(196							|| format!("array index {}", i),197							|| ValuePathItem::Index(i as u64),198							|| elem_type.check(&item.clone()?),199						)?;200					}201					Ok(())202				}203				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),204			},205			Self::ObjectRef(elems) => match value {206				Val::Obj(obj) => {207					for (k, v) in elems.iter() {208						if let Some(got_v) = obj.get((*k).into())? {209							push_type_description(210								|| format!("property {}", k),211								|| ValuePathItem::Field((*k).into()),212								|| v.check(&got_v),213							)?214						} else {215							return Err(216								TypeError::MissingProperty((*k).into(), self.clone()).into()217							);218						}219					}220					Ok(())221				}222				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),223			},224			Self::Union(types) => {225				let mut errors = Vec::new();226				for ty in types.iter() {227					match ty.check(value) {228						Ok(()) => {229							return Ok(());230						}231						Err(e) => match e.error() {232							Error::TypeError(e) => errors.push(e.clone()),233							_ => return Err(e),234						},235					}236				}237				Err(TypeError::UnionFailed(self.clone(), TypeLocErrorList(errors)).into())238			}239			Self::UnionRef(types) => {240				let mut errors = Vec::new();241				for ty in types.iter() {242					match ty.check(value) {243						Ok(()) => {244							return Ok(());245						}246						Err(e) => match e.error() {247							Error::TypeError(e) => errors.push(e.clone()),248							_ => return Err(e),249						},250					}251				}252				Err(TypeError::UnionFailed(self.clone(), TypeLocErrorList(errors)).into())253			}254			Self::Sum(types) => {255				for ty in types.iter() {256					ty.check(value)?257				}258				Ok(())259			}260			Self::SumRef(types) => {261				for ty in types.iter() {262					ty.check(value)?263				}264				Ok(())265			}266		}267	}268}
after · crates/jrsonnet-evaluator/src/typed/mod.rs
1use std::{fmt::Display, rc::Rc};23mod conversions;4pub use conversions::*;56use crate::{7	error::{Error, LocError, Result},8	push_description_frame, Val,9};10use gcmodule::Trace;11pub use jrsonnet_types::{ComplexValType, ValType};12use thiserror::Error;1314#[derive(Debug, Error, Clone, Trace)]15pub enum TypeError {16	#[error("expected {0}, got {1}")]17	ExpectedGot(ComplexValType, ValType),18	#[error("missing property {0} from {1:?}")]19	MissingProperty(#[skip_trace] Rc<str>, ComplexValType),20	#[error("every failed from {0}:\n{1}")]21	UnionFailed(ComplexValType, TypeLocErrorList),22	#[error(23		"number out of bounds: {0} not in {}..{}",24		.1.map(|v|v.to_string()).unwrap_or_else(|| "".to_owned()),25		.2.map(|v|v.to_string()).unwrap_or_else(|| "".to_owned()),26	)]27	BoundsFailed(f64, Option<f64>, Option<f64>),28}29impl From<TypeError> for LocError {30	fn from(e: TypeError) -> Self {31		Error::TypeError(e.into()).into()32	}33}3435#[derive(Debug, Clone, Trace)]36pub struct TypeLocError(Box<TypeError>, ValuePathStack);37impl From<TypeError> for TypeLocError {38	fn from(e: TypeError) -> Self {39		Self(Box::new(e), ValuePathStack(Vec::new()))40	}41}42impl From<TypeLocError> for LocError {43	fn from(e: TypeLocError) -> Self {44		Error::TypeError(e).into()45	}46}47impl Display for TypeLocError {48	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {49		write!(f, "{}", self.0)?;50		if !(self.1).0.is_empty() {51			write!(f, " at {}", self.1)?;52		}53		Ok(())54	}55}5657#[derive(Debug, Clone, Trace)]58pub struct TypeLocErrorList(Vec<TypeLocError>);59impl Display for TypeLocErrorList {60	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {61		use std::fmt::Write;62		let mut out = String::new();63		for (i, err) in self.0.iter().enumerate() {64			if i != 0 {65				writeln!(f)?;66			}67			out.clear();68			write!(out, "{}", err)?;6970			for (i, line) in out.lines().enumerate() {71				if line.trim().is_empty() {72					continue;73				}74				if i != 0 {75					writeln!(f)?;76					write!(f, "    ")?;77				} else {78					write!(f, "  - ")?;79				}80				write!(f, "{}", line)?;81			}82		}83		Ok(())84	}85}8687fn push_type_description(88	error_reason: impl Fn() -> String,89	path: impl Fn() -> ValuePathItem,90	item: impl Fn() -> Result<()>,91) -> Result<()> {92	push_description_frame(error_reason, || match item() {93		Ok(_) => Ok(()),94		Err(mut e) => {95			if let Error::TypeError(e) = &mut e.error_mut() {96				(e.1).0.push(path())97			}98			Err(e)99		}100	})101}102103// TODO: check_fast for fast path of union type checking104pub trait CheckType {105	fn check(&self, value: &Val) -> Result<()>;106}107108impl CheckType for ValType {109	fn check(&self, value: &Val) -> Result<()> {110		let got = value.value_type();111		if got != *self {112			let loc_error: TypeLocError = TypeError::ExpectedGot((*self).into(), got).into();113			return Err(loc_error.into());114		}115		Ok(())116	}117}118119#[derive(Clone, Debug, Trace)]120enum ValuePathItem {121	Field(#[skip_trace] Rc<str>),122	Index(u64),123}124impl Display for ValuePathItem {125	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {126		match self {127			Self::Field(name) => write!(f, ".{:?}", name)?,128			Self::Index(idx) => write!(f, "[{}]", idx)?,129		}130		Ok(())131	}132}133134#[derive(Clone, Debug, Trace)]135struct ValuePathStack(Vec<ValuePathItem>);136impl Display for ValuePathStack {137	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {138		write!(f, "self")?;139		for elem in self.0.iter().rev() {140			write!(f, "{}", elem)?;141		}142		Ok(())143	}144}145146impl CheckType for ComplexValType {147	fn check(&self, value: &Val) -> Result<()> {148		match self {149			Self::Any => Ok(()),150			Self::Simple(s) => s.check(value),151			Self::Char => match value {152				Val::Str(s) if s.len() == 1 || s.chars().count() == 1 => Ok(()),153				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),154			},155			Self::BoundedNumber(from, to) => {156				if let Val::Num(n) = value {157					if from.map(|from| from > *n).unwrap_or(false)158						|| to.map(|to| to < *n).unwrap_or(false)159					{160						return Err(TypeError::BoundsFailed(*n, *from, *to).into());161					}162					Ok(())163				} else {164					Err(TypeError::ExpectedGot(self.clone(), value.value_type()).into())165				}166			}167			Self::Array(elem_type) => match value {168				Val::Arr(a) => {169					for (i, item) in a.iter().enumerate() {170						push_type_description(171							|| format!("array index {}", i),172							|| ValuePathItem::Index(i as u64),173							|| elem_type.check(&item.clone()?),174						)?;175					}176					Ok(())177				}178				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),179			},180			Self::ArrayRef(elem_type) => match value {181				Val::Arr(a) => {182					for (i, item) in a.iter().enumerate() {183						push_type_description(184							|| format!("array index {}", i),185							|| ValuePathItem::Index(i as u64),186							|| elem_type.check(&item.clone()?),187						)?;188					}189					Ok(())190				}191				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),192			},193			Self::ObjectRef(elems) => match value {194				Val::Obj(obj) => {195					for (k, v) in elems.iter() {196						if let Some(got_v) = obj.get((*k).into())? {197							push_type_description(198								|| format!("property {}", k),199								|| ValuePathItem::Field((*k).into()),200								|| v.check(&got_v),201							)?202						} else {203							return Err(204								TypeError::MissingProperty((*k).into(), self.clone()).into()205							);206						}207					}208					Ok(())209				}210				v => Err(TypeError::ExpectedGot(self.clone(), v.value_type()).into()),211			},212			Self::Union(types) => {213				let mut errors = Vec::new();214				for ty in types.iter() {215					match ty.check(value) {216						Ok(()) => {217							return Ok(());218						}219						Err(e) => match e.error() {220							Error::TypeError(e) => errors.push(e.clone()),221							_ => return Err(e),222						},223					}224				}225				Err(TypeError::UnionFailed(self.clone(), TypeLocErrorList(errors)).into())226			}227			Self::UnionRef(types) => {228				let mut errors = Vec::new();229				for ty in types.iter() {230					match ty.check(value) {231						Ok(()) => {232							return Ok(());233						}234						Err(e) => match e.error() {235							Error::TypeError(e) => errors.push(e.clone()),236							_ => return Err(e),237						},238					}239				}240				Err(TypeError::UnionFailed(self.clone(), TypeLocErrorList(errors)).into())241			}242			Self::Sum(types) => {243				for ty in types.iter() {244					ty.check(value)?245				}246				Ok(())247			}248			Self::SumRef(types) => {249				for ty in types.iter() {250					ty.check(value)?251				}252				Ok(())253			}254		}255	}256}
modifiedcrates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -91,7 +91,7 @@
 	Normal(Cc<FuncDesc>),
 	/// Standard library function
 	StaticBuiltin(#[skip_trace] &'static dyn StaticBuiltin),
-
+	/// User-provided function
 	Builtin(Cc<TraceBox<dyn Builtin>>),
 }
 
@@ -99,8 +99,10 @@
 	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 		match self {
 			Self::Normal(arg0) => f.debug_tuple("Normal").field(arg0).finish(),
-			Self::StaticBuiltin(arg0) => f.debug_tuple("Intrinsic").field(&arg0.name()).finish(),
-			Self::Builtin(arg0) => f.debug_tuple("Intrinsic").field(&arg0.name()).finish(),
+			Self::StaticBuiltin(arg0) => {
+				f.debug_tuple("StaticBuiltin").field(&arg0.name()).finish()
+			}
+			Self::Builtin(arg0) => f.debug_tuple("Builtin").field(&arg0.name()).finish(),
 		}
 	}
 }
@@ -338,6 +340,49 @@
 }
 
 impl Val {
+	pub fn as_bool(&self) -> Option<bool> {
+		match self {
+			Val::Bool(v) => Some(*v),
+			_ => None,
+		}
+	}
+	pub fn as_null(&self) -> Option<()> {
+		match self {
+			Val::Null => Some(()),
+			_ => None,
+		}
+	}
+	pub fn as_str(&self) -> Option<IStr> {
+		match self {
+			Val::Str(s) => Some(s.clone()),
+			_ => None,
+		}
+	}
+	pub fn as_num(&self) -> Option<f64> {
+		match self {
+			Val::Num(n) => Some(*n),
+			_ => None,
+		}
+	}
+	pub fn as_arr(&self) -> Option<ArrValue> {
+		match self {
+			Val::Arr(a) => Some(a.clone()),
+			_ => None,
+		}
+	}
+	pub fn as_obj(&self) -> Option<ObjValue> {
+		match self {
+			Val::Obj(o) => Some(o.clone()),
+			_ => None,
+		}
+	}
+	pub fn as_func(&self) -> Option<FuncVal> {
+		match self {
+			Val::Func(f) => Some(f.clone()),
+			_ => None,
+		}
+	}
+
 	/// Creates `Val::Num` after checking for numeric overflow.
 	/// As numbers are `f64`, we can just check for their finity.
 	pub fn new_checked_num(num: f64) -> Result<Self> {
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -1,8 +1,8 @@
 use quote::{quote, quote_spanned};
 use syn::{
 	parenthesized, parse::Parse, parse_macro_input, punctuated::Punctuated, spanned::Spanned,
-	token::Comma, FnArg, GenericArgument, Ident, ItemFn, Pat, PatType, Path, PathArguments, Token,
-	Type,
+	token::Comma, DeriveInput, FnArg, GenericArgument, Ident, ItemFn, Pat, PatType, Path,
+	PathArguments, Token, Type,
 };
 
 fn is_location_arg(t: &PatType) -> bool {
@@ -254,3 +254,86 @@
 	})
 	.into()
 }
+
+#[proc_macro_derive(Typed)]
+pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+	let input = parse_macro_input!(item as DeriveInput);
+	let data = match &input.data {
+		syn::Data::Struct(s) => s,
+		_ => {
+			return syn::Error::new(input.span(), "only structs supported")
+				.to_compile_error()
+				.into()
+		}
+	};
+
+	let ident = &input.ident;
+
+	let fields_def = data.fields.iter().map(|f| {
+		let name = f
+			.ident
+			.as_ref()
+			.expect("only named fields supported")
+			.to_string();
+		let ty = &f.ty;
+		quote! {
+			(#name, #ty::TYPE),
+		}
+	});
+	let fields_parse = data.fields.iter().map(|f| {
+		let ident = f.ident.as_ref().unwrap();
+		let name = ident.to_string();
+		let ty = &f.ty;
+		quote! {
+			#ident: #ty::try_from(obj.get(#name.into())?.expect("shape is correct"))?,
+		}
+	});
+	let fields_serialize = data.fields.iter().map(|f| {
+		let ident = f.ident.as_ref().unwrap();
+		let name = ident.to_string();
+		quote! {
+			out.member(#name.into()).value(self.#ident.try_into()?);
+		}
+	});
+	let field_count = data.fields.len();
+
+	quote! {
+		const _: () = {
+			use ::jrsonnet_evaluator::{
+				typed::{ComplexValType, Typed, CheckType},
+				Val,
+				error::LocError,
+				obj::ObjValueBuilder,
+			};
+
+			const ITEMS: [(&'static str, &'static ComplexValType); #field_count] = [
+				#(#fields_def)*
+			];
+			impl Typed for #ident {
+				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&ITEMS);
+			}
+
+			impl TryFrom<Val> for #ident {
+				type Error = LocError;
+				fn try_from(value: Val) -> Result<Self, Self::Error> {
+					<Self as Typed>::TYPE.check(&value)?;
+					let obj = value.as_obj().expect("shape is correct");
+
+					Ok(Self {
+						#(#fields_parse)*
+					})
+				}
+			}
+			impl TryInto<Val> for #ident {
+				type Error = LocError;
+				fn try_into(self) -> Result<Val, Self::Error> {
+					let mut out = ObjValueBuilder::new();
+					#(#fields_serialize)*
+					Ok(Val::Obj(out.build()))
+				}
+			}
+			()
+		};
+	}
+	.into()
+}
modifiedcrates/jrsonnet-types/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-types/src/lib.rs
+++ b/crates/jrsonnet-types/src/lib.rs
@@ -122,7 +122,7 @@
 	BoundedNumber(Option<f64>, Option<f64>),
 	Array(Box<ComplexValType>),
 	ArrayRef(&'static ComplexValType),
-	ObjectRef(&'static [(&'static str, ComplexValType)]),
+	ObjectRef(&'static [(&'static str, &'static ComplexValType)]),
 	Union(Vec<ComplexValType>),
 	UnionRef(&'static [&'static ComplexValType]),
 	Sum(Vec<ComplexValType>),