git.delta.rocks / jrsonnet / refs/commits / 76f0cfd3dd2f

difftreelog

feat --exp-apply argument

Yaroslav Bolyukin2023-07-26parent: #0f5ef44.patch.diff
in: master

3 files changed

modifiedcmds/jrsonnet/Cargo.tomldiffbeforeafterboth
--- a/cmds/jrsonnet/Cargo.toml
+++ b/cmds/jrsonnet/Cargo.toml
@@ -8,7 +8,7 @@
 edition = "2021"
 
 [features]
-experimental = ["exp-preserve-order", "exp-destruct"]
+experimental = ["exp-preserve-order", "exp-destruct", "exp-null-coaelse", "exp-object-iteration", "exp-bigint", "exp-apply"]
 # Use mimalloc as allocator
 mimalloc = ["mimallocator"]
 # Experimental feature, which allows to preserve order of object fields
@@ -22,6 +22,10 @@
 exp-object-iteration = ["jrsonnet-evaluator/exp-object-iteration"]
 # Bigint type
 exp-bigint = ["jrsonnet-evaluator/exp-bigint", "jrsonnet-cli/exp-bigint"]
+# obj?.field, obj?.['field']
+exp-null-coaelse = ["jrsonnet-evaluator/exp-null-coaelse", "jrsonnet-parser/exp-null-coaelse"]
+# --exp-apply
+exp-apply = []
 
 # std.thisFile support
 legacy-this-file = ["jrsonnet-cli/legacy-this-file"]
