From b8bef138ffa992902cecfe923b747f0498178a68 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Mon, 17 Oct 2022 18:43:32 +0000 Subject: [PATCH] Merge pull request #84 from CertainLach/split-stdlib Separate jrsonnet-evaluator and stdlib implementation #82 --- --- a/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/1.2.3/direnvrc" "sha256-/aHqL/6nLpHcZJcB5/7/5+mO338l28uFbq88DMfWJn4=" - -use flake --- a/.gitlab-ci.yml +++ /dev/null @@ -1,58 +0,0 @@ -variables: - CARGO_HOME: $CI_PROJECT_DIR/cache - -stages: - - prepare - - build - -build-container: - image: docker:19.03.11 - stage: prepare - services: - - docker:19.03.11-dind - before_script: - - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - script: - - docker pull $CI_REGISTRY_IMAGE:build || true - - docker build -t $CI_REGISTRY_IMAGE:build -f build/Dockerfile . - - docker push $CI_REGISTRY_IMAGE:build - -test-library: - image: $CI_REGISTRY_IMAGE:build - stage: build - script: - - cargo clippy - cache: - key: test - paths: - - ./cache - -build-linux: - image: $CI_REGISTRY_IMAGE:build - stage: build - script: - - cargo build --release - cache: - key: linux - paths: - - ./cache - - ./target - artifacts: - paths: - - ./target/release/jrsonnet - expire_in: 30 days - -build-wasm: - image: $CI_REGISTRY_IMAGE:build - stage: build - script: - - cargo build --target=wasm32-wasi --release - cache: - key: wasm - paths: - - ./cache - - ./target - artifacts: - paths: - - ./target/wasm32-wasi/release/jsonnet.wasm - expire_in: 30 days --- a/Cargo.lock +++ b/Cargo.lock @@ -87,16 +87,16 @@ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", "textwrap", @@ -104,18 +104,18 @@ [[package]] name = "clap_complete" -version = "3.1.4" +version = "3.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da92e6facd8d73c22745a5d3cbb59bdf8e46e3235c923e516527d8e81eec14a4" +checksum = "ead064480dfc4880a10764488415a97fdd36a4cf1bb022d372f02e8faf8386e1" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" dependencies = [ "heck", "proc-macro-error", @@ -126,9 +126,9 @@ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] @@ -221,6 +221,7 @@ "jrsonnet-evaluator", "jrsonnet-gcmodule", "jrsonnet-parser", + "jrsonnet-stdlib", ] [[package]] @@ -229,21 +230,17 @@ dependencies = [ "annotate-snippets", "anyhow", - "base64", "bincode", "hashbrown 0.12.1", "jrsonnet-gcmodule", "jrsonnet-interner", "jrsonnet-macros", "jrsonnet-parser", - "jrsonnet-stdlib", "jrsonnet-types", - "md5", "pathdiff", "rustc-hash", "serde", "serde_json", - "serde_yaml_with_quirks", "static_assertions", "strsim", "thiserror", @@ -278,6 +275,7 @@ "jrsonnet-gcmodule", "rustc-hash", "serde", + "structdump", ] [[package]] @@ -295,15 +293,28 @@ dependencies = [ "jrsonnet-gcmodule", "jrsonnet-interner", - "jrsonnet-stdlib", "peg", "serde", "static_assertions", + "structdump", ] [[package]] name = "jrsonnet-stdlib" version = "0.4.2" +dependencies = [ + "base64", + "bincode", + "jrsonnet-evaluator", + "jrsonnet-gcmodule", + "jrsonnet-macros", + "jrsonnet-parser", + "md5", + "serde", + "serde_json", + "serde_yaml_with_quirks", + "structdump", +] [[package]] name = "jrsonnet-types" @@ -314,31 +325,26 @@ ] [[package]] -name = "jsonnet" +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "libjsonnet" version = "0.4.2" dependencies = [ "jrsonnet-evaluator", "jrsonnet-gcmodule", "jrsonnet-parser", + "jrsonnet-stdlib", ] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] -name = "libc" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" - -[[package]] name = "linked-hash-map" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lock_api" @@ -511,18 +517,18 @@ [[package]] name = "serde" -version = "1.0.137" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e" dependencies = [ "proc-macro2", "quote", @@ -531,9 +537,9 @@ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ "indexmap", "itoa", @@ -572,6 +578,28 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] +name = "structdump" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0570327507bf281d8a6e6b0d4c082b12cb6bcee27efce755aa5efacd44076c1" +dependencies = [ + "proc-macro2", + "quote", + "structdump-derive", +] + +[[package]] +name = "structdump-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29cc0b59cfa11f1bceda09a9a7e37e6a6c3138575fd24ade8aa9af6d09aedf28" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "syn" version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -592,6 +620,16 @@ ] [[package]] +name = "tests" +version = "0.1.0" +dependencies = [ + "jrsonnet-evaluator", + "jrsonnet-gcmodule", + "jrsonnet-stdlib", + "serde", +] + +[[package]] name = "textwrap" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*", "bindings/jsonnet", "cmds/jrsonnet"] +members = ["crates/*", "bindings/jsonnet", "cmds/jrsonnet", "tests"] [profile.test] opt-level = 1 --- a/bindings/jsonnet/Cargo.toml +++ b/bindings/jsonnet/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "jsonnet" +name = "libjsonnet" description = "Rust implementation of libjsonnet.so" version = "0.4.2" authors = ["Yaroslav Bolyukin "] @@ -10,12 +10,15 @@ [dependencies] jrsonnet-evaluator = { path = "../../crates/jrsonnet-evaluator", version = "0.4.2" } jrsonnet-parser = { path = "../../crates/jrsonnet-parser", version = "0.4.2" } +jrsonnet-stdlib = { path = "../../crates/jrsonnet-stdlib", version = "0.4.2" } jrsonnet-gcmodule = { version = "0.3.4" } [lib] +name = "jsonnet" crate-type = ["cdylib"] [features] +# Export additional functions for native integration, i.e ability to set custom trace format interop = [] experimental = ["exp-preserve-order", "exp-destruct"] exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"] --- a/bindings/jsonnet/src/import.rs +++ b/bindings/jsonnet/src/import.rs @@ -4,18 +4,18 @@ any::Any, cell::RefCell, collections::HashMap, + env::current_dir, ffi::{c_void, CStr, CString}, - fs::File, - io::Read, os::raw::{c_char, c_int}, - path::{Path, PathBuf}, + path::PathBuf, ptr::null_mut, }; use jrsonnet_evaluator::{ error::{Error::*, Result}, - throw, ImportResolver, State, + throw, FileImportResolver, ImportResolver, State, }; +use jrsonnet_parser::{SourceDirectory, SourceFile, SourcePath}; pub type JsonnetImportCallback = unsafe extern "C" fn( ctx: *mut c_void, @@ -29,28 +29,34 @@ pub struct CallbackImportResolver { cb: JsonnetImportCallback, ctx: *mut c_void, - out: RefCell>>, + out: RefCell>>, } impl ImportResolver for CallbackImportResolver { - fn resolve_file(&self, from: &Path, path: &str) -> Result { - let base = CString::new(from.to_str().unwrap()).unwrap().into_raw(); - let rel = CString::new(path).unwrap().into_raw(); + fn resolve_from(&self, from: &SourcePath, path: &str) -> Result { + let base = if let Some(p) = from.downcast_ref::() { + let mut o = p.path().to_owned(); + o.pop(); + o + } else if let Some(d) = from.downcast_ref::() { + d.path().to_owned() + } else if from.is_default() { + current_dir().map_err(|e| ImportIo(e.to_string()))? + } else { + unreachable!("can't resolve this path"); + }; + let base = unsafe { crate::unparse_path(&base) }; + let rel = CString::new(path).unwrap(); let found_here: *mut c_char = null_mut(); let mut success: i32 = 0; let result_ptr = unsafe { (self.cb)( self.ctx, - base, - rel, + base.as_ptr(), + rel.as_ptr(), &mut (found_here as *const _), &mut success, ) }; - // Release memory occipied by arguments passed - unsafe { - let _ = CString::from_raw(base); - let _ = CString::from_raw(rel); - } let result_raw = unsafe { CStr::from_ptr(result_ptr) }; let result_str = result_raw.to_str().unwrap(); assert!(success == 0 || success == 1); @@ -61,7 +67,9 @@ } let found_here_raw = unsafe { CStr::from_ptr(found_here) }; - let found_here_buf = PathBuf::from(found_here_raw.to_str().unwrap()); + let found_here_buf = SourcePath::new(SourceFile::new(PathBuf::from( + found_here_raw.to_str().unwrap(), + ))); unsafe { let _ = CString::from_raw(found_here); } @@ -74,16 +82,18 @@ Ok(found_here_buf) } - fn load_file_contents(&self, resolved: &Path) -> Result> { + fn load_file_contents(&self, resolved: &SourcePath) -> Result> { Ok(self.out.borrow().get(resolved).unwrap().clone()) } - unsafe fn as_any(&self) -> &dyn Any { + fn as_any(&self) -> &dyn Any { self } } /// # Safety +/// +/// It should be safe to call `cb` using valid values with passed `ctx` #[no_mangle] pub unsafe extern "C" fn jsonnet_import_callback( vm: &State, @@ -97,56 +107,17 @@ })) } -/// Standard FS import resolver -#[derive(Default)] -pub struct NativeImportResolver { - library_paths: RefCell>, -} -impl NativeImportResolver { - fn add_jpath(&self, path: PathBuf) { - self.library_paths.borrow_mut().push(path); - } -} -impl ImportResolver for NativeImportResolver { - fn resolve_file(&self, from: &Path, path: &str) -> Result { - let mut new_path = from.to_owned(); - new_path.push(path); - if new_path.exists() { - Ok(new_path) - } else { - for library_path in self.library_paths.borrow().iter() { - let mut cloned = library_path.clone(); - cloned.push(path); - if cloned.exists() { - return Ok(cloned); - } - } - throw!(ImportFileNotFound(from.to_owned(), path.to_owned())) - } - } - fn load_file_contents(&self, id: &Path) -> Result> { - let mut file = File::open(id).map_err(|_e| ResolvedFileNotFound(id.to_owned()))?; - let mut out = Vec::new(); - file.read_to_end(&mut out) - .map_err(|e| ImportIo(e.to_string()))?; - Ok(out) - } - unsafe fn as_any(&self) -> &dyn Any { - self - } -} - /// # Safety /// -/// This function is safe, if received v is a pointer to normal C string +/// `path` should be a NUL-terminated string #[no_mangle] -pub unsafe extern "C" fn jsonnet_jpath_add(vm: &State, v: *const c_char) { - let cstr = CStr::from_ptr(v); +pub unsafe extern "C" fn jsonnet_jpath_add(vm: &State, path: *const c_char) { + let cstr = CStr::from_ptr(path); let path = PathBuf::from(cstr.to_str().unwrap()); - let any_resolver = &vm.settings().import_resolver; + let any_resolver = vm.import_resolver(); let resolver = any_resolver .as_any() - .downcast_ref::() + .downcast_ref::() .expect("jpaths are not compatible with callback imports!"); resolver.add_jpath(path); } --- a/bindings/jsonnet/src/lib.rs +++ b/bindings/jsonnet/src/lib.rs @@ -10,50 +10,98 @@ use std::{ alloc::Layout, - ffi::{CStr, CString}, + borrow::Cow, + ffi::{CStr, CString, OsStr}, os::raw::{c_char, c_double, c_int, c_uint}, - path::PathBuf, + path::Path, }; -use import::NativeImportResolver; -use jrsonnet_evaluator::{IStr, ManifestFormat, State, Val}; +use jrsonnet_evaluator::{ + trace::PathResolver, FileImportResolver, IStr, ManifestFormat, State, Val, +}; /// WASM stub #[cfg(target_arch = "wasm32")] #[no_mangle] pub extern "C" fn _start() {} +/// Return the version string of the Jsonnet interpreter. +/// Conforms to [semantic versioning](http://semver.org/). +/// If this does not match `LIB_JSONNET_VERSION` +/// then there is a mismatch between header and compiled library. #[no_mangle] pub extern "C" fn jsonnet_version() -> &'static [u8; 8] { b"v0.16.0\0" } +unsafe fn parse_path(input: &CStr) -> Cow { + #[cfg(target_family = "unix")] + { + use std::os::unix::ffi::OsStrExt; + let str = OsStr::from_bytes(input.to_bytes()); + Cow::Borrowed(Path::new(str)) + } + #[cfg(not(target_family = "unix"))] + { + let string = input.to_str().expect("bad utf-8"); + Cow::Borrowed(string.as_ref()) + } +} + +unsafe fn unparse_path(input: &Path) -> Cow { + #[cfg(target_family = "unix")] + { + use std::os::unix::ffi::OsStrExt; + let str = CString::new(input.as_os_str().as_bytes()).expect("input has zero byte in it"); + Cow::Owned(str) + } + #[cfg(not(target_family = "unix"))] + { + let str = input.as_os_str().to_str().expect("bad utf-8"); + let cstr = CString::new(str).expect("input has NUL inside"); + Cow::Owned(cstr) + } +} + +/// Creates a new Jsonnet virtual machine. #[no_mangle] +#[allow(clippy::box_default)] pub extern "C" fn jsonnet_make() -> *mut State { let state = State::default(); - state.with_stdlib(); - state.settings_mut().import_resolver = Box::new(NativeImportResolver::default()); + state.settings_mut().import_resolver = Box::new(FileImportResolver::default()); + state.settings_mut().context_initializer = Box::new(jrsonnet_stdlib::ContextInitializer::new( + state.clone(), + PathResolver::new_cwd_fallback(), + )); Box::into_raw(Box::new(state)) } -/// # Safety +/// Complement of [`jsonnet_vm_make`]. #[no_mangle] #[allow(clippy::boxed_local)] -pub unsafe extern "C" fn jsonnet_destroy(vm: *mut State) { - Box::from_raw(vm); +pub extern "C" fn jsonnet_destroy(vm: Box) { + drop(vm); } +/// Set the maximum stack depth. #[no_mangle] pub extern "C" fn jsonnet_max_stack(vm: &State, v: c_uint) { vm.settings_mut().max_stack = v as usize; } -// jrsonnet currently have no GC, so these functions is no-op +/// Set the number of objects required before a garbage collection cycle is allowed. +/// +/// No-op for now #[no_mangle] pub extern "C" fn jsonnet_gc_min_objects(_vm: &State, _v: c_uint) {} + +/// Run the garbage collector after this amount of growth in the number of objects +/// +/// No-op for now #[no_mangle] pub extern "C" fn jsonnet_gc_growth_trigger(_vm: &State, _v: c_double) {} +/// Expect a string as output and don't JSON encode it. #[no_mangle] pub extern "C" fn jsonnet_string_output(vm: &State, v: c_int) { match v { @@ -67,13 +115,20 @@ } } +/// Allocate, resize, or free a buffer. This will abort if the memory cannot be allocated. It will +/// only return NULL if sz was zero. +/// /// # Safety /// +/// `buf` should be either previosly allocated by this library, or NULL +/// /// This function is most definitely broken, but it works somehow, see TODO inside #[no_mangle] pub unsafe extern "C" fn jsonnet_realloc(_vm: &State, buf: *mut u8, sz: usize) -> *mut u8 { if buf.is_null() { - assert!(sz != 0); + if sz == 0 { + return std::ptr::null_mut(); + } return std::alloc::alloc(Layout::from_size_align(sz, std::mem::align_of::()).unwrap()); } // TODO: Somehow store size of allocation, because its real size is probally not 16 :D @@ -88,30 +143,37 @@ std::alloc::realloc(buf, old_layout, sz) } -/// # Safety +/// Clean up a JSON subtree. +/// +/// This is useful if you want to abort with an error mid-way through building a complex value. #[no_mangle] #[allow(clippy::boxed_local)] -pub unsafe extern "C" fn jsonnet_json_destroy(_vm: &State, v: *mut Val) { - Box::from_raw(v); +pub extern "C" fn jsonnet_json_destroy(_vm: &State, v: Box) { + drop(v); } +/// Set the number of lines of stack trace to display (0 for all of them). #[no_mangle] pub extern "C" fn jsonnet_max_trace(vm: &State, v: c_uint) { vm.set_max_trace(v as usize) } +/// Evaluate a file containing Jsonnet code, return a JSON string. +/// +/// The returned string should be cleaned up with jsonnet_realloc. +/// /// # Safety /// -/// This function is safe, if received v is a pointer to normal C string +/// `filename` should be a NUL-terminated string #[no_mangle] pub unsafe extern "C" fn jsonnet_evaluate_file( vm: &State, filename: *const c_char, error: &mut c_int, ) -> *const c_char { - let filename = CStr::from_ptr(filename); + let filename = parse_path(CStr::from_ptr(filename)); match vm - .import(PathBuf::from(filename.to_str().unwrap())) + .import(&filename) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest(v)) { @@ -127,9 +189,13 @@ } } +/// Evaluate a string containing Jsonnet code, return a JSON string. +/// +/// The returned string should be cleaned up with jsonnet_realloc. +/// /// # Safety /// -/// This function is safe, if received v is a pointer to normal C string +/// `filename`, `snippet` should be a NUL-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_evaluate_snippet( vm: &State, @@ -140,10 +206,7 @@ let filename = CStr::from_ptr(filename); let snippet = CStr::from_ptr(snippet); match vm - .evaluate_snippet( - filename.to_str().unwrap().into(), - snippet.to_str().unwrap().into(), - ) + .evaluate_snippet(filename.to_str().unwrap(), snippet.to_str().unwrap()) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest(v)) { @@ -183,9 +246,9 @@ filename: *const c_char, error: &mut c_int, ) -> *const c_char { - let filename = CStr::from_ptr(filename); + let filename = parse_path(CStr::from_ptr(filename)); match vm - .import(PathBuf::from(filename.to_str().unwrap())) + .import(&filename) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_multi(v)) { @@ -212,10 +275,7 @@ let filename = CStr::from_ptr(filename); let snippet = CStr::from_ptr(snippet); match vm - .evaluate_snippet( - filename.to_str().unwrap().into(), - snippet.to_str().unwrap().into(), - ) + .evaluate_snippet(filename.to_str().unwrap(), snippet.to_str().unwrap()) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_multi(v)) { @@ -253,9 +313,9 @@ filename: *const c_char, error: &mut c_int, ) -> *const c_char { - let filename = CStr::from_ptr(filename); + let filename = parse_path(CStr::from_ptr(filename)); match vm - .import(PathBuf::from(filename.to_str().unwrap())) + .import(&filename) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_stream(v)) { @@ -266,7 +326,9 @@ Err(e) => { *error = 1; let out = vm.stringify_err(&e); - CString::new(&out as &str).unwrap().into_raw() + CString::new(&out as &str) + .expect("there should be no \\0 in the error string") + .into_raw() } } } @@ -283,8 +345,8 @@ let snippet = CStr::from_ptr(snippet); match vm .evaluate_snippet( - filename.to_str().unwrap().into(), - snippet.to_str().unwrap().into(), + filename.to_str().expect("filename is not utf-8"), + snippet.to_str().expect("snippet is not utf-8"), ) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_stream(v)) @@ -296,7 +358,9 @@ Err(e) => { *error = 1; let out = vm.stringify_err(&e); - CString::new(&out as &str).unwrap().into_raw() + CString::new(&out as &str) + .expect("there should be no \\0 in the error string") + .into_raw() } } } --- a/bindings/jsonnet/src/native.rs +++ b/bindings/jsonnet/src/native.rs @@ -1,17 +1,27 @@ use std::{ + borrow::Cow, ffi::{c_void, CStr}, os::raw::{c_char, c_int}, }; use jrsonnet_evaluator::{ error::{Error, LocError}, - function::builtin::{BuiltinParam, NativeCallback, NativeCallbackHandler}, + function::builtin::{NativeCallback, NativeCallbackHandler}, tb, typed::Typed, IStr, State, Val, }; use jrsonnet_gcmodule::Cc; +/// The returned `JsonnetJsonValue*` should be allocated with `jsonnet_realloc`. It will be cleaned up +/// along with the objects rooted at `argv` by `libjsonnet` when no-longer needed. Return a string upon +/// failure, which will appear in Jsonnet as an error. The `argv` pointer is an array whose size +/// matches the array of parameters supplied when the native callback was originally registered. +/// +/// - `ctx` User pointer, given in jsonnet_native_callback. +/// - `argv` Array of arguments from Jsonnet code. +/// - `param` success Set this byref param to 1 to indicate success and 0 for failure. +/// Returns the content of the imported file, or an error message. type JsonnetNativeCallback = unsafe extern "C" fn( ctx: *const c_void, argv: *const *const Val, @@ -44,13 +54,20 @@ if success == 1 { Ok(v) } else { - let e = IStr::from_untyped(v, s).expect("error msg"); + let e = IStr::from_untyped(v, s).expect("error msg should be a string"); Err(Error::RuntimeError(e).into()) } } } +/// Callback to provide native extensions to Jsonnet. +/// /// # Safety +/// +/// `vm` should be a vm allocated by `jsonnet_make` +/// `name` should be a NUL-terminated string +/// `cb` should be a function pointer +/// `raw_params` should point to a NULL-terminated array of NUL-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_native_callback( vm: &State, @@ -59,26 +76,33 @@ ctx: *const c_void, mut raw_params: *const *const c_char, ) { - let name = CStr::from_ptr(name).to_str().expect("utf8 name").into(); + let name = CStr::from_ptr(name) + .to_str() + .expect("name is not utf-8") + .into(); let mut params = Vec::new(); loop { if (*raw_params).is_null() { break; } - let param = CStr::from_ptr(*raw_params).to_str().expect("not utf8"); - params.push(BuiltinParam { - name: param.into(), - has_default: false, - }); + let param = CStr::from_ptr(*raw_params) + .to_str() + .expect("param name is not utf-8"); + params.push(Cow::Owned(param.into())); raw_params = raw_params.offset(1); } - vm.add_native( - name, - #[allow(deprecated)] - Cc::new(tb!(NativeCallback::new( - params, - tb!(JsonnetNativeCallbackHandler { ctx, cb }), - ))), - ) + let any_resolver = vm.context_initializer(); + any_resolver + .as_any() + .downcast_ref::() + .expect("only stdlib context initializer supported") + .add_native( + name, + #[allow(deprecated)] + Cc::new(tb!(NativeCallback::new( + params, + tb!(JsonnetNativeCallbackHandler { ctx, cb }), + ))), + ) } --- a/bindings/jsonnet/src/val_extract.rs +++ b/bindings/jsonnet/src/val_extract.rs @@ -7,13 +7,16 @@ use jrsonnet_evaluator::{State, Val}; +/// If the value is a string, return it as UTF-8, otherwise return `NULL`. #[no_mangle] pub extern "C" fn jsonnet_json_extract_string(_vm: &State, v: &Val) -> *mut c_char { match v { - Val::Str(s) => CString::new(&*s as &str).unwrap().into_raw(), + Val::Str(s) => CString::new(s as &str).unwrap().into_raw(), _ => std::ptr::null_mut(), } } + +/// If the value is a number, return `1` and store the number in out, otherwise return `0`. #[no_mangle] pub extern "C" fn jsonnet_json_extract_number(_vm: &State, v: &Val, out: &mut c_double) -> c_int { match v { @@ -24,6 +27,8 @@ _ => 0, } } + +/// Return `0` if the value is `false`, `1` if it is `true`, and `2` if it is not a `bool`. #[no_mangle] pub extern "C" fn jsonnet_json_extract_bool(_vm: &State, v: &Val) -> c_int { match v { @@ -32,6 +37,8 @@ _ => 2, } } + +/// Return `1` if the value is `null`, otherwise return `0`. #[no_mangle] pub extern "C" fn jsonnet_json_extract_null(_vm: &State, v: &Val) -> c_int { match v { --- a/bindings/jsonnet/src/val_make.rs +++ b/bindings/jsonnet/src/val_make.rs @@ -8,37 +8,46 @@ use jrsonnet_evaluator::{val::ArrValue, ObjValue, State, Val}; use jrsonnet_gcmodule::Cc; +/// Convert the given `UTF-8` string to a `JsonnetJsonValue`. +/// /// # Safety /// -/// This function is safe, if received v is a pointer to normal C string +/// `v` should be a NUL-terminated string #[no_mangle] -pub unsafe extern "C" fn jsonnet_json_make_string(_vm: &State, v: *const c_char) -> *mut Val { - let cstr = CStr::from_ptr(v); - let str = cstr.to_str().unwrap(); - Box::into_raw(Box::new(Val::Str(str.into()))) +pub unsafe extern "C" fn jsonnet_json_make_string(_vm: &State, val: *const c_char) -> *mut Val { + let val = CStr::from_ptr(val); + let val = val.to_str().expect("string is not utf-8"); + Box::into_raw(Box::new(Val::Str(val.into()))) } +/// Convert the given double to a `JsonnetJsonValue`. #[no_mangle] pub extern "C" fn jsonnet_json_make_number(_vm: &State, v: c_double) -> *mut Val { Box::into_raw(Box::new(Val::Num(v))) } +/// Convert the given `bool` (`1` or `0`) to a `JsonnetJsonValue`. #[no_mangle] pub extern "C" fn jsonnet_json_make_bool(_vm: &State, v: c_int) -> *mut Val { - assert!(v == 0 || v == 1); + assert!(v == 0 || v == 1, "bad boolean value"); Box::into_raw(Box::new(Val::Bool(v == 1))) } +/// Make a `JsonnetJsonValue` representing `null`. #[no_mangle] pub extern "C" fn jsonnet_json_make_null(_vm: &State) -> *mut Val { Box::into_raw(Box::new(Val::Null)) } +/// Make a `JsonnetJsonValue` representing an array. +/// +/// Assign elements with [`jsonnet_json_array_append`]. #[no_mangle] pub extern "C" fn jsonnet_json_make_array(_vm: &State) -> *mut Val { Box::into_raw(Box::new(Val::Arr(ArrValue::Eager(Cc::new(Vec::new()))))) } +/// Make a `JsonnetJsonValue` representing an object. #[no_mangle] pub extern "C" fn jsonnet_json_make_object(_vm: &State) -> *mut Val { Box::into_raw(Box::new(Val::Obj(ObjValue::new_empty()))) --- a/bindings/jsonnet/src/val_modify.rs +++ b/bindings/jsonnet/src/val_modify.rs @@ -7,9 +7,12 @@ use jrsonnet_evaluator::{val::ArrValue, State, Thunk, Val}; use jrsonnet_gcmodule::Cc; +/// Adds value to the end of the array `arr`. +/// /// # Safety /// -/// Received arr value should be correct pointer to array allocated by make_array +/// `arr` should be a pointer to array value allocated by make_array, or returned by other library call +/// `val` should be a pointer to value allocated using this library #[no_mangle] pub unsafe extern "C" fn jsonnet_json_array_append(_vm: &State, arr: &mut Val, val: &Val) { match arr { @@ -26,9 +29,14 @@ } } +/// Adds the field to the object, bound to value. +/// +/// This shadows any previous binding of the field. +/// /// # Safety /// -/// This function is safe if passed name is ok +/// `obj` should be a pointer to object value allocated by `make_object`, or returned by other library call +/// `name` should be NUL-terminated string #[no_mangle] pub unsafe extern "C" fn jsonnet_json_object_append( _vm: &State, --- a/bindings/jsonnet/src/vars_tlas.rs +++ b/bindings/jsonnet/src/vars_tlas.rs @@ -4,40 +4,84 @@ use jrsonnet_evaluator::State; +/// Binds a Jsonnet external variable to the given string. +/// +/// Argument values are copied so memory should be managed by the caller. +/// /// # Safety +/// +/// `name`, `code` should be a NUL-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_ext_var(vm: &State, name: *const c_char, value: *const c_char) { let name = CStr::from_ptr(name); let value = CStr::from_ptr(value); - vm.add_ext_str( - name.to_str().unwrap().into(), - value.to_str().unwrap().into(), - ) + + let any_initializer = vm.context_initializer(); + any_initializer + .as_any() + .downcast_ref::() + .expect("only stdlib context initializer supported") + .add_ext_str( + name.to_str().expect("name is not utf-8").into(), + value.to_str().expect("value is not utf-8").into(), + ) } +/// Binds a Jsonnet external variable to the given code. +/// +/// Argument values are copied so memory should be managed by the caller. +/// /// # Safety +/// +/// `name`, `code` should be a NUL-terminated strings #[no_mangle] -pub unsafe extern "C" fn jsonnet_ext_code(vm: &State, name: *const c_char, value: *const c_char) { +pub unsafe extern "C" fn jsonnet_ext_code(vm: &State, name: *const c_char, code: *const c_char) { let name = CStr::from_ptr(name); - let value = CStr::from_ptr(value); - vm.add_ext_code(name.to_str().unwrap(), value.to_str().unwrap().into()) - .unwrap() + let code = CStr::from_ptr(code); + + let any_initializer = vm.context_initializer(); + any_initializer + .as_any() + .downcast_ref::() + .expect("only stdlib context initializer supported") + .add_ext_code( + name.to_str().expect("name is not utf-8"), + code.to_str().expect("code is not utf-8"), + ) + .expect("can't parse ext code") } + +/// Binds a top-level string argument for a top-level parameter. +/// +/// Argument values are copied so memory should be managed by the caller. +/// /// # Safety +/// +/// `name`, `value` should be a NUL-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_tla_var(vm: &State, name: *const c_char, value: *const c_char) { let name = CStr::from_ptr(name); let value = CStr::from_ptr(value); vm.add_tla_str( - name.to_str().unwrap().into(), - value.to_str().unwrap().into(), + name.to_str().expect("name is not utf-8").into(), + value.to_str().expect("value is not utf-8").into(), ) } + +/// Binds a top-level code argument for a top-level parameter. +/// +/// Argument values are copied so memory should be managed by the caller. +/// /// # Safety +/// +/// `name`, `code` should be a NUL-terminated strings #[no_mangle] -pub unsafe extern "C" fn jsonnet_tla_code(vm: &State, name: *const c_char, value: *const c_char) { +pub unsafe extern "C" fn jsonnet_tla_code(vm: &State, name: *const c_char, code: *const c_char) { let name = CStr::from_ptr(name); - let value = CStr::from_ptr(value); - vm.add_tla_code(name.to_str().unwrap().into(), value.to_str().unwrap()) - .unwrap() + let code = CStr::from_ptr(code); + vm.add_tla_code( + name.to_str().expect("name is not utf-8").into(), + code.to_str().expect("code is not utf-8"), + ) + .expect("can't parse tla code") } --- a/build/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM alpine:edge - -RUN apk add --no-cache \ - clang gcc g++ make cmake curl \ - openjdk8-jre-base \ - rustup && \ - rustup-init --default-toolchain nightly -y -t wasm32-wasi -ENV PATH /root/.rustup/toolchains/nightly-x86_64-unknown-linux-musl/bin/:/root/.cargo/bin/:${PATH} --- a/build/make-docker.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -export DOCKER_BUILDKIT=1 -docker build -t jrsonnet -f build/Dockerfile build/ -docker run --rm -it -v $PWD:/build -e CARGO_HOME=/build/cache jrsonnet:latest ash -c "cd /build&&make $@" --- a/build/run-docker.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -export DOCKER_BUILDKIT=1 -docker build -t jrsonnet -f build/Dockerfile build/ -docker run --rm -it -v $PWD:/build -e CARGO_HOME=/build/cache jrsonnet:latest ash -c "cd /build&&$@" --- a/cmds/jrsonnet-fmt/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "jrsonnet-fmt" -version = "0.1.0" -edition = "2021" - -[dependencies] -dprint-core = "0.47.1" -jrsonnet-parser = { path = "../../crates/jrsonnet-parser" } --- a/cmds/jrsonnet-fmt/src/main.rs +++ /dev/null @@ -1,373 +0,0 @@ -use std::path::PathBuf; - -use dprint_core::formatting::{PrintItems, PrintOptions, Signal}; -use jrsonnet_parser::{ - ArgsDesc, BinaryOpType, BindSpec, Expr, FieldName, LocExpr, Member, ObjBody, Param, ParamsDesc, - ParserSettings, Visibility, -}; - -pub trait Printable { - fn print(&self) -> PrintItems; -} - -macro_rules! pi { - (@i; $($t:tt)*) => {{ - let mut o = PrintItems::new(); - pi!(@s; o: $($t)*); - o - }}; - (@s; $o:ident: str($e:expr) $($t:tt)*) => {{ - $o.push_str($e); - pi!(@s; $o: $($t)*); - }}; - (@s; $o:ident: nl $($t:tt)*) => {{ - $o.push_signal(Signal::NewLine); - pi!(@s; $o: $($t)*); - }}; - (@s; $o:ident: >i $($t:tt)*) => {{ - $o.push_signal(Signal::StartIndent); - pi!(@s; $o: $($t)*); - }}; - (@s; $o:ident: {{ - $o.push_signal(Signal::FinishIndent); - pi!(@s; $o: $($t)*); - }}; - (@s; $o:ident: {$expr:expr} $($t:tt)*) => {{ - $o.extend($expr.print()); - pi!(@s; $o: $($t)*); - }}; - (@s; $o:ident: if ($e:expr)($($then:tt)*) $($t:tt)*) => {{ - if $e { - pi!(@s; $o: $($then)*); - } - pi!(@s; $o: $($t)*); - }}; - (@s; $o:ident: ifelse ($e:expr)($($then:tt)*)($($else:tt)*) $($t:tt)*) => {{ - if $e { - pi!(@s; $o: $($then)*); - } else { - pi!(@s; $o: $($else)*); - } - pi!(@s; $o: $($t)*); - }}; - (@s; $i:ident:) => {} -} -macro_rules! p { - (new: $($t:tt)*) => { - pi!(@i; $($t)*) - }; - ($o:ident: $($t:tt)*) => { - pi!(@s; $o: $($t)*) - }; -} - -impl Printable for FieldName { - fn print(&self) -> PrintItems { - match self { - FieldName::Fixed(f) => { - p!(new: str(&f)) - } - FieldName::Dyn(_) => todo!(), - } - } -} - -impl Printable for Visibility { - fn print(&self) -> PrintItems { - match self { - Visibility::Normal => p!(new: str(":")), - Visibility::Hidden => p!(new: str("::")), - Visibility::Unhide => p!(new: str(":::")), - } - } -} - -impl Printable for BinaryOpType { - fn print(&self) -> PrintItems { - let o = self.to_string(); - p!(new: str(&o)) - } -} - -impl Printable for Option { - fn print(&self) -> PrintItems { - if let Some(v) = self { - v.print() - } else { - PrintItems::new() - } - } -} - -impl Printable for Param { - fn print(&self) -> PrintItems { - p!(new: - str(&self.0) - if(self.1.is_some())(str(" = ") {self.1}) - ) - } -} - -impl Printable for ParamsDesc { - fn print(&self) -> PrintItems { - let mut out = PrintItems::new(); - for (i, item) in self.0.iter().enumerate() { - if i != 0 { - p!(out: str(", ")); - } - out.extend(item.print()); - } - out - } -} - -impl Printable for ArgsDesc { - fn print(&self) -> PrintItems { - let mut out = PrintItems::new(); - let mut first = Some(()); - for u in self.unnamed.iter() { - if first.take().is_none() { - p!(out: str(", ")); - } - p!(out: {u}) - } - for (n, u) in self.named.iter() { - if first.take().is_none() { - p!(out: str(", ")); - } - p!(out: str(&n) str(" = ") {u}) - } - - out - } -} - -impl Printable for BindSpec { - fn print(&self) -> PrintItems { - p!(new: str(&self.name) if(self.params.is_some())(str("(") {self.params} str(")")) str(" = ") {self.value}) - } -} - -struct StrExpr<'s>(&'s str); - -impl<'s> Printable for StrExpr<'s> { - fn print(&self) -> PrintItems { - todo!() - } -} - -impl Printable for ObjBody { - fn print(&self) -> PrintItems { - let mut pi = PrintItems::new(); - p!(pi: str("{")); - match self { - ObjBody::MemberList(m) => { - if !m.is_empty() { - p!(pi: nl > i); - for m in m { - match m { - Member::Field(f) => { - p!(pi: - {f.name} {f.params} - if(f.plus)(str("+")) - {f.visibility} str(" ") - {f.value} - str(",") nl - ); - } - Member::BindStmt(s) => { - p!(pi: str("local ") {s} str(",") nl) - } - Member::AssertStmt(a) => p!(pi: str("assert ") {a.0} if(a.1.is_some())( - str(" : ") {a.1} - ) str(",") nl), - } - } - p!(pi: todo!(), - } - p!(pi: str("}")); - pi - } -} - -impl Printable for Expr { - fn print(&self) -> PrintItems { - let mut pi = PrintItems::new(); - match self { - Expr::Literal(l) => match l { - jrsonnet_parser::LiteralType::This => p!(pi: str("self")), - jrsonnet_parser::LiteralType::Super => p!(pi: str("super")), - jrsonnet_parser::LiteralType::Dollar => p!(pi: str("$")), - jrsonnet_parser::LiteralType::Null => p!(pi: str("null")), - jrsonnet_parser::LiteralType::True => p!(pi: str("true")), - jrsonnet_parser::LiteralType::False => p!(pi: str("false")), - }, - Expr::Str(s) => { - p!(pi: str("\"") str(s) str("\"")) - } - Expr::Num(n) => { - let n = n.to_string(); - p!(pi: str(&n)); - } - Expr::Var(v) => p!(pi: str(&v)), - Expr::Arr(a) => { - p!(pi: str("[")); - for (i, v) in a.iter().enumerate() { - if i != 0 { - p!(pi: str(", ")); - } - p!(pi: {v}) - } - p!(pi: str("]")); - } - Expr::ArrComp(_, _) => todo!(), - Expr::Obj(o) => { - p!(pi: {o}); - } - Expr::ObjExtend(a, b) => p!(pi: {a} str(" ") {b}), - Expr::Parened(v) => { - if let Expr::Parened(_) = &v.0 as &Expr { - p!(pi: {v}) - } else { - p!(pi: str("(") {v} str(")")) - } - } - Expr::UnaryOp(_, _) => todo!(), - Expr::BinaryOp(a, o, b) => { - p!(pi: - {a} str(" ") if(!matches!(&b.0 as &Expr, Expr::Obj(_)))({o} str(" ")) {b} - ) - } - Expr::AssertExpr(_, _) => todo!(), - Expr::LocalExpr(s, v) => { - p!(pi: - str("local") nl >i - ); - for spec in s.iter() { - p!(pi: {spec} str(";") nl) - } - p!(pi: - { - let v = i.to_str().unwrap(); - p!(pi: str("import \"") str(&v) str("\"")); - } - Expr::ImportStr(_) => todo!(), - Expr::ErrorStmt(_) => todo!(), - Expr::Apply(f, a, t) => p!(pi: - {f} str("(") {a} str(")") if(*t)(str("tailstrict")) - ), - Expr::Index(a, b) => p!(pi: {a} str("[") {b} str("]")), - Expr::Function(_, _) => todo!(), - Expr::Intrinsic(_) => todo!(), - Expr::IfElse { - cond, - cond_then, - cond_else, - } => p!(pi: - str("if ") {cond.0} str(" then") ifelse(cond_else.is_some())( - nl >i - {cond_then} nl - i - {cond_else} - { - p!(pi: - {v} - str("[") {d.start} str(":") {d.end} - if(d.step.is_some())( - str(":") - {d.step} - ) - str("]") - ) - } - } - pi - } -} - -impl Printable for LocExpr { - fn print(&self) -> PrintItems { - self.0.print() - } -} - -fn main() { - let parsed = jrsonnet_parser::parse( - r#" - - - # Edit me! - local b = import "b.libsonnet"; # comment - local a = import "a.libsonnet"; - - local f(x,y)=x+y; - - - local Template = {z: "foo"}; - - Template + { - local - - h = 3, - assert self.a == 1 - - : "error", - "f": ((((((3)))))) , - "g g": - f(4,2), - arr: [[ - 1, 2, - ], - 3, - { - b: { - c: { - k: [16] - } - } - } - ], - m: a[1::], - m: b[::], - k: if a == b then - - - 2 - - else Template {} - } - - -"#, - &ParserSettings { - file_name: PathBuf::from("example").into(), - }, - ) - .unwrap(); - - let o = dprint_core::formatting::format( - || { - let print_items = parsed.print(); - print_items - }, - PrintOptions { - indent_width: 2, - max_width: 100, - use_tabs: false, - new_line_text: "\n", - }, - ); - println!("{}", o); -} --- a/cmds/jrsonnet/Cargo.toml +++ b/cmds/jrsonnet/Cargo.toml @@ -15,9 +15,12 @@ "jrsonnet-evaluator/exp-preserve-order", "jrsonnet-evaluator/exp-serde-preserve-order", "jrsonnet-cli/exp-preserve-order", + "jrsonnet-cli/exp-serde-preserve-order", ] # Destructuring of locals exp-destruct = ["jrsonnet-evaluator/exp-destruct"] +# std.thisFile support +legacy-this-file = ["jrsonnet-cli/legacy-this-file"] [dependencies] jrsonnet-evaluator = { path = "../../crates/jrsonnet-evaluator", version = "0.4.2" } @@ -27,5 +30,5 @@ mimallocator = { version = "0.1.3", optional = true } thiserror = "1.0" -clap = { version = "3.1", features = ["derive"] } -clap_complete = { version = "3.1" } +clap = { version = "3.2", features = ["derive"] } +clap_complete = { version = "3.2" } --- a/cmds/jrsonnet/src/main.rs +++ b/cmds/jrsonnet/src/main.rs @@ -1,5 +1,4 @@ use std::{ - env::current_dir, fs::{create_dir_all, File}, io::{Read, Write}, }; @@ -133,14 +132,14 @@ let input = opts.input.input.ok_or(Error::MissingInputArgument)?; let val = if opts.input.exec { - s.evaluate_snippet("".to_owned(), (&input as &str).into())? + s.evaluate_snippet("".to_owned(), &input as &str)? } else if input == "-" { let mut input = Vec::new(); std::io::stdin().read_to_end(&mut input)?; - let input_str = std::str::from_utf8(&input)?.into(); + let input_str = std::str::from_utf8(&input)?; s.evaluate_snippet("".to_owned(), input_str)? } else { - s.import(s.resolve_file(¤t_dir().expect("cwd"), &input)?)? + s.import(&input)? }; let val = s.with_tla(val)?; --- a/crates/jrsonnet-cli/Cargo.toml +++ b/crates/jrsonnet-cli/Cargo.toml @@ -7,7 +7,15 @@ edition = "2021" [features] -exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"] +exp-preserve-order = [ + "jrsonnet-evaluator/exp-preserve-order", + "jrsonnet-stdlib/exp-preserve-order", +] +exp-serde-preserve-order = [ + "jrsonnet-evaluator/exp-serde-preserve-order", + "jrsonnet-stdlib/exp-serde-preserve-order", +] +legacy-this-file = ["jrsonnet-stdlib/legacy-this-file"] [dependencies] jrsonnet-evaluator = { path = "../../crates/jrsonnet-evaluator", version = "0.4.2", features = [ @@ -15,5 +23,6 @@ ] } jrsonnet-parser = { path = "../../crates/jrsonnet-parser", version = "0.4.2" } jrsonnet-gcmodule = { version = "0.3.4" } +jrsonnet-stdlib = { path = "../../crates/jrsonnet-stdlib", version = "0.4.2" } -clap = { version = "3.1", features = ["derive"] } +clap = { version = "3.2", features = ["derive"] } --- a/crates/jrsonnet-cli/src/ext.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::{fs::read_to_string, str::FromStr}; - -use clap::Parser; -use jrsonnet_evaluator::{error::Result, State}; - -use crate::ConfigureState; - -#[derive(Clone)] -pub struct ExtStr { - pub name: String, - pub value: String, -} - -impl FromStr for ExtStr { - type Err = &'static str; - fn from_str(s: &str) -> std::result::Result { - let out: Vec<_> = s.split('=').collect(); - match out.len() { - 1 => Ok(ExtStr { - name: out[0].to_owned(), - value: std::env::var(out[0]).or(Err("missing env var"))?, - }), - 2 => Ok(ExtStr { - name: out[0].to_owned(), - value: out[1].to_owned(), - }), - - _ => Err("bad ext-str syntax"), - } - } -} - -#[derive(Clone)] -pub struct ExtFile { - pub name: String, - pub value: String, -} - -impl FromStr for ExtFile { - type Err = String; - - fn from_str(s: &str) -> std::result::Result { - let out: Vec<&str> = s.split('=').collect(); - if out.len() != 2 { - return Err("bad ext-file syntax".to_owned()); - } - let file = read_to_string(&out[1]); - match file { - Ok(content) => Ok(Self { - name: out[0].into(), - value: content, - }), - Err(e) => Err(format!("{}", e)), - } - } -} - -#[derive(Parser)] -#[clap(next_help_heading = "EXTERNAL VARIABLES")] -pub struct ExtVarOpts { - /// Add string external variable. - /// External variables are globally available so it is preferred - /// to use top level arguments whenever it's possible. - /// If [=data] is not set then it will be read from `name` env variable. - /// Can be accessed from code via `std.extVar("name")`. - #[clap( - long, - short = 'V', - name = "name[=var data]", - number_of_values = 1, - multiple_occurrences = true - )] - ext_str: Vec, - /// Read string external variable from file. - /// See also `--ext-str` - #[clap( - long, - name = "name=var path", - number_of_values = 1, - multiple_occurrences = true - )] - ext_str_file: Vec, - /// Add external variable from code. - /// See also `--ext-str` - #[clap( - long, - name = "name[=var source]", - number_of_values = 1, - multiple_occurrences = true - )] - ext_code: Vec, - /// Read string external variable from file. - /// See also `--ext-str` - #[clap( - long, - name = "name=var code path", - number_of_values = 1, - multiple_occurrences = true - )] - ext_code_file: Vec, -} -impl ConfigureState for ExtVarOpts { - fn configure(&self, s: &State) -> Result<()> { - for ext in self.ext_str.iter() { - s.add_ext_str((&ext.name as &str).into(), (&ext.value as &str).into()); - } - for ext in self.ext_str_file.iter() { - s.add_ext_str((&ext.name as &str).into(), (&ext.value as &str).into()); - } - for ext in self.ext_code.iter() { - s.add_ext_code(&ext.name as &str, (&ext.value as &str).into())?; - } - for ext in self.ext_code_file.iter() { - s.add_ext_code(&ext.name as &str, (&ext.value as &str).into())?; - } - Ok(()) - } -} --- a/crates/jrsonnet-cli/src/lib.rs +++ b/crates/jrsonnet-cli/src/lib.rs @@ -1,15 +1,15 @@ -mod ext; mod manifest; +mod stdlib; mod tla; mod trace; use std::{env, path::PathBuf}; use clap::Parser; -pub use ext::*; use jrsonnet_evaluator::{error::Result, FileImportResolver, State}; use jrsonnet_gcmodule::with_thread_object_space; pub use manifest::*; +pub use stdlib::*; pub use tla::*; pub use trace::*; @@ -31,13 +31,6 @@ #[derive(Parser)] #[clap(next_help_heading = "OPTIONS")] pub struct MiscOpts { - /// Disable standard library. - /// By default standard library will be available via global `std` variable. - /// Note that standard library will still be loaded - /// if chosen manifestification method is not `none`. - #[clap(long)] - no_stdlib: bool, - /// Maximal allowed number of stack frames, /// stack overflow error will be raised if this number gets exceeded. #[clap(long, short = 's', default_value = "200")] @@ -52,17 +45,13 @@ } impl ConfigureState for MiscOpts { fn configure(&self, s: &State) -> Result<()> { - if !self.no_stdlib { - s.with_stdlib(); - } - let mut library_paths = self.jpath.clone(); library_paths.reverse(); if let Some(path) = env::var_os("JSONNET_PATH") { library_paths.extend(env::split_paths(path.as_os_str())); } - s.set_import_resolver(Box::new(FileImportResolver { library_paths })); + s.set_import_resolver(Box::new(FileImportResolver::new(library_paths))); s.set_max_stack(self.max_stack); Ok(()) @@ -79,7 +68,7 @@ #[clap(flatten)] tla: TLAOpts, #[clap(flatten)] - ext: ExtVarOpts, + std: StdOpts, #[clap(flatten)] trace: TraceOpts, @@ -91,7 +80,7 @@ self.trace.configure(s)?; self.misc.configure(s)?; self.tla.configure(s)?; - self.ext.configure(s)?; + self.std.configure(s)?; Ok(()) } } @@ -113,6 +102,8 @@ } impl GcOpts { pub fn stats_printer(&self) -> (Option, Option) { + // Constructed structs have side-effects in Drop impl + #[allow(clippy::unnecessary_lazy_evaluations)] ( self.gc_print_stats.then(|| GcStatsPrinter { collect_before_printing_stats: self.gc_collect_before_printing_stats, --- /dev/null +++ b/crates/jrsonnet-cli/src/stdlib.rs @@ -0,0 +1,130 @@ +use std::{fs::read_to_string, str::FromStr}; + +use clap::Parser; +use jrsonnet_evaluator::{error::Result, trace::PathResolver, State}; + +use crate::ConfigureState; + +#[derive(Clone)] +pub struct ExtStr { + pub name: String, + pub value: String, +} + +impl FromStr for ExtStr { + type Err = &'static str; + fn from_str(s: &str) -> std::result::Result { + let out: Vec<_> = s.split('=').collect(); + match out.len() { + 1 => Ok(ExtStr { + name: out[0].to_owned(), + value: std::env::var(out[0]).or(Err("missing env var"))?, + }), + 2 => Ok(ExtStr { + name: out[0].to_owned(), + value: out[1].to_owned(), + }), + + _ => Err("bad ext-str syntax"), + } + } +} + +#[derive(Clone)] +pub struct ExtFile { + pub name: String, + pub value: String, +} + +impl FromStr for ExtFile { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + let out: Vec<&str> = s.split('=').collect(); + if out.len() != 2 { + return Err("bad ext-file syntax".to_owned()); + } + let file = read_to_string(out[1]); + match file { + Ok(content) => Ok(Self { + name: out[0].into(), + value: content, + }), + Err(e) => Err(format!("{}", e)), + } + } +} + +#[derive(Parser)] +#[clap(next_help_heading = "STANDARD LIBRARY")] +pub struct StdOpts { + /// Disable standard library. + /// By default standard library will be available via global `std` variable. + /// Note that standard library will still be loaded + /// if chosen manifestification method is not `none`. + #[clap(long)] + no_stdlib: bool, + /// Add string external variable. + /// External variables are globally available so it is preferred + /// to use top level arguments whenever it's possible. + /// If [=data] is not set then it will be read from `name` env variable. + /// Can be accessed from code via `std.extVar("name")`. + #[clap( + long, + short = 'V', + name = "name[=var data]", + number_of_values = 1, + multiple_occurrences = true + )] + ext_str: Vec, + /// Read string external variable from file. + /// See also `--ext-str` + #[clap( + long, + name = "name=var path", + number_of_values = 1, + multiple_occurrences = true + )] + ext_str_file: Vec, + /// Add external variable from code. + /// See also `--ext-str` + #[clap( + long, + name = "name[=var source]", + number_of_values = 1, + multiple_occurrences = true + )] + ext_code: Vec, + /// Read string external variable from file. + /// See also `--ext-str` + #[clap( + long, + name = "name=var code path", + number_of_values = 1, + multiple_occurrences = true + )] + ext_code_file: Vec, +} +impl ConfigureState for StdOpts { + fn configure(&self, s: &State) -> Result<()> { + if self.no_stdlib { + return Ok(()); + } + let ctx = + jrsonnet_stdlib::ContextInitializer::new(s.clone(), PathResolver::new_cwd_fallback()); + for ext in self.ext_str.iter() { + ctx.add_ext_str((&ext.name as &str).into(), (&ext.value as &str).into()); + } + for ext in self.ext_str_file.iter() { + ctx.add_ext_str((&ext.name as &str).into(), (&ext.value as &str).into()); + } + for ext in self.ext_code.iter() { + ctx.add_ext_code(&ext.name as &str, &ext.value as &str)?; + } + for ext in self.ext_code_file.iter() { + ctx.add_ext_code(&ext.name as &str, &ext.value as &str)?; + } + s.settings_mut().context_initializer = Box::new(ctx); + Ok(()) + } +} --- a/crates/jrsonnet-cli/src/trace.rs +++ b/crates/jrsonnet-cli/src/trace.rs @@ -42,11 +42,7 @@ } impl ConfigureState for TraceOpts { fn configure(&self, s: &State) -> Result<()> { - let resolver = if let Ok(dir) = std::env::current_dir() { - PathResolver::Relative(dir) - } else { - PathResolver::Absolute - }; + let resolver = PathResolver::new_cwd_fallback(); match self .trace_format .as_ref() --- a/crates/jrsonnet-evaluator/Cargo.toml +++ b/crates/jrsonnet-evaluator/Cargo.toml @@ -7,9 +7,7 @@ edition = "2021" [features] -default = ["serialized-stdlib", "explaining-traces", "friendly-errors"] -# Serializes standard library AST instead of parsing them every run -serialized-stdlib = ["bincode", "jrsonnet-parser/serde"] +default = ["explaining-traces", "friendly-errors"] # Rustc-like trace visualization explaining-traces = ["annotate-snippets"] # Allows library authors to throw custom errors @@ -23,11 +21,12 @@ exp-serde-preserve-order = ["serde_json/preserve_order"] # Implements field destructuring exp-destruct = ["jrsonnet-parser/exp-destruct"] +# Provide Typed for conversions to/from serde_json::Value type +serde_json = ["dep:serde_json"] [dependencies] jrsonnet-interner = { path = "../jrsonnet-interner", version = "0.4.2" } jrsonnet-parser = { path = "../jrsonnet-parser", version = "0.4.2" } -jrsonnet-stdlib = { path = "../jrsonnet-stdlib", version = "0.4.2" } jrsonnet-types = { path = "../jrsonnet-types", version = "0.4.2" } jrsonnet-macros = { path = "../jrsonnet-macros", version = "0.4.2" } jrsonnet-gcmodule = { version = "0.3.4" } @@ -36,15 +35,13 @@ hashbrown = "0.12.1" static_assertions = "1.1" -md5 = "0.7.0" -base64 = "0.13.0" rustc-hash = "1.1" thiserror = "1.0" serde = "1.0" -serde_json = "1.0" -serde_yaml_with_quirks = "0.8.24" +# Optional integration +serde_json = { version = "1.0.82", optional = true } anyhow = { version = "1.0", optional = true } # Friendly errors @@ -53,9 +50,3 @@ bincode = { version = "1.3", optional = true } # Explaining traces annotate-snippets = { version = "0.9.1", features = ["color"], optional = true } - -[build-dependencies] -jrsonnet-stdlib = { path = "../jrsonnet-stdlib", version = "0.4.2" } -jrsonnet-parser = { path = "../jrsonnet-parser", version = "0.4.2" } -serde = "1.0" -bincode = "1.3" --- a/crates/jrsonnet-evaluator/build.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::{borrow::Cow, env, fs::File, io::Write, path::Path}; - -use bincode::serialize; -use jrsonnet_parser::{parse, ParserSettings, Source}; -use jrsonnet_stdlib::STDLIB_STR; - -fn main() { - let parsed = parse( - STDLIB_STR, - &ParserSettings { - file_name: Source::new_virtual(Cow::Borrowed("")), - }, - ) - .expect("parse"); - - { - let out_dir = env::var("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("stdlib.bincode"); - let mut f = File::create(&dest_path).unwrap(); - f.write_all(&serialize(&parsed).unwrap()).unwrap(); - } -} --- a/crates/jrsonnet-evaluator/src/ctx.rs +++ b/crates/jrsonnet-evaluator/src/ctx.rs @@ -20,6 +20,9 @@ } } +/// Context keeps information about current lexical code location +/// +/// This information includes local variables, top-level object (`$`), current object (`this`), and super object (`super`) #[derive(Debug, Clone, Trace)] pub struct Context(Cc); impl Context { @@ -138,3 +141,51 @@ Cc::ptr_eq(&self.0, &other.0) } } + +pub struct ContextBuilder { + bindings: GcHashMap>, + extend: Option, +} + +impl ContextBuilder { + pub fn new() -> Self { + Self::with_capacity(0) + } + pub fn with_capacity(capacity: usize) -> Self { + Self { + bindings: GcHashMap::with_capacity(capacity), + extend: None, + } + } + pub fn extend(parent: Context) -> Self { + Self { + bindings: GcHashMap::new(), + extend: Some(parent), + } + } + /// # Panics + /// If `name` is already bound + pub fn bind(&mut self, name: IStr, value: Thunk) -> &mut Self { + let old = self.bindings.insert(name, value); + assert!(old.is_none(), "variable bound twice in single context call"); + self + } + pub fn build(self) -> Context { + if let Some(parent) = self.extend { + parent.extend(self.bindings, None, None, None) + } else { + Context(Cc::new(ContextInternals { + bindings: LayeredHashMap::new(self.bindings), + dollar: None, + sup: None, + this: None, + })) + } + } +} + +impl Default for ContextBuilder { + fn default() -> Self { + Self::new() + } +} --- a/crates/jrsonnet-evaluator/src/dynamic.rs +++ b/crates/jrsonnet-evaluator/src/dynamic.rs @@ -2,6 +2,7 @@ use jrsonnet_gcmodule::{Cc, Trace}; +// TODO: Replace with OnceCell once in std #[derive(Clone, Trace)] pub struct Pending(pub Cc>>); impl Pending { --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -2,14 +2,11 @@ use jrsonnet_gcmodule::Trace; use jrsonnet_interner::IStr; -use jrsonnet_parser::{BinaryOpType, ExprLocation, Source, UnaryOpType}; +use jrsonnet_parser::{BinaryOpType, ExprLocation, Source, SourcePath, UnaryOpType}; use jrsonnet_types::ValType; use thiserror::Error; -use crate::{ - stdlib::{format::FormatError, sort::SortError}, - typed::TypeLocError, -}; +use crate::{stdlib::format::FormatError, typed::TypeLocError}; fn format_found(list: &[IStr], what: &str) -> String { if list.is_empty() { @@ -35,6 +32,31 @@ out } +fn format_signature(sig: &FunctionSignature) -> String { + let mut out = String::new(); + out.push_str("\nFunction has the following signature: "); + out.push('('); + if sig.is_empty() { + out.push_str("/*no arguments*/"); + } else { + for (i, (name, has_default)) in sig.iter().enumerate() { + if i != 0 { + out.push_str(", "); + } + if let Some(name) = name { + out.push_str(name); + } else { + out.push_str(""); + } + if *has_default { + out.push_str(" = "); + } + } + } + out.push(')'); + out +} + const fn format_empty_str(str: &str) -> &str { if str.is_empty() { "\"\" (empty string)" @@ -43,7 +65,12 @@ } } +type FunctionSignature = Vec<(Option, bool)>; + +/// Possible errors +#[allow(missing_docs)] #[derive(Error, Debug, Clone, Trace)] +#[non_exhaustive] pub enum Error { #[error("intrinsic not found: {0}")] IntrinsicNotFound(IStr), @@ -76,7 +103,7 @@ #[error("duplicate local var: {0}")] DuplicateLocalVar(IStr), - #[error("type mismatch: expected {}, got {2} {0}", .1.iter().map(|e| format!("{}", e)).collect::>().join(", "))] + #[error("type mismatch: expected {}, got {2} {0}", .1.iter().map(|e| format!("{e}")).collect::>().join(", "))] TypeMismatch(&'static str, Vec, ValType), #[error("no such field: {}{}", format_empty_str(.0), format_found(.1, "field"))] NoSuchField(IStr, Vec), @@ -87,10 +114,10 @@ UnknownFunctionParameter(String), #[error("argument {0} is already bound")] BindingParameterASecondTime(IStr), - #[error("too many args, function has {0}")] - TooManyArgsFunctionHas(usize), - #[error("function argument is not passed: {0}")] - FunctionParameterNotBoundInCall(IStr), + #[error("too many args, function has {0}{}", format_signature(.1))] + TooManyArgsFunctionHas(usize, FunctionSignature), + #[error("function argument is not passed: {}{}", .0.as_ref().map_or("", IStr::as_str), format_signature(.1))] + FunctionParameterNotBoundInCall(Option, FunctionSignature), #[error("external variable is not defined: {0}")] UndefinedExternalVariable(IStr), @@ -113,26 +140,31 @@ StandaloneSuper, #[error("can't resolve {1} from {0}")] - ImportFileNotFound(PathBuf, String), - #[error("resolved file not found: {0}")] - ResolvedFileNotFound(PathBuf), + ImportFileNotFound(SourcePath, String), + #[error("can't resolve absolute {0}")] + AbsoluteImportFileNotFound(PathBuf), + #[error("resolved file not found: {:?}", .0)] + ResolvedFileNotFound(SourcePath), + #[error("can't import {0}: is a directory")] + ImportIsADirectory(SourcePath), #[error("imported file is not valid utf-8: {0:?}")] - ImportBadFileUtf8(PathBuf), + ImportBadFileUtf8(SourcePath), #[error("import io error: {0}")] ImportIo(String), - #[error("tried to import {1} from {0}, but imports is not supported")] - ImportNotSupported(PathBuf, PathBuf), + #[error("tried to import {1} from {0}, but imports are not supported")] + ImportNotSupported(SourcePath, String), + #[error("tried to import {0}, but absolute imports are not supported")] + AbsoluteImportNotSupported(PathBuf), #[error("can't import from virtual file")] CantImportFromVirtualFile, #[error( "syntax error: expected {}, got {:?}", .error.expected, - .source_code.chars().nth(error.location.offset) + .path.code().chars().nth(error.location.offset) .map_or_else(|| "EOF".into(), |c| c.to_string()) )] ImportSyntaxError { path: Source, - source_code: IStr, #[trace(skip)] error: Box, }, @@ -169,13 +201,6 @@ Format(#[from] FormatError), #[error("type error: {0}")] TypeError(TypeLocError), - #[error("sort error: {0}")] - Sort(#[from] SortError), - - /// Thrown as error, as this is legacy feature, and error here - /// is acceptable for defeating object field cache - #[error("should not reach outside: std.thisFile")] - MagicThisFileUsed, #[cfg(feature = "anyhow-error")] #[error(transparent)] @@ -195,9 +220,13 @@ } } +/// Single stack trace frame #[derive(Clone, Debug, Trace)] pub struct StackTraceElement { + /// Source of this frame + /// Some frames only act as description, without attached source pub location: Option, + /// Frame description pub desc: String, } #[derive(Debug, Clone, Trace)] @@ -227,7 +256,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "{}", self.0 .0)?; for el in &self.0 .1 .0 { - writeln!(f, "\t{:?}", el)?; + writeln!(f, "\t{el:?}")?; } Ok(()) } --- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs @@ -13,10 +13,9 @@ error::Error::*, evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op}, function::{CallLocation, FuncDesc, FuncVal}, - stdlib::{std_slice, BUILTINS}, tb, throw, typed::Typed, - val::{ArrValue, CachedUnbound, Thunk, ThunkValue}, + val::{ArrValue, CachedUnbound, IndexableVal, Thunk, ThunkValue}, Context, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, State, Unbound, Val, }; @@ -270,9 +269,7 @@ } } let this = builder.build(); - let _ctx = ctx - .extend(GcHashMap::new(), None, None, Some(this.clone())) - .into_future(fctx); + fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone()))); Ok(this) } @@ -357,7 +354,7 @@ ctx: Context, value: &LocExpr, args: &ArgsDesc, - loc: CallLocation, + loc: CallLocation<'_>, tailstrict: bool, ) -> Result { let value = evaluate(s.clone(), ctx.clone(), value)?; @@ -437,7 +434,7 @@ UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(s, ctx, v)?)?, Var(name) => s.push( CallLocation::new(loc), - || format!("variable <{}> access", name), + || format!("variable <{name}> access"), || ctx.binding(name.clone())?.evaluate(s.clone()), )?, Index(value, index) => { @@ -447,7 +444,7 @@ ) { (Val::Obj(v), Val::Str(key)) => s.push( CallLocation::new(loc), - || format!("field <{}> access", key), + || format!("field <{key}> access"), || match v.get(s.clone(), key.clone()) { Ok(Some(v)) => Ok(v), #[cfg(not(feature = "friendly-errors"))] @@ -472,9 +469,6 @@ key.clone(), heap.into_iter().map(|(_, v)| v).collect() )) - } - Err(e) if matches!(e.error(), MagicThisFileUsed) => { - Ok(Val::Str(loc.0.full_path().into())) } Err(e) => Err(e), }, @@ -573,13 +567,6 @@ Function(params, body) => { evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone()) } - Intrinsic(name) => Val::Func(FuncVal::StaticBuiltin( - BUILTINS - .with(|b| b.get(name).copied()) - .ok_or_else(|| IntrinsicNotFound(name.clone()))?, - )), - IntrinsicThisFile => return Err(MagicThisFileUsed.into()), - IntrinsicId => Val::Func(FuncVal::identity()), AssertExpr(assert, returned) => { evaluate_assert(s.clone(), ctx.clone(), assert)?; evaluate(s, ctx, returned)? @@ -613,7 +600,7 @@ } Slice(value, desc) => { fn parse_idx( - loc: CallLocation, + loc: CallLocation<'_>, s: State, ctx: &Context, expr: &Option, @@ -622,7 +609,7 @@ if let Some(value) = expr { Ok(Some(s.push( loc, - || format!("slice {}", desc), + || format!("slice {desc}"), || T::from_untyped(evaluate(s.clone(), ctx.clone(), value)?, s.clone()), )?)) } else { @@ -635,29 +622,21 @@ let start = parse_idx(loc, s.clone(), &ctx, &desc.start, "start")?; let end = parse_idx(loc, s.clone(), &ctx, &desc.end, "end")?; - let step = parse_idx(loc, s, &ctx, &desc.step, "step")?; + let step = parse_idx(loc, s.clone(), &ctx, &desc.step, "step")?; - std_slice(indexable.into_indexable()?, start, end, step)? + IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?, s)? } i @ (Import(path) | ImportStr(path) | ImportBin(path)) => { let tmp = loc.clone().0; - let import_location = tmp - .path() - .map(|p| { - let mut p = p.to_owned(); - p.pop(); - p - }) - .unwrap_or_default(); - let resolved_path = s.resolve_file(&import_location, path as &str)?; + let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?; match i { Import(_) => s.push( CallLocation::new(loc), || format!("import {:?}", path.clone()), - || s.import(resolved_path.clone()), + || s.import_resolved(resolved_path), )?, - ImportStr(_) => Val::Str(s.import_str(resolved_path)?), - ImportBin(_) => Val::Arr(ArrValue::Bytes(s.import_bin(resolved_path)?)), + ImportStr(_) => Val::Str(s.import_resolved_str(resolved_path)?), + ImportBin(_) => Val::Arr(ArrValue::Bytes(s.import_resolved_bin(resolved_path)?)), _ => unreachable!(), } } --- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs @@ -21,14 +21,17 @@ pub fn evaluate_add_op(s: State, a: &Val, b: &Val) -> Result { use Val::*; Ok(match (a, b) { + (Str(a), Str(b)) if a.is_empty() => Val::Str(b.clone()), + (Str(a), Str(b)) if b.is_empty() => Val::Str(a.clone()), (Str(v1), Str(v2)) => Str(((**v1).to_owned() + v2).into()), // Can't use generic json serialization way, because it depends on number to string concatenation (std.jsonnet:890) (Num(a), Str(b)) => Str(format!("{a}{b}").into()), (Str(a), Num(b)) => Str(format!("{a}{b}").into()), - (Str(a), o) => Str(format!("{}{}", a, o.clone().to_string(s)?).into()), - (o, Str(a)) => Str(format!("{}{}", o.clone().to_string(s)?, a).into()), + (Str(a), o) | (o, Str(a)) if a.is_empty() => Val::Str(o.clone().to_string(s)?), + (Str(a), o) => Str(format!("{a}{}", o.clone().to_string(s)?).into()), + (o, Str(a)) => Str(format!("{}{a}", o.clone().to_string(s)?).into()), (Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())), (Arr(a), Arr(b)) => { --- a/crates/jrsonnet-evaluator/src/function/arglike.rs +++ b/crates/jrsonnet-evaluator/src/function/arglike.rs @@ -96,6 +96,34 @@ fn named_names(&self, handler: &mut dyn FnMut(&IStr)); } +impl ArgsLike for Vec { + fn unnamed_len(&self) -> usize { + self.len() + } + fn unnamed_iter( + &self, + _s: State, + _ctx: Context, + _tailstrict: bool, + handler: &mut dyn FnMut(usize, Thunk) -> Result<()>, + ) -> Result<()> { + for (idx, el) in self.iter().enumerate() { + handler(idx, Thunk::evaluated(el.clone()))?; + } + Ok(()) + } + fn named_iter( + &self, + _s: State, + _ctx: Context, + _tailstrict: bool, + _handler: &mut dyn FnMut(&IStr, Thunk) -> Result<()>, + ) -> Result<()> { + Ok(()) + } + fn named_names(&self, _handler: &mut dyn FnMut(&IStr)) {} +} + impl ArgsLike for ArgsDesc { fn unnamed_len(&self) -> usize { self.unnamed.len() @@ -273,7 +301,8 @@ } } impl_args_like! { - 0usize; A @ B C D E F G H I J K L + // First argument is already in position, so count starts from 1 + 1usize; A @ B C D E F G H I J K L } impl ArgsLike for () { --- a/crates/jrsonnet-evaluator/src/function/builtin.rs +++ b/crates/jrsonnet-evaluator/src/function/builtin.rs @@ -9,15 +9,28 @@ #[derive(Clone, Trace)] pub struct BuiltinParam { - pub name: BuiltinParamName, + /// Parameter name for named call parsing + pub name: Option, + /// Is implementation allowed to return empty value pub has_default: bool, } -/// Do not implement it directly, instead use #[builtin] macro +/// Description of function defined by native code +/// +/// Prefer to use #[builtin] macro, instead of manual implementation of this trait pub trait Builtin: Trace { + /// Function name to be used in stack traces fn name(&self) -> &str; + /// Parameter names for named calls fn params(&self) -> &[BuiltinParam]; - fn call(&self, s: State, ctx: Context, loc: CallLocation, args: &dyn ArgsLike) -> Result; + /// Call the builtin + fn call( + &self, + s: State, + ctx: Context, + loc: CallLocation<'_>, + args: &dyn ArgsLike, + ) -> Result; } pub trait StaticBuiltin: Builtin + Send + Sync @@ -35,8 +48,20 @@ } impl NativeCallback { #[deprecated = "prefer using builtins directly, use this interface only for bindings"] - pub fn new(params: Vec, handler: TraceBox) -> Self { - Self { params, handler } + pub fn new( + params: Vec>, + handler: TraceBox, + ) -> Self { + Self { + params: params + .into_iter() + .map(|n| BuiltinParam { + name: Some(n), + has_default: false, + }) + .collect(), + handler, + } } } @@ -51,13 +76,20 @@ &self.params } - fn call(&self, s: State, ctx: Context, _loc: CallLocation, args: &dyn ArgsLike) -> Result { + fn call( + &self, + s: State, + ctx: Context, + _loc: CallLocation<'_>, + args: &dyn ArgsLike, + ) -> Result { let args = parse_builtin_call(s.clone(), ctx, &self.params, args, true)?; - let mut out_args = Vec::with_capacity(self.params.len()); - for p in &self.params { - out_args.push(args[&p.name].evaluate(s.clone())?); - } - self.handler.call(s, &out_args) + let args = args + .into_iter() + .map(|a| a.expect("legacy natives have no default params")) + .map(|a| a.evaluate(s.clone())) + .collect::>>()?; + self.handler.call(s, &args) } } --- a/crates/jrsonnet-evaluator/src/function/mod.rs +++ b/crates/jrsonnet-evaluator/src/function/mod.rs @@ -18,43 +18,50 @@ pub mod native; pub mod parse; +/// Function callsite location. +/// Either from other jsonnet code, specified by expression location, or from native (without location). #[derive(Clone, Copy)] pub struct CallLocation<'l>(pub Option<&'l ExprLocation>); impl<'l> CallLocation<'l> { + /// Construct new location for calls coming from specified jsonnet expression location. pub const fn new(loc: &'l ExprLocation) -> Self { Self(Some(loc)) } } impl CallLocation<'static> { + /// Construct new location for calls coming from native code. pub const fn native() -> Self { Self(None) } } -/// Function implemented in jsonnet +/// Represents Jsonnet function defined in code. #[derive(Debug, PartialEq, Trace)] pub struct FuncDesc { - /// In expressions like + /// # Example + /// + /// In expressions like this, deducted to `a`, unspecified otherwise. /// ```jsonnet /// local a = function() ... /// local a() ... /// { a: function() ... } /// { a() = ... } /// ``` - /// - /// Deducted to `a`, unspecified otherwise pub name: IStr, - /// Context, in which this function was evaluated + /// Context, in which this function was evaluated. /// - /// I.e in + /// # Example + /// In /// ```jsonnet /// local a = 2; /// function() ... /// ``` - /// context will contain `a` + /// context will contain `a`. pub ctx: Context, + /// Function parameter definition pub params: ParamsDesc, + /// Function body pub body: LocExpr, } impl FuncDesc { @@ -82,17 +89,17 @@ } } -/// Any possible function value, including plain functions and user-provided builtins +/// Represents a Jsonnet function value, including plain functions and user-provided builtins. #[allow(clippy::module_name_repetitions)] #[derive(Trace, Clone)] pub enum FuncVal { - /// std.id + /// Identity function, kept this way for comparsions. Id, - /// Plain function implemented in jsonnet + /// Plain function implemented in jsonnet. Normal(Cc), - /// Standard library function + /// Standard library function. StaticBuiltin(#[trace(skip)] &'static dyn StaticBuiltin), - /// User-provided function + /// User-provided function. Builtin(Cc>), } @@ -110,9 +117,7 @@ } impl FuncVal { - pub fn into_native(self) -> D::Value { - D::into_native(self) - } + /// Amount of non-default required arguments pub fn params_len(&self) -> usize { match self { Self::Id => 1, @@ -121,6 +126,7 @@ Self::Builtin(i) => i.params().iter().filter(|p| !p.has_default).count(), } } + /// Function name, as defined in code. pub fn name(&self) -> IStr { match self { Self::Id => "id".into(), @@ -129,11 +135,14 @@ Self::Builtin(builtin) => builtin.name().into(), } } + /// Call function using arguments evaluated in specified `call_ctx` [`Context`]. + /// + /// If `tailstrict` is specified - then arguments will be evaluated before being passed to function body. pub fn evaluate( &self, s: State, call_ctx: Context, - loc: CallLocation, + loc: CallLocation<'_>, args: &dyn ArgsLike, tailstrict: bool, ) -> Result { @@ -156,13 +165,22 @@ Self::Builtin(b) => b.call(s, call_ctx, loc, args), } } + /// Helper method, which calls [`Self::evaluate`] with sensible defaults for native code. pub fn evaluate_simple(&self, s: State, args: &dyn ArgsLike) -> Result { self.evaluate(s, Context::default(), CallLocation::native(), args, true) } + /// Convert jsonnet function to plain `Fn` value. + pub fn into_native(self) -> D::Value { + D::into_native(self) + } + /// Is this function an indentity function. + /// + /// Currently only works for builtin `std.id`, aka `Self::Id` value, `function(x) x` defined by jsonnet will not count as identity. pub const fn is_identity(&self) -> bool { matches!(self, Self::Id) } + /// Identity function value. pub const fn identity() -> Self { Self::Id } --- a/crates/jrsonnet-evaluator/src/function/native.rs +++ b/crates/jrsonnet-evaluator/src/function/native.rs @@ -1,5 +1,5 @@ use super::{arglike::ArgLike, CallLocation, FuncVal}; -use crate::{error::Result, typed::Typed, State}; +use crate::{error::Result, typed::Typed, Context, State}; pub trait NativeDesc { type Value; @@ -19,7 +19,8 @@ Box::new(move |s: State, $($gen),*| { let val = val.evaluate( s.clone(), - s.create_default_context(), + // This isn't intended to be used with ArgsDesc + Context::default(), CallLocation::native(), &($($gen,)*), true --- a/crates/jrsonnet-evaluator/src/function/parse.rs +++ b/crates/jrsonnet-evaluator/src/function/parse.rs @@ -1,11 +1,10 @@ +use std::mem::replace; + use jrsonnet_gcmodule::Trace; use jrsonnet_interner::IStr; use jrsonnet_parser::{LocExpr, ParamsDesc}; -use super::{ - arglike::ArgsLike, - builtin::{BuiltinParam, BuiltinParamName}, -}; +use super::{arglike::ArgsLike, builtin::BuiltinParam}; use crate::{ destructure::destruct, error::{Error::*, Result}, @@ -48,7 +47,10 @@ ) -> Result { let mut passed_args = GcHashMap::with_capacity(params.len()); if args.unnamed_len() > params.len() { - throw!(TooManyArgsFunctionHas(params.len())) + throw!(TooManyArgsFunctionHas( + params.len(), + params.iter().map(|p| (p.0.name(), p.1.is_some())).collect() + )) } let mut filled_named = 0; @@ -122,11 +124,8 @@ }); if !found { throw!(FunctionParameterNotBoundInCall( - param - .0 - .clone() - .name() - .unwrap_or_else(|| "".into()) + param.0.clone().name(), + params.iter().map(|p| (p.0.name(), p.1.is_some())).collect() )); } } @@ -156,28 +155,33 @@ params: &[BuiltinParam], args: &dyn ArgsLike, tailstrict: bool, -) -> Result>> { - let mut passed_args = GcHashMap::with_capacity(params.len()); +) -> Result>>> { + let mut passed_args: Vec>> = vec![None; params.len()]; if args.unnamed_len() > params.len() { - throw!(TooManyArgsFunctionHas(params.len())) + throw!(TooManyArgsFunctionHas( + params.len(), + params + .iter() + .map(|p| (p.name.as_ref().map(|v| v.as_ref().into()), p.has_default)) + .collect() + )) } let mut filled_args = 0; args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| { - let name = params[id].name.clone(); - passed_args.insert(name, arg); + passed_args[id] = Some(arg); filled_args += 1; Ok(()) })?; args.named_iter(s, ctx, tailstrict, &mut |name, arg| { // FIXME: O(n) for arg existence check - let p = params + let id = params .iter() - .find(|p| p.name == name as &str) + .position(|p| p.name.as_ref().map_or(false, |v| v as &str == name as &str)) .ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?; - if passed_args.insert(p.name.clone(), arg).is_some() { + if replace(&mut passed_args[id], Some(arg)).is_some() { throw!(BindingParameterASecondTime(name.clone())); } filled_args += 1; @@ -185,8 +189,8 @@ })?; if filled_args < params.len() { - for param in params.iter().filter(|p| p.has_default) { - if passed_args.contains_key(¶m.name) { + for (id, _) in params.iter().enumerate().filter(|(_, p)| p.has_default) { + if passed_args[id].is_some() { continue; } filled_args += 1; @@ -197,12 +201,22 @@ for param in params.iter().skip(args.unnamed_len()) { let mut found = false; args.named_names(&mut |name| { - if name as &str == ¶m.name as &str { + if param + .name + .as_ref() + .map_or(false, |v| v as &str == name as &str) + { found = true; } }); if !found { - throw!(FunctionParameterNotBoundInCall(param.name.clone().into())); + throw!(FunctionParameterNotBoundInCall( + param.name.as_ref().map(|v| v.as_ref().into()), + params + .iter() + .map(|p| (p.name.as_ref().map(|p| p.as_ref().into()), p.has_default)) + .collect() + )); } } unreachable!(); @@ -215,11 +229,15 @@ /// and with unbound values causing error to be returned pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Result { #[derive(Trace)] - struct DependsOnUnbound(IStr); + struct DependsOnUnbound(IStr, ParamsDesc); impl ThunkValue for DependsOnUnbound { type Output = Val; fn get(self: Box, _: State) -> Result { - Err(FunctionParameterNotBoundInCall(self.0.clone()).into()) + Err(FunctionParameterNotBoundInCall( + Some(self.0.clone()), + self.1.iter().map(|p| (p.0.name(), p.1.is_some())).collect(), + ) + .into()) } } @@ -243,7 +261,8 @@ destruct( ¶m.0, Thunk::new(tb!(DependsOnUnbound( - param.0.name().unwrap_or_else(|| "".into()) + param.0.name().unwrap_or_else(|| "".into()), + params.clone() ))), fctx.clone(), &mut bindings, --- a/crates/jrsonnet-evaluator/src/gc.rs +++ b/crates/jrsonnet-evaluator/src/gc.rs @@ -22,7 +22,7 @@ } impl Trace for TraceBox { - fn trace(&self, tracer: &mut Tracer) { + fn trace(&self, tracer: &mut Tracer<'_>) { self.0.trace(tracer); } @@ -53,25 +53,25 @@ impl Borrow for TraceBox { fn borrow(&self) -> &T { - &*self.0 + &self.0 } } impl BorrowMut for TraceBox { fn borrow_mut(&mut self) -> &mut T { - &mut *self.0 + &mut self.0 } } impl AsRef for TraceBox { fn as_ref(&self) -> &T { - &*self.0 + &self.0 } } impl AsMut for TraceBox { fn as_mut(&mut self) -> &mut T { - &mut *self.0 + &mut self.0 } } @@ -92,7 +92,7 @@ where V: Trace, { - fn trace(&self, tracer: &mut jrsonnet_gcmodule::Tracer) { + fn trace(&self, tracer: &mut Tracer<'_>) { for v in &self.0 { v.trace(tracer); } @@ -133,7 +133,7 @@ K: Trace, V: Trace, { - fn trace(&self, tracer: &mut jrsonnet_gcmodule::Tracer) { + fn trace(&self, tracer: &mut Tracer<'_>) { for (k, v) in &self.0 { k.trace(tracer); v.trace(tracer); --- a/crates/jrsonnet-evaluator/src/import.rs +++ b/crates/jrsonnet-evaluator/src/import.rs @@ -1,47 +1,60 @@ use std::{ any::Any, + cell::RefCell, + env::current_dir, fs, - io::Read, + io::{ErrorKind, Read}, path::{Path, PathBuf}, }; use fs::File; +use jrsonnet_parser::{SourceDirectory, SourceFile, SourcePath}; use crate::{ - error::{Error::*, Result}, + error::{ + Error::{self, *}, + Result, + }, throw, }; /// Implements file resolution logic for `import` and `importStr` pub trait ImportResolver { - /// Resolves real file path, e.g. `(/home/user/manifests, b.libjsonnet)` can correspond + /// 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. - fn resolve_file(&self, from: &Path, path: &str) -> Result; + /// + /// `from` should only be returned from [`ImportResolver::resolve`], or from other defined file, any other value + /// may result in panic + fn resolve_from(&self, from: &SourcePath, path: &str) -> Result { + throw!(ImportNotSupported(from.clone(), path.into())) + } + fn resolve_from_default(&self, path: &str) -> Result { + self.resolve_from(&SourcePath::default(), path) + } + /// Resolves absolute path, doesn't supports jpath and other fancy things + fn resolve(&self, path: &Path) -> Result { + throw!(AbsoluteImportNotSupported(path.to_owned())) + } - fn load_file_contents(&self, resolved: &Path) -> Result>; + /// 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`] + fn load_file_contents(&self, resolved: &SourcePath) -> Result>; - /// # Safety - /// - /// For use only in bindings, should not be used elsewhere. - /// Implementations which are not intended to be used in bindings - /// should panic on call to this method. - unsafe fn as_any(&self) -> &dyn Any; + /// For downcasts + fn as_any(&self) -> &dyn Any; } /// Dummy resolver, can't resolve/load any file pub struct DummyImportResolver; impl ImportResolver for DummyImportResolver { - fn resolve_file(&self, from: &Path, path: &str) -> Result { - throw!(ImportNotSupported(from.into(), path.into())) - } - - fn load_file_contents(&self, _resolved: &Path) -> Result> { + fn load_file_contents(&self, _resolved: &SourcePath) -> Result> { panic!("dummy resolver can't load any file") } - unsafe fn as_any(&self) -> &dyn Any { - panic!("`as_any($self)` is not supported by dummy resolver") + fn as_any(&self) -> &dyn Any { + self } } #[allow(clippy::use_self)] @@ -56,34 +69,91 @@ pub struct FileImportResolver { /// Library directories to search for file. /// Referred to as `jpath` in original jsonnet implementation. - pub library_paths: Vec, + library_paths: RefCell>, +} +impl FileImportResolver { + pub fn new(jpath: Vec) -> Self { + Self { + library_paths: RefCell::new(jpath), + } + } + /// Dynamically add new jpath, used by bindings + pub fn add_jpath(&self, path: PathBuf) { + self.library_paths.borrow_mut().push(path); + } } impl ImportResolver for FileImportResolver { - fn resolve_file(&self, from: &Path, path: &str) -> Result { - let mut direct = from.to_path_buf(); + fn resolve_from(&self, from: &SourcePath, path: &str) -> Result { + let mut direct = if let Some(f) = from.downcast_ref::() { + let mut o = f.path().to_owned(); + o.pop(); + o + } else if let Some(d) = from.downcast_ref::() { + d.path().to_owned() + } else if from.is_default() { + current_dir().map_err(|e| Error::ImportIo(e.to_string()))? + } else { + unreachable!("resolver can't return this path") + }; direct.push(path); - if direct.exists() { - Ok(direct.canonicalize().map_err(|e| ImportIo(e.to_string()))?) + if direct.is_file() { + Ok(SourcePath::new(SourceFile::new( + direct.canonicalize().map_err(|e| ImportIo(e.to_string()))?, + ))) } else { - for library_path in &self.library_paths { + for library_path in self.library_paths.borrow().iter() { let mut cloned = library_path.clone(); cloned.push(path); if cloned.exists() { - return Ok(cloned.canonicalize().map_err(|e| ImportIo(e.to_string()))?); + return Ok(SourcePath::new(SourceFile::new( + cloned.canonicalize().map_err(|e| ImportIo(e.to_string()))?, + ))); } } - throw!(ImportFileNotFound(from.to_owned(), path.to_owned())) + throw!(ImportFileNotFound(from.clone(), path.to_owned())) } } + fn resolve(&self, path: &Path) -> Result { + let meta = match fs::metadata(path) { + Ok(v) => v, + Err(e) if e.kind() == ErrorKind::NotFound => { + throw!(AbsoluteImportFileNotFound(path.to_owned())) + } + Err(e) => throw!(Error::ImportIo(e.to_string())), + }; + if meta.is_file() { + Ok(SourcePath::new(SourceFile::new( + path.canonicalize().map_err(|e| ImportIo(e.to_string()))?, + ))) + } else if meta.is_dir() { + Ok(SourcePath::new(SourceDirectory::new( + path.canonicalize().map_err(|e| ImportIo(e.to_string()))?, + ))) + } else { + unreachable!("this can't be a symlink") + } + } - fn load_file_contents(&self, id: &Path) -> Result> { - let mut file = File::open(id).map_err(|_e| ResolvedFileNotFound(id.to_owned()))?; + fn load_file_contents(&self, id: &SourcePath) -> Result> { + let path = if let Some(f) = id.downcast_ref::() { + f.path() + } else if id.downcast_ref::().is_some() || id.is_default() { + throw!(Error::ImportIsADirectory(id.clone())) + } else { + unreachable!("other types are not supported in resolve"); + }; + let mut file = File::open(path).map_err(|_e| ResolvedFileNotFound(id.clone()))?; let mut out = Vec::new(); file.read_to_end(&mut out) .map_err(|e| ImportIo(e.to_string()))?; Ok(out) } - unsafe fn as_any(&self) -> &dyn Any { - panic!("this resolver can't be used as any") + + fn as_any(&self) -> &dyn Any { + self + } + + fn resolve_from_default(&self, path: &str) -> Result { + self.resolve_from(&SourcePath::default(), path) } } --- a/crates/jrsonnet-evaluator/src/integrations/serde.rs +++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs @@ -16,7 +16,7 @@ Self::Null => Val::Null, Self::Bool(v) => Val::Bool(v), Self::Number(n) => Val::Num(n.as_f64().ok_or_else(|| { - RuntimeError(format!("json number can't be represented as jsonnet: {}", n).into()) + RuntimeError(format!("json number can't be represented as jsonnet: {n}").into()) })?), Self::String(s) => Val::Str((&s as &str).into()), Self::Array(a) => { --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -1,4 +1,18 @@ -#![warn(clippy::all, clippy::nursery, clippy::pedantic)] +//! jsonnet interpreter implementation + +#![deny(unsafe_op_in_unsafe_fn)] +#![warn( + clippy::all, + clippy::nursery, + clippy::pedantic, + // missing_docs, + elided_lifetimes_in_paths, + explicit_outlives_requirements, + noop_method_call, + single_use_lifetimes, + variant_size_differences, + rustdoc::all +)] #![allow( macro_expanded_macro_exports_accessed_by_absolute_paths, clippy::ptr_arg, @@ -37,17 +51,17 @@ mod integrations; mod map; mod obj; -mod stdlib; +pub mod stdlib; pub mod trace; pub mod typed; pub mod val; use std::{ - borrow::Cow, + any::Any, cell::{Ref, RefCell, RefMut}, collections::HashMap, fmt::{self, Debug}, - path::{Path, PathBuf}, + path::Path, rc::Rc, }; @@ -55,36 +69,44 @@ pub use dynamic::*; use error::{Error::*, LocError, Result, StackTraceElement}; pub use evaluate::*; -use function::{builtin::Builtin, CallLocation, TlaArg}; +use function::{CallLocation, TlaArg}; use gc::{GcHashMap, TraceBox}; use hashbrown::hash_map::RawEntryMut; pub use import::*; use jrsonnet_gcmodule::{Cc, Trace}; -use jrsonnet_interner::IBytes; -pub use jrsonnet_interner::IStr; +pub use jrsonnet_interner::{IBytes, IStr}; pub use jrsonnet_parser as parser; use jrsonnet_parser::*; pub use obj::*; -use trace::{location_to_offset, offset_to_location, CodeLocation, CompactFormat, TraceFormat}; +use trace::{CompactFormat, TraceFormat}; pub use val::{ManifestFormat, Thunk, Val}; +/// Thunk without bound `super`/`this` +/// object inheritance may be overriden multiple times, and will be fixed only on field read pub trait Unbound: Trace { + /// Type of value after object context is bound type Bound; + /// Create value bound to specified object context fn bind(&self, s: State, sup: Option, this: Option) -> Result; } +/// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code +/// Standard jsonnet fields are always unbound #[derive(Clone, Trace)] -pub enum LazyBinding { - Bindable(Cc>>>), +pub enum MaybeUnbound { + /// Value needs to be bound to `this`/`super` + Unbound(Cc>>>), + /// Value is object-independent Bound(Thunk), } -impl Debug for LazyBinding { +impl Debug for MaybeUnbound { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "LazyBinding") + write!(f, "MaybeUnbound") } } -impl LazyBinding { +impl MaybeUnbound { + /// Attach object context to value, if required pub fn evaluate( &self, s: State, @@ -92,25 +114,44 @@ this: Option, ) -> Result> { match self { - Self::Bindable(v) => v.bind(s, sup, this), + Self::Unbound(v) => v.bind(s, sup, this), Self::Bound(v) => Ok(v.clone()), } } } +/// During import, this trait will be called to create initial context for file. +/// It may initialize global variables, stdlib for example. +pub trait ContextInitializer { + /// Initialize default file context. + fn initialize(&self, state: State, for_file: Source) -> Context; + /// Allows upcasting from abstract to concrete context initializer. + /// jrsonnet by itself doesn't use this method, it is allowed for it to panic. + fn as_any(&self) -> &dyn Any; +} + +/// Context initializer which adds nothing. +pub struct DummyContextInitializer; +impl ContextInitializer for DummyContextInitializer { + fn initialize(&self, _state: State, _for_file: Source) -> Context { + Context::default() + } + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Dynamically reconfigurable evaluation settings pub struct EvaluationSettings { /// Limits recursion by limiting the number of stack frames pub max_stack: usize, /// Limits amount of stack trace items preserved pub max_trace: usize, - /// Used for s`td.extVar` - pub ext_vars: HashMap, - /// Used for ext.native - pub ext_natives: HashMap>>, /// TLA vars pub tla_vars: HashMap, - /// Global variables are inserted in default context - pub globals: HashMap, + /// Context initializer, which will be used for imports and everything + /// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib` + pub context_initializer: Box, /// Used to resolve file locations/contents pub import_resolver: Box, /// Used in manifestification functions @@ -123,9 +164,7 @@ Self { max_stack: 200, max_trace: 20, - globals: HashMap::default(), - ext_vars: HashMap::default(), - ext_natives: HashMap::default(), + context_initializer: Box::new(DummyContextInitializer), tla_vars: HashMap::default(), import_resolver: Box::new(DummyImportResolver), manifest_format: ManifestFormat::Json { @@ -151,9 +190,7 @@ breakpoints: Breakpoints, /// Contains file source codes and evaluation results for imports and pretty-printed stacktraces - files: GcHashMap, - /// Contains tla arguments and others, which aren't needed to be obtained by name - volatile_files: GcHashMap, + files: GcHashMap, } struct FileData { string: Option, @@ -229,7 +266,8 @@ pub struct State(Rc); impl State { - pub fn import_str(&self, path: PathBuf) -> Result { + /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise + pub fn import_resolved_str(&self, path: SourcePath) -> Result { let mut data = self.data_mut(); let mut file = data.files.raw_entry_mut().from_key(&path); @@ -263,7 +301,8 @@ } Ok(file.string.as_ref().expect("just set").clone()) } - pub fn import_bin(&self, path: PathBuf) -> Result { + /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise + pub fn import_resolved_bin(&self, path: SourcePath) -> Result { let mut data = self.data_mut(); let mut file = data.files.raw_entry_mut().from_key(&path); @@ -289,7 +328,8 @@ } Ok(file.bytes.as_ref().expect("just set").clone()) } - pub fn import(&self, path: PathBuf) -> Result { + /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise + pub fn import_resolved(&self, path: SourcePath) -> Result { let mut data = self.data_mut(); let mut file = data.files.raw_entry_mut().from_key(&path); @@ -323,7 +363,7 @@ ); } let code = file.string.as_ref().expect("just set"); - let file_name = Source::new(path.clone()).expect("resolver should return correct name"); + let file_name = Source::new(path.clone(), code.clone()); if file.parsed.is_none() { file.parsed = Some( jrsonnet_parser::parse( @@ -333,8 +373,7 @@ }, ) .map_err(|e| ImportSyntaxError { - path: file_name, - source_code: code.clone(), + path: file_name.clone(), error: Box::new(e), })?, ); @@ -346,7 +385,11 @@ file.evaluating = true; // Dropping file here, as it borrows data, which may be used in evaluation drop(data); - let res = evaluate(self.clone(), self.create_default_context(), &parsed); + let res = evaluate( + self.clone(), + self.create_default_context(file_name), + &parsed, + ); let mut data = self.data_mut(); let mut file = data.files.raw_entry_mut().from_key(&path); @@ -365,58 +408,26 @@ } } - pub fn get_source(&self, name: Source) -> Option { - let data = self.data(); - match name.repr() { - Ok(real) => data - .files - .get(real) - .and_then(|f| f.string.as_ref()) - .map(ToString::to_string), - Err(e) => data.volatile_files.get(e).map(ToOwned::to_owned), - } - } - pub fn map_source_locations(&self, file: Source, locs: &[u32]) -> Vec { - offset_to_location(&self.get_source(file).unwrap_or_else(|| "".into()), locs) - } - pub fn map_from_source_location( - &self, - file: Source, - line: usize, - column: usize, - ) -> Option { - location_to_offset( - &self.get_source(file).expect("file not found"), - line, - column, - ) + /// Has same semantics as `import 'path'` called from `from` file + pub fn import_from(&self, from: &SourcePath, path: &str) -> Result { + let resolved = self.resolve_from(from, path)?; + self.import_resolved(resolved) } - /// Adds standard library global variable (std) to this evaluator - pub fn with_stdlib(&self) -> &Self { - let val = evaluate( - self.clone(), - self.create_default_context(), - &stdlib::get_parsed_stdlib(), - ) - .expect("std should not fail"); - self.settings_mut().globals.insert("std".into(), val); - self + pub fn import(&self, path: impl AsRef) -> Result { + let resolved = self.resolve(path)?; + self.import_resolved(resolved) } /// Creates context with all passed global variables - pub fn create_default_context(&self) -> Context { - let globals = &self.settings().globals; - let mut new_bindings = GcHashMap::with_capacity(globals.len()); - for (name, value) in globals.iter() { - new_bindings.insert(name.clone(), Thunk::evaluated(value.clone())); - } - Context::new().extend(new_bindings, None, None, None) + pub fn create_default_context(&self, source: Source) -> Context { + let context_initializer = &self.settings().context_initializer; + context_initializer.initialize(self.clone(), source) } /// Executes code creating a new stack frame pub fn push( &self, - e: CallLocation, + e: CallLocation<'_>, frame_desc: impl FnOnce() -> String, f: impl FnOnce() -> Result, ) -> Result { @@ -545,7 +556,10 @@ || { func.evaluate( self.clone(), - self.create_default_context(), + self.create_default_context(Source::new_virtual( + "".into(), + IStr::empty(), + )), CallLocation::native(), &self.settings().tla_vars, true, @@ -559,16 +573,13 @@ /// Internals impl State { - fn data(&self) -> Ref { - self.0.data.borrow() - } - fn data_mut(&self) -> RefMut { + fn data_mut(&self) -> RefMut<'_, EvaluationData> { self.0.data.borrow_mut() } - pub fn settings(&self) -> Ref { + pub fn settings(&self) -> Ref<'_, EvaluationSettings> { self.0.settings.borrow() } - pub fn settings_mut(&self) -> RefMut { + pub fn settings_mut(&self) -> RefMut<'_, EvaluationSettings> { self.0.settings.borrow_mut() } } @@ -576,8 +587,9 @@ /// Raw methods evaluate passed values but don't perform TLA execution impl State { /// Parses and evaluates the given snippet - pub fn evaluate_snippet(&self, name: String, code: String) -> Result { - let source = Source::new_virtual(Cow::Owned(name.clone())); + pub fn evaluate_snippet(&self, name: impl Into, code: impl Into) -> Result { + let code = code.into(); + let source = Source::new_virtual(name.into(), code.clone()); let parsed = jrsonnet_parser::parse( &code, &ParserSettings { @@ -585,48 +597,15 @@ }, ) .map_err(|e| ImportSyntaxError { - path: source, - source_code: code.clone().into(), + path: source.clone(), error: Box::new(e), })?; - self.data_mut().volatile_files.insert(name, code); - evaluate(self.clone(), self.create_default_context(), &parsed) + evaluate(self.clone(), self.create_default_context(source), &parsed) } } /// Settings utilities impl State { - pub fn add_ext_var(&self, name: IStr, value: Val) { - self.settings_mut() - .ext_vars - .insert(name, TlaArg::Val(value)); - } - pub fn add_ext_str(&self, name: IStr, value: IStr) { - self.settings_mut() - .ext_vars - .insert(name, TlaArg::String(value)); - } - pub fn add_ext_code(&self, name: &str, code: String) -> Result<()> { - let source_name = format!("", name); - let source = Source::new_virtual(Cow::Owned(source_name.clone())); - let parsed = jrsonnet_parser::parse( - &code, - &ParserSettings { - file_name: source.clone(), - }, - ) - .map_err(|e| ImportSyntaxError { - path: source, - source_code: code.clone().into(), - error: Box::new(e), - })?; - self.data_mut().volatile_files.insert(source_name, code); - self.settings_mut() - .ext_vars - .insert(name.into(), TlaArg::Code(parsed)); - Ok(()) - } - pub fn add_tla(&self, name: IStr, value: Val) { self.settings_mut() .tla_vars @@ -638,8 +617,8 @@ .insert(name, TlaArg::String(value)); } pub fn add_tla_code(&self, name: IStr, code: &str) -> Result<()> { - let source_name = format!("", name); - let source = Source::new_virtual(Cow::Owned(source_name.clone())); + let source_name = format!(""); + let source = Source::new_virtual(source_name.into(), code.into()); let parsed = jrsonnet_parser::parse( code, &ParserSettings { @@ -648,33 +627,33 @@ ) .map_err(|e| ImportSyntaxError { path: source, - source_code: code.into(), error: Box::new(e), })?; - self.data_mut() - .volatile_files - .insert(source_name, code.to_owned()); self.settings_mut() .tla_vars .insert(name, TlaArg::Code(parsed)); Ok(()) } - pub fn resolve_file(&self, from: &Path, path: &str) -> Result { - self.settings() - .import_resolver - .resolve_file(from, path.as_ref()) + // Only panics in case of [`ImportResolver`] contract violation + #[allow(clippy::missing_panics_doc)] + pub fn resolve_from(&self, from: &SourcePath, path: &str) -> Result { + self.import_resolver().resolve_from(from, path.as_ref()) } - pub fn import_resolver(&self) -> Ref { + // Only panics in case of [`ImportResolver`] contract violation + #[allow(clippy::missing_panics_doc)] + pub fn resolve(&self, path: impl AsRef) -> Result { + self.import_resolver().resolve(path.as_ref()) + } + pub fn import_resolver(&self) -> Ref<'_, dyn ImportResolver> { Ref::map(self.settings(), |s| &*s.import_resolver) } pub fn set_import_resolver(&self, resolver: Box) { self.settings_mut().import_resolver = resolver; } - - pub fn add_native(&self, name: IStr, cb: Cc>) { - self.settings_mut().ext_natives.insert(name, cb); + pub fn context_initializer(&self) -> Ref<'_, dyn ContextInitializer> { + Ref::map(self.settings(), |s| &*s.context_initializer) } pub fn manifest_format(&self) -> ManifestFormat { @@ -684,7 +663,7 @@ self.settings_mut().manifest_format = format; } - pub fn trace_format(&self) -> Ref { + pub fn trace_format(&self) -> Ref<'_, dyn TraceFormat> { Ref::map(self.settings(), |s| &*s.trace_format) } pub fn set_trace_format(&self, format: Box) { --- a/crates/jrsonnet-evaluator/src/map.rs +++ b/crates/jrsonnet-evaluator/src/map.rs @@ -23,6 +23,13 @@ } } + pub(crate) fn new(layer: GcHashMap>) -> Self { + Self(Cc::new(LayeredHashMapInternals { + parent: None, + current: layer, + })) + } + pub fn extend(self, new_layer: GcHashMap>) -> Self { Self(Cc::new(LayeredHashMapInternals { parent: Some(self), --- a/crates/jrsonnet-evaluator/src/obj.rs +++ b/crates/jrsonnet-evaluator/src/obj.rs @@ -15,7 +15,7 @@ function::CallLocation, gc::{GcHashMap, GcHashSet, TraceBox}, operator::evaluate_add_op, - throw, LazyBinding, Result, State, Thunk, Unbound, Val, + throw, MaybeUnbound, Result, State, Thunk, Unbound, Val, }; #[cfg(not(feature = "exp-preserve-order"))] @@ -100,7 +100,7 @@ pub add: bool, pub visibility: Visibility, original_index: FieldIndex, - pub invoke: LazyBinding, + pub invoke: MaybeUnbound, pub location: Option, } @@ -109,7 +109,6 @@ } // Field => This -type CacheKey = (IStr, WeakObjValue); #[derive(Trace)] enum CacheValue { @@ -129,7 +128,7 @@ assertions: Cc>>, assertions_ran: RefCell>, this_entries: Cc>, - value_cache: RefCell>, + value_cache: RefCell>, } #[derive(Clone, Trace)] @@ -157,9 +156,9 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(super_obj) = self.0.sup.as_ref() { if f.alternate() { - write!(f, "{:#?}", super_obj)?; + write!(f, "{super_obj:#?}")?; } else { - write!(f, "{:?}", super_obj)?; + write!(f, "{super_obj:?}")?; } write!(f, " + ")?; } @@ -209,7 +208,7 @@ new.insert(key, value); Self::new(Some(self), Cc::new(new), Cc::new(Vec::new())) } - pub fn extend_field(&mut self, name: IStr) -> ObjMemberBuilder { + pub fn extend_field(&mut self, name: IStr) -> ObjMemberBuilder> { ObjMemberBuilder::new(ExtendBuilder(self), name, FieldIndex::default()) } @@ -240,6 +239,8 @@ } /// Run callback for every field found in object + /// + /// Returns true if ended prematurely pub(crate) fn enum_fields( &self, depth: SuperDepth, @@ -369,15 +370,7 @@ pub fn get(&self, s: State, key: IStr) -> Result> { self.run_assertions(s.clone())?; - self.get_raw(s, key, self.0.this.clone().unwrap_or_else(|| self.clone())) - } - - // pub fn extend_with(self, key: ) - - fn get_raw(&self, s: State, key: IStr, real_this: Self) -> Result> { - let cache_key = (key.clone(), WeakObjValue(real_this.0.downgrade())); - - if let Some(v) = self.0.value_cache.borrow().get(&cache_key) { + if let Some(v) = self.0.value_cache.borrow().get(&key) { return Ok(match v { CacheValue::Cached(v) => Some(v.clone()), CacheValue::NotFound => None, @@ -388,26 +381,37 @@ self.0 .value_cache .borrow_mut() - .insert(cache_key.clone(), CacheValue::Pending); - let fill_error = |e: LocError| { - self.0 - .value_cache - .borrow_mut() - .insert(cache_key.clone(), CacheValue::Errored(e.clone())); - e - }; - let value = match (self.0.this_entries.get(&key), &self.0.sup) { - (Some(k), None) => Ok(Some( - self.evaluate_this(s, k, real_this).map_err(fill_error)?, - )), + .insert(key.clone(), CacheValue::Pending); + let value = self + .get_raw( + s, + key.clone(), + self.0.this.clone().unwrap_or_else(|| self.clone()), + ) + .map_err(|e| { + self.0 + .value_cache + .borrow_mut() + .insert(key.clone(), CacheValue::Errored(e.clone())); + e + })?; + self.0.value_cache.borrow_mut().insert( + key, + value + .as_ref() + .map_or(CacheValue::NotFound, |v| CacheValue::Cached(v.clone())), + ); + Ok(value) + } + + fn get_raw(&self, s: State, key: IStr, real_this: Self) -> Result> { + match (self.0.this_entries.get(&key), &self.0.sup) { + (Some(k), None) => Ok(Some(self.evaluate_this(s, k, real_this)?)), (Some(k), Some(super_obj)) => { - let our = self - .evaluate_this(s.clone(), k, real_this.clone()) - .map_err(fill_error)?; + let our = self.evaluate_this(s.clone(), k, real_this.clone())?; if k.add { super_obj - .get_raw(s.clone(), key, real_this) - .map_err(fill_error)? + .get_raw(s.clone(), key, real_this)? .map_or(Ok(Some(our.clone())), |v| { Ok(Some(evaluate_add_op(s.clone(), &v, &our)?)) }) @@ -418,15 +422,6 @@ (None, Some(super_obj)) => super_obj.get_raw(s, key, real_this), (None, None) => Ok(None), } - .map_err(fill_error)?; - self.0.value_cache.borrow_mut().insert( - cache_key, - match &value { - Some(v) => CacheValue::Cached(v.clone()), - None => CacheValue::NotFound, - }, - ); - Ok(value) } fn evaluate_this(&self, s: State, v: &ObjMember, real_this: Self) -> Result { v.invoke @@ -507,7 +502,7 @@ self.assertions.push(assertion); self } - pub fn member(&mut self, name: IStr) -> ObjMemberBuilder { + pub fn member(&mut self, name: IStr) -> ObjMemberBuilder> { let field_index = self.next_field_index; self.next_field_index = self.next_field_index.next(); ObjMemberBuilder::new(ValueBuilder(self), name, field_index) @@ -565,7 +560,7 @@ self.location = Some(location); self } - fn build_member(self, binding: LazyBinding) -> (Kind, IStr, ObjMember) { + fn build_member(self, binding: MaybeUnbound) -> (Kind, IStr, ObjMember) { ( self.kind, self.name, @@ -581,18 +576,18 @@ } pub struct ValueBuilder<'v>(&'v mut ObjValueBuilder); -impl<'v> ObjMemberBuilder> { +impl ObjMemberBuilder> { pub fn value(self, s: State, value: Val) -> Result<()> { - self.binding(s, LazyBinding::Bound(Thunk::evaluated(value))) + self.binding(s, MaybeUnbound::Bound(Thunk::evaluated(value))) } pub fn bindable( self, s: State, bindable: TraceBox>>, ) -> Result<()> { - self.binding(s, LazyBinding::Bindable(Cc::new(bindable))) + self.binding(s, MaybeUnbound::Unbound(Cc::new(bindable))) } - pub fn binding(self, s: State, binding: LazyBinding) -> Result<()> { + pub fn binding(self, s: State, binding: MaybeUnbound) -> Result<()> { let (receiver, name, member) = self.build_member(binding); let location = member.location.clone(); let old = receiver.0.map.insert(name.clone(), member); @@ -608,14 +603,14 @@ } pub struct ExtendBuilder<'v>(&'v mut ObjValue); -impl<'v> ObjMemberBuilder> { +impl ObjMemberBuilder> { pub fn value(self, value: Val) { - self.binding(LazyBinding::Bound(Thunk::evaluated(value))); + self.binding(MaybeUnbound::Bound(Thunk::evaluated(value))); } pub fn bindable(self, bindable: TraceBox>>) { - self.binding(LazyBinding::Bindable(Cc::new(bindable))); + self.binding(MaybeUnbound::Unbound(Cc::new(bindable))); } - pub fn binding(self, binding: LazyBinding) { + pub fn binding(self, binding: MaybeUnbound) { let (receiver, name, member) = self.build_member(binding); let new = receiver.0.clone(); *receiver.0 = new.extend_with_raw_member(name, member); --- a/crates/jrsonnet-evaluator/src/stdlib/expr.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::borrow::Cow; - -use jrsonnet_parser::{LocExpr, ParserSettings, Source}; - -thread_local! { - /// To avoid parsing again when issued from the same thread - #[allow(unreachable_code)] - static PARSED_STDLIB: LocExpr = { - #[cfg(feature = "serialized-stdlib")] - { - // Should not panic, stdlib.bincode is generated in build.rs - return bincode::deserialize(include_bytes!(concat!(env!("OUT_DIR"), "/stdlib.bincode"))) - .unwrap(); - } - - jrsonnet_parser::parse( - jrsonnet_stdlib::STDLIB_STR, - &ParserSettings { - file_name: Source::new_virtual(Cow::Borrowed("")), - }, - ) - .unwrap() - } -} - -pub fn get_parsed_stdlib() -> LocExpr { - PARSED_STDLIB.with(Clone::clone) -} --- a/crates/jrsonnet-evaluator/src/stdlib/format.rs +++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs @@ -36,7 +36,7 @@ type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>; -pub fn try_parse_mapping_key(str: &str) -> ParseResult<&str> { +pub fn try_parse_mapping_key(str: &str) -> ParseResult<'_, &str> { if str.is_empty() { return Err(TruncatedFormatCode); } @@ -45,7 +45,7 @@ let mut i = 1; while i < bytes.len() { if bytes[i] == b')' { - return Ok((&str[1..i as usize], &str[i as usize + 1..])); + return Ok((&str[1..i], &str[i + 1..])); } i += 1; } @@ -96,7 +96,7 @@ pub sign: bool, } -pub fn try_parse_cflags(str: &str) -> ParseResult { +pub fn try_parse_cflags(str: &str) -> ParseResult<'_, CFlags> { if str.is_empty() { return Err(TruncatedFormatCode); } @@ -125,7 +125,7 @@ Star, Fixed(usize), } -pub fn try_parse_field_width(str: &str) -> ParseResult { +pub fn try_parse_field_width(str: &str) -> ParseResult<'_, Width> { if str.is_empty() { return Err(TruncatedFormatCode); } @@ -146,7 +146,7 @@ Ok((Width::Fixed(out), &str[digits..])) } -pub fn try_parse_precision(str: &str) -> ParseResult> { +pub fn try_parse_precision(str: &str) -> ParseResult<'_, Option> { if str.is_empty() { return Err(TruncatedFormatCode); } @@ -159,7 +159,7 @@ } // Only skips -pub fn try_parse_length_modifier(str: &str) -> ParseResult<()> { +pub fn try_parse_length_modifier(str: &str) -> ParseResult<'_, ()> { if str.is_empty() { return Err(TruncatedFormatCode); } @@ -191,7 +191,7 @@ caps: bool, } -pub fn parse_conversion_type(str: &str) -> ParseResult { +pub fn parse_conversion_type(str: &str) -> ParseResult<'_, ConvType> { if str.is_empty() { return Err(TruncatedFormatCode); } @@ -226,7 +226,7 @@ convtype: ConvTypeV, caps: bool, } -pub fn parse_code(str: &str) -> ParseResult { +pub fn parse_code(str: &str) -> ParseResult<'_, Code<'_>> { if str.is_empty() { return Err(TruncatedFormatCode); } @@ -255,7 +255,7 @@ String(&'s str), Code(Code<'s>), } -pub fn parse_codes(mut str: &str) -> Result> { +pub fn parse_codes(mut str: &str) -> Result>> { let mut bytes = str.as_bytes(); let mut out = vec![]; let mut offset = 0; @@ -285,7 +285,7 @@ #[inline] pub fn render_integer( out: &mut String, - iv: i64, + iv: f64, padding: usize, precision: usize, blank: bool, @@ -294,20 +294,23 @@ prefix: &str, caps: bool, ) { + let radix = radix as f64; + let iv = iv.floor(); // Digit char indexes in reverse order, i.e // for radix = 16 and n = 12f: [15, 2, 1] - let digits = if iv == 0 { + let digits = if iv == 0.0 { vec![0u8] } else { let mut v = iv.abs(); let mut nums = Vec::with_capacity(1); - while v > 0 { + while v != 0.0 { nums.push((v % radix) as u8); - v /= radix; + v = (v / radix).floor(); } nums }; - let neg = iv < 0; + let neg = iv < 0.0; + #[allow(clippy::bool_to_int_with_if)] let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 }); let zp2 = zp .max(precision) @@ -335,7 +338,7 @@ pub fn render_decimal( out: &mut String, - iv: i64, + iv: f64, padding: usize, precision: usize, blank: bool, @@ -345,7 +348,7 @@ } pub fn render_octal( out: &mut String, - iv: i64, + iv: f64, padding: usize, precision: usize, alt: bool, @@ -360,7 +363,7 @@ blank, sign, 8, - if alt && iv != 0 { "0" } else { "" }, + if alt && iv != 0.0 { "0" } else { "" }, false, ); } @@ -368,7 +371,7 @@ #[allow(clippy::fn_params_excessive_bools)] pub fn render_hexadecimal( out: &mut String, - iv: i64, + iv: f64, padding: usize, precision: usize, alt: bool, @@ -404,9 +407,10 @@ ensure_pt: bool, trailing: bool, ) { + #[allow(clippy::bool_to_int_with_if)] let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 }; padding = padding.saturating_sub(dot_size + precision); - render_decimal(out, n.floor() as i64, padding, 0, blank, sign); + render_decimal(out, n.floor(), padding, 0, blank, sign); if precision == 0 { if ensure_pt { out.push('.'); @@ -420,7 +424,7 @@ if trailing || frac > 0.0 { out.push('.'); let mut frac_str = String::new(); - render_decimal(&mut frac_str, frac as i64, precision, 0, false, false); + render_decimal(&mut frac_str, frac, precision, 0, false, false); let mut trim = frac_str.len(); if !trailing { for b in frac_str.as_bytes().iter().rev() { @@ -454,7 +458,7 @@ n / 10.0_f64.powf(exponent) }; let mut exponent_str = String::new(); - render_decimal(&mut exponent_str, exponent as i64, 3, 0, false, true); + render_decimal(&mut exponent_str, exponent, 3, 0, false, true); // +1 for e padding = padding.saturating_sub(exponent_str.len() + 1); @@ -471,15 +475,12 @@ s: State, out: &mut String, value: &Val, - code: &Code, + code: &Code<'_>, width: usize, precision: Option, ) -> Result<()> { let clfags = &code.cflags; - let (fpprec, iprec) = match precision { - Some(v) => (v, v), - None => (6, 0), - }; + let (fpprec, iprec) = precision.map_or((6, 0), |v| (v, v)); let padding = if clfags.zero && !clfags.left { width } else { @@ -495,7 +496,7 @@ let value = f64::from_untyped(value.clone(), s)?; render_decimal( &mut tmp_out, - value as i64, + value, padding, iprec, clfags.blank, @@ -506,7 +507,7 @@ let value = f64::from_untyped(value.clone(), s)?; render_octal( &mut tmp_out, - value as i64, + value, padding, iprec, clfags.alt, @@ -518,7 +519,7 @@ let value = f64::from_untyped(value.clone(), s)?; render_hexadecimal( &mut tmp_out, - value as i64, + value, padding, iprec, clfags.alt, @@ -584,8 +585,10 @@ } } ConvTypeV::Char => match value.clone() { - Val::Num(n) => tmp_out - .push(std::char::from_u32(n as u32).ok_or(InvalidUnicodeCodepointGot(n as u32))?), + Val::Num(n) => tmp_out.push( + std::char::from_u32(n as u32) + .ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?, + ), Val::Str(s) => { if s.chars().count() != 1 { throw!(RuntimeError( @@ -774,10 +777,7 @@ format_arr(s.clone(), "%+-4o", &[Val::Num(8.0)]).unwrap(), "+10 " ); - assert_eq!( - format_arr(s.clone(), "%+-04o", &[Val::Num(8.0)]).unwrap(), - "+10 " - ); + assert_eq!(format_arr(s, "%+-04o", &[Val::Num(8.0)]).unwrap(), "+10 "); } #[test] --- a/crates/jrsonnet-evaluator/src/stdlib/manifest.rs +++ b/crates/jrsonnet-evaluator/src/stdlib/manifest.rs @@ -49,7 +49,7 @@ } Val::Null => buf.push_str("null"), Val::Str(s) => escape_string_json_buf(s, buf), - Val::Num(n) => write!(buf, "{}", n).unwrap(), + Val::Num(n) => write!(buf, "{n}").unwrap(), Val::Arr(items) => { buf.push('['); if !items.is_empty() { @@ -116,7 +116,7 @@ || { let value = obj.get(s.clone(), field.clone())?.unwrap(); manifest_json_ex_buf(s.clone(), &value, buf, cur_padding, options)?; - Ok(Val::Null) + Ok(()) }, )?; } --- a/crates/jrsonnet-evaluator/src/stdlib/mod.rs +++ b/crates/jrsonnet-evaluator/src/stdlib/mod.rs @@ -1,742 +1,24 @@ // All builtins should return results #![allow(clippy::unnecessary_wraps)] -use std::collections::HashMap; - use format::{format_arr, format_obj}; -use jrsonnet_gcmodule::Cc; -use jrsonnet_interner::{IBytes, IStr}; -use serde::Deserialize; -use serde_yaml_with_quirks::DeserializingQuirks; - -use crate::{ - error::{Error::*, Result}, - function::{builtin::StaticBuiltin, ArgLike, CallLocation, FuncVal}, - operator::evaluate_mod_op, - stdlib::manifest::{manifest_yaml_ex, ManifestYamlOptions}, - throw, - typed::{Any, BoundedUsize, Either2, Either4, PositiveF64, Typed, VecVal, M1}, - val::{equals, primitive_equals, ArrValue, IndexableVal, Slice}, - Either, ObjValue, State, Val, -}; - -pub mod expr; -pub use expr::*; +use jrsonnet_interner::IStr; -use self::manifest::{escape_string_json, manifest_json_ex, ManifestJsonOptions, ManifestType}; +use crate::{error::Result, function::CallLocation, State, Val}; pub mod format; pub mod manifest; -pub mod sort; pub fn std_format(s: State, str: IStr, vals: Val) -> Result { s.push( CallLocation::native(), - || format!("std.format of {}", str), + || format!("std.format of {str}"), || { Ok(match vals { Val::Arr(vals) => format_arr(s.clone(), &str, &vals.evaluated(s.clone())?)?, Val::Obj(obj) => format_obj(s.clone(), &str, &obj)?, o => format_arr(s.clone(), &str, &[o])?, }) - }, - ) -} - -pub fn std_slice( - indexable: IndexableVal, - index: Option>, - end: Option>, - step: Option>, -) -> Result { - match &indexable { - IndexableVal::Str(s) => { - let index = index.as_deref().copied().unwrap_or(0); - let end = end.as_deref().copied().unwrap_or(usize::MAX); - let step = step.as_deref().copied().unwrap_or(1); - - if index >= end { - return Ok(Val::Str("".into())); - } - - Ok(Val::Str( - (s.chars() - .skip(index) - .take(end - index) - .step_by(step) - .collect::()) - .into(), - )) - } - IndexableVal::Arr(arr) => { - let index = index.as_deref().copied().unwrap_or(0); - let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len()); - let step = step.as_deref().copied().unwrap_or(1); - - if index >= end { - return Ok(Val::Arr(ArrValue::new_eager())); - } - - Ok(Val::Arr(ArrValue::Slice(Box::new(Slice { - inner: arr.clone(), - from: index as u32, - to: end as u32, - step: step as u32, - })))) - } - } -} - -type BuiltinsType = HashMap; - -thread_local! { - pub static BUILTINS: BuiltinsType = { - [ - ("length".into(), builtin_length::INST), - ("type".into(), builtin_type::INST), - ("makeArray".into(), builtin_make_array::INST), - ("codepoint".into(), builtin_codepoint::INST), - ("objectFieldsEx".into(), builtin_object_fields_ex::INST), - ("objectHasEx".into(), builtin_object_has_ex::INST), - ("slice".into(), builtin_slice::INST), - ("substr".into(), builtin_substr::INST), - ("primitiveEquals".into(), builtin_primitive_equals::INST), - ("equals".into(), builtin_equals::INST), - ("modulo".into(), builtin_modulo::INST), - ("mod".into(), builtin_mod::INST), - ("floor".into(), builtin_floor::INST), - ("ceil".into(), builtin_ceil::INST), - ("log".into(), builtin_log::INST), - ("pow".into(), builtin_pow::INST), - ("sqrt".into(), builtin_sqrt::INST), - ("sin".into(), builtin_sin::INST), - ("cos".into(), builtin_cos::INST), - ("tan".into(), builtin_tan::INST), - ("asin".into(), builtin_asin::INST), - ("acos".into(), builtin_acos::INST), - ("atan".into(), builtin_atan::INST), - ("exp".into(), builtin_exp::INST), - ("mantissa".into(), builtin_mantissa::INST), - ("exponent".into(), builtin_exponent::INST), - ("extVar".into(), builtin_ext_var::INST), - ("native".into(), builtin_native::INST), - ("filter".into(), builtin_filter::INST), - ("map".into(), builtin_map::INST), - ("flatMap".into(), builtin_flatmap::INST), - ("foldl".into(), builtin_foldl::INST), - ("foldr".into(), builtin_foldr::INST), - ("sort".into(), builtin_sort::INST), - ("format".into(), builtin_format::INST), - ("range".into(), builtin_range::INST), - ("char".into(), builtin_char::INST), - ("encodeUTF8".into(), builtin_encode_utf8::INST), - ("decodeUTF8".into(), builtin_decode_utf8::INST), - ("md5".into(), builtin_md5::INST), - ("base64".into(), builtin_base64::INST), - ("base64DecodeBytes".into(), builtin_base64_decode_bytes::INST), - ("base64Decode".into(), builtin_base64_decode::INST), - ("trace".into(), builtin_trace::INST), - ("join".into(), builtin_join::INST), - ("escapeStringJson".into(), builtin_escape_string_json::INST), - ("manifestJsonEx".into(), builtin_manifest_json_ex::INST), - ("manifestYamlDoc".into(), builtin_manifest_yaml_doc::INST), - ("reverse".into(), builtin_reverse::INST), - ("strReplace".into(), builtin_str_replace::INST), - ("splitLimit".into(), builtin_splitlimit::INST), - ("parseJson".into(), builtin_parse_json::INST), - ("parseYaml".into(), builtin_parse_yaml::INST), - ("asciiUpper".into(), builtin_ascii_upper::INST), - ("asciiLower".into(), builtin_ascii_lower::INST), - ("member".into(), builtin_member::INST), - ("count".into(), builtin_count::INST), - ("any".into(), builtin_any::INST), - ("all".into(), builtin_all::INST), - ].iter().cloned().collect() - }; -} - -#[jrsonnet_macros::builtin] -fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> Result { - use Either4::*; - Ok(match x { - A(x) => x.chars().count(), - B(x) => x.len(), - C(x) => x.len(), - D(f) => f.params_len(), - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_type(x: Any) -> Result { - Ok(x.0.value_type().name().into()) -} - -#[jrsonnet_macros::builtin] -fn builtin_make_array(s: State, sz: usize, func: FuncVal) -> Result { - let mut out = Vec::with_capacity(sz); - for i in 0..sz { - out.push(func.evaluate_simple(s.clone(), &(i as f64,))?); - } - Ok(VecVal(Cc::new(out))) -} - -#[jrsonnet_macros::builtin] -const fn builtin_codepoint(str: char) -> Result { - Ok(str as u32) -} - -#[jrsonnet_macros::builtin] -fn builtin_object_fields_ex( - obj: ObjValue, - inc_hidden: bool, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, -) -> Result { - #[cfg(feature = "exp-preserve-order")] - let preserve_order = preserve_order.unwrap_or(false); - let out = obj.fields_ex( - inc_hidden, - #[cfg(feature = "exp-preserve-order")] - preserve_order, - ); - Ok(VecVal(Cc::new( - out.into_iter().map(Val::Str).collect::>(), - ))) -} - -#[jrsonnet_macros::builtin] -fn builtin_object_has_ex(obj: ObjValue, f: IStr, inc_hidden: bool) -> Result { - Ok(obj.has_field_ex(f, inc_hidden)) -} - -#[jrsonnet_macros::builtin] -fn builtin_parse_json(st: State, s: IStr) -> Result { - use serde_json::Value; - let value: Value = serde_json::from_str(&s) - .map_err(|e| RuntimeError(format!("failed to parse json: {}", e).into()))?; - Ok(Any(Value::into_untyped(value, st)?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_parse_yaml(st: State, s: IStr) -> Result { - use serde_json::Value; - let value = serde_yaml_with_quirks::Deserializer::from_str_with_quirks( - &s, - DeserializingQuirks { old_octals: true }, - ); - let mut out = vec![]; - for item in value { - let value = Value::deserialize(item) - .map_err(|e| RuntimeError(format!("failed to parse yaml: {}", e).into()))?; - let val = Value::into_untyped(value, st.clone())?; - out.push(val); - } - Ok(Any(if out.is_empty() { - Val::Null - } else if out.len() == 1 { - out.into_iter().next().unwrap() - } else { - Val::Arr(out.into()) - })) -} - -#[jrsonnet_macros::builtin] -fn builtin_slice( - indexable: IndexableVal, - index: Option>, - end: Option>, - step: Option>, -) -> Result { - std_slice(indexable, index, end, step).map(Any) -} - -#[jrsonnet_macros::builtin] -fn builtin_substr(str: IStr, from: usize, len: usize) -> Result { - Ok(str.chars().skip(from as usize).take(len as usize).collect()) -} - -#[jrsonnet_macros::builtin] -fn builtin_primitive_equals(a: Any, b: Any) -> Result { - primitive_equals(&a.0, &b.0) -} - -#[jrsonnet_macros::builtin] -fn builtin_equals(s: State, a: Any, b: Any) -> Result { - equals(s, &a.0, &b.0) -} - -#[jrsonnet_macros::builtin] -fn builtin_modulo(a: f64, b: f64) -> Result { - Ok(a % b) -} - -#[jrsonnet_macros::builtin] -fn builtin_mod(s: State, a: Either![f64, IStr], b: Any) -> Result { - use Either2::*; - Ok(Any(evaluate_mod_op( - s, - &match a { - A(v) => Val::Num(v), - B(s) => Val::Str(s), - }, - &b.0, - )?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_floor(x: f64) -> Result { - Ok(x.floor()) -} - -#[jrsonnet_macros::builtin] -fn builtin_ceil(x: f64) -> Result { - Ok(x.ceil()) -} - -#[jrsonnet_macros::builtin] -fn builtin_log(n: f64) -> Result { - Ok(n.ln()) -} - -#[jrsonnet_macros::builtin] -fn builtin_pow(x: f64, n: f64) -> Result { - Ok(x.powf(n)) -} - -#[jrsonnet_macros::builtin] -fn builtin_sqrt(x: PositiveF64) -> Result { - Ok(x.0.sqrt()) -} - -#[jrsonnet_macros::builtin] -fn builtin_sin(x: f64) -> Result { - Ok(x.sin()) -} - -#[jrsonnet_macros::builtin] -fn builtin_cos(x: f64) -> Result { - Ok(x.cos()) -} - -#[jrsonnet_macros::builtin] -fn builtin_tan(x: f64) -> Result { - Ok(x.tan()) -} - -#[jrsonnet_macros::builtin] -fn builtin_asin(x: f64) -> Result { - Ok(x.asin()) -} - -#[jrsonnet_macros::builtin] -fn builtin_acos(x: f64) -> Result { - Ok(x.acos()) -} - -#[jrsonnet_macros::builtin] -fn builtin_atan(x: f64) -> Result { - Ok(x.atan()) -} - -#[jrsonnet_macros::builtin] -fn builtin_exp(x: f64) -> Result { - Ok(x.exp()) -} - -fn frexp(s: f64) -> (f64, i16) { - if 0.0 == s { - (s, 0) - } else { - let lg = s.abs().log2(); - let x = (lg - lg.floor() - 1.0).exp2(); - let exp = lg.floor() + 1.0; - (s.signum() * x, exp as i16) - } -} - -#[jrsonnet_macros::builtin] -fn builtin_mantissa(x: f64) -> Result { - Ok(frexp(x).0) -} - -#[jrsonnet_macros::builtin] -fn builtin_exponent(x: f64) -> Result { - Ok(frexp(x).1) -} - -#[jrsonnet_macros::builtin] -fn builtin_ext_var(s: State, x: IStr) -> Result { - let ctx = s.create_default_context(); - Ok(Any(s - .clone() - .settings() - .ext_vars - .get(&x) - .cloned() - .ok_or(UndefinedExternalVariable(x))? - .evaluate_arg(s.clone(), ctx, true)? - .evaluate(s)?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_native(s: State, name: IStr) -> Result { - Ok(Any(s - .settings() - .ext_natives - .get(&name) - .cloned() - .map_or(Val::Null, |v| { - Val::Func(FuncVal::Builtin(v.clone())) - }))) -} - -#[jrsonnet_macros::builtin] -fn builtin_filter(s: State, func: FuncVal, arr: ArrValue) -> Result { - arr.filter(s.clone(), |val| { - bool::from_untyped( - func.evaluate_simple(s.clone(), &(Any(val.clone()),))?, - s.clone(), - ) - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_map(s: State, func: FuncVal, arr: ArrValue) -> Result { - arr.map(s.clone(), |val| { - func.evaluate_simple(s.clone(), &(Any(val),)) - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_flatmap(s: State, func: FuncVal, arr: IndexableVal) -> Result { - match arr { - IndexableVal::Str(str) => { - let mut out = String::new(); - for c in str.chars() { - match func.evaluate_simple(s.clone(), &(c.to_string(),))? { - Val::Str(o) => out.push_str(&o), - Val::Null => continue, - _ => throw!(RuntimeError( - "in std.join all items should be strings".into() - )), - }; - } - Ok(IndexableVal::Str(out.into())) - } - IndexableVal::Arr(a) => { - let mut out = Vec::new(); - for el in a.iter(s.clone()) { - let el = el?; - match func.evaluate_simple(s.clone(), &(Any(el),))? { - Val::Arr(o) => { - for oe in o.iter(s.clone()) { - out.push(oe?); - } - } - Val::Null => continue, - _ => throw!(RuntimeError( - "in std.join all items should be arrays".into() - )), - }; - } - Ok(IndexableVal::Arr(out.into())) - } - } -} - -#[jrsonnet_macros::builtin] -fn builtin_foldl(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { - let mut acc = init.0; - for i in arr.iter(s.clone()) { - acc = func.evaluate_simple(s.clone(), &(Any(acc), Any(i?)))?; - } - Ok(Any(acc)) -} - -#[jrsonnet_macros::builtin] -fn builtin_foldr(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { - let mut acc = init.0; - for i in arr.iter(s.clone()).rev() { - acc = func.evaluate_simple(s.clone(), &(Any(i?), Any(acc)))?; - } - Ok(Any(acc)) -} - -#[jrsonnet_macros::builtin] -#[allow(non_snake_case)] -fn builtin_sort(s: State, arr: ArrValue, keyF: Option) -> Result { - if arr.len() <= 1 { - return Ok(arr); - } - Ok(ArrValue::Eager(sort::sort( - s.clone(), - arr.evaluated(s)?, - keyF.unwrap_or_else(FuncVal::identity), - )?)) -} - -#[jrsonnet_macros::builtin] -fn builtin_format(s: State, str: IStr, vals: Any) -> Result { - std_format(s, str, vals.0) -} - -#[jrsonnet_macros::builtin] -fn builtin_range(from: i32, to: i32) -> Result { - if to < from { - return Ok(ArrValue::new_eager()); - } - Ok(ArrValue::new_range(from, to)) -} - -#[jrsonnet_macros::builtin] -fn builtin_char(n: u32) -> Result { - Ok(std::char::from_u32(n as u32).ok_or(InvalidUnicodeCodepointGot(n as u32))?) -} - -#[jrsonnet_macros::builtin] -fn builtin_encode_utf8(str: IStr) -> Result { - Ok(str.cast_bytes()) -} - -#[jrsonnet_macros::builtin] -fn builtin_decode_utf8(arr: IBytes) -> Result { - Ok(arr - .cast_str() - .ok_or_else(|| RuntimeError("bad utf8".into()))?) -} - -#[jrsonnet_macros::builtin] -fn builtin_md5(str: IStr) -> Result { - Ok(format!("{:x}", md5::compute(&str.as_bytes()))) -} - -#[jrsonnet_macros::builtin] -fn builtin_trace(s: State, loc: CallLocation, str: IStr, rest: Any) -> Result { - eprint!("TRACE:"); - if let Some(loc) = loc.0 { - let locs = s.map_source_locations(loc.0.clone(), &[loc.1]); - eprint!(" {}:{}", loc.0.short_display(), locs[0].line); - } - eprintln!(" {}", str); - Ok(rest) as Result -} - -#[jrsonnet_macros::builtin] -fn builtin_base64(input: Either![IBytes, IStr]) -> Result { - use Either2::*; - Ok(match input { - A(a) => base64::encode(a.as_slice()), - B(l) => base64::encode(l.bytes().collect::>()), - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_base64_decode_bytes(input: IStr) -> Result { - Ok(base64::decode(&input.as_bytes()) - .map_err(|_| RuntimeError("bad base64".into()))? - .as_slice() - .into()) -} - -#[jrsonnet_macros::builtin] -fn builtin_base64_decode(input: IStr) -> Result { - let bytes = base64::decode(&input.as_bytes()).map_err(|_| RuntimeError("bad base64".into()))?; - Ok(String::from_utf8(bytes).map_err(|_| RuntimeError("bad utf8".into()))?) -} - -#[jrsonnet_macros::builtin] -fn builtin_join(s: State, sep: IndexableVal, arr: ArrValue) -> Result { - Ok(match sep { - IndexableVal::Arr(joiner_items) => { - let mut out = Vec::new(); - - let mut first = true; - for item in arr.iter(s.clone()) { - let item = item?.clone(); - if let Val::Arr(items) = item { - if !first { - out.reserve(joiner_items.len()); - // TODO: extend - for item in joiner_items.iter(s.clone()) { - out.push(item?); - } - } - first = false; - out.reserve(items.len()); - for item in items.iter(s.clone()) { - out.push(item?); - } - } else if matches!(item, Val::Null) { - continue; - } else { - throw!(RuntimeError( - "in std.join all items should be arrays".into() - )); - } - } - - IndexableVal::Arr(out.into()) - } - IndexableVal::Str(sep) => { - let mut out = String::new(); - - let mut first = true; - for item in arr.iter(s) { - let item = item?.clone(); - if let Val::Str(item) = item { - if !first { - out += &sep; - } - first = false; - out += &item; - } else if matches!(item, Val::Null) { - continue; - } else { - throw!(RuntimeError( - "in std.join all items should be strings".into() - )); - } - } - - IndexableVal::Str(out.into()) - } - }) -} - -#[jrsonnet_macros::builtin] -fn builtin_escape_string_json(str_: IStr) -> Result { - Ok(escape_string_json(&str_)) -} - -#[jrsonnet_macros::builtin] -fn builtin_manifest_json_ex( - s: State, - value: Any, - indent: IStr, - newline: Option, - key_val_sep: Option, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, -) -> Result { - let newline = newline.as_deref().unwrap_or("\n"); - let key_val_sep = key_val_sep.as_deref().unwrap_or(": "); - manifest_json_ex( - s, - &value.0, - &ManifestJsonOptions { - padding: &indent, - mtype: ManifestType::Std, - newline, - key_val_sep, - #[cfg(feature = "exp-preserve-order")] - preserve_order: preserve_order.unwrap_or(false), }, ) -} - -#[jrsonnet_macros::builtin] -fn builtin_manifest_yaml_doc( - s: State, - value: Any, - indent_array_in_object: Option, - quote_keys: Option, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, -) -> Result { - manifest_yaml_ex( - s, - &value.0, - &ManifestYamlOptions { - padding: " ", - arr_element_padding: if indent_array_in_object.unwrap_or(false) { - " " - } else { - "" - }, - quote_keys: quote_keys.unwrap_or(true), - #[cfg(feature = "exp-preserve-order")] - preserve_order: preserve_order.unwrap_or(false), - }, - ) -} - -#[jrsonnet_macros::builtin] -fn builtin_reverse(value: ArrValue) -> Result { - Ok(value.reversed()) -} - -#[jrsonnet_macros::builtin] -fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result { - Ok(str.replace(&from as &str, &to as &str)) -} - -#[jrsonnet_macros::builtin] -fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> Result { - use Either2::*; - Ok(VecVal(Cc::new(match maxsplits { - A(n) => str - .splitn(n + 1, &c as &str) - .map(|s| Val::Str(s.into())) - .collect(), - B(_) => str.split(&c as &str).map(|s| Val::Str(s.into())).collect(), - }))) -} - -#[jrsonnet_macros::builtin] -fn builtin_ascii_upper(str: IStr) -> Result { - Ok(str.to_ascii_uppercase()) -} - -#[jrsonnet_macros::builtin] -fn builtin_ascii_lower(str: IStr) -> Result { - Ok(str.to_ascii_lowercase()) -} - -#[jrsonnet_macros::builtin] -fn builtin_member(s: State, arr: IndexableVal, x: Any) -> Result { - match arr { - IndexableVal::Str(str) => { - let x: IStr = IStr::from_untyped(x.0, s)?; - Ok(!x.is_empty() && str.contains(&*x)) - } - IndexableVal::Arr(a) => { - for item in a.iter(s.clone()) { - let item = item?; - if equals(s.clone(), &item, &x.0)? { - return Ok(true); - } - } - Ok(false) - } - } -} - -#[jrsonnet_macros::builtin] -fn builtin_count(s: State, arr: Vec, v: Any) -> Result { - let mut count = 0; - for item in &arr { - if equals(s.clone(), &item.0, &v.0)? { - count += 1; - } - } - Ok(count) -} - -#[jrsonnet_macros::builtin] -fn builtin_any(s: State, arr: ArrValue) -> Result { - for v in arr.iter(s.clone()) { - let v = bool::from_untyped(v?, s.clone())?; - if v { - return Ok(true); - } - } - Ok(false) -} - -#[jrsonnet_macros::builtin] -fn builtin_all(s: State, arr: ArrValue) -> Result { - for v in arr.iter(s.clone()) { - let v = bool::from_untyped(v?, s.clone())?; - if !v { - return Ok(false); - } - } - Ok(true) } --- a/crates/jrsonnet-evaluator/src/stdlib/sort.rs +++ /dev/null @@ -1,110 +0,0 @@ -use jrsonnet_gcmodule::{Cc, Trace}; - -use crate::{ - error::{Error, LocError, Result}, - function::FuncVal, - throw, - typed::Any, - State, Val, -}; - -#[derive(Debug, Clone, thiserror::Error, Trace)] -pub enum SortError { - #[error("sort key should be string or number")] - SortKeyShouldBeStringOrNumber, - #[error("sort elements should have equal types")] - SortElementsShouldHaveEqualType, -} - -impl From for LocError { - fn from(s: SortError) -> Self { - Self::new(Error::Sort(s)) - } -} - -#[derive(Copy, Clone)] -enum SortKeyType { - Number, - String, - Unknown, -} - -#[derive(PartialEq)] -struct NonNaNf64(f64); -impl PartialOrd for NonNaNf64 { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } -} -impl Eq for NonNaNf64 {} -impl Ord for NonNaNf64 { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other).expect("non nan") - } -} - -fn get_sort_type( - values: &mut Vec, - key_getter: impl Fn(&mut T) -> &mut Val, -) -> Result { - let mut sort_type = SortKeyType::Unknown; - for i in values.iter_mut() { - let i = key_getter(i); - match (i, sort_type) { - (Val::Str(_), SortKeyType::Unknown) => sort_type = SortKeyType::String, - (Val::Num(_), SortKeyType::Unknown) => sort_type = SortKeyType::Number, - (Val::Str(_), SortKeyType::String) | (Val::Num(_), SortKeyType::Number) => {} - (Val::Str(_) | Val::Num(_), _) => { - throw!(SortError::SortElementsShouldHaveEqualType) - } - _ => throw!(SortError::SortKeyShouldBeStringOrNumber), - } - } - Ok(sort_type) -} - -/// * `key_getter` - None, if identity sort required -pub fn sort(s: State, values: Cc>, key_getter: FuncVal) -> Result>> { - if values.len() <= 1 { - return Ok(values); - } - if key_getter.is_identity() { - // Fast path, identity key getter - let mut values = (*values).clone(); - let sort_type = get_sort_type(&mut values, |k| k)?; - match sort_type { - SortKeyType::Number => values.sort_unstable_by_key(|v| match v { - Val::Num(n) => NonNaNf64(*n), - _ => unreachable!(), - }), - SortKeyType::String => values.sort_unstable_by_key(|v| match v { - Val::Str(s) => s.clone(), - _ => unreachable!(), - }), - SortKeyType::Unknown => unreachable!(), - }; - Ok(Cc::new(values)) - } else { - // Slow path, user provided key getter - let mut vk = Vec::with_capacity(values.len()); - for value in values.iter() { - vk.push(( - value.clone(), - key_getter.evaluate_simple(s.clone(), &(Any(value.clone()),))?, - )); - } - let sort_type = get_sort_type(&mut vk, |v| &mut v.1)?; - match sort_type { - SortKeyType::Number => vk.sort_by_key(|v| match v.1 { - Val::Num(n) => NonNaNf64(n), - _ => unreachable!(), - }), - SortKeyType::String => vk.sort_by_key(|v| match &v.1 { - Val::Str(s) => s.clone(), - _ => unreachable!(), - }), - SortKeyType::Unknown => unreachable!(), - }; - Ok(Cc::new(vk.into_iter().map(|v| v.0).collect())) - } -} --- a/crates/jrsonnet-evaluator/src/trace/location.rs +++ /dev/null @@ -1,124 +0,0 @@ -#[allow(clippy::module_name_repetitions)] -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct CodeLocation { - pub offset: usize, - - pub line: usize, - pub column: usize, - - pub line_start_offset: usize, - pub line_end_offset: usize, -} - -#[allow(clippy::module_name_repetitions)] -pub fn location_to_offset(mut file: &str, mut line: usize, column: usize) -> Option { - let mut offset = 0; - while line > 1 { - let pos = file.find('\n')?; - offset += pos + 1; - file = &file[pos + 1..]; - line -= 1; - } - offset += column - 1; - Some(offset) -} - -#[allow(clippy::module_name_repetitions)] -pub fn offset_to_location(file: &str, offsets: &[u32]) -> Vec { - if offsets.is_empty() { - return vec![]; - } - let mut line = 1; - let mut column = 1; - let max_offset = *offsets.iter().max().expect("offsets is not empty"); - - let mut offset_map = offsets - .iter() - .enumerate() - .map(|(pos, offset)| (*offset, pos)) - .collect::>(); - offset_map.sort_by_key(|v| v.0); - offset_map.reverse(); - - let mut out = vec![ - CodeLocation { - offset: 0, - column: 0, - line: 0, - line_start_offset: 0, - line_end_offset: 0 - }; - offsets.len() - ]; - let mut with_no_known_line_ending = vec![]; - let mut this_line_offset = 0; - for (pos, ch) in file - .chars() - .enumerate() - .chain(std::iter::once((file.len(), ' '))) - { - column += 1; - match offset_map.last() { - Some(x) if x.0 == pos as u32 => { - let out_idx = x.1; - with_no_known_line_ending.push(out_idx); - out[out_idx].offset = pos; - out[out_idx].line = line; - out[out_idx].column = column; - out[out_idx].line_start_offset = this_line_offset; - offset_map.pop(); - } - _ => {} - } - if ch == '\n' { - line += 1; - column = 1; - - for idx in with_no_known_line_ending.drain(..) { - out[idx].line_end_offset = pos; - } - this_line_offset = pos + 1; - - if pos == max_offset as usize + 1 { - break; - } - } - } - let file_end = file.chars().count(); - for idx in with_no_known_line_ending { - out[idx].line_end_offset = file_end; - } - - out -} - -#[cfg(test)] -pub mod tests { - use super::{offset_to_location, CodeLocation}; - - #[test] - fn test() { - assert_eq!( - offset_to_location( - "hello world\n_______________________________________________________", - &[0, 14] - ), - vec![ - CodeLocation { - offset: 0, - line: 1, - column: 2, - line_start_offset: 0, - line_end_offset: 11, - }, - CodeLocation { - offset: 14, - line: 2, - column: 4, - line_start_offset: 12, - line_end_offset: 67 - } - ] - ) - } -} --- a/crates/jrsonnet-evaluator/src/trace/mod.rs +++ b/crates/jrsonnet-evaluator/src/trace/mod.rs @@ -1,13 +1,11 @@ -mod location; - use std::path::{Path, PathBuf}; -use jrsonnet_parser::Source; -pub use location::*; +use jrsonnet_parser::{CodeLocation, Source}; use crate::{error::Error, LocError, State}; /// The way paths should be displayed +#[derive(Clone)] pub enum PathResolver { /// Only filename FileName, @@ -18,6 +16,10 @@ } impl PathResolver { + /// Will return `Self::Relative(cwd)`, or `Self::Absolute` on cwd failure + pub fn new_cwd_fallback() -> Self { + std::env::current_dir().map_or(Self::Absolute, Self::Relative) + } pub fn resolve(&self, from: &Path) -> String { match self { Self::FileName => from @@ -84,31 +86,27 @@ fn write_trace( &self, out: &mut dyn std::fmt::Write, - s: &State, + _s: &State, error: &LocError, ) -> Result<(), std::fmt::Error> { write!(out, "{}", error.error())?; - if let Error::ImportSyntaxError { - path, - source_code, - error, - } = error.error() - { + if let Error::ImportSyntaxError { path, error } = error.error() { use std::fmt::Write; writeln!(out)?; - let mut n = match path.repr() { - Ok(r) => self.resolver.resolve(r), - Err(v) => v.to_string(), - }; + let mut n = path.source_path().path().map_or_else( + || path.source_path().to_string(), + |r| self.resolver.resolve(r), + ); let mut offset = error.location.offset; - let is_eof = if offset >= source_code.len() { - offset = source_code.len().saturating_sub(1); + let is_eof = if offset >= path.code().len() { + offset = path.code().len().saturating_sub(1); true } else { false }; - let mut location = offset_to_location(source_code, &[offset as u32]) + let mut location = path + .map_source_locations(&[offset as u32]) .into_iter() .next() .unwrap(); @@ -118,7 +116,7 @@ write!(n, ":").unwrap(); print_code_location(&mut n, &location, &location).unwrap(); - write!(out, "{: self.resolver.resolve(r), - Err(v) => v.to_string(), + let mut resolved_path = match location.0.source_path().path() { + Some(r) => self.resolver.resolve(r), + None => location.0.source_path().to_string(), }; // TODO: Process all trace elements first - let location = - s.map_source_locations(location.0.clone(), &[location.1, location.2]); + let location = location.0.map_source_locations(&[location.1, location.2]); write!(resolved_path, ":").unwrap(); print_code_location(&mut resolved_path, &location[0], &location[1]).unwrap(); write!(resolved_path, ":").unwrap(); @@ -176,7 +173,7 @@ fn write_trace( &self, out: &mut dyn std::fmt::Write, - s: &State, + _s: &State, error: &LocError, ) -> Result<(), std::fmt::Error> { write!(out, "{}", error.error())?; @@ -184,11 +181,11 @@ writeln!(out)?; let desc = &item.desc; if let Some(source) = &item.location { - let start_end = s.map_source_locations(source.0.clone(), &[source.1, source.2]); - let resolved_path = match source.0.repr() { - Ok(r) => r.display().to_string(), - Err(v) => v.to_string(), - }; + let start_end = source.0.map_source_locations(&[source.1, source.2]); + let resolved_path = source.0.source_path().path().map_or_else( + || source.0.source_path().to_string(), + |r| r.display().to_string(), + ); write!( out, @@ -196,7 +193,7 @@ desc, resolved_path, start_end[0].line, start_end[0].column, )?; } else { - write!(out, " during {}", desc)?; + write!(out, " during {desc}")?; } } Ok(()) @@ -213,19 +210,15 @@ fn write_trace( &self, out: &mut dyn std::fmt::Write, - s: &State, + _s: &State, error: &LocError, ) -> Result<(), std::fmt::Error> { write!(out, "{}", error.error())?; - if let Error::ImportSyntaxError { - path, - source_code, - error, - } = error.error() - { + if let Error::ImportSyntaxError { path, error } = error.error() { writeln!(out)?; let offset = error.location.offset; - let location = offset_to_location(source_code, &[offset as u32]) + let location = path + .map_source_locations(&[offset as u32]) .into_iter() .next() .unwrap(); @@ -234,7 +227,7 @@ self.print_snippet( out, - source_code, + path.code(), path, &location, &end_location, @@ -246,17 +239,17 @@ writeln!(out)?; let desc = &item.desc; if let Some(source) = &item.location { - let start_end = s.map_source_locations(source.0.clone(), &[source.1, source.2]); + let start_end = source.0.map_source_locations(&[source.1, source.2]); self.print_snippet( out, - &s.get_source(source.0.clone()).unwrap(), + source.0.code(), &source.0, &start_end[0], &start_end[1], desc, )?; } else { - write!(out, "{}", desc)?; + write!(out, "{desc}")?; } } Ok(()) @@ -284,10 +277,10 @@ .take(end.line_end_offset - end.line_start_offset) .collect(); - let origin = match origin.repr() { - Ok(r) => self.resolver.resolve(r), - Err(v) => v.to_string(), - }; + let origin = origin.source_path().path().map_or_else( + || origin.source_path().to_string(), + |r| self.resolver.resolve(r), + ); let snippet = Snippet { opt: FormatOptions { color: true, @@ -312,7 +305,7 @@ }; let dl = DisplayList::from(snippet); - write!(out, "{}", dl)?; + write!(out, "{dl}")?; Ok(()) } --- a/crates/jrsonnet-evaluator/src/typed/conversions.rs +++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs @@ -393,6 +393,7 @@ ($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty) => {Either6<$a, $b, $c, $d, $e, $f>}; ($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty, $g:ty) => {Either7<$a, $b, $c, $d, $e, $f, $g>}; } +pub use Either; pub type MyType = Either![u32, f64, String]; --- a/crates/jrsonnet-evaluator/src/typed/mod.rs +++ b/crates/jrsonnet-evaluator/src/typed/mod.rs @@ -21,8 +21,8 @@ UnionFailed(ComplexValType, TypeLocErrorList), #[error( "number out of bounds: {0} not in {}..{}", - .1.map(|v|v.to_string()).unwrap_or_else(|| "".to_owned()), - .2.map(|v|v.to_string()).unwrap_or_else(|| "".to_owned()), + .1.map(|v|v.to_string()).unwrap_or_default(), + .2.map(|v|v.to_string()).unwrap_or_default(), )] BoundsFailed(f64, Option, Option), } @@ -65,7 +65,7 @@ writeln!(f)?; } out.clear(); - write!(out, "{}", err)?; + write!(out, "{err}")?; for (i, line) in out.lines().enumerate() { if line.trim().is_empty() { @@ -77,7 +77,7 @@ writeln!(f)?; write!(f, " ")?; } - write!(f, "{}", line)?; + write!(f, "{line}")?; } } Ok(()) @@ -125,8 +125,8 @@ impl Display for ValuePathItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Field(name) => write!(f, ".{:?}", name)?, - Self::Index(idx) => write!(f, "[{}]", idx)?, + Self::Field(name) => write!(f, ".{name:?}")?, + Self::Index(idx) => write!(f, "[{idx}]")?, } Ok(()) } @@ -138,7 +138,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "self")?; for elem in self.0.iter().rev() { - write!(f, "{}", elem)?; + write!(f, "{elem}")?; } Ok(()) } @@ -171,7 +171,7 @@ for (i, item) in a.iter(s.clone()).enumerate() { push_type_description( s.clone(), - || format!("array index {}", i), + || format!("array index {i}"), || ValuePathItem::Index(i as u64), || elem_type.check(s.clone(), &item.clone()?), )?; @@ -185,7 +185,7 @@ for (i, item) in a.iter(s.clone()).enumerate() { push_type_description( s.clone(), - || format!("array index {}", i), + || format!("array index {i}"), || ValuePathItem::Index(i as u64), || elem_type.check(s.clone(), &item.clone()?), )?; @@ -200,7 +200,7 @@ if let Some(got_v) = obj.get(s.clone(), (*k).into())? { push_type_description( s.clone(), - || format!("property {}", k), + || format!("property {k}"), || ValuePathItem::Field((*k).into()), || v.check(s.clone(), &got_v), )?; --- a/crates/jrsonnet-evaluator/src/val.rs +++ b/crates/jrsonnet-evaluator/src/val.rs @@ -11,7 +11,9 @@ stdlib::manifest::{ manifest_json_ex, manifest_yaml_ex, ManifestJsonOptions, ManifestType, ManifestYamlOptions, }, - throw, ObjValue, Result, State, Unbound, WeakObjValue, + throw, + typed::BoundedUsize, + ObjValue, Result, State, Unbound, WeakObjValue, }; pub trait ThunkValue: Trace { @@ -184,16 +186,26 @@ } } +/// Represents a Jsonnet array value. #[derive(Debug, Clone, Trace)] // may contrain other ArrValue #[trace(tracking(force))] pub enum ArrValue { + /// Layout optimized byte array. Bytes(#[trace(skip)] IBytes), + /// Every element is lazy evaluated. Lazy(Cc>>), + /// Every field is already evaluated. Eager(Cc>), + /// Concatenation of two arrays of any kind. Extended(Box<(Self, Self)>), + /// Represents a integer array in form `[start, start + 1, ... end - 1, end]`. + /// This kind of arrays is generated by `std.range(start, end)` call, and used for loops. Range(i32, i32), + /// Sliced array view. Slice(Box), + /// Reversed array view. + /// Returned by `std.reverse(other)` call Reversed(Box), } @@ -204,9 +216,13 @@ pub fn new_eager() -> Self { Self::Eager(Cc::new(Vec::new())) } + pub fn empty() -> Self { + Self::new_range(0, 0) + } /// # Panics /// If a > b + #[inline] pub fn new_range(a: i32, b: i32) -> Self { assert!(a <= b); Self::Range(a, b) @@ -231,6 +247,7 @@ })) } + /// Array length. pub fn len(&self) -> usize { match self { Self::Bytes(i) => i.len(), @@ -243,10 +260,14 @@ } } + /// Is array contains no elements? pub fn is_empty(&self) -> bool { self.len() == 0 } + /// Get array element by index, evaluating it, if it is lazy. + /// + /// Returns `None` on out-of-bounds condition. pub fn get(&self, s: State, index: usize) -> Result> { match self { Self::Bytes(i) => i @@ -286,11 +307,14 @@ if index >= v.to() { return Ok(None); } - v.inner.get(s, index as usize) + v.inner.get(s, index) } } } + /// Get array element by index, without evaluation. + /// + /// Returns `None` on out-of-bounds condition. pub fn get_lazy(&self, index: usize) -> Option> { match self { Self::Bytes(i) => i @@ -326,11 +350,12 @@ if index >= s.to() { return None; } - s.inner.get_lazy(index as usize) + s.inner.get_lazy(index) } } } + /// Evaluate all array elements, returning new array. pub fn evaluated(&self, s: State) -> Result>> { Ok(match self { Self::Bytes(i) => { @@ -383,6 +408,7 @@ }) } + /// Iterate over elements, evaluating them. pub fn iter(&self, s: State) -> impl DoubleEndedIterator> + '_ { (0..self.len()).map(move |idx| match self { Self::Bytes(b) => Ok(Val::Num(f64::from(b[idx]))), @@ -394,6 +420,7 @@ }) } + /// Iterate over elements, returning lazy values. pub fn iter_lazy(&self) -> impl DoubleEndedIterator> + '_ { (0..self.len()).map(move |idx| match self { Self::Bytes(b) => Thunk::evaluated(Val::Num(f64::from(b[idx]))), @@ -405,11 +432,13 @@ }) } + /// Return a reversed view on current array. #[must_use] pub fn reversed(self) -> Self { Self::Reversed(Box::new(self)) } + /// Return a new array, produced by passing every element of current array to specified callback function. pub fn map(self, s: State, mapper: impl Fn(Val) -> Result) -> Result { let mut out = Vec::with_capacity(self.len()); @@ -420,6 +449,7 @@ Ok(Self::Eager(Cc::new(out))) } + /// Return a new array, produced from current array by removing every value, for which specified callback function returns false. pub fn filter(self, s: State, filter: impl Fn(&Val) -> Result) -> Result { let mut out = Vec::with_capacity(self.len()); @@ -454,25 +484,100 @@ } } +/// Represents a Jsonnet value, which can be spliced or indexed (string or array). #[allow(clippy::module_name_repetitions)] pub enum IndexableVal { + /// String. Str(IStr), + /// Array. Arr(ArrValue), } +impl IndexableVal { + /// Slice the value. + /// + /// # Implementation + /// + /// For strings, will create a copy of specified interval. + /// + /// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned. + pub fn slice( + self, + index: Option>, + end: Option>, + step: Option>, + ) -> Result { + match &self { + IndexableVal::Str(s) => { + let index = index.as_deref().copied().unwrap_or(0); + let end = end.as_deref().copied().unwrap_or(usize::MAX); + let step = step.as_deref().copied().unwrap_or(1); + if index >= end { + return Ok(Self::Str("".into())); + } + + Ok(Self::Str( + (s.chars() + .skip(index) + .take(end - index) + .step_by(step) + .collect::()) + .into(), + )) + } + IndexableVal::Arr(arr) => { + let index = index.as_deref().copied().unwrap_or(0); + let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len()); + let step = step.as_deref().copied().unwrap_or(1); + + if index >= end { + return Ok(Self::Arr(ArrValue::new_eager())); + } + + Ok(Self::Arr(ArrValue::Slice(Box::new(Slice { + inner: arr.clone(), + from: index as u32, + to: end as u32, + step: step as u32, + })))) + } + } + } +} + +/// Represents any valid Jsonnet value. #[derive(Debug, Clone, Trace)] pub enum Val { + /// Represents a Jsonnet boolean. Bool(bool), + /// Represents a Jsonnet null value. Null, + /// Represents a Jsonnet string. Str(IStr), + /// Represents a Jsonnet number. + /// Should be finite, and not NaN + /// This restriction isn't enforced by enum, as enum field can't be marked as private Num(f64), + /// Represents a Jsonnet array. Arr(ArrValue), + /// Represents a Jsonnet object. Obj(ObjValue), + /// Represents a Jsonnet function. Func(FuncVal), } -#[cfg(target_pointer_width = "64")] -static_assertions::assert_eq_size!(Val, [u8; 32]); +impl From for Val { + fn from(v: IndexableVal) -> Self { + match v { + IndexableVal::Str(s) => Self::Str(s), + IndexableVal::Arr(a) => Self::Arr(a), + } + } +} + +// Broken between stable and nightly, as there is new layout size optimization +// #[cfg(target_pointer_width = "64")] +// static_assertions::assert_eq_size!(Val, [u8; 24]); impl Val { pub const fn as_bool(&self) -> Option { --- a/crates/jrsonnet-evaluator/tests/as_native.rs +++ /dev/null @@ -1,19 +0,0 @@ -use jrsonnet_evaluator::{error::Result, State}; - -mod common; - -#[test] -fn as_native() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - - let val = s.evaluate_snippet("snip".to_owned(), r#"function(a, b) a + b"#.into())?; - let func = val.as_func().expect("this is function"); - - let native = func.into_native::<((u32, u32), u32)>(); - - ensure_eq!(native(s.clone(), 1, 2)?, 3); - ensure_eq!(native(s, 3, 4)?, 7); - - Ok(()) -} --- a/crates/jrsonnet-evaluator/tests/builtin.rs +++ /dev/null @@ -1,100 +0,0 @@ -mod common; - -use jrsonnet_evaluator::{ - error::Result, - function::{builtin, builtin::Builtin, CallLocation, FuncVal}, - tb, - typed::Typed, - State, Val, -}; -use jrsonnet_gcmodule::Cc; - -#[builtin] -fn a() -> Result { - Ok(1) -} - -#[test] -fn basic_function() -> Result<()> { - let s = State::default(); - let a: a = a {}; - let v = u32::from_untyped( - a.call( - s.clone(), - s.create_default_context(), - CallLocation::native(), - &(), - )?, - s, - )?; - - ensure_eq!(v, 1); - Ok(()) -} - -#[builtin] -fn native_add(a: u32, b: u32) -> Result { - Ok(a + b) -} - -#[test] -fn call_from_code() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - s.settings_mut().globals.insert( - "nativeAdd".into(), - Val::Func(FuncVal::StaticBuiltin(native_add::INST)), - ); - - let v = s.evaluate_snippet( - "snip".to_owned(), - " - assert nativeAdd(1, 2) == 3; - assert nativeAdd(100, 200) == 300; - null - " - .into(), - )?; - ensure_val_eq!(s, v, Val::Null); - Ok(()) -} - -#[builtin(fields( - a: u32 -))] -fn curried_add(this: &curried_add, b: u32) -> Result { - Ok(this.a + b) -} - -#[builtin] -fn curry_add(a: u32) -> Result { - Ok(FuncVal::Builtin(Cc::new(tb!(curried_add { a })))) -} - -#[test] -fn nonstatic_builtin() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - s.settings_mut().globals.insert( - "curryAdd".into(), - Val::Func(FuncVal::StaticBuiltin(curry_add::INST)), - ); - - let v = s.evaluate_snippet( - "snip".to_owned(), - " - local a = curryAdd(1); - local b = curryAdd(4); - - assert a(2) == 3; - assert a(200) == 201; - - assert b(2) == 6; - assert b(200) == 204; - null - " - .into(), - )?; - ensure_val_eq!(s, v, Val::Null); - Ok(()) -} --- a/crates/jrsonnet-evaluator/tests/common.rs +++ /dev/null @@ -1,78 +0,0 @@ -use jrsonnet_evaluator::{ - error::Result, - function::{builtin, FuncVal}, - throw_runtime, ObjValueBuilder, State, Thunk, Val, -}; - -#[macro_export] -macro_rules! ensure_eq { - ($a:expr, $b:expr $(,)?) => {{ - let a = &$a; - let b = &$b; - if a != b { - ::jrsonnet_evaluator::throw_runtime!("assertion failed: a != b\na={:#?}\nb={:#?}", a, b) - } - }}; -} - -#[macro_export] -macro_rules! ensure { - ($v:expr $(,)?) => { - if !$v { - ::jrsonnet_evaluator::throw_runtime!("assertion failed: {}", stringify!($v)) - } - }; -} - -#[macro_export] -macro_rules! ensure_val_eq { - ($s:expr, $a:expr, $b:expr) => {{ - if !::jrsonnet_evaluator::val::equals($s.clone(), &$a.clone(), &$b.clone())? { - ::jrsonnet_evaluator::throw_runtime!( - "assertion failed: a != b\na={:#?}\nb={:#?}", - $a.to_json( - $s.clone(), - 2, - #[cfg(feature = "exp-preserve-order")] - false - )?, - $b.to_json( - $s.clone(), - 2, - #[cfg(feature = "exp-preserve-order")] - false - )?, - ) - } - }}; -} - -#[builtin] -fn assert_throw(s: State, lazy: Thunk, message: String) -> Result { - match lazy.evaluate(s) { - Ok(_) => { - throw_runtime!("expected argument to throw on evaluation, but it returned instead") - } - Err(e) => { - let error = format!("{}", e.error()); - ensure_eq!(message, error); - } - } - Ok(true) -} - -#[allow(dead_code)] -pub fn with_test(s: &State) { - let mut bobj = ObjValueBuilder::new(); - bobj.member("assertThrow".into()) - .hide() - .value( - s.clone(), - Val::Func(FuncVal::StaticBuiltin(assert_throw::INST)), - ) - .expect("no error"); - - s.settings_mut() - .globals - .insert("test".into(), Val::Obj(bobj.build())); -} --- a/crates/jrsonnet-evaluator/tests/golden.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{ - fs, io, - path::{Path, PathBuf}, -}; - -use jrsonnet_evaluator::{ - trace::{CompactFormat, PathResolver}, - FileImportResolver, State, -}; - -mod common; - -fn run(root: &Path, file: &Path) -> String { - let s = State::default(); - s.set_trace_format(Box::new(CompactFormat { - resolver: PathResolver::Relative(root.to_owned()), - padding: 3, - })); - s.with_stdlib(); - common::with_test(&s); - s.set_import_resolver(Box::new(FileImportResolver::default())); - - let v = match s.import(file.to_owned()) { - Ok(v) => v, - Err(e) => return s.stringify_err(&e), - }; - match v.to_json( - s.clone(), - 3, - #[cfg(feature = "exp-preserve-order")] - false, - ) { - Ok(v) => v.to_string(), - Err(e) => s.stringify_err(&e), - } -} - -#[test] -fn test() -> io::Result<()> { - let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - root.push("tests/golden"); - - for entry in fs::read_dir(&root)? { - let entry = entry?; - if !entry.path().extension().map_or(false, |e| e == "jsonnet") { - continue; - } - - let result = run(&root, &entry.path()); - - let mut golden_path = entry.path(); - golden_path.set_extension("jsonnet.golden"); - - if !golden_path.exists() { - fs::write(golden_path, &result)?; - } else { - let golden = fs::read_to_string(golden_path)?; - - assert_eq!( - result, - golden, - "golden didn't match for {}", - entry.path().display() - ) - } - } - - Ok(()) -} --- a/crates/jrsonnet-evaluator/tests/golden/array_comp.jsonnet +++ /dev/null @@ -1 +0,0 @@ -[[a, b] for a in [1, 2, 3] for b in [4, 5, 6]] --- a/crates/jrsonnet-evaluator/tests/golden/array_comp.jsonnet.golden +++ /dev/null @@ -1,38 +0,0 @@ -[ - [ - 1, - 4 - ], - [ - 1, - 5 - ], - [ - 1, - 6 - ], - [ - 2, - 4 - ], - [ - 2, - 5 - ], - [ - 2, - 6 - ], - [ - 3, - 4 - ], - [ - 3, - 5 - ], - [ - 3, - 6 - ] -] \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/builtin_json.jsonnet +++ /dev/null @@ -1 +0,0 @@ -std.manifestJsonEx({ a: 3, b: 4, c: 6 }, '') --- a/crates/jrsonnet-evaluator/tests/golden/builtin_json.jsonnet.golden +++ /dev/null @@ -1 +0,0 @@ -"{\n\"a\": 3,\n\"b\": 4,\n\"c\": 6\n}" \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/builtin_json_minified.jsonnet +++ /dev/null @@ -1 +0,0 @@ -std.manifestJsonMinified({ a: 3, b: 4, c: 6 }) --- a/crates/jrsonnet-evaluator/tests/golden/builtin_json_minified.jsonnet.golden +++ /dev/null @@ -1 +0,0 @@ -"{\"a\":3,\"b\":4,\"c\":6}" \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/builtin_parseJson.jsonnet +++ /dev/null @@ -1 +0,0 @@ -std.parseJson('{"a": -1,"b": 1,"c": 3.141,"d": []}') --- a/crates/jrsonnet-evaluator/tests/golden/builtin_parseJson.jsonnet.golden +++ /dev/null @@ -1,6 +0,0 @@ -{ - "a": -1, - "b": 1, - "c": 3.141, - "d": [ ] -} \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/issue23.jsonnet +++ /dev/null @@ -1 +0,0 @@ -import 'issue23.jsonnet' --- a/crates/jrsonnet-evaluator/tests/golden/issue23.jsonnet.golden +++ /dev/null @@ -1,2 +0,0 @@ -infinite recursion detected - issue23.jsonnet:1:1-26: import "issue23.jsonnet" \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/issue40.jsonnet +++ /dev/null @@ -1,9 +0,0 @@ -local conf = { - n: '', -}; - -local result = conf { - assert std.isNumber(self.n) : 'is number', -}; - -std.manifestJsonEx(result, '') --- a/crates/jrsonnet-evaluator/tests/golden/issue40.jsonnet.golden +++ /dev/null @@ -1,3 +0,0 @@ -assert failed: is number - issue40.jsonnet:6:10-31: assertion failure - issue40.jsonnet:9:1-32: function call \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/missing_binding.jsonnet +++ /dev/null @@ -1 +0,0 @@ -sta --- a/crates/jrsonnet-evaluator/tests/golden/missing_binding.jsonnet.golden +++ /dev/null @@ -1,3 +0,0 @@ -variable is not defined: sta -There is variable with similar name present: std - missing_binding.jsonnet:1:1-5: variable access \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/object_comp.jsonnet +++ /dev/null @@ -1 +0,0 @@ -{ local t = 'a', ['h' + i + '_' + z]: if 'h' + (i - 1) + '_' + z in self then t + 1 else 0 + t for i in [1, 2, 3] for z in [2, 3, 4] if z != i } --- a/crates/jrsonnet-evaluator/tests/golden/object_comp.jsonnet.golden +++ /dev/null @@ -1,9 +0,0 @@ -{ - "h1_2": "0a", - "h1_3": "0a", - "h1_4": "0a", - "h2_3": "a1", - "h2_4": "a1", - "h3_2": "0a", - "h3_4": "a1" -} \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/golden/test_assertThrow.jsonnet +++ /dev/null @@ -1,2 +0,0 @@ -// Test that test.assertThrow will return error, if body is not errored -test.assertThrow(1, '1') --- a/crates/jrsonnet-evaluator/tests/golden/test_assertThrow.jsonnet.golden +++ /dev/null @@ -1,2 +0,0 @@ -runtime error: expected argument to throw on evaluation, but it returned instead - test_assertThrow.jsonnet:2:1-26: function call \ No newline at end of file --- a/crates/jrsonnet-evaluator/tests/sanity.rs +++ /dev/null @@ -1,41 +0,0 @@ -use jrsonnet_evaluator::{error::Result, throw_runtime, State, Val}; - -mod common; - -#[test] -fn assert_positive() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - - let v = s.evaluate_snippet("snip".to_owned(), "assert 1 == 1: 'fail'; null".into())?; - ensure_val_eq!(s, v, Val::Null); - let v = s.evaluate_snippet("snip".to_owned(), "std.assertEqual(1, 1)".into())?; - ensure_val_eq!(s, v, Val::Bool(true)); - - Ok(()) -} - -#[test] -fn assert_negative() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - - { - let e = match s.evaluate_snippet("snip".to_owned(), "assert 1 == 2: 'fail'; null".into()) { - Ok(_) => throw_runtime!("assertion should fail"), - Err(e) => e, - }; - let e = s.stringify_err(&e); - ensure!(e.starts_with("assert failed: fail\n")); - } - { - let e = match s.evaluate_snippet("snip".to_owned(), "std.assertEqual(1, 2)".into()) { - Ok(_) => throw_runtime!("assertion should fail"), - Err(e) => e, - }; - let e = s.stringify_err(&e); - ensure!(e.starts_with("runtime error: Assertion failed. 1 != 2")) - } - - Ok(()) -} --- a/crates/jrsonnet-evaluator/tests/suite.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::{ - fs, io, - path::{Path, PathBuf}, -}; - -use jrsonnet_evaluator::{ - trace::{CompactFormat, PathResolver}, - FileImportResolver, State, Val, -}; - -mod common; - -fn run(root: &Path, file: &Path) { - let s = State::default(); - s.set_trace_format(Box::new(CompactFormat { - resolver: PathResolver::Relative(root.to_owned()), - padding: 3, - })); - s.with_stdlib(); - common::with_test(&s); - s.set_import_resolver(Box::new(FileImportResolver::default())); - - match s.import(file.to_owned()) { - Ok(Val::Bool(true)) => {} - Ok(Val::Bool(false)) => panic!("test {} returned false", file.display()), - Ok(_) => panic!("test {} returned wrong type as result", file.display()), - Err(e) => panic!("test {} failed:\n{}", file.display(), s.stringify_err(&e)), - }; -} - -#[test] -fn test() -> io::Result<()> { - let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - root.push("tests/suite"); - - for entry in fs::read_dir(&root)? { - let entry = entry?; - if !entry.path().extension().map_or(false, |e| e == "jsonnet") { - continue; - } - - run(&root, &entry.path()); - } - - Ok(()) -} --- a/crates/jrsonnet-evaluator/tests/suite/builtin_ascii.jsonnet +++ /dev/null @@ -1,3 +0,0 @@ -std.assertEqual(std.asciiUpper('aBc😀'), 'ABC😀') && -std.assertEqual(std.asciiLower('aBc😀'), 'abc😀') && -true --- a/crates/jrsonnet-evaluator/tests/suite/builtin_base64.jsonnet +++ /dev/null @@ -1,2 +0,0 @@ -std.assertEqual(std.base64('test'), 'dGVzdA==') && -true --- a/crates/jrsonnet-evaluator/tests/suite/builtin_chars.jsonnet +++ /dev/null @@ -1,3 +0,0 @@ -local c = '😎'; -std.assertEqual({ c: std.codepoint(c), l: std.length(c) }, { c: 128526, l: 1 }) && -true --- a/crates/jrsonnet-evaluator/tests/suite/builtin_constant.jsonnet +++ /dev/null @@ -1,3 +0,0 @@ -local std2 = std; local std = std2 { primitiveEquals(a, b):: false }; -// In jsonnet, this expression was failing because of being desugared to std.primitiveEquals(1, 1) -std.assertEqual(1 == 1, true) --- a/crates/jrsonnet-evaluator/tests/suite/builtin_count.jsonnet +++ /dev/null @@ -1,4 +0,0 @@ -std.assertEqual(std.count([], ''), 0) && -std.assertEqual(std.count(['a', 'b', 'a'], 'd'), 0) && -std.assertEqual(std.count(['a', 'b', 'a'], 'a'), 2) && -true --- a/crates/jrsonnet-evaluator/tests/suite/builtin_join.jsonnet +++ /dev/null @@ -1,4 +0,0 @@ -std.assertEqual(std.join([0, 0], [[1, 2], [3, 4], [5, 6]]), [1, 2, 0, 0, 3, 4, 0, 0, 5, 6]) && -std.assertEqual(std.join(',', ['1', '2', '3', '4']), '1,2,3,4') && -std.assertEqual(std.join(',', ['1', null, '2', null, '3']), '1,2,3') && -true --- a/crates/jrsonnet-evaluator/tests/suite/builtin_member.jsonnet +++ /dev/null @@ -1,7 +0,0 @@ -!std.member('', '') && -std.member('abc', 'a') && -!std.member('abc', 'd') && -!std.member([], '') && -std.member(['a', 'b', 'c'], 'a') && -!std.member(['a', 'b', 'c'], 'd') && -true --- a/crates/jrsonnet-evaluator/tests/suite/function_args.jsonnet +++ /dev/null @@ -1,3 +0,0 @@ -std.assertEqual(local a = function(b, c=2) b + c; a(2), 4) && -std.assertEqual(local a = function(b, c='Dear') b + c + d, d = 'World'; a('Hello'), 'HelloDearWorld') && -true --- a/crates/jrsonnet-evaluator/tests/suite/function_context.jsonnet +++ /dev/null @@ -1,10 +0,0 @@ -local k = { - t(name=self.h): [self.h, name], - h: 3, -}; -local f = { - t: k.t(), - h: 4, -}; -std.assertEqual(f.t[0], f.t[1]) && -true --- a/crates/jrsonnet-evaluator/tests/suite/function_lazy_args.jsonnet +++ /dev/null @@ -1,5 +0,0 @@ -local fun(a) = 2; -std.assertEqual(fun(error '3'), 2) && -// But in tailstrict mode arguments are evaluated eagerly -test.assertThrow(fun(error '3') tailstrict, 'runtime error: 3') && -true --- a/crates/jrsonnet-evaluator/tests/suite/local.jsonnet +++ /dev/null @@ -1,4 +0,0 @@ -std.assertEqual(local a = 2; local b = 3; a + b, 5) && -std.assertEqual(local a = 1, b = a + 1; a + b, 3) && -std.assertEqual(local a = 1; local a = 2; a, 2) && -true --- a/crates/jrsonnet-evaluator/tests/suite/math.jsonnet +++ /dev/null @@ -1,3 +0,0 @@ -std.assertEqual(2 + 2 * 2, 6) && -std.assertEqual(3 + (2 + 2 * 2), 9) && -true --- a/crates/jrsonnet-evaluator/tests/suite/object_assertion.jsonnet +++ /dev/null @@ -1,3 +0,0 @@ -std.assertEqual({ assert 'a' in self : 'missing a' } + { a: 2 }, { a: 2 }) && -test.assertThrow({ assert 'a' in self : 'missing a', b: 1 }.b, 'assert failed: missing a') && -true --- a/crates/jrsonnet-evaluator/tests/suite/object_comp_self.jsonnet +++ /dev/null @@ -1,8 +0,0 @@ -std.assertEqual(std.objectFields({ - a: { - [name]: name - for name in std.objectFields(self) - }, - b: 2, - c: 3, -}.a), ['a', 'b', 'c']) --- a/crates/jrsonnet-evaluator/tests/suite/object_context.jsonnet +++ /dev/null @@ -1,13 +0,0 @@ -// `self` assigned to `me` was lost when being -// referenced from field -std.assertEqual({ - local me = self, - a: 3, - b: me.a, -}.b, 3) && -std.assertEqual({ - local me = self, - a: 3, - b(): me.a, -}.b(), 3) && -true --- a/crates/jrsonnet-evaluator/tests/suite/object_fields.jsonnet +++ /dev/null @@ -1,4 +0,0 @@ -local a = 'a', b = null; -std.assertEqual({ [a]: 2 }, { a: 2 }) && -std.assertEqual({ [b]: 2 }, {}) && -true --- a/crates/jrsonnet-evaluator/tests/suite/object_inheritance.jsonnet +++ /dev/null @@ -1,17 +0,0 @@ -std.assertEqual({ a: self.b } + { b: 3 }, { a: 3, b: 3 }) && -std.assertEqual( - { - name: 'Alice', - welcome: 'Hello ' + self.name + '!', - }, - { name: 'Alice', welcome: 'Hello Alice!' }, -) && -std.assertEqual( - { - name: 'Alice', - welcome: 'Hello ' + self.name + '!', - } + { - name: 'Bob', - }, { name: 'Bob', welcome: 'Hello Bob!' } -) && -true --- a/crates/jrsonnet-evaluator/tests/suite/object_locals.jsonnet +++ /dev/null @@ -1,4 +0,0 @@ -std.assertEqual({ local a = 3, b: a }, { b: 3 }) && -std.assertEqual({ local a = 3, local c = a, b: c }, { b: 3 }) && -std.assertEqual({ local a = function(b) { [b]: 4 }, test: a('test') }, { test: { test: 4 } }) && -true --- a/crates/jrsonnet-evaluator/tests/suite/object_super_standalone.jsonnet +++ /dev/null @@ -1,11 +0,0 @@ -local obj = { - a: 1, - b: 2, - c: 3, -}; -local test = obj + { - fields: std.objectFields(super), - d: 5, -}; -std.assertEqual(test.fields, ['a', 'b', 'c']) && -true --- a/crates/jrsonnet-evaluator/tests/suite/sjsonnet_issue_127.jsonnet +++ /dev/null @@ -1,6 +0,0 @@ -local myFunc = function(a) - if (a) then "a" else "b"; - -local b = "aaa"; - -std.assertEqual(myFunc(b == [] || b == ['e']), "b") --- a/crates/jrsonnet-evaluator/tests/suite/string_concat.jsonnet +++ /dev/null @@ -1,4 +0,0 @@ -std.assertEqual('Hello' + 'World', 'HelloWorld') && -std.assertEqual('Hello' * 3, 'HelloHelloHello') && -std.assertEqual('Hello' + 'World' * 3, 'HelloWorldWorldWorld') && -true --- a/crates/jrsonnet-evaluator/tests/typed_obj.rs +++ /dev/null @@ -1,194 +0,0 @@ -mod common; - -use std::fmt::Debug; - -use jrsonnet_evaluator::{error::Result, typed::Typed, State}; - -#[derive(Clone, Typed, PartialEq, Debug)] -struct A { - a: u32, - b: u16, -} - -fn test_roundtrip(value: T, s: State) -> Result<()> { - let untyped = T::into_untyped(value.clone(), s.clone())?; - let value2 = T::from_untyped(untyped.clone(), s.clone())?; - ensure_eq!(value, value2); - let untyped2 = T::into_untyped(value2, s.clone())?; - ensure_val_eq!(s, untyped, untyped2); - - Ok(()) -} - -#[test] -fn simple_object() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - let a = A::from_untyped( - s.evaluate_snippet("snip".to_owned(), "{a: 1, b: 2}".into())?, - s.clone(), - )?; - ensure_eq!(a, A { a: 1, b: 2 }); - test_roundtrip(a, s)?; - Ok(()) -} - -#[derive(Clone, Typed, PartialEq, Debug)] -struct B { - a: u32, - #[typed(rename = "c")] - b: u16, -} - -#[test] -fn renamed_field() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - let b = B::from_untyped( - s.evaluate_snippet("snip".to_owned(), "{a: 1, c: 2}".into())?, - s.clone(), - )?; - ensure_eq!(b, B { a: 1, b: 2 }); - ensure_eq!( - &B::into_untyped(b.clone(), s.clone())?.to_string(s.clone())? as &str, - r#"{"a": 1, "c": 2}"#, - ); - test_roundtrip(b, s)?; - Ok(()) -} - -#[derive(Clone, Typed, PartialEq, Debug)] -struct ObjectKind { - #[typed(rename = "apiVersion")] - api_version: String, - #[typed(rename = "kind")] - kind: String, -} - -#[derive(Clone, Typed, PartialEq, Debug)] -struct Object { - #[typed(flatten)] - kind: ObjectKind, - b: u16, -} - -#[test] -fn flattened_object() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - let obj = Object::from_untyped( - s.evaluate_snippet( - "snip".to_owned(), - "{apiVersion: 'ver', kind: 'kind', b: 2}".into(), - )?, - s.clone(), - )?; - ensure_eq!( - obj, - Object { - kind: ObjectKind { - api_version: "ver".into(), - kind: "kind".into(), - }, - b: 2 - } - ); - ensure_eq!( - &Object::into_untyped(obj.clone(), s.clone())?.to_string(s.clone())? as &str, - r#"{"apiVersion": "ver", "b": 2, "kind": "kind"}"#, - ); - test_roundtrip(obj, s)?; - Ok(()) -} - -#[derive(Clone, Typed, PartialEq, Debug)] -struct C { - a: Option, - b: u16, -} - -#[test] -fn optional_field_some() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - let c = C::from_untyped( - s.evaluate_snippet("snip".to_owned(), "{a: 1, b: 2}".into())?, - s.clone(), - )?; - ensure_eq!(c, C { a: Some(1), b: 2 }); - ensure_eq!( - &C::into_untyped(c.clone(), s.clone())?.to_string(s.clone())? as &str, - r#"{"a": 1, "b": 2}"#, - ); - test_roundtrip(c, s)?; - Ok(()) -} - -#[test] -fn optional_field_none() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - let c = C::from_untyped( - s.evaluate_snippet("snip".to_owned(), "{b: 2}".into())?, - s.clone(), - )?; - ensure_eq!(c, C { a: None, b: 2 }); - ensure_eq!( - &C::into_untyped(c.clone(), s.clone())?.to_string(s.clone())? as &str, - r#"{"b": 2}"#, - ); - test_roundtrip(c, s)?; - Ok(()) -} - -#[derive(Clone, Typed, PartialEq, Debug)] -struct D { - #[typed(flatten(ok))] - e: Option, - b: u16, -} - -#[derive(Clone, Typed, PartialEq, Debug)] -struct E { - v: u32, -} - -#[test] -fn flatten_optional_some() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - let d = D::from_untyped( - s.evaluate_snippet("snip".to_owned(), "{b: 2, v:1}".into())?, - s.clone(), - )?; - ensure_eq!( - d, - D { - e: Some(E { v: 1 }), - b: 2 - } - ); - ensure_eq!( - &D::into_untyped(d.clone(), s.clone())?.to_string(s.clone())? as &str, - r#"{"b": 2, "v": 1}"#, - ); - test_roundtrip(d, s)?; - Ok(()) -} - -#[test] -fn flatten_optional_none() -> Result<()> { - let s = State::default(); - s.with_stdlib(); - let d = D::from_untyped( - s.evaluate_snippet("snip".to_owned(), "{b: 2, v: '1'}".into())?, - s.clone(), - )?; - ensure_eq!(d, D { e: None, b: 2 }); - ensure_eq!( - &D::into_untyped(d.clone(), s.clone())?.to_string(s.clone())? as &str, - r#"{"b": 2}"#, - ); - test_roundtrip(d, s)?; - Ok(()) -} --- a/crates/jrsonnet-interner/Cargo.toml +++ b/crates/jrsonnet-interner/Cargo.toml @@ -7,12 +7,19 @@ edition = "2021" [features] -default = ["serde"] +default = [] +# Implement value serialization using structdump +structdump = ["dep:structdump"] +# Implement value serialization using serde +# +# Warning: serialized values won't be deduplicated serde = ["dep:serde"] [dependencies] jrsonnet-gcmodule = { version = "0.3.4" } serde = { version = "1.0", optional = true } +structdump = { version = "0.2.0", optional = true } + rustc-hash = "1.1" hashbrown = { version = "0.12.1", features = ["inline-more"] } --- a/crates/jrsonnet-interner/src/inner.rs +++ b/crates/jrsonnet-interner/src/inner.rs @@ -84,6 +84,8 @@ unsafe { Self::new_raw(str.as_bytes(), true) } } + // `slice::from_raw_parts` is not yet stabilized + #[allow(clippy::missing_const_for_fn)] pub fn as_slice(&self) -> &[u8] { let header = Self::header(self); // SAFETY: data is not null, and it is correctly initialized --- a/crates/jrsonnet-interner/src/lib.rs +++ b/crates/jrsonnet-interner/src/lib.rs @@ -33,6 +33,10 @@ impl IStr { #[must_use] + pub fn empty() -> Self { + "".into() + } + #[must_use] pub fn as_str(&self) -> &str { self as &str } @@ -201,6 +205,7 @@ } } +#[cfg(feature = "serde")] impl serde::Serialize for IStr { fn serialize(&self, serializer: S) -> Result where @@ -210,6 +215,7 @@ } } +#[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for IStr { fn deserialize(deserializer: D) -> Result where @@ -220,6 +226,24 @@ } } +#[cfg(feature = "structdump")] +impl structdump::Codegen for IStr { + fn gen_code( + &self, + res: &mut structdump::CodegenResult, + _unique: bool, + ) -> structdump::TokenStream { + let s: &str = self; + res.add_code( + structdump::quote! { + structdump_import::IStr::from(#s) + }, + Some(structdump::quote![structdump_import::IStr]), + false, + ) + } +} + thread_local! { static POOL: RefCell>> = RefCell::new(HashMap::with_capacity_and_hasher(200, BuildHasherDefault::default())); } --- a/crates/jrsonnet-macros/src/lib.rs +++ b/crates/jrsonnet-macros/src/lib.rs @@ -122,13 +122,13 @@ Normal { ty: Box, is_option: bool, - name: String, + name: Option, cfg_attrs: Vec, // ident: Ident, }, Lazy { is_option: bool, - name: String, + name: Option, }, State, Location, @@ -142,8 +142,8 @@ FnArg::Typed(a) => a, }; let ident = match &arg.pat as &Pat { - Pat::Ident(i) => i.ident.clone(), - _ => return Err(Error::new(arg.pat.span(), "arg should be plain identifier")), + Pat::Ident(i) => Some(i.ident.clone()), + _ => None, }; let ty = &arg.ty; if type_is_path(ty, "State").is_some() { @@ -153,7 +153,7 @@ } else if type_is_path(ty, "Thunk").is_some() { return Ok(Self::Lazy { is_option: false, - name: ident.to_string(), + name: ident.map(|v| v.to_string()), }); } @@ -166,7 +166,7 @@ if type_is_path(ty, "Thunk").is_some() { return Ok(Self::Lazy { is_option: true, - name: ident.to_string(), + name: ident.map(|v| v.to_string()), }); } @@ -185,7 +185,7 @@ Ok(Self::Normal { ty, is_option, - name: ident.to_string(), + name: ident.map(|v| v.to_string()), cfg_attrs, }) } @@ -248,69 +248,95 @@ name, cfg_attrs, .. - } => Some(quote! { - #(#cfg_attrs)* - BuiltinParam { - name: std::borrow::Cow::Borrowed(#name), - has_default: #is_option, - }, - }), - ArgInfo::Lazy { is_option, name } => Some(quote! { - BuiltinParam { - name: std::borrow::Cow::Borrowed(#name), - has_default: #is_option, - }, - }), + } => { + let name = name + .as_ref() + .map(|n| quote! {Some(std::borrow::Cow::Borrowed(#n))}) + .unwrap_or_else(|| quote! {None}); + Some(quote! { + #(#cfg_attrs)* + BuiltinParam { + name: #name, + has_default: #is_option, + }, + }) + } + ArgInfo::Lazy { is_option, name } => { + let name = name + .as_ref() + .map(|n| quote! {Some(std::borrow::Cow::Borrowed(#n))}) + .unwrap_or_else(|| quote! {None}); + Some(quote! { + BuiltinParam { + name: #name, + has_default: #is_option, + }, + }) + } ArgInfo::State => None, ArgInfo::Location => None, ArgInfo::This => None, }); - let pass = args.iter().map(|a| match a { - ArgInfo::Normal { - ty, - is_option, - name, - cfg_attrs, - } => { - let eval = quote! {s.push_description( - || format!("argument <{}> evaluation", #name), - || <#ty>::from_untyped(value.evaluate(s.clone())?, s.clone()), - )?}; - let value = if *is_option { - quote! {if let Some(value) = parsed.get(#name) { - Some(#eval) + let mut id = 0usize; + let pass = args + .iter() + .map(|a| match a { + ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => { + let cid = id; + id += 1; + (quote! {#cid}, a) + } + ArgInfo::State | ArgInfo::Location | ArgInfo::This => { + (quote! {compile_error!("should not use id")}, a) + } + }) + .map(|(id, a)| match a { + ArgInfo::Normal { + ty, + is_option, + name, + cfg_attrs, + } => { + let name = name.as_ref().map(|v| v.as_str()).unwrap_or(""); + let eval = quote! {s.push_description( + || format!("argument <{}> evaluation", #name), + || <#ty>::from_untyped(value.evaluate(s.clone())?, s.clone()), + )?}; + let value = if *is_option { + quote! {if let Some(value) = &parsed[#id] { + Some(#eval) + } else { + None + },} } else { - None - },} - } else { - quote! {{ - let value = parsed.get(#name).expect("args shape is checked"); - #eval - },} - }; - quote! { - #(#cfg_attrs)* - #value + quote! {{ + let value = parsed[#id].as_ref().expect("args shape is checked"); + #eval + },} + }; + quote! { + #(#cfg_attrs)* + #value + } } - } - ArgInfo::Lazy { is_option, name } => { - if *is_option { - quote! {if let Some(value) = parsed.get(#name) { - Some(value.clone()) + ArgInfo::Lazy { is_option, .. } => { + if *is_option { + quote! {if let Some(value) = &parsed[#id] { + Some(value.clone()) + } else { + None + }} } else { - None - }} - } else { - quote! { - parsed.get(#name).expect("args shape is correct").clone(), + quote! { + parsed[#id].as_ref().expect("args shape is correct").clone(), + } } } - } - ArgInfo::State => quote! {s.clone(),}, - ArgInfo::Location => quote! {location,}, - ArgInfo::This => quote! {self,}, - }); + ArgInfo::State => quote! {s.clone(),}, + ArgInfo::Location => quote! {location,}, + ArgInfo::This => quote! {self,}, + }); let fields = attr.fields.iter().map(|field| { let name = &field.name; --- a/crates/jrsonnet-parser/Cargo.toml +++ b/crates/jrsonnet-parser/Cargo.toml @@ -7,7 +7,23 @@ edition = "2021" [features] +default = [] exp-destruct = [] +# Implement serialization of AST using structdump +# +# Structdump generates code, which exactly replicated passed AST +# Contrary to serde, has no code bloat problem, and is recommended +# +# The only limitation is serialized form is only useable if built from build script +structdump = ["dep:structdump", "jrsonnet-interner/structdump"] +# Implement serialization of AST using serde +# +# Warning: as serde doesn't deduplicate strings, `Source` struct will bloat +# output binary with repeating source code. To resolve this issue, you should either +# override serialization of this struct using custom `Serializer`/`Deserializer`, +# not rely on Source, and fill its `source_code` with empty value, or use `structdump` +# instead +serde = ["dep:serde"] [dependencies] jrsonnet-interner = { path = "../jrsonnet-interner", version = "0.4.2" } @@ -18,6 +34,4 @@ peg = "0.8.0" serde = { version = "1.0", features = ["derive", "rc"], optional = true } - -[dev-dependencies] -jrsonnet-stdlib = { path = "../jrsonnet-stdlib", version = "0.4.2" } +structdump = { version = "0.2.0", features = ["derive"], optional = true } --- a/crates/jrsonnet-parser/src/expr.rs +++ b/crates/jrsonnet-parser/src/expr.rs @@ -8,10 +8,13 @@ use jrsonnet_interner::IStr; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "structdump")] +use structdump::Codegen; use crate::source::Source; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "structdump", derive(Codegen))] #[derive(Debug, PartialEq, Trace)] pub enum FieldName { /// {fixed: 2} @@ -20,6 +23,7 @@ Dyn(LocExpr), } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Trace)] pub enum Visibility { @@ -37,10 +41,12 @@ } } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, Debug, PartialEq, Trace)] pub struct AssertStmt(pub LocExpr, pub Option); +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub struct FieldMember { @@ -51,6 +57,7 @@ pub value: LocExpr, } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub enum Member { @@ -59,6 +66,7 @@ AssertStmt(AssertStmt), } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Trace)] pub enum UnaryOpType { @@ -84,6 +92,7 @@ } } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Trace)] pub enum BinaryOpType { @@ -150,11 +159,13 @@ } /// name, default value +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub struct Param(pub Destruct, pub Option); /// Defined function parameters +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Trace)] pub struct ParamsDesc(pub Rc>); @@ -166,6 +177,7 @@ } } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub struct ArgsDesc { @@ -187,6 +199,7 @@ Drop, } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Trace)] pub enum Destruct { @@ -216,6 +229,7 @@ } } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Trace)] pub enum BindSpec { @@ -230,14 +244,17 @@ }, } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub struct IfSpecData(pub LocExpr); +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub struct ForSpecData(pub IStr, pub LocExpr); +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub enum CompSpec { @@ -245,6 +262,7 @@ ForSpec(ForSpecData), } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub struct ObjComp { @@ -256,6 +274,7 @@ pub compspecs: Vec, } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub enum ObjBody { @@ -263,6 +282,7 @@ ObjComp(ObjComp), } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Eq, Clone, Copy, Trace)] pub enum LiteralType { @@ -274,6 +294,7 @@ False, } +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub struct SliceDesc { @@ -283,6 +304,7 @@ } /// Syntax base +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq, Trace)] pub enum Expr { @@ -341,12 +363,6 @@ Index(LocExpr, LocExpr), /// function(x) x Function(ParamsDesc, LocExpr), - /// std.thisFile - IntrinsicThisFile, - /// std.id, - IntrinsicId, - /// std.primitiveEquals - Intrinsic(IStr), /// if true == false then 1 else 2 IfElse { cond: IfSpecData, @@ -357,6 +373,7 @@ } /// file, begin offset, end offset +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Trace)] #[trace(skip)] @@ -379,6 +396,7 @@ /// Holds AST expression and its location in source file #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "structdump", derive(Codegen))] #[derive(Clone, PartialEq, Trace)] pub struct LocExpr(pub Rc, pub ExprLocation); --- a/crates/jrsonnet-parser/src/lib.rs +++ b/crates/jrsonnet-parser/src/lib.rs @@ -7,9 +7,11 @@ pub use expr::*; pub use jrsonnet_interner::IStr; pub use peg; +mod location; mod source; mod unescape; -pub use source::Source; +pub use location::CodeLocation; +pub use source::{Source, SourceDirectory, SourceFile, SourcePath, SourcePathT, SourceVirtual}; pub struct ParserSettings { pub file_name: Source, @@ -193,7 +195,7 @@ / assertion:assertion(s) {expr::Member::AssertStmt(assertion)} / field:field(s) {expr::Member::Field(field)} pub rule objinside(s: &ParserSettings) -> expr::ObjBody - = pre_locals:(b: obj_local(s) comma() {b})* "[" _ key:expr(s) _ "]" _ plus:"+"? _ ":" _ value:expr(s) post_locals:(comma() b:obj_local(s) {b})* _ forspec:forspec(s) others:(_ rest:compspec(s) {rest})? { + = pre_locals:(b: obj_local(s) comma() {b})* "[" _ key:expr(s) _ "]" _ plus:"+"? _ ":" _ value:expr(s) post_locals:(comma() b:obj_local(s) {b})* _ ("," _)? forspec:forspec(s) others:(_ rest:compspec(s) {rest})? { let mut compspecs = vec![CompSpec::ForSpec(forspec)]; compspecs.extend(others.unwrap_or_default()); expr::ObjBody::ObjComp(expr::ObjComp{ @@ -251,10 +253,6 @@ pub rule expr_basic(s: &ParserSettings) -> Expr = literal(s) - - / quiet!{"$intrinsicThisFile" {Expr::IntrinsicThisFile}} - / quiet!{"$intrinsicId" {Expr::IntrinsicId}} - / quiet!{"$intrinsic(" name:id() ")" {Expr::Intrinsic(name)}} / string_expr(s) / number_expr(s) / array_expr(s) @@ -362,8 +360,7 @@ #[cfg(test)] pub mod tests { - use std::borrow::Cow; - + use jrsonnet_interner::IStr; use BinaryOpType::*; use super::{expr::*, parse}; @@ -374,7 +371,7 @@ parse( $s, &ParserSettings { - file_name: Source::new_virtual(Cow::Borrowed("")), + file_name: Source::new_virtual("".into(), IStr::empty()), }, ) .unwrap() @@ -385,7 +382,11 @@ ($expr:expr, $from:expr, $to:expr$(,)?) => { LocExpr( std::rc::Rc::new($expr), - ExprLocation(Source::new_virtual(Cow::Borrowed("")), $from, $to), + ExprLocation( + Source::new_virtual("".into(), IStr::empty()), + $from, + $to, + ), ) }; } @@ -715,15 +716,10 @@ } #[test] - fn can_parse_stdlib() { - parse!(jrsonnet_stdlib::STDLIB_STR); - } - - #[test] fn add_location_info_to_all_sub_expressions() { use Expr::*; - let file_name = Source::new_virtual(Cow::Borrowed("")); + let file_name = Source::new_virtual("".into(), IStr::empty()); let expr = parse( "{} { local x = 1, x: x } + {}", &ParserSettings { file_name }, --- /dev/null +++ b/crates/jrsonnet-parser/src/location.rs @@ -0,0 +1,124 @@ +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CodeLocation { + pub offset: usize, + + pub line: usize, + pub column: usize, + + pub line_start_offset: usize, + pub line_end_offset: usize, +} + +#[allow(clippy::module_name_repetitions)] +pub fn location_to_offset(mut file: &str, mut line: usize, column: usize) -> Option { + let mut offset = 0; + while line > 1 { + let pos = file.find('\n')?; + offset += pos + 1; + file = &file[pos + 1..]; + line -= 1; + } + offset += column - 1; + Some(offset) +} + +#[allow(clippy::module_name_repetitions)] +pub fn offset_to_location(file: &str, offsets: &[u32]) -> Vec { + if offsets.is_empty() { + return vec![]; + } + let mut line = 1; + let mut column = 1; + let max_offset = *offsets.iter().max().expect("offsets is not empty"); + + let mut offset_map = offsets + .iter() + .enumerate() + .map(|(pos, offset)| (*offset, pos)) + .collect::>(); + offset_map.sort_by_key(|v| v.0); + offset_map.reverse(); + + let mut out = vec![ + CodeLocation { + offset: 0, + column: 0, + line: 0, + line_start_offset: 0, + line_end_offset: 0 + }; + offsets.len() + ]; + let mut with_no_known_line_ending = vec![]; + let mut this_line_offset = 0; + for (pos, ch) in file + .chars() + .enumerate() + .chain(std::iter::once((file.len(), ' '))) + { + column += 1; + match offset_map.last() { + Some(x) if x.0 == pos as u32 => { + let out_idx = x.1; + with_no_known_line_ending.push(out_idx); + out[out_idx].offset = pos; + out[out_idx].line = line; + out[out_idx].column = column; + out[out_idx].line_start_offset = this_line_offset; + offset_map.pop(); + } + _ => {} + } + if ch == '\n' { + line += 1; + column = 1; + + for idx in with_no_known_line_ending.drain(..) { + out[idx].line_end_offset = pos; + } + this_line_offset = pos + 1; + + if pos == max_offset as usize + 1 { + break; + } + } + } + let file_end = file.chars().count(); + for idx in with_no_known_line_ending { + out[idx].line_end_offset = file_end; + } + + out +} + +#[cfg(test)] +pub mod tests { + use super::{offset_to_location, CodeLocation}; + + #[test] + fn test() { + assert_eq!( + offset_to_location( + "hello world\n_______________________________________________________", + &[0, 14] + ), + vec![ + CodeLocation { + offset: 0, + line: 1, + column: 2, + line_start_offset: 0, + line_end_offset: 11, + }, + CodeLocation { + offset: 14, + line: 2, + column: 4, + line_start_offset: 12, + line_end_offset: 67 + } + ] + ) + } +} --- a/crates/jrsonnet-parser/src/source.rs +++ b/crates/jrsonnet-parser/src/source.rs @@ -1,26 +1,267 @@ use std::{ - borrow::Cow, - fmt, - path::{Component, Path, PathBuf}, + any::Any, + fmt::{self, Debug, Display}, + hash::{Hash, Hasher}, + path::{Path, PathBuf}, rc::Rc, }; use jrsonnet_gcmodule::{Trace, Tracer}; +use jrsonnet_interner::IStr; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "structdump")] +use structdump::Codegen; + +use crate::location::{location_to_offset, offset_to_location, CodeLocation}; + +macro_rules! any_ext_methods { + ($T:ident) => { + fn as_any(&self) -> &dyn Any; + fn dyn_hash(&self, hasher: &mut dyn Hasher); + fn dyn_eq(&self, other: &dyn $T) -> bool; + fn dyn_debug(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; + }; +} +macro_rules! any_ext_impl { + ($T:ident) => { + fn as_any(&self) -> &dyn Any { + self + } + fn dyn_hash(&self, mut hasher: &mut dyn Hasher) { + self.hash(&mut hasher) + } + fn dyn_eq(&self, other: &dyn $T) -> bool { + let other = if let Some(v) = other.as_any().downcast_ref::() { + v + } else { + return false; + }; + let this = ::as_any(self) + .downcast_ref::() + .expect("restricted by impl"); + this == other + } + fn dyn_debug(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ::fmt(self, fmt) + } + }; +} +macro_rules! any_ext { + ($T:ident) => { + impl Hash for dyn $T { + fn hash(&self, state: &mut H) { + self.dyn_hash(state) + } + } + impl PartialEq for dyn $T { + fn eq(&self, other: &Self) -> bool { + self.dyn_eq(other) + } + } + impl Eq for dyn $T {} + }; +} +pub trait SourcePathT: Trace + Debug + Display { + /// This method should be checked by resolver before panicking with bad SourcePath input + /// if `true` - then resolver may threat this path as default, and default is usally a CWD + fn is_default(&self) -> bool; + fn path(&self) -> Option<&Path>; + any_ext_methods!(SourcePathT); +} +any_ext!(SourcePathT); + +/// Represents location of a file +/// +/// Standard CLI only operates using +/// - [`SourceFile`] - for any file +/// - [`SourceDirectory`] - for resolution from CWD +/// - [`SourceVirtual`] - for stdlib/ext-str +/// +/// From all of those, only [`SourceVirtual`] may be constructed manually, any other path kind should be only obtained +/// from assigned `ImportResolver` +/// However, you should always check `is_default` method return, as it will return true for any paths, where default +/// search location is applicable +/// +/// Resolver may also return custom implementations of this trait, for example it may return http url in case of remotely loaded files +#[derive(Eq, Debug, Clone)] +pub struct SourcePath(Rc); +impl SourcePath { + pub fn new(inner: impl SourcePathT) -> Self { + Self(Rc::new(inner)) + } + pub fn downcast_ref(&self) -> Option<&T> { + self.0.as_any().downcast_ref() + } + pub fn is_default(&self) -> bool { + self.0.is_default() + } + pub fn path(&self) -> Option<&Path> { + self.0.path() + } +} +impl Hash for SourcePath { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} +impl PartialEq for SourcePath { + #[allow(clippy::op_ref)] + fn eq(&self, other: &Self) -> bool { + &*self.0 == &*other.0 + } +} +impl Trace for SourcePath { + fn trace(&self, tracer: &mut Tracer) { + (*self.0).trace(tracer) + } + + fn is_type_tracked() -> bool + where + Self: Sized, + { + true + } +} +impl Display for SourcePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} +impl Default for SourcePath { + fn default() -> Self { + Self(Rc::new(SourceDefault)) + } +} -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(PartialEq, Eq, Debug, Hash)] -enum Inner { - Real(PathBuf), - Virtual(Cow<'static, str>), +#[cfg(feature = "structdump")] +impl Codegen for SourcePath { + fn gen_code( + &self, + res: &mut structdump::CodegenResult, + unique: bool, + ) -> structdump::TokenStream { + let source_virtual = self + .0 + .as_any() + .downcast_ref::() + .expect("can only codegen for virtual source paths!") + .0 + .clone(); + let val = res.add_value(source_virtual, false); + res.add_code( + structdump::quote! { + structdump_import::SourcePath::new(structdump_import::SourceVirtual(#val)) + }, + Some(structdump::quote!(SourcePath)), + unique, + ) + } } +#[derive(Trace, Hash, PartialEq, Eq, Debug)] +struct SourceDefault; +impl Display for SourceDefault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} +impl SourcePathT for SourceDefault { + fn is_default(&self) -> bool { + true + } + fn path(&self) -> Option<&Path> { + None + } + any_ext_impl!(SourcePathT); +} + +/// Represents path to the file on the disk +/// Directories shouldn't be put here, as resolution for files differs from resolution for directories: +/// +/// When `file` is being resolved from `SourceFile(a/b/c)`, it should be resolved to `SourceFile(a/b/file)`, +/// however if it is being resolved from `SourceDirectory(a/b/c)`, then it should be resolved to `SourceDirectory(a/b/c/file)` +#[derive(Trace, Hash, PartialEq, Eq, Debug)] +pub struct SourceFile(PathBuf); +impl SourceFile { + pub fn new(path: PathBuf) -> Self { + Self(path) + } + pub fn path(&self) -> &Path { + &self.0 + } +} +impl Display for SourceFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.display()) + } +} +impl SourcePathT for SourceFile { + fn is_default(&self) -> bool { + false + } + fn path(&self) -> Option<&Path> { + Some(&self.0) + } + any_ext_impl!(SourcePathT); +} + +/// Represents path to the directory on the disk +/// +/// See also [`SourceFile`] +#[derive(Trace, Hash, PartialEq, Eq, Debug)] +pub struct SourceDirectory(PathBuf); +impl SourceDirectory { + pub fn new(path: PathBuf) -> Self { + Self(path) + } + pub fn path(&self) -> &Path { + &self.0 + } +} +impl Display for SourceDirectory { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.display()) + } +} +impl SourcePathT for SourceDirectory { + fn is_default(&self) -> bool { + false + } + fn path(&self) -> Option<&Path> { + Some(&self.0) + } + any_ext_impl!(SourcePathT); +} + +/// Represents virtual file, whose are located in memory, and shouldn't be cached +/// +/// It is used for --ext-code=.../--tla-code=.../standard library source code by default, +/// and user can construct arbitrary values by hand, without asking import resolver +#[cfg_attr(feature = "structdump", derive(Codegen))] +#[derive(Trace, Hash, PartialEq, Eq, Debug, Clone)] +pub struct SourceVirtual(pub IStr); +impl Display for SourceVirtual { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} +impl SourcePathT for SourceVirtual { + fn is_default(&self) -> bool { + true + } + fn path(&self) -> Option<&Path> { + None + } + any_ext_impl!(SourcePathT); +} + /// Either real file, or virtual /// Hash of FileName always have same value as raw Path, to make it possible to use with raw_entry_mut +#[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Debug)] -pub struct Source(Rc); +pub struct Source(pub Rc<(SourcePath, IStr)>); static_assertions::assert_eq_size!(Source, *const ()); impl Trace for Source { @@ -32,62 +273,26 @@ } impl Source { - /// Fails when path contains inner /../ or /./ references, or not absolute - pub fn new(path: PathBuf) -> Option { - if !path.is_absolute() - || path - .components() - .any(|c| matches!(c, Component::CurDir | Component::ParentDir)) - { - return None; - } - Some(Self(Rc::new(Inner::Real(path)))) + pub fn new(path: SourcePath, code: IStr) -> Self { + Self(Rc::new((path, code))) } - pub fn new_virtual(n: Cow<'static, str>) -> Self { - Self(Rc::new(Inner::Virtual(n))) + pub fn new_virtual(name: IStr, code: IStr) -> Self { + Self::new(SourcePath::new(SourceVirtual(name)), code) } - pub fn short_display(&self) -> ShortDisplay { - ShortDisplay(self.clone()) - } - pub fn full_path(&self) -> String { - match self.inner() { - Inner::Real(r) => r.display().to_string(), - Inner::Virtual(v) => v.to_string(), - } + pub fn code(&self) -> &str { + &self.0 .1 } - /// Returns None if file is virtual - pub fn path(&self) -> Option<&Path> { - match self.inner() { - Inner::Real(r) => Some(r), - Inner::Virtual(_) => None, - } - } - pub fn repr(&self) -> Result<&Path, &str> { - match self.inner() { - Inner::Real(r) => Ok(r), - Inner::Virtual(v) => Err(v.as_ref()), - } + pub fn source_path(&self) -> &SourcePath { + &self.0 .0 } - fn inner(&self) -> &Inner { - &self.0 as &Inner + pub fn map_source_locations(&self, locs: &[u32]) -> Vec { + offset_to_location(&self.0 .1, locs) } -} -pub struct ShortDisplay(Source); -impl fmt::Display for ShortDisplay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.0 .0 as &Inner { - Inner::Real(r) => { - write!( - f, - "{}", - r.file_name().expect("path is valid").to_string_lossy() - ) - } - Inner::Virtual(n) => write!(f, "{}", n), - } + pub fn map_from_source_location(&self, line: usize, column: usize) -> Option { + location_to_offset(&self.0 .1, line, column) } } --- a/crates/jrsonnet-stdlib/Cargo.toml +++ b/crates/jrsonnet-stdlib/Cargo.toml @@ -7,5 +7,44 @@ edition = "2021" [features] +default = ["codegenerated-stdlib"] +# Speed-up initialization by generating code for parsed stdlib, instead +# of invoking parser for it +codegenerated-stdlib = ["jrsonnet-parser/structdump"] +# Enables legacy `std.thisFile` support, at the cost of worse caching +legacy-this-file = [] +# Add order preservation flag to some functions +exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"] +# Preserve order for files parsed via `std.parseJson` +# Shame it isn't possible to enable per parse call, instead of globally +exp-serde-preserve-order = [ + "serde_json/preserve_order", + "jrsonnet-evaluator/exp-serde-preserve-order", +] [dependencies] +jrsonnet-evaluator = { path = "../jrsonnet-evaluator", features = [ + # std.parseJson parses file via serde, then converts Value to evaluator Val + "serde_json", +], version = "0.4.2" } +jrsonnet-macros = { path = "../jrsonnet-macros", version = "0.4.2" } +jrsonnet-parser = { path = "../jrsonnet-parser", version = "0.4.2" } +jrsonnet-gcmodule = "0.3.4" + +# Used for stdlib AST serialization +bincode = { version = "1.3", optional = true } +# Used both for stdlib AST serialization and std.parseJson/std.parseYaml +serde = "1.0" + +# std.md5 +md5 = "0.7.0" +# std.base64 +base64 = "0.13.0" +# std.parseJson +serde_json = "1.0" +# std.parseYaml, custom library fork is used for C++/golang compatibility +serde_yaml_with_quirks = "0.8.24" + +[build-dependencies] +jrsonnet-parser = { path = "../jrsonnet-parser", version = "0.4.2" } +structdump = { version = "0.2.0", features = ["derive"] } --- a/crates/jrsonnet-stdlib/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# jrsonnet-stdlib - -Jsonnet standard library packaged as crate --- /dev/null +++ b/crates/jrsonnet-stdlib/build.rs @@ -0,0 +1,33 @@ +use std::{env, fs::File, io::Write, path::Path}; + +use jrsonnet_parser::{parse, ParserSettings, Source}; +use structdump::CodegenResult; + +fn main() { + let parsed = parse( + include_str!("./src/std.jsonnet"), + &ParserSettings { + file_name: Source::new_virtual( + "".into(), + include_str!("./src/std.jsonnet").into(), + ), + }, + ) + .expect("parse"); + + let mut out = CodegenResult::default(); + + let v = out.codegen(&parsed, true); + + { + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("stdlib.rs"); + let mut f = File::create(&dest_path).unwrap(); + f.write_all( + ("#[allow(clippy::redundant_clone)]".to_owned() + &v.to_string()) + .replace(';', ";\n") + .as_bytes(), + ) + .unwrap(); + } +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/arrays.rs @@ -0,0 +1,216 @@ +use jrsonnet_evaluator::{ + error::Result, + function::{builtin, FuncVal}, + throw_runtime, + typed::{Any, BoundedUsize, Typed, VecVal}, + val::{equals, ArrValue, IndexableVal}, + IStr, State, Val, +}; +use jrsonnet_gcmodule::Cc; + +#[builtin] +pub fn builtin_make_array(s: State, sz: usize, func: FuncVal) -> Result { + let mut out = Vec::with_capacity(sz); + for i in 0..sz { + out.push(func.evaluate_simple(s.clone(), &(i as f64,))?); + } + Ok(VecVal(Cc::new(out))) +} + +#[builtin] +pub fn builtin_slice( + indexable: IndexableVal, + index: Option>, + end: Option>, + step: Option>, +) -> Result { + indexable.slice(index, end, step).map(Val::from).map(Any) +} + +#[builtin] +pub fn builtin_map(s: State, func: FuncVal, arr: ArrValue) -> Result { + arr.map(s.clone(), |val| { + func.evaluate_simple(s.clone(), &(Any(val),)) + }) +} + +#[builtin] +pub fn builtin_flatmap(s: State, func: FuncVal, arr: IndexableVal) -> Result { + match arr { + IndexableVal::Str(str) => { + let mut out = String::new(); + for c in str.chars() { + match func.evaluate_simple(s.clone(), &(c.to_string(),))? { + Val::Str(o) => out.push_str(&o), + Val::Null => continue, + _ => throw_runtime!("in std.join all items should be strings"), + }; + } + Ok(IndexableVal::Str(out.into())) + } + IndexableVal::Arr(a) => { + let mut out = Vec::new(); + for el in a.iter(s.clone()) { + let el = el?; + match func.evaluate_simple(s.clone(), &(Any(el),))? { + Val::Arr(o) => { + for oe in o.iter(s.clone()) { + out.push(oe?); + } + } + Val::Null => continue, + _ => throw_runtime!("in std.join all items should be arrays"), + }; + } + Ok(IndexableVal::Arr(out.into())) + } + } +} + +#[builtin] +pub fn builtin_filter(s: State, func: FuncVal, arr: ArrValue) -> Result { + arr.filter(s.clone(), |val| { + bool::from_untyped( + func.evaluate_simple(s.clone(), &(Any(val.clone()),))?, + s.clone(), + ) + }) +} + +#[builtin] +pub fn builtin_foldl(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { + let mut acc = init.0; + for i in arr.iter(s.clone()) { + acc = func.evaluate_simple(s.clone(), &(Any(acc), Any(i?)))?; + } + Ok(Any(acc)) +} + +#[builtin] +pub fn builtin_foldr(s: State, func: FuncVal, arr: ArrValue, init: Any) -> Result { + let mut acc = init.0; + for i in arr.iter(s.clone()).rev() { + acc = func.evaluate_simple(s.clone(), &(Any(i?), Any(acc)))?; + } + Ok(Any(acc)) +} + +#[builtin] +pub fn builtin_range(from: i32, to: i32) -> Result { + if to < from { + return Ok(ArrValue::new_eager()); + } + Ok(ArrValue::new_range(from, to)) +} + +#[builtin] +pub fn builtin_join(s: State, sep: IndexableVal, arr: ArrValue) -> Result { + Ok(match sep { + IndexableVal::Arr(joiner_items) => { + let mut out = Vec::new(); + + let mut first = true; + for item in arr.iter(s.clone()) { + let item = item?.clone(); + if let Val::Arr(items) = item { + if !first { + out.reserve(joiner_items.len()); + // TODO: extend + for item in joiner_items.iter(s.clone()) { + out.push(item?); + } + } + first = false; + out.reserve(items.len()); + for item in items.iter(s.clone()) { + out.push(item?); + } + } else if matches!(item, Val::Null) { + continue; + } else { + throw_runtime!("in std.join all items should be arrays"); + } + } + + IndexableVal::Arr(out.into()) + } + IndexableVal::Str(sep) => { + let mut out = String::new(); + + let mut first = true; + for item in arr.iter(s) { + let item = item?.clone(); + if let Val::Str(item) = item { + if !first { + out += &sep; + } + first = false; + out += &item; + } else if matches!(item, Val::Null) { + continue; + } else { + throw_runtime!("in std.join all items should be strings"); + } + } + + IndexableVal::Str(out.into()) + } + }) +} + +#[builtin] +pub fn builtin_reverse(value: ArrValue) -> Result { + Ok(value.reversed()) +} + +#[builtin] +pub fn builtin_any(s: State, arr: ArrValue) -> Result { + for v in arr.iter(s.clone()) { + let v = bool::from_untyped(v?, s.clone())?; + if v { + return Ok(true); + } + } + Ok(false) +} + +#[builtin] +pub fn builtin_all(s: State, arr: ArrValue) -> Result { + for v in arr.iter(s.clone()) { + let v = bool::from_untyped(v?, s.clone())?; + if !v { + return Ok(false); + } + } + Ok(true) +} + +#[builtin] +pub fn builtin_member(s: State, arr: IndexableVal, x: Any) -> Result { + match arr { + IndexableVal::Str(str) => { + let x: IStr = IStr::from_untyped(x.0, s)?; + Ok(!x.is_empty() && str.contains(&*x)) + } + IndexableVal::Arr(a) => { + for item in a.iter(s.clone()) { + let item = item?; + if equals(s.clone(), &item, &x.0)? { + return Ok(true); + } + } + Ok(false) + } + } +} + +#[builtin] +pub fn builtin_count(s: State, arr: Vec, v: Any) -> Result { + let mut count = 0; + for item in &arr { + if equals(s.clone(), &item.0, &v.0)? { + count += 1; + } + } + Ok(count) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/encoding.rs @@ -0,0 +1,41 @@ +use jrsonnet_evaluator::{ + error::{Error::RuntimeError, Result}, + function::builtin, + typed::{Either, Either2}, + IBytes, IStr, +}; + +#[builtin] +pub fn builtin_encode_utf8(str: IStr) -> Result { + Ok(str.cast_bytes()) +} + +#[builtin] +pub fn builtin_decode_utf8(arr: IBytes) -> Result { + Ok(arr + .cast_str() + .ok_or_else(|| RuntimeError("bad utf8".into()))?) +} + +#[builtin] +pub fn builtin_base64(input: Either![IBytes, IStr]) -> Result { + use Either2::*; + Ok(match input { + A(a) => base64::encode(a.as_slice()), + B(l) => base64::encode(l.bytes().collect::>()), + }) +} + +#[builtin] +pub fn builtin_base64_decode_bytes(input: IStr) -> Result { + Ok(base64::decode(input.as_bytes()) + .map_err(|_| RuntimeError("bad base64".into()))? + .as_slice() + .into()) +} + +#[builtin] +pub fn builtin_base64_decode(input: IStr) -> Result { + let bytes = base64::decode(input.as_bytes()).map_err(|_| RuntimeError("bad base64".into()))?; + Ok(String::from_utf8(bytes).map_err(|_| RuntimeError("bad utf8".into()))?) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/expr.rs @@ -0,0 +1,101 @@ +use jrsonnet_parser::LocExpr; + +mod structdump_import { + pub(super) use std::{option::Option, rc::Rc, vec}; + + pub(super) use jrsonnet_parser::*; +} + +pub fn stdlib_expr() -> LocExpr { + #[cfg(feature = "serialized-stdlib")] + { + use bincode::{BincodeRead, DefaultOptions, Options}; + use serde::{Deserialize, Deserializer}; + + struct LocDeserializer { + source: Source, + wrapped: bincode::Deserializer, + } + macro_rules! delegate { + ($(fn $name:ident($($arg:ident: $ty:ty),*))+) => {$( + fn $name(mut self $(, $arg: $ty)*, visitor: V) -> Result + where V: serde::de::Visitor<'de>, + { + self.wrapped.$name($($arg,)* visitor) + } + )+}; + } + impl<'de, R, O> Deserializer<'de> for LocDeserializer + where + R: BincodeRead<'de>, + O: Options, + { + type Error = <&'de mut bincode::Deserializer as Deserializer<'de>>::Error; + + delegate! { + fn deserialize_any() + fn deserialize_bool() + fn deserialize_u16() + fn deserialize_u32() + fn deserialize_u64() + fn deserialize_i16() + fn deserialize_i32() + fn deserialize_i64() + fn deserialize_f32() + fn deserialize_f64() + fn deserialize_u128() + fn deserialize_i128() + fn deserialize_u8() + fn deserialize_i8() + fn deserialize_unit() + fn deserialize_char() + fn deserialize_str() + fn deserialize_string() + fn deserialize_bytes() + fn deserialize_byte_buf() + fn deserialize_enum(name: &'static str, variants: &'static [&'static str]) + fn deserialize_tuple(len: usize) + fn deserialize_option() + fn deserialize_seq() + fn deserialize_map() + fn deserialize_struct(name: &'static str, fields: &'static [&'static str]) + fn deserialize_identifier() + fn deserialize_newtype_struct(name: &'static str) + fn deserialize_unit_struct(name: &'static str) + fn deserialize_tuple_struct(name: &'static str, len: usize) + fn deserialize_ignored_any() + } + + fn is_human_readable(&self) -> bool { + false + } + } + + // In build.rs, Source object is populated with empty values, deserializer wrapper loads correct values on deserialize + let mut deserializer = bincode::Deserializer::from_slice( + include_bytes!(concat!(env!("OUT_DIR"), "/stdlib.bincode")), + DefaultOptions::new() + .with_fixint_encoding() + .allow_trailing_bytes(), + ); + + // Should not panic, stdlib.bincode is generated in build.rs + LocExpr::deserialize(&mut deserializer).unwrap() + } + + #[cfg(feature = "codegenerated-stdlib")] + { + include!(concat!(env!("OUT_DIR"), "/stdlib.rs")) + } + + #[cfg(not(feature = "codegenerated-stdlib"))] + { + jrsonnet_parser::parse( + STDLIB_STR, + &ParserSettings { + file_name: Source::new_virtual(Cow::Borrowed(""), STDLIB_STR.into()), + }, + ) + .unwrap() + } +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/hash.rs @@ -0,0 +1,6 @@ +use jrsonnet_evaluator::{error::Result, function::builtin, IStr}; + +#[builtin] +pub fn builtin_md5(str: IStr) -> Result { + Ok(format!("{:x}", md5::compute(str.as_bytes()))) +} --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -1 +1,562 @@ -pub const STDLIB_STR: &str = include_str!("./std.jsonnet"); +use std::{ + cell::{Ref, RefCell, RefMut}, + collections::HashMap, + rc::Rc, +}; + +use jrsonnet_evaluator::{ + error::{Error::*, Result}, + function::{builtin::Builtin, ArgLike, CallLocation, FuncVal, TlaArg}, + gc::{GcHashMap, TraceBox}, + tb, throw_runtime, + trace::PathResolver, + typed::{Any, Either, Either2, Either4, VecVal, M1}, + val::{equals, ArrValue}, + Context, ContextBuilder, IStr, ObjValue, ObjValueBuilder, State, Thunk, Val, +}; +use jrsonnet_gcmodule::Cc; +use jrsonnet_macros::builtin; +use jrsonnet_parser::Source; + +mod expr; +mod types; +pub use types::*; +mod arrays; +pub use arrays::*; +mod math; +pub use math::*; +mod operator; +pub use operator::*; +mod sort; +pub use sort::*; +mod hash; +pub use hash::*; +mod encoding; +pub use encoding::*; +mod objects; +pub use objects::*; +mod manifest; +pub use manifest::*; +mod parse; +pub use parse::*; + +pub fn stdlib_uncached(s: State, settings: Rc>) -> ObjValue { + let mut builder = ObjValueBuilder::new(); + + let expr = expr::stdlib_expr(); + let eval = jrsonnet_evaluator::evaluate(s.clone(), Context::default(), &expr) + .expect("stdlib.jsonnet should have no errors") + .as_obj() + .expect("stdlib.jsonnet should evaluate to object"); + + builder.with_super(eval); + + for (name, builtin) in [ + ("length", builtin_length::INST), + // Types + ("type", builtin_type::INST), + ("isString", builtin_is_string::INST), + ("isNumber", builtin_is_number::INST), + ("isBoolean", builtin_is_boolean::INST), + ("isObject", builtin_is_object::INST), + ("isArray", builtin_is_array::INST), + ("isFunction", builtin_is_function::INST), + // Arrays + ("makeArray", builtin_make_array::INST), + ("slice", builtin_slice::INST), + ("map", builtin_map::INST), + ("flatMap", builtin_flatmap::INST), + ("filter", builtin_filter::INST), + ("foldl", builtin_foldl::INST), + ("foldr", builtin_foldr::INST), + ("range", builtin_range::INST), + ("join", builtin_join::INST), + ("reverse", builtin_reverse::INST), + ("any", builtin_any::INST), + ("all", builtin_all::INST), + ("member", builtin_member::INST), + ("count", builtin_count::INST), + // Math + ("modulo", builtin_modulo::INST), + ("floor", builtin_floor::INST), + ("ceil", builtin_ceil::INST), + ("log", builtin_log::INST), + ("pow", builtin_pow::INST), + ("sqrt", builtin_sqrt::INST), + ("sin", builtin_sin::INST), + ("cos", builtin_cos::INST), + ("tan", builtin_tan::INST), + ("asin", builtin_asin::INST), + ("acos", builtin_acos::INST), + ("atan", builtin_atan::INST), + ("exp", builtin_exp::INST), + ("mantissa", builtin_mantissa::INST), + ("exponent", builtin_exponent::INST), + // Operator + ("mod", builtin_mod::INST), + ("primitiveEquals", builtin_primitive_equals::INST), + ("equals", builtin_equals::INST), + ("format", builtin_format::INST), + // Sort + ("sort", builtin_sort::INST), + // Hash + ("md5", builtin_md5::INST), + // Encoding + ("encodeUTF8", builtin_encode_utf8::INST), + ("decodeUTF8", builtin_decode_utf8::INST), + ("base64", builtin_base64::INST), + ("base64Decode", builtin_base64_decode::INST), + ("base64DecodeBytes", builtin_base64_decode_bytes::INST), + // Objects + ("objectFieldsEx", builtin_object_fields_ex::INST), + ("objectHasEx", builtin_object_has_ex::INST), + // Manifest + ("escapeStringJson", builtin_escape_string_json::INST), + ("manifestJsonEx", builtin_manifest_json_ex::INST), + ("manifestYamlDoc", builtin_manifest_yaml_doc::INST), + // Parsing + ("parseJson", builtin_parse_json::INST), + ("parseYaml", builtin_parse_yaml::INST), + // Misc + ("codepoint", builtin_codepoint::INST), + ("substr", builtin_substr::INST), + ("char", builtin_char::INST), + ("strReplace", builtin_str_replace::INST), + ("splitLimit", builtin_splitlimit::INST), + ("asciiUpper", builtin_ascii_upper::INST), + ("asciiLower", builtin_ascii_lower::INST), + ("findSubstr", builtin_find_substr::INST), + ("startsWith", builtin_starts_with::INST), + ("endsWith", builtin_ends_with::INST), + ] + .iter() + .cloned() + { + builder + .member(name.into()) + .hide() + .value(s.clone(), Val::Func(FuncVal::StaticBuiltin(builtin))) + .expect("no conflict"); + } + + builder + .member("extVar".into()) + .hide() + .value( + s.clone(), + Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_ext_var { + settings: settings.clone() + })))), + ) + .expect("no conflict"); + builder + .member("native".into()) + .hide() + .value( + s.clone(), + Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_native { + settings: settings.clone() + })))), + ) + .expect("no conflict"); + builder + .member("trace".into()) + .hide() + .value( + s.clone(), + Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_trace { settings })))), + ) + .expect("no conflict"); + + builder + .member("id".into()) + .hide() + .value(s, Val::Func(FuncVal::Id)) + .expect("no conflict"); + + builder.build() +} + +pub trait TracePrinter { + fn print_trace(&self, s: State, loc: CallLocation, value: IStr); +} + +pub struct StdTracePrinter { + resolver: PathResolver, +} +impl StdTracePrinter { + pub fn new(resolver: PathResolver) -> Self { + Self { resolver } + } +} +impl TracePrinter for StdTracePrinter { + fn print_trace(&self, _s: State, loc: CallLocation, value: IStr) { + eprint!("TRACE:"); + if let Some(loc) = loc.0 { + let locs = loc.0.map_source_locations(&[loc.1]); + eprint!( + " {}:{}", + match loc.0.source_path().path() { + Some(p) => self.resolver.resolve(p), + None => loc.0.source_path().to_string(), + }, + locs[0].line + ); + } + eprintln!(" {}", value); + } +} + +pub struct Settings { + /// Used for `std.extVar` + pub ext_vars: HashMap, + /// Used for `std.native` + pub ext_natives: HashMap>>, + /// Helper to add globals without implementing custom ContextInitializer + pub globals: GcHashMap>, + /// Used for `std.trace` + pub trace_printer: Box, + /// Used for `std.thisFile` + pub path_resolver: PathResolver, +} + +pub fn extvar_source(name: &str, code: impl Into) -> Source { + let source_name = format!("", name); + Source::new_virtual(source_name.into(), code.into()) +} + +pub struct ContextInitializer { + // When we don't need to support legacy-this-file, we can reuse same context for all files + #[cfg(not(feature = "legacy-this-file"))] + context: Context, + // Otherwise, we can only keep first stdlib layer, and then stack thisFile on top of it + #[cfg(feature = "legacy-this-file")] + stdlib_obj: ObjValue, + settings: Rc>, +} +impl ContextInitializer { + pub fn new(s: State, resolver: PathResolver) -> Self { + let settings = Settings { + ext_vars: Default::default(), + ext_natives: Default::default(), + globals: Default::default(), + trace_printer: Box::new(StdTracePrinter::new(resolver.clone())), + path_resolver: resolver, + }; + let settings = Rc::new(RefCell::new(settings)); + Self { + #[cfg(not(feature = "legacy-this-file"))] + context: { + let mut context = ContextBuilder::with_capacity(1); + context.bind( + "std".into(), + Thunk::evaluated(Val::Obj(stdlib_uncached(s, settings.clone()))), + ); + context.build() + }, + #[cfg(feature = "legacy-this-file")] + stdlib_obj: stdlib_uncached(s, settings.clone()), + settings, + } + } + pub fn settings(&self) -> Ref { + self.settings.borrow() + } + pub fn settings_mut(&self) -> RefMut { + self.settings.borrow_mut() + } + pub fn add_ext_var(&self, name: IStr, value: Val) { + self.settings_mut() + .ext_vars + .insert(name, TlaArg::Val(value)); + } + pub fn add_ext_str(&self, name: IStr, value: IStr) { + self.settings_mut() + .ext_vars + .insert(name, TlaArg::String(value)); + } + pub fn add_ext_code(&self, name: &str, code: impl Into) -> Result<()> { + let code = code.into(); + let source = extvar_source(name, code.clone()); + let parsed = jrsonnet_parser::parse( + &code, + &jrsonnet_parser::ParserSettings { + file_name: source.clone(), + }, + ) + .map_err(|e| ImportSyntaxError { + path: source, + error: Box::new(e), + })?; + // self.data_mut().volatile_files.insert(source_name, code); + self.settings_mut() + .ext_vars + .insert(name.into(), TlaArg::Code(parsed)); + Ok(()) + } + pub fn add_native(&self, name: IStr, cb: Cc>) { + self.settings_mut().ext_natives.insert(name, cb); + } +} +impl jrsonnet_evaluator::ContextInitializer for ContextInitializer { + #[cfg(not(feature = "legacy-this-file"))] + fn initialize(&self, _s: State, _source: Source) -> jrsonnet_evaluator::Context { + let out = self.context.clone(); + let globals = &self.settings().globals; + if globals.is_empty() { + return out; + } + + let mut out = ContextBuilder::extend(out); + for (k, v) in globals.iter() { + out.bind(k.clone(), v.clone()); + } + out.build() + } + #[cfg(feature = "legacy-this-file")] + fn initialize(&self, s: State, source: Source) -> jrsonnet_evaluator::Context { + let mut builder = ObjValueBuilder::new(); + builder.with_super(self.stdlib_obj.clone()); + builder + .member("thisFile".into()) + .hide() + .value( + s, + Val::Str(match source.source_path().path() { + Some(p) => self.settings().path_resolver.resolve(p).into(), + None => source.source_path().to_string().into(), + }), + ) + .expect("this object builder is empty"); + let stdlib_with_this_file = builder.build(); + + let mut context = ContextBuilder::with_capacity(1); + context.bind( + "std".into(), + Thunk::evaluated(Val::Obj(stdlib_with_this_file)), + ); + for (k, v) in self.settings().globals.iter() { + context.bind(k.clone(), v.clone()); + } + context.build() + } + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[builtin] +fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> Result { + use Either4::*; + Ok(match x { + A(x) => x.chars().count(), + B(x) => x.len(), + C(x) => x.len(), + D(f) => f.params_len(), + }) +} + +#[builtin] +const fn builtin_codepoint(str: char) -> Result { + Ok(str as u32) +} + +#[builtin] +fn builtin_substr(str: IStr, from: usize, len: usize) -> Result { + Ok(str.chars().skip(from).take(len).collect()) +} + +#[builtin(fields( + settings: Rc>, +))] +fn builtin_ext_var(this: &builtin_ext_var, s: State, x: IStr) -> Result { + let ctx = s.create_default_context(extvar_source(&x, "")); + Ok(Any(this + .settings + .borrow() + .ext_vars + .get(&x) + .cloned() + .ok_or_else(|| UndefinedExternalVariable(x))? + .evaluate_arg(s.clone(), ctx, true)? + .evaluate(s)?)) +} + +#[builtin(fields( + settings: Rc>, +))] +fn builtin_native(this: &builtin_native, name: IStr) -> Result { + Ok(Any(this + .settings + .borrow() + .ext_natives + .get(&name) + .cloned() + .map_or(Val::Null, |v| { + Val::Func(FuncVal::Builtin(v.clone())) + }))) +} + +#[builtin] +fn builtin_char(n: u32) -> Result { + Ok(std::char::from_u32(n).ok_or_else(|| InvalidUnicodeCodepointGot(n))?) +} + +#[builtin(fields( + settings: Rc>, +))] +fn builtin_trace( + this: &builtin_trace, + s: State, + loc: CallLocation, + str: IStr, + rest: Thunk, +) -> Result { + this.settings + .borrow() + .trace_printer + .print_trace(s.clone(), loc, str); + Ok(Any(rest.evaluate(s)?)) +} + +#[builtin] +fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result { + Ok(str.replace(&from as &str, &to as &str)) +} + +#[builtin] +fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> Result { + use Either2::*; + Ok(VecVal(Cc::new(match maxsplits { + A(n) => str + .splitn(n + 1, &c as &str) + .map(|s| Val::Str(s.into())) + .collect(), + B(_) => str.split(&c as &str).map(|s| Val::Str(s.into())).collect(), + }))) +} + +#[builtin] +fn builtin_ascii_upper(str: IStr) -> Result { + Ok(str.to_ascii_uppercase()) +} + +#[builtin] +fn builtin_ascii_lower(str: IStr) -> Result { + Ok(str.to_ascii_lowercase()) +} + +#[builtin] +fn builtin_find_substr(pat: IStr, str: IStr) -> Result { + if pat.is_empty() || str.is_empty() || pat.len() > str.len() { + return Ok(ArrValue::empty()); + } + + let str = str.as_str(); + let pat = pat.as_bytes(); + let strb = str.as_bytes(); + + let max_pos = str.len() - pat.len(); + + let mut out: Vec = Vec::new(); + for (ch_idx, (i, _)) in str + .char_indices() + .take_while(|(i, _)| i <= &max_pos) + .enumerate() + { + if &strb[i..i + pat.len()] == pat { + out.push(Val::Num(ch_idx as f64)) + } + } + Ok(out.into()) +} + +#[allow(clippy::comparison_chain)] +#[builtin] +fn builtin_starts_with( + s: State, + a: Either![IStr, ArrValue], + b: Either![IStr, ArrValue], +) -> Result { + Ok(match (a, b) { + (Either2::A(a), Either2::A(b)) => a.starts_with(b.as_str()), + (Either2::B(a), Either2::B(b)) => { + if b.len() > a.len() { + return Ok(false); + } else if b.len() == a.len() { + return equals(s, &Val::Arr(a), &Val::Arr(b)); + } else { + for (a, b) in a + .slice(None, Some(b.len()), None) + .iter(s.clone()) + .zip(b.iter(s.clone())) + { + let a = a?; + let b = b?; + if !equals(s.clone(), &a, &b)? { + return Ok(false); + } + } + true + } + } + _ => throw_runtime!("both arguments should be of the same type"), + }) +} + +#[allow(clippy::comparison_chain)] +#[builtin] +fn builtin_ends_with( + s: State, + a: Either![IStr, ArrValue], + b: Either![IStr, ArrValue], +) -> Result { + Ok(match (a, b) { + (Either2::A(a), Either2::A(b)) => a.ends_with(b.as_str()), + (Either2::B(a), Either2::B(b)) => { + if b.len() > a.len() { + return Ok(false); + } else if b.len() == a.len() { + return equals(s, &Val::Arr(a), &Val::Arr(b)); + } else { + let a_len = a.len(); + for (a, b) in a + .slice(Some(a_len - b.len()), None, None) + .iter(s.clone()) + .zip(b.iter(s.clone())) + { + let a = a?; + let b = b?; + if !equals(s.clone(), &a, &b)? { + return Ok(false); + } + } + true + } + } + _ => throw_runtime!("both arguments should be of the same type"), + }) +} + +pub trait StateExt { + /// This method was previously implemented in jrsonnet-evaluator itself + fn with_stdlib(&self); + fn add_global(&self, name: IStr, value: Thunk); +} + +impl StateExt for State { + fn with_stdlib(&self) { + let initializer = ContextInitializer::new(self.clone(), PathResolver::new_cwd_fallback()); + self.settings_mut().context_initializer = Box::new(initializer) + } + fn add_global(&self, name: IStr, value: Thunk) { + self.settings() + .context_initializer + .as_any() + .downcast_ref::() + .expect("not standard context initializer") + .settings_mut() + .globals + .insert(name, value); + } +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/manifest.rs @@ -0,0 +1,65 @@ +use jrsonnet_evaluator::{ + error::Result, + function::builtin, + stdlib::manifest::{ + escape_string_json, manifest_json_ex, manifest_yaml_ex, ManifestJsonOptions, ManifestType, + ManifestYamlOptions, + }, + typed::Any, + IStr, State, +}; + +#[builtin] +pub fn builtin_escape_string_json(str_: IStr) -> Result { + Ok(escape_string_json(&str_)) +} + +#[builtin] +pub fn builtin_manifest_json_ex( + s: State, + value: Any, + indent: IStr, + newline: Option, + key_val_sep: Option, + #[cfg(feature = "exp-preserve-order")] preserve_order: Option, +) -> Result { + let newline = newline.as_deref().unwrap_or("\n"); + let key_val_sep = key_val_sep.as_deref().unwrap_or(": "); + manifest_json_ex( + s, + &value.0, + &ManifestJsonOptions { + padding: &indent, + mtype: ManifestType::Std, + newline, + key_val_sep, + #[cfg(feature = "exp-preserve-order")] + preserve_order: preserve_order.unwrap_or(false), + }, + ) +} + +#[builtin] +pub fn builtin_manifest_yaml_doc( + s: State, + value: Any, + indent_array_in_object: Option, + quote_keys: Option, + #[cfg(feature = "exp-preserve-order")] preserve_order: Option, +) -> Result { + manifest_yaml_ex( + s, + &value.0, + &ManifestYamlOptions { + padding: " ", + arr_element_padding: if indent_array_in_object.unwrap_or(false) { + " " + } else { + "" + }, + quote_keys: quote_keys.unwrap_or(true), + #[cfg(feature = "exp-preserve-order")] + preserve_order: preserve_order.unwrap_or(false), + }, + ) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/math.rs @@ -0,0 +1,87 @@ +use jrsonnet_evaluator::{error::Result, function::builtin, typed::PositiveF64}; + +#[builtin] +pub fn builtin_modulo(a: f64, b: f64) -> Result { + Ok(a % b) +} + +#[builtin] +pub fn builtin_floor(x: f64) -> Result { + Ok(x.floor()) +} + +#[builtin] +pub fn builtin_ceil(x: f64) -> Result { + Ok(x.ceil()) +} + +#[builtin] +pub fn builtin_log(n: f64) -> Result { + Ok(n.ln()) +} + +#[builtin] +pub fn builtin_pow(x: f64, n: f64) -> Result { + Ok(x.powf(n)) +} + +#[builtin] +pub fn builtin_sqrt(x: PositiveF64) -> Result { + Ok(x.0.sqrt()) +} + +#[builtin] +pub fn builtin_sin(x: f64) -> Result { + Ok(x.sin()) +} + +#[builtin] +pub fn builtin_cos(x: f64) -> Result { + Ok(x.cos()) +} + +#[builtin] +pub fn builtin_tan(x: f64) -> Result { + Ok(x.tan()) +} + +#[builtin] +pub fn builtin_asin(x: f64) -> Result { + Ok(x.asin()) +} + +#[builtin] +pub fn builtin_acos(x: f64) -> Result { + Ok(x.acos()) +} + +#[builtin] +pub fn builtin_atan(x: f64) -> Result { + Ok(x.atan()) +} + +#[builtin] +pub fn builtin_exp(x: f64) -> Result { + Ok(x.exp()) +} + +fn frexp(s: f64) -> (f64, i16) { + if s == 0.0 { + (s, 0) + } else { + let lg = s.abs().log2(); + let x = (lg - lg.floor() - 1.0).exp2(); + let exp = lg.floor() + 1.0; + (s.signum() * x, exp as i16) + } +} + +#[builtin] +pub fn builtin_mantissa(x: f64) -> Result { + Ok(frexp(x).0) +} + +#[builtin] +pub fn builtin_exponent(x: f64) -> Result { + Ok(frexp(x).1) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/objects.rs @@ -0,0 +1,27 @@ +use jrsonnet_evaluator::{ + error::Result, function::builtin, typed::VecVal, val::Val, IStr, ObjValue, +}; +use jrsonnet_gcmodule::Cc; + +#[builtin] +pub fn builtin_object_fields_ex( + obj: ObjValue, + inc_hidden: bool, + #[cfg(feature = "exp-preserve-order")] preserve_order: Option, +) -> Result { + #[cfg(feature = "exp-preserve-order")] + let preserve_order = preserve_order.unwrap_or(false); + let out = obj.fields_ex( + inc_hidden, + #[cfg(feature = "exp-preserve-order")] + preserve_order, + ); + Ok(VecVal(Cc::new( + out.into_iter().map(Val::Str).collect::>(), + ))) +} + +#[builtin] +pub fn builtin_object_has_ex(obj: ObjValue, f: IStr, inc_hidden: bool) -> Result { + Ok(obj.has_field_ex(f, inc_hidden)) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/operator.rs @@ -0,0 +1,40 @@ +//! Some jsonnet operations are desugared to stdlib functions... +//! However, in our case we instead implement them in native, and implement native functions on top of core for backwards compatibility + +use jrsonnet_evaluator::{ + error::Result, + function::builtin, + operator::evaluate_mod_op, + stdlib::std_format, + typed::{Any, Either, Either2}, + val::{equals, primitive_equals}, + IStr, State, Val, +}; + +#[builtin] +pub fn builtin_mod(s: State, a: Either![f64, IStr], b: Any) -> Result { + use Either2::*; + Ok(Any(evaluate_mod_op( + s, + &match a { + A(v) => Val::Num(v), + B(s) => Val::Str(s), + }, + &b.0, + )?)) +} + +#[builtin] +pub fn builtin_primitive_equals(a: Any, b: Any) -> Result { + primitive_equals(&a.0, &b.0) +} + +#[builtin] +pub fn builtin_equals(s: State, a: Any, b: Any) -> Result { + equals(s, &a.0, &b.0) +} + +#[builtin] +pub fn builtin_format(s: State, str: IStr, vals: Any) -> Result { + std_format(s, str, vals.0) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/parse.rs @@ -0,0 +1,39 @@ +use jrsonnet_evaluator::{ + error::{Error::RuntimeError, Result}, + function::builtin, + typed::{Any, Typed}, + IStr, State, Val, +}; +use serde::Deserialize; + +#[builtin] +pub fn builtin_parse_json(st: State, s: IStr) -> Result { + use serde_json::Value; + let value: Value = serde_json::from_str(&s) + .map_err(|e| RuntimeError(format!("failed to parse json: {}", e).into()))?; + Ok(Any(Value::into_untyped(value, st)?)) +} + +#[builtin] +pub fn builtin_parse_yaml(st: State, s: IStr) -> Result { + use serde_json::Value; + use serde_yaml_with_quirks::DeserializingQuirks; + let value = serde_yaml_with_quirks::Deserializer::from_str_with_quirks( + &s, + DeserializingQuirks { old_octals: true }, + ); + let mut out = vec![]; + for item in value { + let value = Value::deserialize(item) + .map_err(|e| RuntimeError(format!("failed to parse yaml: {}", e).into()))?; + let val = Value::into_untyped(value, st.clone())?; + out.push(val); + } + Ok(Any(if out.is_empty() { + Val::Null + } else if out.len() == 1 { + out.into_iter().next().unwrap() + } else { + Val::Arr(out.into()) + })) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/sort.rs @@ -0,0 +1,109 @@ +use jrsonnet_evaluator::{ + error::Result, + function::{builtin, FuncVal}, + throw_runtime, + typed::Any, + val::ArrValue, + State, Val, +}; +use jrsonnet_gcmodule::Cc; + +#[derive(Copy, Clone)] +enum SortKeyType { + Number, + String, + Unknown, +} + +#[derive(PartialEq)] +struct NonNaNf64(f64); +impl PartialOrd for NonNaNf64 { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} +impl Eq for NonNaNf64 {} +impl Ord for NonNaNf64 { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).expect("non nan") + } +} + +fn get_sort_type( + values: &mut [T], + key_getter: impl Fn(&mut T) -> &mut Val, +) -> Result { + let mut sort_type = SortKeyType::Unknown; + for i in values.iter_mut() { + let i = key_getter(i); + match (i, sort_type) { + (Val::Str(_), SortKeyType::Unknown) => sort_type = SortKeyType::String, + (Val::Num(_), SortKeyType::Unknown) => sort_type = SortKeyType::Number, + (Val::Str(_), SortKeyType::String) | (Val::Num(_), SortKeyType::Number) => {} + (Val::Str(_) | Val::Num(_), _) => { + throw_runtime!("sort elements should have the same types") + } + _ => throw_runtime!("sort key should either be a string or a number"), + } + } + Ok(sort_type) +} + +/// * `key_getter` - None, if identity sort required +pub fn sort(s: State, values: Cc>, key_getter: FuncVal) -> Result>> { + if values.len() <= 1 { + return Ok(values); + } + if key_getter.is_identity() { + // Fast path, identity key getter + let mut values = (*values).clone(); + let sort_type = get_sort_type(&mut values, |k| k)?; + match sort_type { + SortKeyType::Number => values.sort_unstable_by_key(|v| match v { + Val::Num(n) => NonNaNf64(*n), + _ => unreachable!(), + }), + SortKeyType::String => values.sort_unstable_by_key(|v| match v { + Val::Str(s) => s.clone(), + _ => unreachable!(), + }), + SortKeyType::Unknown => unreachable!(), + }; + Ok(Cc::new(values)) + } else { + // Slow path, user provided key getter + let mut vk = Vec::with_capacity(values.len()); + for value in values.iter() { + vk.push(( + value.clone(), + key_getter.evaluate_simple(s.clone(), &(Any(value.clone()),))?, + )); + } + let sort_type = get_sort_type(&mut vk, |v| &mut v.1)?; + match sort_type { + SortKeyType::Number => vk.sort_by_key(|v| match v.1 { + Val::Num(n) => NonNaNf64(n), + _ => unreachable!(), + }), + SortKeyType::String => vk.sort_by_key(|v| match &v.1 { + Val::Str(s) => s.clone(), + _ => unreachable!(), + }), + SortKeyType::Unknown => unreachable!(), + }; + Ok(Cc::new(vk.into_iter().map(|v| v.0).collect())) + } +} + +#[builtin] +#[allow(non_snake_case)] +pub fn builtin_sort(s: State, arr: ArrValue, keyF: Option) -> Result { + if arr.len() <= 1 { + return Ok(arr); + } + Ok(ArrValue::Eager(super::sort::sort( + s.clone(), + arr.evaluated(s)?, + keyF.unwrap_or_else(FuncVal::identity), + )?)) +} --- a/crates/jrsonnet-stdlib/src/std.jsonnet +++ b/crates/jrsonnet-stdlib/src/std.jsonnet @@ -2,73 +2,9 @@ local std = self, local id = std.id, - # Magic legacy field - thisFile:: $intrinsicThisFile, - id:: $intrinsicId, + thisFile:: error 'std.thisFile is deprecated, to enable its support in jrsonnet - recompile it with "legacy-this-file" support.\nThis will slow down stdlib caching a bit, though', - # Those functions aren't normally located in stdlib - length:: $intrinsic(length), - type:: $intrinsic(type), - makeArray:: $intrinsic(makeArray), - codepoint:: $intrinsic(codepoint), - objectFieldsEx:: $intrinsic(objectFieldsEx), - objectHasEx:: $intrinsic(objectHasEx), - primitiveEquals:: $intrinsic(primitiveEquals), - modulo:: $intrinsic(modulo), - floor:: $intrinsic(floor), - ceil:: $intrinsic(ceil), - extVar:: $intrinsic(extVar), - native:: $intrinsic(native), - filter:: $intrinsic(filter), - char:: $intrinsic(char), - encodeUTF8:: $intrinsic(encodeUTF8), - decodeUTF8:: $intrinsic(decodeUTF8), - md5:: $intrinsic(md5), - trace:: $intrinsic(trace), - parseJson:: $intrinsic(parseJson), - parseYaml:: $intrinsic(parseYaml), - - log:: $intrinsic(log), - pow:: $intrinsic(pow), - sqrt:: $intrinsic(sqrt), - - sin:: $intrinsic(sin), - cos:: $intrinsic(cos), - tan:: $intrinsic(tan), - asin:: $intrinsic(asin), - acos:: $intrinsic(acos), - atan:: $intrinsic(atan), - - exp:: $intrinsic(exp), - mantissa:: $intrinsic(mantissa), - exponent:: $intrinsic(exponent), - - any:: $intrinsic(any), - all:: $intrinsic(all), - - isString(v):: std.type(v) == 'string', - isNumber(v):: std.type(v) == 'number', - isBoolean(v):: std.type(v) == 'boolean', - isObject(v):: std.type(v) == 'object', - isArray(v):: std.type(v) == 'array', - isFunction(v):: std.type(v) == 'function', - - toString(a):: - if std.type(a) == 'string' then a else '' + a, - - substr:: $intrinsic(substr), - - startsWith(a, b):: - if std.length(a) < std.length(b) then - false - else - std.substr(a, 0, std.length(b)) == b, - - endsWith(a, b):: - if std.length(a) < std.length(b) then - false - else - std.substr(a, std.length(a) - std.length(b), std.length(b)) == b, + toString(a):: '' + a, lstripChars(str, chars):: if std.length(str) > 0 && std.member(chars, str[0]) then @@ -127,32 +63,12 @@ split(str, c):: std.splitLimit(str, c, -1), - splitLimit:: $intrinsic(splitLimit), - - strReplace:: $intrinsic(strReplace), - - asciiUpper:: $intrinsic(asciiUpper), - - asciiLower:: $intrinsic(asciiLower), - - range:: $intrinsic(range), - repeat(what, count):: local joiner = if std.isString(what) then '' else if std.isArray(what) then [] else error 'std.repeat first argument must be an array or a string'; std.join(joiner, std.makeArray(count, function(i) what)), - - slice:: $intrinsic(slice), - - member:: $intrinsic(member), - - count:: $intrinsic(count), - - mod:: $intrinsic(mod), - - map:: $intrinsic(map), mapWithIndex(func, arr):: if !std.isFunction(func) then @@ -169,11 +85,7 @@ error ('std.mapWithKey second param must be object, got ' + std.type(obj)) else { [k]: func(k, obj[k]) for k in std.objectFields(obj) }, - - flatMap:: $intrinsic(flatMap), - join:: $intrinsic(join), - lines(arr):: std.join('\n', arr + ['']), @@ -185,13 +97,6 @@ else error 'Expected string or array, got %s' % std.type(arr), - - format:: $intrinsic(format), - - foldr:: $intrinsic(foldr), - - foldl:: $intrinsic(foldl), - filterMap(filter_func, map_func, arr):: if !std.isFunction(filter_func) then error ('std.filterMap first param must be function, got ' + std.type(filter_func)) @@ -349,8 +254,6 @@ renderTableInternal(value, [], [], '') else error 'TOML body must be an object. Got ' + std.type(value), - - escapeStringJson:: $intrinsic(escapeStringJson), escapeStringPython(str):: std.escapeStringJson(str), @@ -377,10 +280,6 @@ manifestJsonMinified(value):: std.manifestJsonEx(value, '', '', ':'), - manifestJsonEx:: $intrinsic(manifestJsonEx), - - manifestYamlDoc:: $intrinsic(manifestYamlDoc), - manifestYamlStream(value, indent_array_in_object=false, c_document_end=true):: if !std.isArray(value) then error 'manifestYamlStream only takes arrays, got ' + std.type(value) @@ -434,19 +333,6 @@ aux(value), - local base64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', - local base64_inv = { [base64_table[i]]: i for i in std.range(0, 63) }, - - base64:: $intrinsic(base64), - - base64DecodeBytes:: $intrinsic(base64DecodeBytes), - - base64Decode:: $intrinsic(base64Decode), - - reverse:: $intrinsic(reverse), - - sort:: $intrinsic(sort), - uniq(arr, keyF=id):: local f(a, b) = if std.length(a) == 0 then @@ -534,7 +420,7 @@ else patch, - get(o, f, default = null, inc_hidden = true):: + get(o, f, default=null, inc_hidden=true):: if std.objectHasEx(o, f, inc_hidden) then o[f] else default, objectFields(o):: @@ -554,8 +440,6 @@ objectValuesAll(o):: [o[k] for k in std.objectFieldsAll(o)], - - equals:: $intrinsic(equals), resolvePath(f, r):: local arr = std.split(f, '/'); @@ -579,19 +463,6 @@ if isContent(std.prune(a[x])) } else a, - - findSubstr(pat, str):: - if !std.isString(pat) then - error 'findSubstr first parameter should be a string, got ' + std.type(pat) - else if !std.isString(str) then - error 'findSubstr second parameter should be a string, got ' + std.type(str) - else - local pat_len = std.length(pat); - local str_len = std.length(str); - if pat_len == 0 || str_len == 0 || pat_len > str_len then - [] - else - std.filter(function(i) str[i:i + pat_len] == pat, std.range(0, str_len - pat_len)), find(value, arr):: if !std.isArray(arr) then --- /dev/null +++ b/crates/jrsonnet-stdlib/src/types.rs @@ -0,0 +1,31 @@ +use jrsonnet_evaluator::{error::Result, function::builtin, typed::Any, IStr, Val}; + +#[builtin] +pub fn builtin_type(x: Any) -> Result { + Ok(x.0.value_type().name().into()) +} + +#[builtin] +pub fn builtin_is_string(x: Any) -> Result { + Ok(matches!(x.0, Val::Str(_))) +} +#[builtin] +pub fn builtin_is_number(x: Any) -> Result { + Ok(matches!(x.0, Val::Num(_))) +} +#[builtin] +pub fn builtin_is_boolean(x: Any) -> Result { + Ok(matches!(x.0, Val::Bool(_))) +} +#[builtin] +pub fn builtin_is_object(x: Any) -> Result { + Ok(matches!(x.0, Val::Obj(_))) +} +#[builtin] +pub fn builtin_is_array(x: Any) -> Result { + Ok(matches!(x.0, Val::Arr(_))) +} +#[builtin] +pub fn builtin_is_function(x: Any) -> Result { + Ok(matches!(x.0, Val::Func(_))) +} --- a/flake.lock +++ /dev/null @@ -1,43 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1625281901, - "narHash": "sha256-DkZDtTIPzhXATqIps2ifNFpnI+PTcfMYdcrx/oFm00Q=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "09c38c29f2c719cd76ca17a596c2fdac9e186ceb", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} --- a/flake.nix +++ /dev/null @@ -1,59 +0,0 @@ -{ - description = "Dotfiles manager"; - inputs = { - nixpkgs.url = "github:nixos/nixpkgs"; - flake-utils.url = "github:numtide/flake-utils"; - naersk.url = "github:nix-community/naersk"; - rust-overlay.url = "github:oxalica/rust-overlay"; - pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix"; - }; - outputs = { self, nixpkgs, flake-utils, rust-overlay, pre-commit-hooks, naersk }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs - { - inherit system; - overlays = [ rust-overlay.overlay ]; - }; - rust = ((pkgs.rustChannelOf { date = "2021-11-11"; channel = "nightly"; }).default.override { - extensions = [ "rust-src" ]; - }); - naersk-lib = naersk.lib."${system}".override { - rustc = rust; - cargo = rust; - }; - in - rec { - checks = { - pre-commit-check = pre-commit-hooks.lib.${system}.run { - src = ./.; - hooks = { - nixpkgs-fmt.enable = true; - }; - }; - }; - defaultPackage = naersk-lib.buildPackage { - pname = "dotman"; - root = ./.; - buildInputs = with pkgs; [ - pkgs.sqlite - ]; - }; - devShell = pkgs.mkShell { - inherit (checks.pre-commit-check) shellHook; - nativeBuildInputs = with pkgs;[ - pkgs.binutils - pkgs.pkgconfig - pkgs.clang - pkgs.x11 - pkgs.alsaLib - pkgs.libudev - pkgs.sqlite - rust - cargo-edit - go-jsonnet - ]; - }; - } - ); -} --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tests" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +jrsonnet-evaluator = { path = "../crates/jrsonnet-evaluator" } +jrsonnet-gcmodule = "0.3.4" +jrsonnet-stdlib = { path = "../crates/jrsonnet-stdlib" } +serde = "1.0.142" --- /dev/null +++ b/tests/golden/array_comp.jsonnet @@ -0,0 +1 @@ +[[a, b] for a in [1, 2, 3] for b in [4, 5, 6]] --- /dev/null +++ b/tests/golden/array_comp.jsonnet.golden @@ -0,0 +1,38 @@ +[ + [ + 1, + 4 + ], + [ + 1, + 5 + ], + [ + 1, + 6 + ], + [ + 2, + 4 + ], + [ + 2, + 5 + ], + [ + 2, + 6 + ], + [ + 3, + 4 + ], + [ + 3, + 5 + ], + [ + 3, + 6 + ] +] \ No newline at end of file --- /dev/null +++ b/tests/golden/builtin_json.jsonnet @@ -0,0 +1 @@ +std.manifestJsonEx({ a: 3, b: 4, c: 6 }, '') --- /dev/null +++ b/tests/golden/builtin_json.jsonnet.golden @@ -0,0 +1 @@ +"{\n\"a\": 3,\n\"b\": 4,\n\"c\": 6\n}" \ No newline at end of file --- /dev/null +++ b/tests/golden/builtin_json_minified.jsonnet @@ -0,0 +1 @@ +std.manifestJsonMinified({ a: 3, b: 4, c: 6 }) --- /dev/null +++ b/tests/golden/builtin_json_minified.jsonnet.golden @@ -0,0 +1 @@ +"{\"a\":3,\"b\":4,\"c\":6}" \ No newline at end of file --- /dev/null +++ b/tests/golden/builtin_parseJson.jsonnet @@ -0,0 +1 @@ +std.parseJson('{"a": -1,"b": 1,"c": 3.141,"d": []}') --- /dev/null +++ b/tests/golden/builtin_parseJson.jsonnet.golden @@ -0,0 +1,6 @@ +{ + "a": -1, + "b": 1, + "c": 3.141, + "d": [ ] +} \ No newline at end of file --- /dev/null +++ b/tests/golden/issue23.jsonnet @@ -0,0 +1 @@ +import 'issue23.jsonnet' --- /dev/null +++ b/tests/golden/issue23.jsonnet.golden @@ -0,0 +1,2 @@ +infinite recursion detected + issue23.jsonnet:1:1-26: import "issue23.jsonnet" \ No newline at end of file --- /dev/null +++ b/tests/golden/issue40.jsonnet @@ -0,0 +1,9 @@ +local conf = { + n: '', +}; + +local result = conf { + assert std.isNumber(self.n) : 'is number', +}; + +std.manifestJsonEx(result, '') --- /dev/null +++ b/tests/golden/issue40.jsonnet.golden @@ -0,0 +1,3 @@ +assert failed: is number + issue40.jsonnet:6:10-31: assertion failure + issue40.jsonnet:9:1-32: function call \ No newline at end of file --- /dev/null +++ b/tests/golden/missing_binding.jsonnet @@ -0,0 +1 @@ +sta --- /dev/null +++ b/tests/golden/missing_binding.jsonnet.golden @@ -0,0 +1,3 @@ +variable is not defined: sta +There is variable with similar name present: std + missing_binding.jsonnet:1:1-5: variable access \ No newline at end of file --- /dev/null +++ b/tests/golden/object_comp.jsonnet @@ -0,0 +1 @@ +{ local t = 'a', ['h' + i + '_' + z]: if 'h' + (i - 1) + '_' + z in self then t + 1 else 0 + t for i in [1, 2, 3] for z in [2, 3, 4] if z != i } --- /dev/null +++ b/tests/golden/object_comp.jsonnet.golden @@ -0,0 +1,9 @@ +{ + "h1_2": "0a", + "h1_3": "0a", + "h1_4": "0a", + "h2_3": "a1", + "h2_4": "a1", + "h3_2": "0a", + "h3_4": "a1" +} \ No newline at end of file --- /dev/null +++ b/tests/golden/test_assertThrow.jsonnet @@ -0,0 +1,2 @@ +// Test that test.assertThrow will return error, if body is not errored +test.assertThrow(1, '1') --- /dev/null +++ b/tests/golden/test_assertThrow.jsonnet.golden @@ -0,0 +1,2 @@ +runtime error: expected argument to throw on evaluation, but it returned instead + test_assertThrow.jsonnet:2:1-26: function call \ No newline at end of file --- /dev/null +++ b/tests/src/lib.rs @@ -0,0 +1 @@ +//! See tests/, suite/ and golden/ directories for tests --- /dev/null +++ b/tests/suite/builtin_ascii.jsonnet @@ -0,0 +1,3 @@ +std.assertEqual(std.asciiUpper('aBc😀'), 'ABC😀') && +std.assertEqual(std.asciiLower('aBc😀'), 'abc😀') && +true --- /dev/null +++ b/tests/suite/builtin_base64.jsonnet @@ -0,0 +1,2 @@ +std.assertEqual(std.base64('test'), 'dGVzdA==') && +true --- /dev/null +++ b/tests/suite/builtin_chars.jsonnet @@ -0,0 +1,3 @@ +local c = '😎'; +std.assertEqual({ c: std.codepoint(c), l: std.length(c) }, { c: 128526, l: 1 }) && +true --- /dev/null +++ b/tests/suite/builtin_constant.jsonnet @@ -0,0 +1,3 @@ +local std2 = std; local std = std2 { primitiveEquals(a, b):: false }; +// In jsonnet, this expression was failing because of being desugared to std.primitiveEquals(1, 1) +std.assertEqual(1 == 1, true) --- /dev/null +++ b/tests/suite/builtin_count.jsonnet @@ -0,0 +1,4 @@ +std.assertEqual(std.count([], ''), 0) && +std.assertEqual(std.count(['a', 'b', 'a'], 'd'), 0) && +std.assertEqual(std.count(['a', 'b', 'a'], 'a'), 2) && +true --- /dev/null +++ b/tests/suite/builtin_join.jsonnet @@ -0,0 +1,4 @@ +std.assertEqual(std.join([0, 0], [[1, 2], [3, 4], [5, 6]]), [1, 2, 0, 0, 3, 4, 0, 0, 5, 6]) && +std.assertEqual(std.join(',', ['1', '2', '3', '4']), '1,2,3,4') && +std.assertEqual(std.join(',', ['1', null, '2', null, '3']), '1,2,3') && +true --- /dev/null +++ b/tests/suite/builtin_member.jsonnet @@ -0,0 +1,7 @@ +!std.member('', '') && +std.member('abc', 'a') && +!std.member('abc', 'd') && +!std.member([], '') && +std.member(['a', 'b', 'c'], 'a') && +!std.member(['a', 'b', 'c'], 'd') && +true --- /dev/null +++ b/tests/suite/function_args.jsonnet @@ -0,0 +1,3 @@ +std.assertEqual(local a = function(b, c=2) b + c; a(2), 4) && +std.assertEqual(local a = function(b, c='Dear') b + c + d, d = 'World'; a('Hello'), 'HelloDearWorld') && +true --- /dev/null +++ b/tests/suite/function_context.jsonnet @@ -0,0 +1,10 @@ +local k = { + t(name=self.h): [self.h, name], + h: 3, +}; +local f = { + t: k.t(), + h: 4, +}; +std.assertEqual(f.t[0], f.t[1]) && +true --- /dev/null +++ b/tests/suite/function_lazy_args.jsonnet @@ -0,0 +1,5 @@ +local fun(a) = 2; +std.assertEqual(fun(error '3'), 2) && +// But in tailstrict mode arguments are evaluated eagerly +test.assertThrow(fun(error '3') tailstrict, 'runtime error: 3') && +true --- /dev/null +++ b/tests/suite/local.jsonnet @@ -0,0 +1,4 @@ +std.assertEqual(local a = 2; local b = 3; a + b, 5) && +std.assertEqual(local a = 1, b = a + 1; a + b, 3) && +std.assertEqual(local a = 1; local a = 2; a, 2) && +true --- /dev/null +++ b/tests/suite/math.jsonnet @@ -0,0 +1,3 @@ +std.assertEqual(2 + 2 * 2, 6) && +std.assertEqual(3 + (2 + 2 * 2), 9) && +true --- /dev/null +++ b/tests/suite/object_assertion.jsonnet @@ -0,0 +1,3 @@ +std.assertEqual({ assert 'a' in self : 'missing a' } + { a: 2 }, { a: 2 }) && +test.assertThrow({ assert 'a' in self : 'missing a', b: 1 }.b, 'assert failed: missing a') && +true --- /dev/null +++ b/tests/suite/object_comp_self.jsonnet @@ -0,0 +1,8 @@ +std.assertEqual(std.objectFields({ + a: { + [name]: name + for name in std.objectFields(self) + }, + b: 2, + c: 3, +}.a), ['a', 'b', 'c']) --- /dev/null +++ b/tests/suite/object_context.jsonnet @@ -0,0 +1,13 @@ +// `self` assigned to `me` was lost when being +// referenced from field +std.assertEqual({ + local me = self, + a: 3, + b: me.a, +}.b, 3) && +std.assertEqual({ + local me = self, + a: 3, + b(): me.a, +}.b(), 3) && +true --- /dev/null +++ b/tests/suite/object_fields.jsonnet @@ -0,0 +1,4 @@ +local a = 'a', b = null; +std.assertEqual({ [a]: 2 }, { a: 2 }) && +std.assertEqual({ [b]: 2 }, {}) && +true --- /dev/null +++ b/tests/suite/object_inheritance.jsonnet @@ -0,0 +1,17 @@ +std.assertEqual({ a: self.b } + { b: 3 }, { a: 3, b: 3 }) && +std.assertEqual( + { + name: 'Alice', + welcome: 'Hello ' + self.name + '!', + }, + { name: 'Alice', welcome: 'Hello Alice!' }, +) && +std.assertEqual( + { + name: 'Alice', + welcome: 'Hello ' + self.name + '!', + } + { + name: 'Bob', + }, { name: 'Bob', welcome: 'Hello Bob!' } +) && +true --- /dev/null +++ b/tests/suite/object_locals.jsonnet @@ -0,0 +1,4 @@ +std.assertEqual({ local a = 3, b: a }, { b: 3 }) && +std.assertEqual({ local a = 3, local c = a, b: c }, { b: 3 }) && +std.assertEqual({ local a = function(b) { [b]: 4 }, test: a('test') }, { test: { test: 4 } }) && +true --- /dev/null +++ b/tests/suite/object_super_standalone.jsonnet @@ -0,0 +1,11 @@ +local obj = { + a: 1, + b: 2, + c: 3, +}; +local test = obj + { + fields: std.objectFields(super), + d: 5, +}; +std.assertEqual(test.fields, ['a', 'b', 'c']) && +true --- /dev/null +++ b/tests/suite/sjsonnet_issue_127.jsonnet @@ -0,0 +1,6 @@ +local myFunc = function(a) + if (a) then "a" else "b"; + +local b = "aaa"; + +std.assertEqual(myFunc(b == [] || b == ['e']), "b") --- /dev/null +++ b/tests/suite/string_concat.jsonnet @@ -0,0 +1,4 @@ +std.assertEqual('Hello' + 'World', 'HelloWorld') && +std.assertEqual('Hello' * 3, 'HelloHelloHello') && +std.assertEqual('Hello' + 'World' * 3, 'HelloWorldWorldWorld') && +true --- /dev/null +++ b/tests/tests/as_native.rs @@ -0,0 +1,20 @@ +use jrsonnet_evaluator::{error::Result, State}; +use jrsonnet_stdlib::StateExt; + +mod common; + +#[test] +fn as_native() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + + let val = s.evaluate_snippet("snip".to_owned(), r#"function(a, b) a + b"#)?; + let func = val.as_func().expect("this is function"); + + let native = func.into_native::<((u32, u32), u32)>(); + + ensure_eq!(native(s.clone(), 1, 2)?, 3); + ensure_eq!(native(s, 3, 4)?, 7); + + Ok(()) +} --- /dev/null +++ b/tests/tests/builtin.rs @@ -0,0 +1,94 @@ +mod common; + +use jrsonnet_evaluator::{ + error::Result, + function::{builtin, builtin::Builtin, CallLocation, FuncVal}, + tb, + typed::Typed, + Context, State, Thunk, Val, +}; +use jrsonnet_gcmodule::Cc; +use jrsonnet_stdlib::StateExt; + +#[builtin] +fn a() -> Result { + Ok(1) +} + +#[test] +fn basic_function() -> Result<()> { + let s = State::default(); + let a: a = a {}; + let v = u32::from_untyped( + a.call(s.clone(), Context::new(), CallLocation::native(), &())?, + s, + )?; + + ensure_eq!(v, 1); + Ok(()) +} + +#[builtin] +fn native_add(a: u32, b: u32) -> Result { + Ok(a + b) +} + +#[test] +fn call_from_code() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + s.add_global( + "nativeAdd".into(), + Thunk::evaluated(Val::Func(FuncVal::StaticBuiltin(native_add::INST))), + ); + + let v = s.evaluate_snippet( + "snip".to_owned(), + " + assert nativeAdd(1, 2) == 3; + assert nativeAdd(100, 200) == 300; + null + ", + )?; + ensure_val_eq!(s, v, Val::Null); + Ok(()) +} + +#[builtin(fields( + a: u32 +))] +fn curried_add(this: &curried_add, b: u32) -> Result { + Ok(this.a + b) +} + +#[builtin] +fn curry_add(a: u32) -> Result { + Ok(FuncVal::Builtin(Cc::new(tb!(curried_add { a })))) +} + +#[test] +fn nonstatic_builtin() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + s.add_global( + "curryAdd".into(), + Thunk::evaluated(Val::Func(FuncVal::StaticBuiltin(curry_add::INST))), + ); + + let v = s.evaluate_snippet( + "snip".to_owned(), + " + local a = curryAdd(1); + local b = curryAdd(4); + + assert a(2) == 3; + assert a(200) == 201; + + assert b(2) == 6; + assert b(200) == 204; + null + ", + )?; + ensure_val_eq!(s, v, Val::Null); + Ok(()) +} --- /dev/null +++ b/tests/tests/common.rs @@ -0,0 +1,77 @@ +use jrsonnet_evaluator::{ + error::Result, + function::{builtin, FuncVal}, + throw_runtime, ObjValueBuilder, State, Thunk, Val, +}; +use jrsonnet_stdlib::StateExt; + +#[macro_export] +macro_rules! ensure_eq { + ($a:expr, $b:expr $(,)?) => {{ + let a = &$a; + let b = &$b; + if a != b { + ::jrsonnet_evaluator::throw_runtime!("assertion failed: a != b\na={:#?}\nb={:#?}", a, b) + } + }}; +} + +#[macro_export] +macro_rules! ensure { + ($v:expr $(,)?) => { + if !$v { + ::jrsonnet_evaluator::throw_runtime!("assertion failed: {}", stringify!($v)) + } + }; +} + +#[macro_export] +macro_rules! ensure_val_eq { + ($s:expr, $a:expr, $b:expr) => {{ + if !::jrsonnet_evaluator::val::equals($s.clone(), &$a.clone(), &$b.clone())? { + ::jrsonnet_evaluator::throw_runtime!( + "assertion failed: a != b\na={:#?}\nb={:#?}", + $a.to_json( + $s.clone(), + 2, + #[cfg(feature = "exp-preserve-order")] + false + )?, + $b.to_json( + $s.clone(), + 2, + #[cfg(feature = "exp-preserve-order")] + false + )?, + ) + } + }}; +} + +#[builtin] +fn assert_throw(s: State, lazy: Thunk, message: String) -> Result { + match lazy.evaluate(s) { + Ok(_) => { + throw_runtime!("expected argument to throw on evaluation, but it returned instead") + } + Err(e) => { + let error = format!("{}", e.error()); + ensure_eq!(message, error); + } + } + Ok(true) +} + +#[allow(dead_code)] +pub fn with_test(s: &State) { + let mut bobj = ObjValueBuilder::new(); + bobj.member("assertThrow".into()) + .hide() + .value( + s.clone(), + Val::Func(FuncVal::StaticBuiltin(assert_throw::INST)), + ) + .expect("no error"); + + s.add_global("test".into(), Thunk::evaluated(Val::Obj(bobj.build()))) +} --- /dev/null +++ b/tests/tests/golden.rs @@ -0,0 +1,70 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +use jrsonnet_evaluator::{ + trace::{CompactFormat, PathResolver}, + FileImportResolver, State, +}; +use jrsonnet_stdlib::StateExt; + +mod common; + +fn run(root: &Path, file: &Path) -> String { + let s = State::default(); + s.set_trace_format(Box::new(CompactFormat { + resolver: PathResolver::Relative(root.to_owned()), + padding: 3, + })); + s.with_stdlib(); + common::with_test(&s); + s.set_import_resolver(Box::new(FileImportResolver::default())); + + let v = match s.import(file) { + Ok(v) => v, + Err(e) => return s.stringify_err(&e), + }; + match v.to_json( + s.clone(), + 3, + #[cfg(feature = "exp-preserve-order")] + false, + ) { + Ok(v) => v.to_string(), + Err(e) => s.stringify_err(&e), + } +} + +#[test] +fn test() -> io::Result<()> { + let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + root.push("golden"); + + for entry in fs::read_dir(&root)? { + let entry = entry?; + if !entry.path().extension().map_or(false, |e| e == "jsonnet") { + continue; + } + + let result = run(&root, &entry.path()); + + let mut golden_path = entry.path(); + golden_path.set_extension("jsonnet.golden"); + + if !golden_path.exists() { + fs::write(golden_path, &result)?; + } else { + let golden = fs::read_to_string(golden_path)?; + + assert_eq!( + result, + golden, + "golden didn't match for {}", + entry.path().display() + ) + } + } + + Ok(()) +} --- /dev/null +++ b/tests/tests/sanity.rs @@ -0,0 +1,42 @@ +use jrsonnet_evaluator::{error::Result, throw_runtime, State, Val}; +use jrsonnet_stdlib::StateExt; + +mod common; + +#[test] +fn assert_positive() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + + let v = s.evaluate_snippet("snip".to_owned(), "assert 1 == 1: 'fail'; null")?; + ensure_val_eq!(s, v, Val::Null); + let v = s.evaluate_snippet("snip".to_owned(), "std.assertEqual(1, 1)")?; + ensure_val_eq!(s, v, Val::Bool(true)); + + Ok(()) +} + +#[test] +fn assert_negative() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + + { + let e = match s.evaluate_snippet("snip".to_owned(), "assert 1 == 2: 'fail'; null") { + Ok(_) => throw_runtime!("assertion should fail"), + Err(e) => e, + }; + let e = s.stringify_err(&e); + ensure!(e.starts_with("assert failed: fail\n")); + } + { + let e = match s.evaluate_snippet("snip".to_owned(), "std.assertEqual(1, 2)") { + Ok(_) => throw_runtime!("assertion should fail"), + Err(e) => e, + }; + let e = s.stringify_err(&e); + ensure!(e.starts_with("runtime error: Assertion failed. 1 != 2")) + } + + Ok(()) +} --- /dev/null +++ b/tests/tests/suite.rs @@ -0,0 +1,47 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +use jrsonnet_evaluator::{ + trace::{CompactFormat, PathResolver}, + FileImportResolver, State, Val, +}; +use jrsonnet_stdlib::StateExt; + +mod common; + +fn run(root: &Path, file: &Path) { + let s = State::default(); + s.set_trace_format(Box::new(CompactFormat { + resolver: PathResolver::Relative(root.to_owned()), + padding: 3, + })); + s.with_stdlib(); + common::with_test(&s); + s.set_import_resolver(Box::new(FileImportResolver::default())); + + match s.import(file) { + Ok(Val::Bool(true)) => {} + Ok(Val::Bool(false)) => panic!("test {} returned false", file.display()), + Ok(_) => panic!("test {} returned wrong type as result", file.display()), + Err(e) => panic!("test {} failed:\n{}", file.display(), s.stringify_err(&e)), + }; +} + +#[test] +fn test() -> io::Result<()> { + let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + root.push("suite"); + + for entry in fs::read_dir(&root)? { + let entry = entry?; + if !entry.path().extension().map_or(false, |e| e == "jsonnet") { + continue; + } + + run(&root, &entry.path()); + } + + Ok(()) +} --- /dev/null +++ b/tests/tests/typed_obj.rs @@ -0,0 +1,189 @@ +mod common; + +use std::fmt::Debug; + +use jrsonnet_evaluator::{error::Result, typed::Typed, State}; +use jrsonnet_stdlib::StateExt; + +#[derive(Clone, Typed, PartialEq, Debug)] +struct A { + a: u32, + b: u16, +} + +fn test_roundtrip(value: T, s: State) -> Result<()> { + let untyped = T::into_untyped(value.clone(), s.clone())?; + let value2 = T::from_untyped(untyped.clone(), s.clone())?; + ensure_eq!(value, value2); + let untyped2 = T::into_untyped(value2, s.clone())?; + ensure_val_eq!(s, untyped, untyped2); + + Ok(()) +} + +#[test] +fn simple_object() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + let a = A::from_untyped( + s.evaluate_snippet("snip".to_owned(), "{a: 1, b: 2}")?, + s.clone(), + )?; + ensure_eq!(a, A { a: 1, b: 2 }); + test_roundtrip(a, s)?; + Ok(()) +} + +#[derive(Clone, Typed, PartialEq, Debug)] +struct B { + a: u32, + #[typed(rename = "c")] + b: u16, +} + +#[test] +fn renamed_field() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + let b = B::from_untyped( + s.evaluate_snippet("snip".to_owned(), "{a: 1, c: 2}")?, + s.clone(), + )?; + ensure_eq!(b, B { a: 1, b: 2 }); + ensure_eq!( + &B::into_untyped(b.clone(), s.clone())?.to_string(s.clone())? as &str, + r#"{"a": 1, "c": 2}"#, + ); + test_roundtrip(b, s)?; + Ok(()) +} + +#[derive(Clone, Typed, PartialEq, Debug)] +struct ObjectKind { + #[typed(rename = "apiVersion")] + api_version: String, + #[typed(rename = "kind")] + kind: String, +} + +#[derive(Clone, Typed, PartialEq, Debug)] +struct Object { + #[typed(flatten)] + kind: ObjectKind, + b: u16, +} + +#[test] +fn flattened_object() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + let obj = Object::from_untyped( + s.evaluate_snippet("snip".to_owned(), "{apiVersion: 'ver', kind: 'kind', b: 2}")?, + s.clone(), + )?; + ensure_eq!( + obj, + Object { + kind: ObjectKind { + api_version: "ver".into(), + kind: "kind".into(), + }, + b: 2 + } + ); + ensure_eq!( + &Object::into_untyped(obj.clone(), s.clone())?.to_string(s.clone())? as &str, + r#"{"apiVersion": "ver", "b": 2, "kind": "kind"}"#, + ); + test_roundtrip(obj, s)?; + Ok(()) +} + +#[derive(Clone, Typed, PartialEq, Debug)] +struct C { + a: Option, + b: u16, +} + +#[test] +fn optional_field_some() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + let c = C::from_untyped( + s.evaluate_snippet("snip".to_owned(), "{a: 1, b: 2}")?, + s.clone(), + )?; + ensure_eq!(c, C { a: Some(1), b: 2 }); + ensure_eq!( + &C::into_untyped(c.clone(), s.clone())?.to_string(s.clone())? as &str, + r#"{"a": 1, "b": 2}"#, + ); + test_roundtrip(c, s)?; + Ok(()) +} + +#[test] +fn optional_field_none() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + let c = C::from_untyped(s.evaluate_snippet("snip".to_owned(), "{b: 2}")?, s.clone())?; + ensure_eq!(c, C { a: None, b: 2 }); + ensure_eq!( + &C::into_untyped(c.clone(), s.clone())?.to_string(s.clone())? as &str, + r#"{"b": 2}"#, + ); + test_roundtrip(c, s)?; + Ok(()) +} + +#[derive(Clone, Typed, PartialEq, Debug)] +struct D { + #[typed(flatten(ok))] + e: Option, + b: u16, +} + +#[derive(Clone, Typed, PartialEq, Debug)] +struct E { + v: u32, +} + +#[test] +fn flatten_optional_some() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + let d = D::from_untyped( + s.evaluate_snippet("snip".to_owned(), "{b: 2, v:1}")?, + s.clone(), + )?; + ensure_eq!( + d, + D { + e: Some(E { v: 1 }), + b: 2 + } + ); + ensure_eq!( + &D::into_untyped(d.clone(), s.clone())?.to_string(s.clone())? as &str, + r#"{"b": 2, "v": 1}"#, + ); + test_roundtrip(d, s)?; + Ok(()) +} + +#[test] +fn flatten_optional_none() -> Result<()> { + let s = State::default(); + s.with_stdlib(); + let d = D::from_untyped( + s.evaluate_snippet("snip".to_owned(), "{b: 2, v: '1'}")?, + s.clone(), + )?; + ensure_eq!(d, D { e: None, b: 2 }); + ensure_eq!( + &D::into_untyped(d.clone(), s.clone())?.to_string(s.clone())? as &str, + r#"{"b": 2}"#, + ); + test_roundtrip(d, s)?; + Ok(()) +} -- gitstuff