git.delta.rocks / jrsonnet / refs/commits / 9cb049ebb4a6

difftreelog

feat proper js wasm bindings

nlwklwxtYaroslav Bolyukin2026-05-05parent: #b9cb94a.patch.diff
in: master

12 files changed

addedbindings/jrsonnet-web/.gitignorediffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/.gitignore
@@ -0,0 +1 @@
+/lib
addedbindings/jrsonnet-web/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "jrsonnet-web"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[dependencies]
+console_error_panic_hook.workspace = true
+getrandom = { workspace = true, features = ["wasm_js"] }
+hi-doc.workspace = true
+jrsonnet-evaluator.workspace = true
+jrsonnet-formatter.workspace = true
+jrsonnet-gcmodule.workspace = true
+jrsonnet-stdlib.workspace = true
+jrsonnet-types.workspace = true
+js-sys.workspace = true
+url.workspace = true
+wasm-bindgen.workspace = true
+wasm-bindgen-futures.workspace = true
+
+[lints]
+workspace = true
+
+[lib]
+name = "jsonnet_web"
+crate-type = ["cdylib"]
addedbindings/jrsonnet-web/deno.jsondiffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/deno.json
@@ -0,0 +1,13 @@
+{
+	"name": "@jrsonnet/jrsonnet",
+	"tasks": {
+		"wasmbuild": "deno run -A @deno/wasmbuild -p jrsonnet-web --skip-opt"
+	},
+	"imports": {
+		"@deno/wasmbuild": "jsr:@deno/wasmbuild@^0.21.1",
+		"@std/assert": "jsr:@std/assert@^1.0.19"
+	},
+	"exports": {
+		".": "./mod.ts"
+	}
+}
addedbindings/jrsonnet-web/deno.lockdiffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/deno.lock
@@ -0,0 +1,92 @@
+{
+  "version": "5",
+  "specifiers": {
+    "jsr:@david/path@0.2": "0.2.0",
+    "jsr:@david/temp@~0.1.1": "0.1.1",
+    "jsr:@deno/wasmbuild@~0.21.1": "0.21.1",
+    "jsr:@std/assert@^1.0.19": "1.0.19",
+    "jsr:@std/cli@^1.0.11": "1.0.29",
+    "jsr:@std/encoding@^1.0.6": "1.0.10",
+    "jsr:@std/fmt@^1.0.4": "1.0.10",
+    "jsr:@std/fs@1": "1.0.23",
+    "jsr:@std/fs@^1.0.10": "1.0.23",
+    "jsr:@std/internal@^1.0.12": "1.0.13",
+    "jsr:@std/path@1": "1.1.4",
+    "jsr:@std/path@^1.1.4": "1.1.4",
+    "jsr:@std/streams@^1.0.17": "1.1.0",
+    "jsr:@std/tar@~0.1.4": "0.1.10"
+  },
+  "jsr": {
+    "@david/path@0.2.0": {
+      "integrity": "f2d7aa7f02ce5a55e27c09f9f1381794acb09d328f8d3c8a2e3ab3ffc294dccd",
+      "dependencies": [
+        "jsr:@std/fs@1",
+        "jsr:@std/path@1"
+      ]
+    },
+    "@david/temp@0.1.1": {
+      "integrity": "3974e3aa76536bd831294b72f5b70bfb67af3f4119ae334360e321f6ded18840",
+      "dependencies": [
+        "jsr:@david/path"
+      ]
+    },
+    "@deno/wasmbuild@0.21.1": {
+      "integrity": "f60afdffefbd61067cb8a0083cde7d22f5eb74084b074c672a87b8e92042b456",
+      "dependencies": [
+        "jsr:@david/path",
+        "jsr:@david/temp",
+        "jsr:@std/cli",
+        "jsr:@std/encoding",
+        "jsr:@std/fmt",
+        "jsr:@std/fs@^1.0.10",
+        "jsr:@std/tar"
+      ]
+    },
+    "@std/assert@1.0.19": {
+      "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
+      "dependencies": [
+        "jsr:@std/internal"
+      ]
+    },
+    "@std/cli@1.0.29": {
+      "integrity": "fa4ef29130baa834d8a13b7d138240c3a2fcfba740bfb7afa646a360a15ec84f"
+    },
+    "@std/encoding@1.0.10": {
+      "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
+    },
+    "@std/fmt@1.0.10": {
+      "integrity": "90dfba288802ac6de82fb31d0917eb9e4450b9925b954d5e51fc29ac07419db5"
+    },
+    "@std/fs@1.0.23": {
+      "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37",
+      "dependencies": [
+        "jsr:@std/internal",
+        "jsr:@std/path@^1.1.4"
+      ]
+    },
+    "@std/internal@1.0.13": {
+      "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0"
+    },
+    "@std/path@1.1.4": {
+      "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
+      "dependencies": [
+        "jsr:@std/internal"
+      ]
+    },
+    "@std/streams@1.1.0": {
+      "integrity": "2f7024d841f343fd478afe0c958a3f0f068ef2a0d2bcc954f550f97ac1fa22e3"
+    },
+    "@std/tar@0.1.10": {
+      "integrity": "6bf907f3a4bc8bfef42973ba132d946756a6161ef6b914a9e1c06debe664db17",
+      "dependencies": [
+        "jsr:@std/streams"
+      ]
+    }
+  },
+  "workspace": {
+    "dependencies": [
+      "jsr:@deno/wasmbuild@~0.21.1",
+      "jsr:@std/assert@^1.0.19"
+    ]
+  }
+}
addedbindings/jrsonnet-web/example.jsonnetdiffbeforeafterboth