modifiedcmds/jrsonnet/src/main.rsdiffbeforeafterboth
before · cmds/jrsonnet/src/main.rs
1use std::{2	fs::{create_dir_all, File},3	io::{Read, Write},4};56use clap::{CommandFactory, Parser};7use clap_complete::Shell;8use jrsonnet_cli::{GcOpts, ManifestOpts, MiscOpts, OutputOpts, StdOpts, TlaOpts, TraceOpts};9use jrsonnet_evaluator::{10	apply_tla,11	error::{Error as JrError, ErrorKind},12	throw, ResultExt, State, Val,13};1415#[cfg(feature = "mimalloc")]16#[global_allocator]17static GLOBAL: mimallocator::Mimalloc = mimallocator::Mimalloc;1819#[derive(Parser)]20enum SubOpts {21	/// Generate completions for specified shell22	Generate {23		/// Target shell name24		shell: Shell,25	},26}2728#[derive(Parser)]29#[clap(next_help_heading = "DEBUG")]30struct DebugOpts {31	/// Required OS stack size.32	/// This shouldn't be changed unless jrsonnet is failing with stack overflow error.33	#[clap(long, name = "size")]34	pub os_stack: Option<usize>,35}3637#[derive(Parser)]38#[clap(next_help_heading = "INPUT")]39struct InputOpts {40	/// Treat input as code, evaluate them instead of reading file41	#[clap(long, short = 'e')]42	pub exec: bool,4344	/// Path to the file to be compiled if `--evaluate` is unset, otherwise code itself45	pub input: Option<String>,46}4748/// Jsonnet commandline interpreter (Rust implementation)49#[derive(Parser)]50#[clap(51	args_conflicts_with_subcommands = true,52	disable_version_flag = true,53	version,54	author55)]56struct Opts {57	#[clap(subcommand)]58	sub: Option<SubOpts>,59	/// Print version60	#[clap(long)]61	version: bool,6263	#[clap(flatten)]64	input: InputOpts,65	#[clap(flatten)]66	misc: MiscOpts,67	#[clap(flatten)]68	tla: TlaOpts,69	#[clap(flatten)]70	std: StdOpts,71	#[clap(flatten)]72	gc: GcOpts,7374	#[clap(flatten)]75	trace: TraceOpts,76	#[clap(flatten)]77	manifest: ManifestOpts,78	#[clap(flatten)]79	output: OutputOpts,80	#[clap(flatten)]81	debug: DebugOpts,82}8384fn main() {85	let opts: Opts = Opts::parse();8687	if opts.version {88		print!("{}", Opts::command().render_version());89		std::process::exit(0)90	}9192	if let Some(sub) = opts.sub {93		match sub {94			SubOpts::Generate { shell } => {95				use clap_complete::generate;96				let app = &mut Opts::command();97				let buf = &mut std::io::stdout();98				generate(shell, app, "jrsonnet", buf);99				std::process::exit(0)100			}101		}102	}103104	let success = if let Some(size) = opts.debug.os_stack {105		std::thread::Builder::new()106			.stack_size(size * 1024 * 1024)107			.spawn(|| main_catch(opts))108			.expect("new thread spawned")109			.join()110			.expect("thread finished successfully")111	} else {112		main_catch(opts)113	};114	if !success {115		std::process::exit(1);116	}117}118119#[derive(thiserror::Error, Debug)]120enum Error {121	// Handled differently122	#[error("evaluation error")]123	Evaluation(JrError),124	#[error("io error")]125	Io(#[from] std::io::Error),126	#[error("input is not utf8 encoded")]127	Utf8(#[from] std::str::Utf8Error),128	#[error("missing input argument")]129	MissingInputArgument,130}131impl From<JrError> for Error {132	fn from(e: JrError) -> Self {133		Self::Evaluation(e)134	}135}136impl From<ErrorKind> for Error {137	fn from(e: ErrorKind) -> Self {138		Self::from(JrError::from(e))139	}140}141142fn main_catch(opts: Opts) -> bool {143	let s = State::default();144	let trace = opts.trace.trace_format();145	if let Err(e) = main_real(&s, opts) {146		if let Error::Evaluation(e) = e {147			let mut out = String::new();148			trace.write_trace(&mut out, &e).expect("format error");149			eprintln!("{out}")150		} else {151			eprintln!("{e}");152		}153		return false;154	}155	true156}157158fn main_real(s: &State, opts: Opts) -> Result<(), Error> {159	let _gc_leak_guard = opts.gc.leak_on_exit();160	let _gc_print_stats = opts.gc.stats_printer();161	let _stack_depth_override = opts.misc.stack_size_override();162163	let import_resolver = opts.misc.import_resolver();164	s.set_import_resolver(import_resolver);165166	let std = opts.std.context_initializer(s)?;167	if let Some(std) = std {168		s.set_context_initializer(std);169	}170171	let input = opts.input.input.ok_or(Error::MissingInputArgument)?;172	let val = if opts.input.exec {173		s.evaluate_snippet("<cmdline>".to_owned(), &input as &str)?174	} else if input == "-" {175		let mut input = Vec::new();176		std::io::stdin().read_to_end(&mut input)?;177		let input_str = std::str::from_utf8(&input)?;178		s.evaluate_snippet("<stdin>".to_owned(), input_str)?179	} else {180		s.import(&input)?181	};182183	let tla = opts.tla.tla_opts()?;184	let val = apply_tla(s.clone(), &tla, val)?;185186	let manifest_format = opts.manifest.manifest_format();187	if let Some(multi) = opts.output.multi {188		if opts.output.create_output_dirs {189			let mut dir = multi.clone();190			dir.pop();191			create_dir_all(dir)?;192		}193		let Val::Obj(obj) = val else {194			throw!("value should be object for --multi manifest, got {}", val.value_type())195		};196		for (field, data) in obj.iter(197			#[cfg(feature = "exp-preserve-order")]198			opts.manifest.preserve_order,199		) {200			let data = data.with_description(|| format!("getting field {field} for manifest"))?;201202			let mut path = multi.clone();203			path.push(&field as &str);204			if opts.output.create_output_dirs {205				let mut dir = path.clone();206				dir.pop();207				create_dir_all(dir)?;208			}209			println!("{}", path.to_str().expect("path"));210			let mut file = File::create(path)?;211			write!(212				file,213				"{}",214				data.manifest(&manifest_format)215					.with_description(|| format!("manifesting {field}"))?,216			)?;217			if manifest_format.file_trailing_newline() {218				writeln!(file)?;219			}220			file.flush()?;221		}222	} else if let Some(path) = opts.output.output_file {223		if opts.output.create_output_dirs {224			let mut dir = path.clone();225			dir.pop();226			create_dir_all(dir)?;227		}228		let mut file = File::create(path)?;229		writeln!(file, "{}", val.manifest(manifest_format)?)?;230	} else {231		let output = val.manifest(manifest_format)?;232		if !output.is_empty() {233			println!("{output}");234		}235	}236237	Ok(())238}
after · cmds/jrsonnet/src/main.rs
1use std::{2	fs::{create_dir_all, File},3	io::{Read, Write},4};56use clap::{CommandFactory, Parser};7use clap_complete::Shell;8use jrsonnet_cli::{GcOpts, ManifestOpts, MiscOpts, OutputOpts, StdOpts, TlaOpts, TraceOpts};9use jrsonnet_evaluator::{10	apply_tla,11	error::{Error as JrError, ErrorKind},12	throw, ResultExt, State, Val,13};1415#[cfg(feature = "mimalloc")]16#[global_allocator]17static GLOBAL: mimallocator::Mimalloc = mimallocator::Mimalloc;1819#[derive(Parser)]20enum SubOpts {21	/// Generate completions for specified shell22	Generate {23		/// Target shell name24		shell: Shell,25	},26}2728#[derive(Parser)]29#[clap(next_help_heading = "DEBUG")]30struct DebugOpts {31	/// Required OS stack size.32	/// This shouldn't be changed unless jrsonnet is failing with stack overflow error.33	#[clap(long, name = "size")]34	pub os_stack: Option<usize>,35}3637#[derive(Parser)]38#[clap(next_help_heading = "INPUT")]39struct InputOpts {40	/// Treat input as code, evaluate them instead of reading file41	#[clap(long, short = 'e')]42	pub exec: bool,4344	/// Path to the file to be compiled if `--evaluate` is unset, otherwise code itself45	pub input: Option<String>,4647	/// After executing input, apply specified code.48	/// Output of the initial input will be accessible using `$`49	#[cfg(feature = "exp-apply")]50	#[clap(long)]51	pub exp_apply: Vec<String>,52}5354/// Jsonnet commandline interpreter (Rust implementation)55#[derive(Parser)]56#[clap(57	args_conflicts_with_subcommands = true,58	disable_version_flag = true,59	version,60	author61)]62struct Opts {63	#[clap(subcommand)]64	sub: Option<SubOpts>,65	/// Print version66	#[clap(long)]67	version: bool,6869	#[clap(flatten)]70	input: InputOpts,71	#[clap(flatten)]72	misc: MiscOpts,73	#[clap(flatten)]74	tla: TlaOpts,75	#[clap(flatten)]76	std: StdOpts,77	#[clap(flatten)]78	gc: GcOpts,7980	#[clap(flatten)]81	trace: TraceOpts,82	#[clap(flatten)]83	manifest: ManifestOpts,84	#[clap(flatten)]85	output: OutputOpts,86	#[clap(flatten)]87	debug: DebugOpts,88}8990fn main() {91	let opts: Opts = Opts::parse();9293	if opts.version {94		print!("{}", Opts::command().render_version());95		std::process::exit(0)96	}9798	if let Some(sub) = opts.sub {99		match sub {100			SubOpts::Generate { shell } => {101				use clap_complete::generate;102				let app = &mut Opts::command();103				let buf = &mut std::io::stdout();104				generate(shell, app, "jrsonnet", buf);105				std::process::exit(0)106			}107		}108	}109110	let success = if let Some(size) = opts.debug.os_stack {111		std::thread::Builder::new()112			.stack_size(size * 1024 * 1024)113			.spawn(|| main_catch(opts))114			.expect("new thread spawned")115			.join()116			.expect("thread finished successfully")117	} else {118		main_catch(opts)119	};120	if !success {121		std::process::exit(1);122	}123}124125#[derive(thiserror::Error, Debug)]126enum Error {127	// Handled differently128	#[error("evaluation error")]129	Evaluation(JrError),130	#[error("io error")]131	Io(#[from] std::io::Error),132	#[error("input is not utf8 encoded")]133	Utf8(#[from] std::str::Utf8Error),134	#[error("missing input argument")]135	MissingInputArgument,136}137impl From<JrError> for Error {138	fn from(e: JrError) -> Self {139		Self::Evaluation(e)140	}141}142impl From<ErrorKind> for Error {143	fn from(e: ErrorKind) -> Self {144		Self::from(JrError::from(e))145	}146}147148fn main_catch(opts: Opts) -> bool {149	let s = State::default();150	let trace = opts.trace.trace_format();151	if let Err(e) = main_real(&s, opts) {152		if let Error::Evaluation(e) = e {153			let mut out = String::new();154			trace.write_trace(&mut out, &e).expect("format error");155			eprintln!("{out}")156		} else {157			eprintln!("{e}");158		}159		return false;160	}161	true162}163164fn main_real(s: &State, opts: Opts) -> Result<(), Error> {165	let _gc_leak_guard = opts.gc.leak_on_exit();166	let _gc_print_stats = opts.gc.stats_printer();167	let _stack_depth_override = opts.misc.stack_size_override();168169	let import_resolver = opts.misc.import_resolver();170	s.set_import_resolver(import_resolver);171172	let std = opts.std.context_initializer(s)?;173	if let Some(std) = std {174		s.set_context_initializer(std);175	}176177	let input = opts.input.input.ok_or(Error::MissingInputArgument)?;178	let val = if opts.input.exec {179		s.evaluate_snippet("<cmdline>".to_owned(), &input as &str)?180	} else if input == "-" {181		let mut input = Vec::new();182		std::io::stdin().read_to_end(&mut input)?;183		let input_str = std::str::from_utf8(&input)?;184		s.evaluate_snippet("<stdin>".to_owned(), input_str)?185	} else {186		s.import(&input)?187	};188189	let tla = opts.tla.tla_opts()?;190	#[allow(unused_mut)]191	let mut val = apply_tla(s.clone(), &tla, val)?;192193	#[cfg(feature = "exp-apply")]194	for apply in opts.input.exp_apply {195		use jrsonnet_evaluator::{InitialUnderscore, Thunk};196		val = s.evaluate_snippet_with(197			"<exp_apply>".to_owned(),198			&apply,199			InitialUnderscore(Thunk::evaluated(val)),200		)?;201	}202203	let manifest_format = opts.manifest.manifest_format();204	if let Some(multi) = opts.output.multi {205		if opts.output.create_output_dirs {206			let mut dir = multi.clone();207			dir.pop();208			create_dir_all(dir)?;209		}210		let Val::Obj(obj) = val else {211			throw!("value should be object for --multi manifest, got {}", val.value_type())212		};213		for (field, data) in obj.iter(214			#[cfg(feature = "exp-preserve-order")]215			opts.manifest.preserve_order,216		) {217			let data = data.with_description(|| format!("getting field {field} for manifest"))?;218219			let mut path = multi.clone();220			path.push(&field as &str);221			if opts.output.create_output_dirs {222				let mut dir = path.clone();223				dir.pop();224				create_dir_all(dir)?;225			}226			println!("{}", path.to_str().expect("path"));227			let mut file = File::create(path)?;228			write!(229				file,230				"{}",231				data.manifest(&manifest_format)232					.with_description(|| format!("manifesting {field}"))?,233			)?;234			if manifest_format.file_trailing_newline() {235				writeln!(file)?;236			}237			file.flush()?;238		}239	} else if let Some(path) = opts.output.output_file {240		if opts.output.create_output_dirs {241			let mut dir = path.clone();242			dir.pop();243			create_dir_all(dir)?;244		}245		let mut file = File::create(path)?;246		writeln!(file, "{}", val.manifest(manifest_format)?)?;247	} else {248		let output = val.manifest(manifest_format)?;249		if !output.is_empty() {250			println!("{output}");251		}252	}253254	Ok(())255}
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -154,6 +154,37 @@
 	}
 }
 
