git.delta.rocks / jrsonnet / refs/commits / 8439ee7839a3

difftreelog

feat async import building block

Yaroslav Bolyukin2022-12-07parent: #fc51ec0.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
before · crates/jrsonnet-evaluator/Cargo.toml
1[package]2name = "jrsonnet-evaluator"3description = "jsonnet interpreter"4version.workspace = true5authors = ["Yaroslav Bolyukin <iam@lach.pw>"]6license = "MIT"7edition = "2021"89[features]10default = ["explaining-traces", "friendly-errors"]11# Rustc-like trace visualization12explaining-traces = ["annotate-snippets"]13# Allows library authors to throw custom errors14anyhow-error = ["anyhow"]15# Provides helpful explaintations to errors, at cost of adding16# more dependencies and slowing down error path17friendly-errors = ["strsim"]1819# Allows to preserve field order in objects20exp-preserve-order = []21# Implements field destructuring22exp-destruct = ["jrsonnet-parser/exp-destruct"]23# Iteration over objects yields [key, value] elements24exp-object-iteration = []2526# Improves performance, and implements some useful things using nightly-only features27nightly = ["hashbrown/nightly"]2829[dependencies]30jrsonnet-interner.workspace = true31jrsonnet-parser.workspace = true32jrsonnet-types.workspace = true33jrsonnet-macros.workspace = true34jrsonnet-gcmodule = { version = "0.3.4" }3536pathdiff = "0.2.1"37hashbrown = "0.13.1"38static_assertions = "1.1"3940rustc-hash = "1.1"4142thiserror = "1.0"4344serde = "1.0"4546anyhow = { version = "1.0", optional = true }47# Friendly errors48strsim = { version = "0.10.0", optional = true }49# Serialized stdlib50bincode = { version = "1.3", optional = true }51# Explaining traces52annotate-snippets = { version = "0.9.1", features = ["color"], optional = true }
after · crates/jrsonnet-evaluator/Cargo.toml
1[package]2name = "jrsonnet-evaluator"3description = "jsonnet interpreter"4version.workspace = true5authors = ["Yaroslav Bolyukin <iam@lach.pw>"]6license = "MIT"7edition = "2021"89[features]10default = ["explaining-traces", "friendly-errors"]11# Rustc-like trace visualization12explaining-traces = ["annotate-snippets"]13# Allows library authors to throw custom errors14anyhow-error = ["anyhow"]15# Provides helpful explaintations to errors, at cost of adding16# more dependencies and slowing down error path17friendly-errors = ["strsim"]18# Adds ability to build import closure in async19async-import = ["async-trait"]2021# Allows to preserve field order in objects22exp-preserve-order = []23# Implements field destructuring24exp-destruct = ["jrsonnet-parser/exp-destruct"]25# Iteration over objects yields [key, value] elements26exp-object-iteration = []2728# Improves performance, and implements some useful things using nightly-only features29nightly = ["hashbrown/nightly"]3031[dependencies]32jrsonnet-interner.workspace = true33jrsonnet-parser.workspace = true34jrsonnet-types.workspace = true35jrsonnet-macros.workspace = true36jrsonnet-gcmodule = { version = "0.3.4" }3738pathdiff = "0.2.1"39hashbrown = "0.13.1"40static_assertions = "1.1"4142rustc-hash = "1.1"4344thiserror = "1.0"4546serde = "1.0"4748anyhow = { version = "1.0", optional = true }49# Friendly errors50strsim = { version = "0.10.0", optional = true }51# Serialized stdlib52bincode = { version = "1.3", optional = true }53# Explaining traces54annotate-snippets = { version = "0.9.1", features = ["color"], optional = true }55# Async imports56async-trait = { version = "0.1.59", optional = true }
addedcrates/jrsonnet-evaluator/src/async_import.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-evaluator/src/async_import.rs
@@ -0,0 +1,356 @@
+use std::{cell::RefCell, path::Path};
+
+use async_trait::async_trait;
+use jrsonnet_gcmodule::Trace;
+use jrsonnet_interner::IStr;
+use jrsonnet_parser::{
+	ArgsDesc, AssertStmt, BindSpec, CompSpec, Destruct, Expr, FieldMember, FieldName, ForSpecData,
+	IfSpecData, LocExpr, Member, ObjBody, Param, ParamsDesc, ParserSettings, SliceDesc, Source,
+	SourcePath,
+};
+
+use crate::{gc::GcHashMap, throw, FileData, ImportResolver, State};
+
+pub struct Import {
+	path: IStr,
+	expression: bool,
+}
+
+pub struct FoundImports(Vec<Import>);
+
+// Visits all nodes, trying to find import statements
+#[allow(clippy::too_many_lines)]
+pub fn find_imports(expr: &LocExpr, out: &mut FoundImports) {
+	fn in_destruct(dest: &Destruct, #[allow(unused_variables)] out: &mut FoundImports) {
+		match dest {
+			#[cfg(feature = "exp-destruct")]
+			Destruct::Array {
+				start,
+				rest: _,
+				end,
+			} => {
+				for dest in start {
+					in_destruct(dest, out);
+				}
+				for dest in end {
+					in_destruct(dest, out);
+				}
+			}
+			#[cfg(feature = "exp-destruct")]
+			Destruct::Object { fields, rest: _ } => {
+				for (_, dest, default) in fields {
+					if let Some(dest) = dest {
+						in_destruct(dest, out);
+					}
+					if let Some(expr) = default {
+						find_imports(expr, out);
+					}
+				}
+			}
+			#[cfg(feature = "exp-destruct")]
+			Destruct::Skip => {}
+			Destruct::Full(_) => {}
+		}
+	}
+	fn in_compspec(specs: &[CompSpec], out: &mut FoundImports) {
+		for spec in specs {
+			match spec {
+				CompSpec::IfSpec(IfSpecData(expr)) => find_imports(expr, out),
+				CompSpec::ForSpec(ForSpecData(destruct, expr)) => {
+					in_destruct(destruct, out);
+					find_imports(expr, out);
+				}
+			}
+		}
+	}
+	fn in_params(params: &ParamsDesc, out: &mut FoundImports) {
+		for Param(dest, default) in &*params.0 {
+			in_destruct(dest, out);
+			if let Some(expr) = default {
+				find_imports(expr, out);
+			}
+		}
+	}
+	fn in_bind(specs: &[BindSpec], out: &mut FoundImports) {
+		for spec in specs {
+			match spec {
+				BindSpec::Field {
+					into: dest,
+					value: expr,
+				} => {
+					in_destruct(dest, out);
+					find_imports(expr, out);
+				}
+				BindSpec::Function {
+					name: _,
+					params,
+					value: expr,
+				} => {
+					in_params(params, out);
+					find_imports(expr, out);
+				}
+			}
+		}
+	}
+	fn in_args(ArgsDesc { unnamed, named }: &ArgsDesc, out: &mut FoundImports) {
+		for expr in unnamed {
+			find_imports(expr, out);
+		}
+		for (_, expr) in named {
+			find_imports(expr, out);
+		}
+	}
+	fn in_obj(obj: &ObjBody, out: &mut FoundImports) {
+		match obj {
+			ObjBody::MemberList(v) => {
+				for member in v {
+					match member {
+						Member::Field(FieldMember {
+							name,
+							params,
+							value,
+							..
+						}) => {
+							match name {
+								FieldName::Fixed(_) => {}
+								FieldName::Dyn(expr) => find_imports(expr, out),
+							}
+							if let Some(params) = params {
+								in_params(params, out);
+							}
+							find_imports(value, out);
+						}
+						Member::BindStmt(_) => todo!(),
+						Member::AssertStmt(AssertStmt(expr, expr2)) => {
+							find_imports(expr, out);
+							if let Some(expr) = expr2 {
+								find_imports(expr, out);
+							}
+						}
+					}
+				}
+			}
+			ObjBody::ObjComp(_) => todo!(),
+		}
+	}
+	match &*expr.0 {
+		Expr::Import(v) | Expr::ImportStr(v) | Expr::ImportBin(v) => {
+			if let Expr::Str(s) = &*v.0 {
+				out.0.push(Import {
+					path: s.clone(),
+					expression: matches!(&*expr.0, Expr::Import(_)),
+				});
+			}
+			// Non-string import will fail in runtime
+		}
+
+		Expr::Literal(_) | Expr::Str(_) | Expr::Num(_) | Expr::Var(_) => {}
+
+		Expr::Arr(arr) => {
+			for expr in arr {
+				find_imports(expr, out);
+			}
+		}
+		Expr::ArrComp(expr, specs) => {
+			find_imports(expr, out);
+			in_compspec(specs, out);
+		}
+		Expr::Obj(obj) => in_obj(obj, out),
+		Expr::ObjExtend(expr, obj) => {
+			find_imports(expr, out);
+			in_obj(obj, out);
+		}
+		Expr::BinaryOp(a, _, b) => {
+			find_imports(a, out);
+			find_imports(b, out);
+		}
+		Expr::AssertExpr(AssertStmt(expr, expr2), then) => {
+			find_imports(expr, out);
+			if let Some(expr) = expr2 {
+				find_imports(expr, out);
+			}
+			find_imports(then, out);
+		}
+		Expr::LocalExpr(specs, expr) => {
+			in_bind(specs, out);
+			find_imports(expr, out);
+		}
+		Expr::Apply(expr, args, _) => {
+			find_imports(expr, out);
+			in_args(args, out);
+		}
+		Expr::Index(expr, index) => {
+			find_imports(expr, out);
+			find_imports(index, out);
+		}
+		Expr::Function(params, expr) => {
+			in_params(params, out);
+			find_imports(expr, out);
+		}
+		Expr::IfElse {
+			cond: IfSpecData(expr),
+			cond_then,
+			cond_else,
+		} => {
+			find_imports(expr, out);
+			find_imports(cond_then, out);
+			if let Some(expr) = cond_else {
+				find_imports(expr, out);
+			}
+		}
+		Expr::Slice(expr, SliceDesc { start, end, step }) => {
+			find_imports(expr, out);
+			if let Some(expr) = start {
+				find_imports(expr, out);
+			}
+			if let Some(expr) = end {
+				find_imports(expr, out);
+			}
+			if let Some(expr) = step {
+				find_imports(expr, out);
+			}
+		}
+		Expr::Parened(expr) | Expr::UnaryOp(_, expr) | Expr::ErrorStmt(expr) => {
+			find_imports(expr, out);
+		}
+	}
+}
+
+#[async_trait(?Send)]
+pub trait AsyncImportResolver {
+	type Error;
+	/// Resolves file path, e.g. `(/home/user/manifests, b.libjsonnet)` can correspond
+	/// both to `/home/user/manifests/b.libjsonnet` and to `/home/user/${vendor}/b.libjsonnet`
+	/// where `${vendor}` is a library path.
+	///
+	/// `from` should only be returned from [`ImportResolver::resolve`], or from other defined file, any other value
+	/// may result in panic
+	async fn resolve_from(&self, from: &SourcePath, path: &str) -> Result<SourcePath, Self::Error>;
+	async fn resolve_from_default(&self, path: &str) -> Result<SourcePath, Self::Error> {
+		self.resolve_from(&SourcePath::default(), path).await
+	}
+	/// Resolves absolute path, doesn't supports jpath and other fancy things
+	async fn resolve(&self, path: &Path) -> Result<SourcePath, Self::Error>;
+
+	/// Load resolved file
+	/// This should only be called with value returned from [`ImportResolver::resolve_file`]/[`ImportResolver::resolve`],
+	/// this cannot be resolved using associated type, as evaluator uses object instead of generic for [`ImportResolver`]
+	async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, Self::Error>;
+}
+
+#[derive(Trace)]
+struct ResolvedImportResolver {
+	resolved: RefCell<GcHashMap<(SourcePath, IStr), (SourcePath, bool)>>,
+}
+impl ImportResolver for ResolvedImportResolver {
+	fn load_file_contents(&self, _resolved: &SourcePath) -> crate::Result<Vec<u8>> {
+		unreachable!("all files should be loaded at this point");
+	}
+
+	fn resolve_from(&self, from: &SourcePath, path: &str) -> crate::Result<SourcePath> {
+		Ok(self
+			.resolved
+			.borrow()
+			.get(&(from.clone(), path.into()))
+			.expect("all imports should be resolved at this point")
+			.0
+			.clone())
+	}
+
+	fn resolve_from_default(&self, path: &str) -> crate::Result<SourcePath> {
+		self.resolve_from(&SourcePath::default(), path)
+	}
+
+	fn resolve(&self, path: &Path) -> crate::Result<SourcePath> {
+		throw!(crate::error::ErrorKind::AbsoluteImportNotSupported(
+			path.to_owned()
+		))
+	}
+
+	fn as_any(&self) -> &dyn std::any::Any {
+		self
+	}
+}
+
+enum Job {
+	LoadFile { path: SourcePath, parse: bool },
+	ParseFile(SourcePath),
+	ResolveImport { from: SourcePath, import: Import },
+}
+
+#[allow(clippy::future_not_send)]
+pub async fn async_import<H>(s: State, handler: H, path: impl AsRef<Path>) -> Result<(), H::Error>
+where
+	H: AsyncImportResolver,
+{
+	let mut resolved = s
+		.import_resolver()
+		.as_any()
+		.downcast_ref::<ResolvedImportResolver>()
+		.map_or_else(GcHashMap::new, |resolver| {
+			std::mem::take(&mut *resolver.resolved.borrow_mut())
+		});
+	let mut queue = vec![Job::LoadFile {
+		path: handler.resolve(path.as_ref()).await?,
+		parse: true,
+	}];
+	// let mut resolved = HashMap::<(SourcePath, IStr), (SourcePath, bool)>::new();
+	while let Some(job) = queue.pop() {
+		match job {
+			Job::LoadFile { path, parse } => {
+				if !s.0.file_cache.borrow().contains_key(&path) {
+					let data = handler.load_file_contents(&path).await?;
+					s.0.file_cache
+						.borrow_mut()
+						.insert(path.clone(), FileData::new_bytes(data.as_slice().into()));
+				}
+				if parse {
+					queue.push(Job::ParseFile(path));
+				}
+			}
+			Job::ParseFile(path) => {
+				if let Some(file) = s.0.file_cache.borrow_mut().get_mut(&path) {
+					if file.parsed.is_none() {
+						let Some(code) = file.get_string() else {
+							continue;
+						};
+						let source = Source::new(path.clone(), code.clone());
+						// If failed - then skip import
+						file.parsed =
+							jrsonnet_parser::parse(&code, &ParserSettings { source }).ok();
+						if let Some(parsed) = &file.parsed {
+							let mut imports = FoundImports(vec![]);
+							find_imports(parsed, &mut imports);
+							for import in imports.0 {
+								queue.push(Job::ResolveImport {
+									from: path.clone(),
+									import,
+								});
+							}
+						}
+					}
+				}
+			}
+			Job::ResolveImport { from, import } => {
+				if let Some((resolved, expression)) =
+					resolved.get_mut(&(from.clone(), import.path.clone()))
+				{
+					if import.expression && !*expression {
+						*expression = true;
+						queue.push(Job::ParseFile(resolved.clone()));
+					}
+					continue;
+				}
+				let resolved = handler.resolve_from(&from, &import.path).await?;
+				queue.push(Job::LoadFile {
+					path: resolved,
+					parse: import.expression,
+				});
+			}
+		}
+	}
+	s.set_import_resolver(Box::new(ResolvedImportResolver {
+		resolved: RefCell::new(resolved),
+	}));
+	Ok(())
+}