no changes

addedbindings/jrsonnet-web/fmt.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/fmt.test.ts
@@ -0,0 +1,7 @@
+import { assertEquals } from "@std/assert";
+import { format, FormatOptions } from "./mod.ts";
+
+Deno.test("format", () => {
+  const opts = new FormatOptions();
+  assertEquals(format("{a:1+1}", opts), "{ a: 1 + 1 }\n");
+});
addedbindings/jrsonnet-web/fmt.tsdiffbeforeafterboth

no changes

addedbindings/jrsonnet-web/mod.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/mod.test.ts
@@ -0,0 +1,8 @@
+import { assertEquals } from "@std/assert";
+import { WasmState } from "./mod.ts";
+
+Deno.test("basic", () => {
+  const state = new WasmState();
+
+  assertEquals(state.evaluate_snippet("test.jsonnet", "1 + 2").as_num(), 3);
+});
addedbindings/jrsonnet-web/mod.tsdiffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/mod.ts
@@ -0,0 +1,60 @@
+import { assert } from "@std/assert";
+import {
+  format as formatRaw,
+  type ImportResolver,
+  WasmFormatOptions,
+  WasmState,
+  WasmVal,
+} from "./lib/jsonnet_web.js";
+
+export { type ImportResolver, WasmFormatOptions, WasmState, WasmVal };
+
+class FetchImportResolver implements ImportResolver {
+  constructor(public base: string) {}
+
+  resolution = new Map<string, URL>();
+  response = new Map<string, Response>();
+
+  async resolveFrom(from: string | undefined, path: string): Promise<string> {
+    let resolved: URL;
+    if (from) {
+      resolved = new URL(path, from);
+    } else {
+      resolved = new URL(path, this.base);
+    }
+    const resolvingStr = resolved.toString();
+    resolved = this.resolution.get(resolvingStr) ?? resolved;
+
+    const resolvedStr = resolved.toString();
+    if (!this.response.has(resolvedStr)) {
+      console.log(resolved);
+      const v = await fetch(resolved);
+      this.response.set(resolvedStr, v);
+      resolved = new URL(v.url);
+      this.resolution.set(resolvingStr, resolved);
+    }
+    return resolved.toString();
+  }
+  loadFileContents(resolved: string): Promise<Uint8Array> {
+    console.log(resolved);
+    const v = this.response.get(resolved);
+    assert(v, "should be resolved");
+    return v.bytes();
+  }
+}
+
+//
+// try {
+//   console.log("eval file");
+//   await state.evaluate_file("example.jsonnet");
+//   console.log("eval file done");
+// } catch (e) {
+//   console.log(e);
+// }
+//
+export function format(
+  code: string,
+  opts: WasmFormatOptions = new WasmFormatOptions(),
+): string {
+  return formatRaw(code, opts);
+}
addedbindings/jrsonnet-web/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/bindings/jrsonnet-web/src/lib.rs
@@ -0,0 +1,421 @@
+use std::result::Result;
+
+use jrsonnet_evaluator::{
+	NumValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,
+	async_import::{ResolvedImportResolver, async_import},
+	error,
+	function::builtin::{NativeCallback, NativeCallbackHandler},
+	manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},
+	trace::{JsFormat, PathResolver, TraceFormat},
+	with_state,
+};
+use jrsonnet_formatter::FormatOptions;
+use jrsonnet_gcmodule::Trace;
+use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};
+use jrsonnet_types::ValType;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+#[derive(Clone, Copy)]
+pub enum ValKind {
+	Null,
+	Bool,
+	Num,
+	Str,
+	Arr,
+	Obj,
+	Func,
+}
+
+#[wasm_bindgen(inline_js = r"
+export class JrsonnetError extends Error {
+	constructor(message, frames) {
+		super(message);
+		this.name = 'JrsonnetError';
+		this.frames = frames;
+	}
+}
+export function makeJrsonnetError(message, frames) {
+	return new JrsonnetError(message, frames);
+}
+")]
+extern "C" {
+	#[wasm_bindgen(js_name = makeJrsonnetError)]
+	fn make_jrsonnet_error(message: &str, frames: js_sys::Array) -> JsValue;
+}
+
+#[wasm_bindgen(typescript_custom_section)]
+const TS_JRSONNET_ERROR: &'static str = r"
+export interface JrsonnetFrame {
+	desc: string;
+	path?: string;
+	line?: number;
+	column?: number;
+}
+export class JrsonnetError extends Error {
+	name: 'JrsonnetError';
+	frames: JrsonnetFrame[];
+}
+";
+
+fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {
+	let msg = e.error().to_string();
+	// let msg = format.format(e).unwrap_or_else(|_| e.to_string());
+	let frames = js_sys::Array::new();
+	for el in &e.trace().0 {
+		let frame = js_sys::Object::new();
+		let _ = js_sys::Reflect::set(
+			&frame,
+			&JsValue::from_str("desc"),
+			&JsValue::from_str(&el.desc),
+		);
+		if let Some(loc) = &el.location {
+			let path = loc.0.source_path().to_string();
+			let _ = js_sys::Reflect::set(
+				&frame,
+				&JsValue::from_str("path"),
+				&JsValue::from_str(&path),
+			);
+			let mapped = loc.0.map_source_locations(&[loc.1, loc.2]);
+			let _ = js_sys::Reflect::set(
+				&frame,
+				&JsValue::from_str("line"),
+				&JsValue::from(mapped[0].line),
+			);
+			let _ = js_sys::Reflect::set(
+				&frame,
+				&JsValue::from_str("column"),
+				&JsValue::from(mapped[0].column),
+			);
+		}
+		frames.push(&frame);
+	}
+	make_jrsonnet_error(&msg, frames)
+}
+
+impl From<ValType> for ValKind {
+	fn from(v: ValType) -> Self {
+		match v {
+			ValType::Null => Self::Null,
+			ValType::Bool => Self::Bool,
+			ValType::Num => Self::Num,
+			ValType::Str => Self::Str,
+			ValType::Arr => Self::Arr,
+			ValType::Obj => Self::Obj,
+			ValType::Func => Self::Func,
+		}
+	}
+}
+
+#[wasm_bindgen]
+pub struct WasmVal {
+	val: Val,
+	state: Option<State>,
+}
+
+impl WasmVal {
+	fn new(val: Val) -> Self {
+		Self { val, state: None }
+	}
+	fn with_state(val: Val, state: State) -> Self {
+		Self {
+			val,
+			state: Some(state),
+		}
+	}
+	fn child(&self, val: Val) -> Self {
+		Self {
+			val,
+			state: self.state.clone(),
+		}
+	}
+	fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {
+		if let Some(state) = &self.state {
+			let _guard = state.try_enter();
+			f(&self.val)
+		} else {
+			f(&self.val)
+		}
+	}
+	fn manifest_with(&self, format: impl ManifestFormat) -> Result<String, JsValue> {
+		self.run(|v| v.manifest(format))
+			.map_err(|e| jrsonnet_js_error(&e))
+	}
+}
+
+#[wasm_bindgen]
+impl WasmVal {
+	pub fn null() -> Self {
+		Self::new(Val::Null)
+	}
+	pub fn bool(b: bool) -> Self {
+		Self::new(Val::Bool(b))
+	}
+	pub fn num(n: f64) -> Result<Self, JsError> {
+		let n = NumValue::new(n)
+			.ok_or_else(|| JsError::new("only finite numbers are supported by jsonnet"))?;
+		Ok(Self::new(Val::num(n)))
+	}
+	pub fn string(s: String) -> Self {
+		Self::new(Val::string(s))
+	}
+	pub fn arr(items: Vec<WasmVal>) -> Self {
+		Self::new(Val::arr(
+			items.into_iter().map(|v| v.val).collect::<Vec<_>>(),
+		))
+	}
+	pub fn func(
+		params: Vec<String>,
+
+		#[wasm_bindgen(unchecked_param_type = "(...args: WasmVal[]) => WasmVal")]
+		callback: js_sys::Function,
+	) -> Self {
+		#[allow(deprecated)]
+		Self::new(Val::function(NativeCallback::new(
+			params,
+			JsHandler { func: callback },
+		)))
+	}
+
+	#[wasm_bindgen(getter)]
+	pub fn kind(&self) -> ValKind {
+		self.val.value_type().into()
+	}
+	pub fn as_bool(&self) -> Option<bool> {
+		self.val.as_bool()
+	}
+	pub fn as_num(&self) -> Option<f64> {
+		self.val.as_num()
+	}
+	pub fn as_string(&self) -> Option<String> {
+		self.val.as_str().map(|s| s.to_string())
+	}
+	pub fn arr_len(&self) -> Option<u32> {
+		self.val.as_arr().map(|a| a.len())
+	}
+	pub fn arr_at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {
+		let Some(a) = self.val.as_arr() else {
+			return Ok(None);
+		};
+		self.run(|_| a.get(index))
+			.map(|opt| opt.map(|v| self.child(v)))
+			.map_err(|e| jrsonnet_js_error(&e))
+	}
+	pub fn obj_keys(&self) -> Option<Vec<String>> {
+		self.val
+			.as_obj()
+			.map(|o| o.fields().into_iter().map(|s| s.to_string()).collect())
+	}
+	pub fn obj_get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {
+		let Some(o) = self.val.as_obj() else {
+			return Ok(None);
+		};
+		self.run(|_| o.get(key.into()))
+			.map(|opt| opt.map(|v| self.child(v)))
+			.map_err(|e| jrsonnet_js_error(&e))
+	}
+
+	pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {
+		self.manifest_with(JsonFormat::cli(indent as usize))
+	}
+	pub fn manifest_to_string(&self) -> Result<String, JsValue> {
+		self.manifest_with(ToStringFormat)
+	}
+	pub fn manifest_string(&self) -> Result<String, JsValue> {
+		self.manifest_with(StringFormat)
+	}
+	pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {
+		self.manifest_with(YamlFormat::std_to_yaml(indent != 0, quote_keys))
+	}
+	pub fn manifest_yaml_stream(
+		&self,
+		indent: u32,
+		quote_keys: bool,
+		c_document_end: bool,
+	) -> Result<String, JsValue> {
+		self.manifest_with(YamlStreamFormat::std_yaml_stream(
+			YamlFormat::std_to_yaml(indent != 0, quote_keys),
+			c_document_end,
+		))
+	}
+	pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {
+		self.manifest_with(XmlJsonmlFormat::std_to_xml())
+	}
+	pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {
+		self.manifest_with(TomlFormat::std_to_toml(" ".repeat(indent as usize)))
+	}
+	pub fn manifest_ini(&self) -> Result<String, JsValue> {
+		self.manifest_with(IniFormat::std())
+	}
+}
+
+#[derive(Trace)]
+struct JsHandler {
+	#[trace(skip)]
+	func: js_sys::Function,
+}
+
+#[wasm_bindgen(inline_js = r"
+export function js_invoke_val_callback(cb, args) {
+	return cb.apply(null, args);
+}
+")]
+extern "C" {
+	#[wasm_bindgen(catch)]
+	fn js_invoke_val_callback(
+		cb: &js_sys::Function,
+		args: &js_sys::Array,
+	) -> Result<WasmVal, JsValue>;
+}
+
+impl NativeCallbackHandler for JsHandler {
+	fn call(&self, args: &[Val]) -> JrResult<Val> {
+		let js_args = js_sys::Array::new();
+		let state = with_state(|s| s);
+		for arg in args {
+			js_args.push(&JsValue::from(WasmVal::with_state(
+				arg.clone(),
+				state.clone(),
+			)));
+		}
+		let result = js_invoke_val_callback(&self.func, &js_args).map_err(|e| {
+			let msg = e
+				.as_string()
+				.or_else(|| {
+					e.dyn_ref::<js_sys::Error>()
+						.map(|err| String::from(err.message()))
+				})
+				.unwrap_or_else(|| format!("{e:?}"));
+			error!("js callback threw: {msg}")
+		})?;
+		Ok(result.val)
+	}
+}
+
+#[wasm_bindgen]
+pub struct WasmState {
+	state: State,
+	resolver: JsAsyncResolver,
+}
+#[wasm_bindgen]
+impl WasmState {
+	#[wasm_bindgen(constructor)]
+	pub fn new(resolver: ImportResolverJs) -> Self {
+		console_error_panic_hook::set_once();
+		let mut state = StateBuilder::default();
+		state.import_resolver(ResolvedImportResolver::new());
+		let std = jrsonnet_stdlib::ContextInitializer::new(PathResolver::Absolute);
+		state.context_initializer(std);
+		let state = state.build();
+		Self {
+			state,
+			resolver: JsAsyncResolver { js: resolver },
+		}
+	}
+
+	#[wasm_bindgen]
+	pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {
+		let _guard = self.state.enter();
+		self.state
+			.evaluate_snippet(name, snippet)
+			.map(|v| WasmVal::with_state(v, self.state.clone()))
+			.map_err(|e| jrsonnet_js_error(&e))
+	}
+
+	pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {
+		let path = async_import(self.state.clone(), self.resolver.clone(), &path.as_str()).await?;
+		let _guard = self.state.enter();
+		self.state
+			.import_resolved(path)
+			.map(|v| WasmVal::with_state(v, self.state.clone()))
+			.map_err(|e| jrsonnet_js_error(&e))
+	}
+}
+
+#[wasm_bindgen]
+extern "C" {
+	#[wasm_bindgen(typescript_type = "ImportResolver")]
+	#[derive(Clone)]
+	pub type ImportResolverJs;
+
+	#[wasm_bindgen(catch, method, structural, js_name = resolveFrom)]
+	fn resolve_from(
+		this: &ImportResolverJs,
+		from: Option<String>,
+		path: &str,
+	) -> Result<js_sys::Promise, JsValue>;
+
+	#[wasm_bindgen(catch, method, structural, js_name = loadFileContents)]
+	fn load_file_contents(
+		this: &ImportResolverJs,
+		resolved: &str,
+	) -> Result<js_sys::Promise, JsValue>;
+}
+
+#[wasm_bindgen(typescript_custom_section)]
+const TS_IMPORT_RESOLVER: &'static str = r"
+export interface ImportResolver {
+	resolveFrom(from: string | undefined, path: string): Promise<string>;
+	loadFileContents(resolved: string): Promise<Uint8Array>;
+}
+";
+
+#[derive(Clone)]
+struct JsAsyncResolver {
+	js: ImportResolverJs,
+}
+
+impl jrsonnet_evaluator::async_import::AsyncImportResolver for JsAsyncResolver {
+	type Error = JsValue;
+
+	async fn resolve_from(
+		&self,
+		from: &SourcePath,
+		path: &dyn jrsonnet_evaluator::AsPathLike,
+	) -> Result<SourcePath, JsValue> {
+		let from_js = (!from.is_default()).then(|| from.to_string());
+		let path_str = path.as_path().as_ref().to_string_lossy().into_owned();
+		let promise = self.js.resolve_from(from_js, &path_str)?;
+		let resolved_js = wasm_bindgen_futures::JsFuture::from(promise).await?;
+		let resolved_str = resolved_js
+			.as_string()
+			.ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;
+		let url = url::Url::parse(&resolved_str).map_err(|e| JsValue::from_str(&e.to_string()))?;
+		Ok(SourcePath::new(SourceUrl::new(url)))
+	}
+
+	async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {
+		let resolved_str = resolved.to_string();
+		let promise = self.js.load_file_contents(&resolved_str)?;
+		let bytes_js = wasm_bindgen_futures::JsFuture::from(promise).await?;
+		let arr = bytes_js
+			.dyn_into::<js_sys::Uint8Array>()
+			.map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;
+		Ok(arr.to_vec())
+	}
+}
+
+#[wasm_bindgen]
+pub struct WasmFormatOptions {}
+#[wasm_bindgen]
+impl WasmFormatOptions {
+	#[wasm_bindgen(constructor)]
+	pub fn new() -> Self {
+		Self {}
+	}
+
+	fn build(&self) -> FormatOptions {
+		FormatOptions { indent: 0 }
+	}
+}
+
+#[wasm_bindgen]
+pub fn format(src: &str, opts: &WasmFormatOptions) -> Result<String, String> {
+	match jrsonnet_formatter::format(src, &opts.build()) {
+		Ok(v) => Ok(v),
+		Err(e) => {
+			let e = e.build();
+			Err(hi_doc::source_to_ansi(&e))
+		}
+	}
+}
deletedbindings/js/index.jsdiffbeforeafterboth
--- a/bindings/js/index.js
+++ /dev/null
@@ -1,183 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-const { WASI } = require('wasi');
-const wasi = new WASI({
-	args: process.argv,
-	env: process.env,
-	preopens: {},
-});
-
-class JsonnetVM {
-	constructor(wasm, vm) {
-		this.wasm = wasm;
-		this.vm = vm;
-		this.wasm.exports.jrsonnet_set_trace_format(this.vm, 1);
-
-		this.setImportCallback((from, to) => {
-			const resolved = path.resolve(from, to);
-			return {
-				value: fs.readFileSync(resolved).toString('utf-8'),
-				foundHere: resolved,
-			};
-		})
-	}
-
-	/**
-	 * @param {(from: string, to: string) => {foundHere: string, value: string}} cb
-	 */
-	setImportCallback(cb) {
-		this.wasm.importCbs.set(this.vm, (base, rel, foundHere, success) => {
-			const baseStr = this.wasm.readString(base);
-			const relStr = this.wasm.readString(rel);
-			try {
-				const value = cb(baseStr, relStr);
-				this.wasm.memorySlice32Len(foundHere, 1)[0] = this.allocateString(value.foundHere);
-				this.wasm.memorySlice32Len(success, 1)[0] = 1;
-				return this.allocateString(value.value);
-			} catch (e) {
-				this.wasm.memorySlice32Len(success, 1)[0] = 0;
-				return this.allocateString(e.stack)
-			}
-		});
-		this.wasm.exports.jrsonnet_apply_static_import_callback(
-			this.vm,
-			this.vm,
-		);
-	}
-
-	alloc(length) {
-		return this.wasm.exports.jsonnet_realloc(this.vm, 0, length);
-	}
-	allocateString(string) {
-		const byteLength = new TextEncoder().encode(string).length;
-		const addr = this.alloc(byteLength + 1);
-		this.wasm.writeString(addr, string);
-		return addr;
-	}
-	dealloc(addr) {
-		return this.wasm.exports.jsonnet_realloc(this.vm, addr, 0);
-	}
-
-	evaluateFile(path) {
-		const pathAddr = this.allocateString(path);
-		const resultCodeAddr = this.alloc(4);
-		const resultAddr = this.wasm.exports.jsonnet_evaluate_file(this.vm, pathAddr, resultCodeAddr);
-		this.dealloc(pathAddr);
-		const resultCode = this.wasm.memorySliceLen(resultCodeAddr, 4);
-		this.dealloc(resultCodeAddr);
-		const result = this.wasm.readString(resultAddr).trim();
-		this.dealloc(resultAddr);
-		if (resultCode[0] === 1) {
-			const error = new Error(result);
-			throw error;
-		} else {
-			return result;
-		}
-	}
-	evaluateSnippet(path, snippet) {
-		const pathAddr = this.allocateString(path);
-		const snippetAddr = this.allocateString(snippet);
-		const resultCodeAddr = this.alloc(4);
-		const resultAddr = this.wasm.exports.jsonnet_evaluate_snippet(this.vm, pathAddr, snippetAddr, resultCodeAddr);
-		this.dealloc(pathAddr);
-		this.dealloc(snippetAddr);
-		const resultCode = this.wasm.memorySliceLen(resultCodeAddr, 4);
-		this.dealloc(resultCodeAddr);
-		const result = this.wasm.readString(resultAddr);
-		this.dealloc(resultAddr);
-		if (resultCode[0] === 1) {
-			const error = new Error(result);
-			throw error;
-		} else {
-			return result;
-		}
-	}
-
-	/**
-	 * Destroys vm, any future call to this object will fail, and all resources will be freed
-	 */
-	destroy() {
-		this.wasm.exports.jsonnet_destroy(this.vm);
-		this.wasm.importCbs.delete(this.vm);
-	}
-}
-
-class JsonnetWASM {
-	constructor() {
-		this.importCbs = new Map();
-	}
-
-	async init(buf) {
-		const wasm = await WebAssembly.compile(buf);
-		const instance = await WebAssembly.instantiate(wasm, {
-			wasi_snapshot_preview1: wasi.wasiImport,
-			env: {
-				_jrsonnet_static_import_callback: (ctx, base, rel, found_here, success) => {
-					if (!this.importCbs.has(ctx)) {
-						throw new Error(`Got unknown ctx callback: ${ctx}`);
-					}
-					return this.importCbs.get(ctx)(base, rel, found_here, success);
-				}
-			}
-		});
-		wasi.start(instance);
-		this.instance = instance;
-	}
-	/**
-	 * @type Record<string, WebAssembly.ExportValue>
-	 */
-	get exports() {
-		return this.instance.exports;
-	}
-	get memory() {
-		return this.exports.memory;
-	}
-	get memoryBuffer() {
-		return this.memory.buffer;
-	}
-	memorySliceLen(start, length) {
-		return new Uint8Array(this.memoryBuffer, start, length);
-	}
-	memorySlice32Len(start, length) {
-		return new Uint32Array(this.memoryBuffer, start, length);
-	}
-	memorySlice(start, end) {
-		return new Uint8Array(this.memoryBuffer, start, start && end && (end - start));
-	}
-
-	readString(addr) {
-		let end;
-		let slice = this.memorySlice();
-		for (end = addr; slice[end]; end++);
-		return (new TextDecoder()).decode(this.memorySlice(addr, end));
-	}
-	writeString(addr, string) {
-		let slice = this.memorySlice(addr);
-		let result = new TextEncoder().encodeInto(string, slice);
-		slice[result.written] = 0;
-	}
-
-	version() {
-		return this.readString(this.exports.jsonnet_version());
-	}
-
-	newVM() {
-		return new JsonnetVM(this, this.exports.jsonnet_make());
-	}
-}
-
-(async () => {
-	try {
-		const jsonnet = new JsonnetWASM();
-		await jsonnet.init(fs.readFileSync(`${__dirname}/../../target/wasm32-wasi/release/jsonnet.wasm`));
-		console.log(`Version = ${jsonnet.version()}`);
-
-		const vm = jsonnet.newVM();
-		console.log(vm.evaluateSnippet('./snip.jsonnet', `
-			2+2
-		`));
-		console.log(vm.evaluateFile('./test.jsonnet'));
-	} catch (e) {
-		console.log(e.stack);
-	}
-})();
modifiedflake.nixdiffbeforeafterboth
before · flake.nix
1{2  description = "Jrsonnet";3  inputs = {4    nixpkgs.url = "github:nixos/nixpkgs/release-25.11";5    fenix = {6      url = "github:CertainLach/fenix/fix/libatomic";7      inputs.nixpkgs.follows = "nixpkgs";8    };9    flake-parts = {10      url = "github:hercules-ci/flake-parts";11      inputs.nixpkgs-lib.follows = "nixpkgs";12    };13    hercules-ci-effects = {14      url = "github:hercules-ci/hercules-ci-effects";15      inputs.flake-parts.follows = "flake-parts";16      inputs.nixpkgs.follows = "nixpkgs";17    };18    treefmt-nix = {19      url = "github:numtide/treefmt-nix";20      inputs.nixpkgs.follows = "nixpkgs";21    };22    crane.url = "github:CertainLach/crane/refactor/drop-remarshal";23    shelly.url = "github:CertainLach/shelly";24  };25  outputs =26    inputs:27    let28      inherit (inputs.nixpkgs.lib)29        mkIf30        mkForce31        optionals32        optionalAttrs33        ;34    in35    inputs.flake-parts.lib.mkFlake { inherit inputs; } {36      imports = [37        inputs.shelly.flakeModule38        inputs.hercules-ci-effects.flakeModule39      ];40      systems = [41        "x86_64-linux"42        "i686-linux"43        "aarch64-linux"44        "armv7l-linux"45        "aarch64-darwin"46      ];47      perSystem =48        {49          config,50          self',51          system,52          ...53        }:54        let55          pkgs = import inputs.nixpkgs {56            inherit system;57            overlays = [ inputs.fenix.overlays.default ];58            config.allowUnsupportedSystem = true;59            config.allowUnfreePredicate = pkg: pkg.name == "Xcode.app";60          };61          targetArch = pkgs.stdenv.hostPlatform.parsed.cpu.name;62          rustfmt = (pkgs.fenix.complete or pkgs.fenix.stable).rustfmt;63          rust-analyzer = (pkgs.fenix.complete or pkgs.fenix.stable).rust-analyzer;64          toolchain = pkgs.fenix.combine [65            (pkgs.fenix.stable.withComponents [66              "cargo"67              "clippy"68              "rustc"69              "rust-src"70            ])71            rustfmt72            rust-analyzer73          ];74          craneLib = (inputs.crane.mkLib pkgs).overrideToolchain toolchain;75          treefmt =76            (inputs.treefmt-nix.lib.evalModule pkgs (import ./treefmt.nix { inherit rustfmt; })).config.build;7778          # Cross-compilation toolchains79          crossToolchain = pkgs.fenix.combine [80            (pkgs.fenix.stable.withComponents [81              "cargo"82              "rustc"83            ])84            pkgs.fenix.targets."${targetArch}-unknown-linux-musl".stable.rust-std85            pkgs.fenix.targets."${targetArch}-apple-darwin".stable.rust-std86          ];87          craneLibCross = (inputs.crane.mkLib pkgs).overrideToolchain crossToolchain;8889          # Windows cross-compilation90          pkgsWindows = import inputs.nixpkgs {91            overlays = [ inputs.fenix.overlays.default ];92            localSystem = system;93            crossSystem = {94              config = "${targetArch}-w64-mingw32";95              libc = "msvcrt";96            };97          };98          windowsToolchain = pkgs.fenix.combine [99            (pkgs.fenix.stable.withComponents [100              "cargo"101              "rustc"102            ])103            pkgs.fenix.targets."${targetArch}-pc-windows-gnu".stable.rust-std104          ];105          craneLibWindows = (inputs.crane.mkLib pkgsWindows).overrideToolchain (_: windowsToolchain);106107        in108        {109          legacyPackages = {110            release = optionalAttrs pkgs.stdenv.hostPlatform.isLinux (111              {112                jrsonnet-linux-glibc = self'.packages.jrsonnet;113                jrsonnet-experimental-linux-glibc = self'.packages.jrsonnet-experimental;114              }115              // optionalAttrs pkgs.stdenv.hostPlatform.is64bit rec {116                jrsonnet-linux-musl = pkgs.callPackage ./nix/jrsonnet-cross-musl.nix {117                  craneLib = craneLibCross;118                  targetTriple = "${targetArch}-unknown-linux-musl";119                  muslCC = pkgs.pkgsMusl.stdenv.cc;120                };121                jrsonnet-experimental-linux-musl = jrsonnet-linux-musl.override {122                  withExperimentalFeatures = true;123                };124              }125              // optionalAttrs (targetArch == "aarch64") rec {126                jrsonnet-darwin = pkgs.callPackage ./nix/jrsonnet-cross-darwin.nix {127                  craneLib = craneLibCross;128                  targetTriple = "${targetArch}-apple-darwin";129                };130                jrsonnet-experimental-darwin = jrsonnet-darwin.override {131                  withExperimentalFeatures = true;132                };133              }134              // optionalAttrs (targetArch == "x86_64") rec {135                jrsonnet-windows = pkgsWindows.callPackage ./nix/jrsonnet-cross-windows.nix {136                  craneLib = craneLibWindows;137                  targetTriple = "${targetArch}-pc-windows-gnu";138                };139                jrsonnet-experimental-windows = jrsonnet-windows.override {140                  withExperimentalFeatures = true;141                };142              }143            );144            benchmarks = optionalAttrs (system == "x86_64-linux" || system == "aarch64-linux") {145              default = pkgs.callPackage ./nix/benchmarks.nix {146                inherit (config.legacyPackages.jsonnetImpls)147                  go-jsonnet148                  sjsonnet149                  cpp-jsonnet150                  rsjsonnet151                  ;152                jrsonnetVariants = [153                  {154                    drv = self'.packages.jrsonnet.override { forBenchmarks = true; };155                    name = "";156                  }157                ];158              };159              quick = pkgs.callPackage ./nix/benchmarks.nix {160                inherit (config.legacyPackages.jsonnetImpls)161                  go-jsonnet162                  sjsonnet163                  cpp-jsonnet164                  rsjsonnet165                  ;166                quick = true;167                jrsonnetVariants = [168                  {169                    drv = self'.packages.jrsonnet.override { forBenchmarks = true; };170                    name = "";171                  }172                ];173              };174              against-release = pkgs.callPackage ./nix/benchmarks.nix {175                inherit (config.legacyPackages.jsonnetImpls)176                  go-jsonnet177                  sjsonnet178                  cpp-jsonnet179                  rsjsonnet180                  ;181                jrsonnetVariants = [182                  {183                    drv = self'.packages.jrsonnet.override { forBenchmarks = true; };184                    name = "current";185                  }186                  {187                    drv = self'.packages.jrsonnet-experimental.override { forBenchmarks = true; };188                    name = "current-experimental";189                  }190                  {191                    drv = self'.legacyPackages.jsonnetImpls.jrsonnet-release.override { forBenchmarks = true; };192                    name = "release";193                  }194                ];195              };196              quick-against-release = pkgs.callPackage ./nix/benchmarks.nix {197                inherit (config.legacyPackages.jsonnetImpls)198                  go-jsonnet199                  sjsonnet200                  cpp-jsonnet201                  rsjsonnet202                  ;203                quick = true;204                jrsonnetVariants = [205                  {206                    drv = self'.packages.jrsonnet.override { forBenchmarks = true; };207                    name = "current";208                  }209                  {210                    drv = self'.packages.jrsonnet-experimental.override { forBenchmarks = true; };211                    name = "current-experimental";212                  }213                  {214                    drv = self'.legacyPackages.jsonnetImpls.jrsonnet-release.override { forBenchmarks = true; };215                    name = "release";216                  }217                ];218              };219            };220            jsonnetImpls = {221              go-jsonnet = pkgs.callPackage ./nix/go-jsonnet.nix { };222              sjsonnet = pkgs.callPackage ./nix/sjsonnet.nix { };223              cpp-jsonnet = pkgs.callPackage ./nix/cpp-jsonnet.nix { };224              # I didn't managed to build it, and nixpkgs version is marked as broken225              # haskell-jsonnet = pkgs.callPackage ./nix/haskell-jsonnet.nix { };226              rsjsonnet = pkgs.callPackage ./nix/rsjsonnet.nix { };227              # Older released version of jrsonnet itself, for benchmarking purposes228              jrsonnet-release = pkgs.callPackage ./nix/jrsonnet-release.nix {229                rustPlatform = pkgs.makeRustPlatform {230                  rustc = toolchain;231                  cargo = toolchain;232                };233              };234            };235          };236          packages =237            let238              jrsonnet = pkgs.callPackage ./nix/jrsonnet.nix {239                inherit craneLib;240              };241              jrsonnet-experimental = pkgs.callPackage ./nix/jrsonnet.nix {242                inherit craneLib;243                withExperimentalFeatures = true;244              };245            in246            {247              default = jrsonnet;248              inherit jrsonnet jrsonnet-experimental;249            };250          checks = optionalAttrs (system != "armv7l-linux") {251            formatting = treefmt.check inputs.self;252          };253          formatter = mkIf (system != "armv7l-linux") treefmt.wrapper;254          shelly.shells.default = {255            factory = craneLib.devShell;256            packages =257              with pkgs;258              [259                cargo-edit260                cargo-outdated261                cargo-watch262                cargo-insta263                cargo-hack264                cargo-show-asm265                lld266                hyperfine267                graphviz268              ]269              ++ optionals (!stdenv.isDarwin) [270                valgrind271                kdePackages.kcachegrind272                samply273              ];274          };275        };276      hercules-ci.github-releases.files =277        let278          rel = system: inputs.self.legacyPackages.${system}.release;279          bin = drv: "${drv}/bin/jrsonnet";280          exe = drv: "${drv}/bin/jrsonnet.exe";281        in282        [283          {284            label = "jrsonnet-x86_64-linux-musl";285            path = bin (rel "x86_64-linux").jrsonnet-linux-musl;286          }287          {288            label = "jrsonnet-experimental-x86_64-linux-musl";289            path = bin (rel "x86_64-linux").jrsonnet-experimental-linux-musl;290          }291          {292            label = "jrsonnet-aarch64-darwin";293            path = bin (rel "aarch64-linux").jrsonnet-darwin;294          }295          {296            label = "jrsonnet-experimental-aarch64-darwin";297            path = bin (rel "aarch64-linux").jrsonnet-experimental-darwin;298          }299          {300            label = "jrsonnet-x86_64-windows.exe";301            path = exe (rel "x86_64-linux").jrsonnet-windows;302          }303          {304            label = "jrsonnet-experimental-x86_64-windows.exe";305            path = exe (rel "x86_64-linux").jrsonnet-experimental-windows;306          }307308          {309            label = "jrsonnet-aarch64-linux-musl";310            path = bin (rel "aarch64-linux").jrsonnet-linux-musl;311          }312          {313            label = "jrsonnet-experimental-aarch64-linux-musl";314            path = bin (rel "aarch64-linux").jrsonnet-experimental-linux-musl;315          }316317          {318            label = "jrsonnet-x86_64-linux-glibc";319            path = bin (rel "x86_64-linux").jrsonnet-linux-glibc;320          }321          {322            label = "jrsonnet-experimental-x86_64-linux-glibc";323            path = bin (rel "x86_64-linux").jrsonnet-experimental-linux-glibc;324          }325          {326            label = "jrsonnet-aarch64-linux-glibc";327            path = bin (rel "aarch64-linux").jrsonnet-linux-glibc;328          }329          {330            label = "jrsonnet-experimental-aarch64-linux-glibc";331            path = bin (rel "aarch64-linux").jrsonnet-experimental-linux-glibc;332          }333          {334            label = "jrsonnet-i686-linux-glibc";335            path = bin (rel "i686-linux").jrsonnet-linux-glibc;336          }337          {338            label = "jrsonnet-experimental-i686-linux-glibc";339            path = bin (rel "i686-linux").jrsonnet-experimental-linux-glibc;340          }341          {342            label = "jrsonnet-armv7l-linux-glibc";343            path = bin (rel "armv7l-linux").jrsonnet-linux-glibc;344          }345          {346            label = "jrsonnet-experimental-armv7l-linux-glibc";347            path = bin (rel "armv7l-linux").jrsonnet-experimental-linux-glibc;348          }349        ];350      hercules-ci.cargo-publish = {351        enable = true;352        secretName = "crates-io";353        extraPublishArgs = [ "--workspace" ];354      };355      hercules-ci.flake-update = {356        enable = true;357        baseMerge.enable = true;358        baseMerge.method = "fast-forward";359        when = {360          dayOfWeek = [ "Sat" ];361        };362      };363      herculesCI =364        { lib, config, ... }:365        {366          ciSystems = [367            "x86_64-linux"368            "i686-linux"369            "aarch64-linux"370            "armv7l-linux"371            # TODO: add workers for these platforms372            # "aarch64-darwin"373          ];374          onPush.default.outputs = {375            benchmarks.x86_64-linux = inputs.self.legacyPackages.x86_64-linux.benchmarks.default;376377            # Cross: musl/mingw/darwin-zigbuild378            release.x86_64-linux = inputs.self.legacyPackages.x86_64-linux.release;379            release.aarch64-linux = inputs.self.legacyPackages.aarch64-linux.release;380            release.armv7l-linux = inputs.self.legacyPackages.armv7l-linux.release;381            release.i686-linux = inputs.self.legacyPackages.i686-linux.release;382383            # Too much to build for CI purposes384            devShells = mkForce { };385            formatter = mkForce { };386387            # No need to run them on different arch, pretty large derivations and might try to compile GHC388            checks.i686-linux.formatting = mkForce { };389            checks.aarch64-linux.formatting = mkForce { };390          };391        };392    };393}