+macro_rules! impl_context_initializer {
+	($($gen:ident)*) => {
+		#[allow(non_snake_case)]
+		impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {
+			fn reserve_vars(&self) -> usize {
+				let mut out = 0;
+				let ($($gen,)*) = self;
+				$(out += $gen.reserve_vars();)*
+				out
+			}
+			fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
+				let ($($gen,)*) = self;
+				$($gen.populate(for_file.clone(), builder);)*
+			}
+			fn as_any(&self) -> &dyn Any {
+				self
+			}
+		}
+	};
+	($($cur:ident)* @ $c:ident $($rest:ident)*) => {
+		impl_context_initializer!($($cur)*);
+		impl_context_initializer!($($cur)* $c @ $($rest)*);
+	};
+	($($cur:ident)* @) => {
+		impl_context_initializer!($($cur)*);
+	}
+}
+impl_context_initializer! {
+	A B @ C D E
+}
+
 /// Dynamically reconfigurable evaluation settings
 #[derive(Trace)]
 pub struct EvaluationSettings {
@@ -361,6 +392,23 @@
 		context_initializer.initialize(self.clone(), source)
 	}
 
+	/// Creates context with all passed global variables, calling custom modifier
+	pub fn create_default_context_with(
+		&self,
+		source: Source,
+		context_initializer: impl ContextInitializer,
+	) -> Context {
+		let default_initializer = &self.settings().context_initializer;
+		let mut builder = ContextBuilder::with_capacity(
+			self.clone(),
+			default_initializer.reserve_vars() + context_initializer.reserve_vars(),
+		);
+		default_initializer.populate(source.clone(), &mut builder);
+		context_initializer.populate(source, &mut builder);
+
+		builder.build()
+	}
+
 	/// Executes code creating a new stack frame
 	pub fn push<T>(
 		e: CallLocation<'_>,
@@ -428,25 +476,34 @@
 		}
 		let mut settings = self.settings_mut();
 		let initializer = &mut settings.context_initializer;
-		match initializer.as_any().downcast_ref::<GlobalsCtx>() {
-			Some(glob) => {
-				glob.globals.borrow_mut().insert(name, value);
-			}
-			None => {
-				let inner = std::mem::replace(&mut settings.context_initializer, tb!(()));
-				settings.context_initializer = tb!(GlobalsCtx {
-					globals: {
-						let mut out = GcHashMap::with_capacity(1);
-						out.insert(name, value);
-						RefCell::new(out)
-					},
-					inner
-				})
-			}
+		if let Some(global) = initializer.as_any().downcast_ref::<GlobalsCtx>() {
+			global.globals.borrow_mut().insert(name, value);
+		} else {
+			let inner = std::mem::replace(&mut settings.context_initializer, tb!(()));
+			settings.context_initializer = tb!(GlobalsCtx {
+				globals: {
+					let mut out = GcHashMap::with_capacity(1);
+					out.insert(name, value);
+					RefCell::new(out)
+				},
+				inner
+			});
 		}
 	}
 }
 
+#[derive(Trace)]
+pub struct InitialUnderscore(pub Thunk<Val>);
+impl ContextInitializer for InitialUnderscore {
+	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+		builder.bind("_".into(), self.0.clone());
+	}
+
+	fn as_any(&self) -> &dyn Any {
+		self
+	}
+}
+
 /// Raw methods evaluate passed values but don't perform TLA execution
 impl State {
 	/// Parses and evaluates the given snippet
@@ -465,6 +522,30 @@
 		})?;
 		evaluate(self.create_default_context(source), &parsed)
 	}
+	/// Parses and evaluates the given snippet with custom context modifier
+	pub fn evaluate_snippet_with(
+		&self,
+		name: impl Into<IStr>,
+		code: impl Into<IStr>,
+		context_initializer: impl ContextInitializer,
+	) -> Result<Val> {
+		let code = code.into();
+		let source = Source::new_virtual(name.into(), code.clone());
+		let parsed = jrsonnet_parser::parse(
+			&code,
+			&ParserSettings {
+				source: source.clone(),
+			},
+		)
+		.map_err(|e| ImportSyntaxError {
+			path: source.clone(),
+			error: Box::new(e),
+		})?;
+		evaluate(
+			self.create_default_context_with(source, context_initializer),
+			&parsed,
+		)
+	}
 }
 
 /// Settings utilities