diff --git a/Cargo.lock b/Cargo.lock index e958e4e9a..f2d1bec47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1337,6 +1337,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "half" version = "2.4.0" @@ -2911,6 +2917,7 @@ dependencies = [ "futures-util", "fxhash", "getrandom", + "glob", "im-lists", "im-rc", "lasso", diff --git a/crates/steel-core/Cargo.toml b/crates/steel-core/Cargo.toml index 3225ccf47..84fd81fae 100644 --- a/crates/steel-core/Cargo.toml +++ b/crates/steel-core/Cargo.toml @@ -68,6 +68,9 @@ anyhow = { version = "1", optional = true } stacker = { version = "0.1.15", optional = true } +# For capabilities +glob = { version = "0.3.1" } + [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "*", features = ["js"] } diff --git a/crates/steel-core/src/compiler/modules.rs b/crates/steel-core/src/compiler/modules.rs index d94ad88c9..fe94a076b 100644 --- a/crates/steel-core/src/compiler/modules.rs +++ b/crates/steel-core/src/compiler/modules.rs @@ -46,7 +46,7 @@ use super::{ begin::FlattenBegin, mangle::{collect_globals, NameMangler}, }, - program::{FOR_SYNTAX, ONLY_IN, PREFIX_IN, REQUIRE_IDENT_SPEC}, + program::{CAPABILITIES_IN, FOR_SYNTAX, ONLY_IN, PREFIX_IN, REQUIRE_IDENT_SPEC}, }; macro_rules! time { @@ -133,6 +133,8 @@ pub(crate) struct ModuleManager { file_metadata: FxHashMap, visited: FxHashSet, custom_builtins: HashMap, + // Where we are in the tree traversal + compilation_stack: Vec, } impl ModuleManager { @@ -145,6 +147,7 @@ impl ModuleManager { file_metadata, visited: FxHashSet::default(), custom_builtins: HashMap::new(), + compilation_stack: Vec::new(), } } @@ -194,6 +197,7 @@ impl ModuleManager { global_macro_map, &self.custom_builtins, &[], + &mut self.compilation_stack, )?; module_builder.compile()?; @@ -218,6 +222,7 @@ impl ModuleManager { ) -> Result> { // Wipe the visited set on entry self.visited.clear(); + self.compilation_stack.clear(); // TODO // This is also explicitly wrong -> we should separate the global macro map from the macros found locally in this module @@ -238,6 +243,7 @@ impl ModuleManager { global_macro_map, &self.custom_builtins, search_dirs, + &mut self.compilation_stack, )?; let mut module_statements = module_builder.compile()?; @@ -319,78 +325,6 @@ impl ModuleManager { ExprKind::List(l) => { if let Some(qualifier) = l.first_ident() { match *qualifier { - // x if x == *CONTRACT_OUT => { - // // Directly expand into define/contract, but with the value just being the hash get below - - // // (bind/c contract name 'name) - - // let name = l.args.get(1).unwrap(); - - // if !explicit_requires.is_empty() - // && !name - // .atom_identifier() - // .map(|x| explicit_requires.contains_key(x)) - // .unwrap_or_default() - // { - // continue; - // } - - // // TODO: This should surface an error - cannot use contract - // // out on a macro - // if module - // .macro_map - // .contains_key(name.atom_identifier().unwrap()) - // { - // continue; - // } - - // // TODO: THe contract has to get mangled with the prefix as well? - // let _contract = l.args.get(2).unwrap(); - - // let hash_get = expr_list![ - // ExprKind::atom(*PROTO_HASH_GET), - // ExprKind::atom( - // "__module-".to_string() + &other_module_prefix - // ), - // ExprKind::Quote(Box::new(Quote::new( - // name.clone(), - // SyntaxObject::default(TokenType::Quote) - // ))), - // ]; - - // let mut owned_name = name.clone(); - - // // If we have the alias listed, we should use it - // if !explicit_requires.is_empty() { - // if let Some(alias) = explicit_requires - // .get(name.atom_identifier().unwrap()) - // .copied() - // .flatten() - // { - // *owned_name.atom_identifier_mut().unwrap() = - // alias.clone(); - // } - // } - - // if let Some(prefix) = &require_object.prefix { - // if let Some(existing) = owned_name.atom_identifier_mut() - // { - // let mut prefixed_identifier = prefix.clone(); - // prefixed_identifier.push_str(existing.resolve()); - - // // Update the existing identifier to point to a new one with the prefix applied - // *existing = prefixed_identifier.into(); - // } - // } - - // let define = ExprKind::Define(Box::new(Define::new( - // owned_name, - // hash_get, - // SyntaxObject::default(TokenType::Define), - // ))); - - // require_defines.push(define); - // } x if x == *REQUIRE_IDENT_SPEC => { // Directly expand into define/contract, but with the value just being the hash get below @@ -414,7 +348,7 @@ impl ModuleManager { continue; } - let hash_get = expr_list![ + let mut hash_get = expr_list![ ExprKind::atom(*PROTO_HASH_GET), ExprKind::atom( "__module-".to_string() + &other_module_prefix @@ -450,6 +384,17 @@ impl ModuleManager { } } + // Just replace with the function call? + + if let Some(capability) = &require_object.with_capabilities + { + hash_get = ExprKind::List(List::new(vec![ + ExprKind::atom("#%wrap-with-capability"), + capability.clone(), + hash_get, + ])); + } + let define = ExprKind::Define(Box::new(Define::new( owned_name, hash_get, @@ -483,7 +428,7 @@ impl ModuleManager { continue; } - let hash_get = expr_list![ + let mut hash_get = expr_list![ ExprKind::atom(*PROTO_HASH_GET), ExprKind::atom("__module-".to_string() + &other_module_prefix), ExprKind::Quote(Box::new(Quote::new( @@ -518,6 +463,14 @@ impl ModuleManager { } } + if let Some(capability) = &require_object.with_capabilities { + hash_get = ExprKind::List(List::new(vec![ + ExprKind::atom("#%wrap-with-capability"), + capability.clone(), + hash_get, + ])); + } + let define = ExprKind::Define(Box::new(Define::new( owned_provide, hash_get, @@ -1024,6 +977,7 @@ impl CompiledModule { &self, modules: &FxHashMap, global_macro_map: &FxHashMap, + compilation_stack: &[CapabilitySpec], ) -> Result { let mut globals = collect_globals(&self.ast); @@ -1270,6 +1224,8 @@ impl CompiledModule { // have to mangle this differently globals.insert(*provide_ident); + // TODO: This is where we'll have to insert the wrapper call + // to figure out how to expand to the proper capability function. let define = ExprKind::Define(Box::new(Define::new( ExprKind::atom(prefix.clone() + provide_ident.resolve()), expr_list![ @@ -1332,10 +1288,6 @@ impl CompiledModule { if let Some(qualifier) = l.first_ident() { match qualifier { x if *x == *REQUIRE_IDENT_SPEC => { - // *provide = expand(l.get(2).unwrap().clone(), global_macro_map)?; - - // *provide = expand(l.) - provide.0 = l.get(1).unwrap().clone(); let mut provide_expr = l.get(2).unwrap().clone(); @@ -1344,39 +1296,9 @@ impl CompiledModule { provide.1 = provide_expr; continue; - - // name_unmangler.unmangle_expr(provide); } - // x if *x == *CONTRACT_OUT => { - // // Update the item to point to just the name - // // - // // *provide = l.get(1).unwrap().clone(); - // // { - // // println!("---------"); - // // println!("Provide expr: {}", l.to_string()); - // // } - - // provide.0 = l.get(1).unwrap().clone(); - - // let mut provide_expr = expr_list![ - // ExprKind::ident("bind/c"), - // l.get(2).unwrap().clone(), - // l.get(1).unwrap().clone(), - // ExprKind::Quote(Box::new(Quote::new( - // l.get(1).unwrap().clone(), - // SyntaxObject::default(TokenType::Quote) - // ))), - // ]; - - // expand(&mut provide_expr, global_macro_map)?; - - // provide.1 = provide_expr; - - // name_unmangler.unmangle_expr(&mut provide.1); - // // continue; - // } _ => { - stop!(TypeMismatch => "bar provide expects either an identifier, (for-syntax ), or (contract/out ...)") + stop!(TypeMismatch => "provide expects either an identifier, (for-syntax ), or (contract/out ...)") } } } else { @@ -1481,17 +1403,113 @@ impl CompiledModule { let mut builtin_definitions = Vec::new(); + struct Accumulator<'a> { + prev: &'a PathBuf, + capabilities: Vec<&'a ExprKind>, + } + + let result = compilation_stack + .iter() + .try_rfold( + Accumulator { + prev: &self.name, + capabilities: Vec::new(), + }, + |mut acc, spec| { + if let Some(capability_expr) = spec.capabilities.get(acc.prev) { + acc.capabilities.push(capability_expr); + + Ok(Accumulator { + prev: &spec.caller, + capabilities: acc.capabilities, + }) + } else { + Err(()) + } + }, + ) + .map(|x| x.capabilities) + .unwrap_or_default(); + + // The capabilities to apply to myself, alongside any upstream capabilities? + // let capabilities_to_apply: Vec<_> = compilation_stack + // .iter() + // .filter_map(|x| x.capabilities.get(&self.name)) + // .collect(); + + let maybe_wrap_with_capabilities = |expr: &mut ExprKind| { + if result.is_empty() { + return; + } + + let matching_span = expr.span(); + + let mut expressions: Vec = result.iter().map(|x| (*x).clone()).collect(); + + expressions.insert(0, std::mem::take(expr)); + + let mut caller = ExprKind::ident("#%with-capabilities"); + + if let Some(matching_span) = matching_span { + caller.atom_syntax_object_mut().unwrap().span = matching_span; + } + + expressions.insert(0, caller); + + *expr = ExprKind::List(List::new(expressions)); + }; + exprs.retain_mut(|expr| { - if let ExprKind::Define(d) = expr { - if is_a_builtin_definition(d) { - builtin_definitions.push(std::mem::take(expr)); - false - } else { + match expr { + ExprKind::Define(d) => { + if is_a_builtin_definition(d) { + builtin_definitions.push(std::mem::take(expr)); + false + } else { + if let ExprKind::LambdaFunction(_) = d.body { + true + } else { + maybe_wrap_with_capabilities(&mut d.body); + true + } + } + } + ExprKind::Begin(b) => { + for expr in b.exprs.iter_mut() { + if let ExprKind::Define(d) = expr { + if let ExprKind::LambdaFunction(_) = d.body { + continue; + } else { + maybe_wrap_with_capabilities(&mut d.body); + continue; + } + } + + maybe_wrap_with_capabilities(expr); + } + + true + } + _ => { + maybe_wrap_with_capabilities(expr); + true } - } else { - true } + + // if let ExprKind::Define(d) = expr { + // if is_a_builtin_definition(d) { + // builtin_definitions.push(std::mem::take(expr)); + // false + // } else { + // true + // } + // } else { + // // This is a top level expression, or at least it seems like it + // // so we should _probably_ do something about it? + // maybe_wrap_with_capabilities(expr); + // true + // } }); builtin_definitions.append(&mut provide_definitions); @@ -1595,6 +1613,9 @@ pub struct RequireObject { for_syntax: bool, idents_to_import: Vec, prefix: Option, + // Capabilities to apply post macro expansion, assuming it is + // a function + with_capabilities: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -1619,6 +1640,9 @@ struct RequireObjectBuilder { idents_to_import: Vec, // Built up prefix prefix: Option, + // Capabilities to apply post macro expansion, assuming it is + // a function + with_capabilities: Option, } impl RequireObjectBuilder { @@ -1632,10 +1656,15 @@ impl RequireObjectBuilder { for_syntax: self.for_syntax, idents_to_import: self.idents_to_import, prefix: self.prefix, + with_capabilities: self.with_capabilities, }) } } +// TODO: Refactor more or less this whole thing. It is some of the ugliest +// code, and needs to be pared down in order for this whole operation to +// be scalable. Realisticallly we shouldn't pass these individual references, they +// should just be references to the struct that owns them. struct ModuleBuilder<'a> { name: PathBuf, main: bool, @@ -1643,7 +1672,6 @@ struct ModuleBuilder<'a> { macro_map: FxHashMap, // TODO: Change the requires / requires_for_syntax to just be a require enum? require_objects: Vec, - provides: Vec, provides_for_syntax: Vec, compiled_modules: &'a mut FxHashMap, @@ -1655,6 +1683,17 @@ struct ModuleBuilder<'a> { global_macro_map: &'a FxHashMap, custom_builtins: &'a HashMap, search_dirs: &'a [PathBuf], + // This really should just be a capability wrapper + compilation_stack: &'a mut Vec, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +struct CapabilitySpec { + // The calling module + caller: PathBuf, + + // What capabilities did this module apply to the caller? + capabilities: HashMap, } impl<'a> ModuleBuilder<'a> { @@ -1672,6 +1711,7 @@ impl<'a> ModuleBuilder<'a> { global_macro_map: &'a FxHashMap, custom_builtins: &'a HashMap, search_dirs: &'a [PathBuf], + compilation_stack: &'a mut Vec, ) -> Result { // TODO don't immediately canonicalize the path unless we _know_ its coming from a path // change the path to not always be required @@ -1704,6 +1744,7 @@ impl<'a> ModuleBuilder<'a> { global_macro_map, custom_builtins, search_dirs, + compilation_stack, }) } @@ -1753,6 +1794,23 @@ impl<'a> ModuleBuilder<'a> { self.visited.insert(self.name.clone()); + // We're now entering this module + self.compilation_stack.push({ + CapabilitySpec { + caller: self.name.clone(), + // Include a mapping of applied capabilities, if there are any. + capabilities: self + .require_objects + .iter() + .filter_map(|x| { + x.with_capabilities + .clone() + .map(|c| (x.path.get_path().into_owned(), c)) + }) + .collect(), + } + }); + if self.main { let exprs = std::mem::take(&mut self.source_ast); self.source_ast = exprs @@ -1819,6 +1877,7 @@ impl<'a> ModuleBuilder<'a> { self.builtin_modules.clone(), self.global_macro_map, self.custom_builtins, + self.compilation_stack, )?; // Walk the tree and compile any dependencies @@ -1883,6 +1942,7 @@ impl<'a> ModuleBuilder<'a> { self.global_macro_map, self.custom_builtins, self.search_dirs, + self.compilation_stack, )?; // Walk the tree and compile any dependencies @@ -1930,6 +1990,8 @@ impl<'a> ModuleBuilder<'a> { // new_exprs.pretty_print(); + self.compilation_stack.pop(); + Ok(new_exprs) } @@ -2291,7 +2353,11 @@ impl<'a> ModuleBuilder<'a> { // self.name // ); - let result = module.to_top_level_module(self.compiled_modules, self.global_macro_map)?; + let result = module.to_top_level_module( + self.compiled_modules, + self.global_macro_map, + self.compilation_stack, + )?; // println!("{}", result.to_pretty(60)); @@ -2621,6 +2687,18 @@ impl<'a> ModuleBuilder<'a> { } } + Some(x) if *x == *CAPABILITIES_IN => { + if l.args.len() != 3 { + stop!(BadSyntax => "capabilities-in expects a capability expression to apply to a given file or module"; opt l.location); + } + + let expression = l.args[1].clone(); + + require_object.with_capabilities = Some(expression); + + self.parse_require_object_inner(home, r, &l.args[2], require_object)?; + } + Some(x) if *x == *PREFIX_IN => { if l.args.len() != 3 { stop!(BadSyntax => "prefix-in expects a prefix to prefix a given file or module"; opt l.location); @@ -2786,6 +2864,7 @@ impl<'a> ModuleBuilder<'a> { builtin_modules: ModuleContainer, global_macro_map: &'a FxHashMap, custom_builtins: &'a HashMap, + compilation_stack: &'a mut Vec, ) -> Result { ModuleBuilder::raw( name, @@ -2798,6 +2877,7 @@ impl<'a> ModuleBuilder<'a> { global_macro_map, custom_builtins, &[], + compilation_stack, ) .parse_builtin(input) } @@ -2813,6 +2893,7 @@ impl<'a> ModuleBuilder<'a> { global_macro_map: &'a FxHashMap, custom_builtins: &'a HashMap, search_dirs: &'a [PathBuf], + compilation_stack: &'a mut Vec, ) -> Result { ModuleBuilder::raw( name, @@ -2825,6 +2906,7 @@ impl<'a> ModuleBuilder<'a> { global_macro_map, custom_builtins, search_dirs, + compilation_stack, ) .parse_from_path() } @@ -2840,6 +2922,7 @@ impl<'a> ModuleBuilder<'a> { global_macro_map: &'a FxHashMap, custom_builtins: &'a HashMap, search_dirs: &'a [PathBuf], + compilation_stack: &'a mut Vec, ) -> Self { ModuleBuilder { name, @@ -2860,6 +2943,7 @@ impl<'a> ModuleBuilder<'a> { global_macro_map, custom_builtins, search_dirs, + compilation_stack, } } diff --git a/crates/steel-core/src/compiler/program.rs b/crates/steel-core/src/compiler/program.rs index fdfbc1486..b095f1dce 100644 --- a/crates/steel-core/src/compiler/program.rs +++ b/crates/steel-core/src/compiler/program.rs @@ -403,6 +403,7 @@ define_symbols! { FOR_SYNTAX => "for-syntax", PREFIX_IN => "prefix-in", ONLY_IN => "only-in", + CAPABILITIES_IN => "capabilities-in", DATUM_SYNTAX => "datum->syntax", SYNTAX_SPAN => "#%syntax-span", IF => "if", diff --git a/crates/steel-core/src/primitives/fs.rs b/crates/steel-core/src/primitives/fs.rs index 8af4edc36..8c69dd28c 100644 --- a/crates/steel-core/src/primitives/fs.rs +++ b/crates/steel-core/src/primitives/fs.rs @@ -1,5 +1,6 @@ use crate::rvals::{Custom, Result, SteelString, SteelVal}; use crate::steel_vm::builtin::BuiltInModule; +use crate::values::capabilities::{FileSystemAccessKind, FileSystemAccessRequest}; use crate::{steelerr, stop, throw}; use dirs; use std::env::current_dir; @@ -59,6 +60,13 @@ pub fn fs_module() -> BuiltInModule { /// Deletes the directory #[steel_derive::function(name = "delete-directory!")] pub fn delete_directory(directory: &SteelString) -> Result { + // Check that we have access to this directory at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Write, + resource: directory.as_str(), + } + .check()?; + std::fs::remove_dir_all(directory.as_str())?; Ok(SteelVal::Void) } @@ -66,6 +74,13 @@ pub fn delete_directory(directory: &SteelString) -> Result { /// Creates the directory #[steel_derive::function(name = "create-directory!")] pub fn create_directory(directory: &SteelString) -> Result { + // Check that we have access to this directory at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Write, + resource: directory.as_str(), + } + .check()?; + std::fs::create_dir_all(directory.as_str())?; Ok(SteelVal::Void) @@ -77,6 +92,20 @@ pub fn copy_directory_recursively( source: &SteelString, destination: &SteelString, ) -> Result { + // Check that we have access to read this directory at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Read, + resource: source.as_str(), + } + .check()?; + + // Check that we have access to this directory at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Write, + resource: destination.as_str(), + } + .check()?; + copy_recursively(source.as_str(), destination.as_str())?; Ok(SteelVal::Void) @@ -85,18 +114,39 @@ pub fn copy_directory_recursively( /// Checks if a path exists #[steel_derive::function(name = "path-exists?")] pub fn path_exists(path: &SteelString) -> Result { + // Check that we have access to read this path at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Read, + resource: path.as_str(), + } + .check()?; + Ok(SteelVal::BoolV(Path::new(path.as_ref()).exists())) } /// Checks if a path is a file #[steel_derive::function(name = "is-file?")] pub fn is_file(path: &SteelString) -> Result { + // Check that we have access to read this path at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Read, + resource: path.as_str(), + } + .check()?; + Ok(SteelVal::BoolV(Path::new(path.as_ref()).is_file())) } /// Checks if a path is a directory #[steel_derive::function(name = "is-dir?")] pub fn is_dir(path: &SteelString) -> Result { + // Check that we have access to read this path at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Read, + resource: path.as_str(), + } + .check()?; + Ok(SteelVal::BoolV(Path::new(path.as_ref()).is_dir())) } @@ -125,6 +175,13 @@ pub fn file_name(path: &SteelString) -> Result { /// Returns canonical path with all components normalized #[steel_derive::function(name = "canonicalize-path")] pub fn canonicalize_path(path: &SteelString) -> Result { + // Check that we have access to read this path at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Read, + resource: path.as_str(), + } + .check()?; + let path = path.as_str(); let canonicalized = if path.len() > 0 && path.starts_with('~') { if path.len() > 1 && !path.starts_with("~/") { @@ -151,6 +208,13 @@ pub fn canonicalize_path(path: &SteelString) -> Result { /// Returns the contents of the directory as a list #[steel_derive::function(name = "read-dir")] pub fn read_dir(path: &SteelString) -> Result { + // Check that we have access to read this path at all + FileSystemAccessRequest { + kind: FileSystemAccessKind::Read, + resource: path.as_str(), + } + .check()?; + let p = Path::new(path.as_ref()); if p.is_dir() { let iter = p.read_dir(); diff --git a/crates/steel-core/src/scheme/modules/parameters.scm b/crates/steel-core/src/scheme/modules/parameters.scm index fc4687deb..92f8ebd46 100644 --- a/crates/steel-core/src/scheme/modules/parameters.scm +++ b/crates/steel-core/src/scheme/modules/parameters.scm @@ -3,7 +3,10 @@ call/cc call-with-current-continuation make-parameter - continuation?) + continuation? + + (for-syntax with-capability) + #%wrap-with-capability) ;;;;;; Parameters ;;;;; @@ -165,3 +168,29 @@ (set! winders (cdr winders)) (out) ans*))) + +;; Use this during macro expansion to make sure that things +;; get wrapped in the proper capabilities +(define-syntax with-capability + (syntax-rules () + [(_ capability-expr guarded-expr) + ; (let ([evaluated-capability capability-expr]) + (dynamic-wind (lambda () (#%push-capability capability-expr)) + (lambda () guarded-expr) + (lambda () (#%pop-capability)))])) + +(define-syntax #%with-capabilities + (syntax-rules () + [(_ guarded-expr capabilities ...) + (let ([evaluated-capabilities (list capabilities ...)]) + (dynamic-wind (lambda () (apply #%push-capabilities evaluated-capabilities)) + (lambda () guarded-expr) + (lambda () (#%pop-n-capabilities (length evaluated-capabilities)))))])) + +;; Wrap the function with a given capability. +;; At the moment, this does not attempt to specialize the arity, however +;; we definitely should because otherwise this becomes rather slow +(define (#%wrap-with-capability capability maybe-function) + (if (function? maybe-function) + (lambda args (with-capability capability (apply maybe-function args))) + maybe-function)) diff --git a/crates/steel-core/src/steel_vm/primitives.rs b/crates/steel-core/src/steel_vm/primitives.rs index 9b5aec31f..8440471f8 100644 --- a/crates/steel-core/src/steel_vm/primitives.rs +++ b/crates/steel-core/src/steel_vm/primitives.rs @@ -41,6 +41,7 @@ use crate::{ vm::threads::threading_module, }, values::{ + capabilities::capabilities_module, closed::HeapRef, functions::{attach_contract_struct, get_contract, LambdaMetadataTable}, lists::List, @@ -316,6 +317,8 @@ thread_local! { pub static TYPE_ID_MODULE: BuiltInModule = build_type_id_module(); pub static OPTION_MODULE: BuiltInModule = build_option_structs(); + pub static CAPABILITIES_MODULE: BuiltInModule = capabilities_module(); + #[cfg(feature = "dylibs")] pub static FFI_MODULE: BuiltInModule = ffi_module(); @@ -340,7 +343,6 @@ pub fn prelude() -> BuiltInModule { .with_module(STRING_MODULE.with(|x| x.clone())) .with_module(VECTOR_MODULE.with(|x| x.clone())) .with_module(STREAM_MODULE.with(|x| x.clone())) - // .with_module(CONTRACT_MODULE.with(|x| x.clone())) .with_module(IDENTITY_MODULE.with(|x| x.clone())) .with_module(NUMBER_MODULE.with(|x| x.clone())) .with_module(EQUALITY_MODULE.with(|x| x.clone())) @@ -361,6 +363,7 @@ pub fn prelude() -> BuiltInModule { .with_module(TIME_MODULE.with(|x| x.clone())) .with_module(THREADING_MODULE.with(|x| x.clone())) .with_module(BYTEVECTOR_MODULE.with(|x| x.clone())) + .with_module(CAPABILITIES_MODULE.with(|x| x.clone())) } pub fn register_builtin_modules_without_io(engine: &mut Engine) { @@ -383,7 +386,6 @@ pub fn register_builtin_modules_without_io(engine: &mut Engine) { .register_module(STRING_MODULE.with(|x| x.clone())) .register_module(VECTOR_MODULE.with(|x| x.clone())) .register_module(STREAM_MODULE.with(|x| x.clone())) - // .register_module(CONTRACT_MODULE.with(|x| x.clone())) .register_module(IDENTITY_MODULE.with(|x| x.clone())) .register_module(NUMBER_MODULE.with(|x| x.clone())) .register_module(EQUALITY_MODULE.with(|x| x.clone())) @@ -391,12 +393,11 @@ pub fn register_builtin_modules_without_io(engine: &mut Engine) { .register_module(TRANSDUCER_MODULE.with(|x| x.clone())) .register_module(SYMBOL_MODULE.with(|x| x.clone())) .register_module(SANDBOXED_IO_MODULE.with(|x| x.clone())) - // .register_module(FS_MODULE.with(|x| x.clone())) - // .register_module(PORT_MODULE.with(|x| x.clone())) .register_module(SANDBOXED_META_MODULE.with(|x| x.clone())) .register_module(JSON_MODULE.with(|x| x.clone())) .register_module(CONSTANTS_MODULE.with(|x| x.clone())) .register_module(SYNTAX_MODULE.with(|x| x.clone())) + .register_module(CAPABILITIES_MODULE.with(|x| x.clone())) .register_module(PRELUDE_MODULE.with(|x| x.clone())); } @@ -477,7 +478,8 @@ pub fn register_builtin_modules(engine: &mut Engine) { .register_module(TIME_MODULE.with(|x| x.clone())) .register_module(RANDOM_MODULE.with(|x| x.clone())) .register_module(THREADING_MODULE.with(|x| x.clone())) - .register_module(BYTEVECTOR_MODULE.with(|x| x.clone())); + .register_module(BYTEVECTOR_MODULE.with(|x| x.clone())) + .register_module(CAPABILITIES_MODULE.with(|x| x.clone())); #[cfg(feature = "dylibs")] engine.register_module(FFI_MODULE.with(|x| x.clone())); @@ -572,6 +574,7 @@ pub static ALL_MODULES: &str = r#" (require-builtin steel/core/types) (require-builtin steel/threads) (require-builtin steel/bytevectors) + (require-builtin steel/capabilities) (require-builtin steel/hash as #%prim.) (require-builtin steel/sets as #%prim.) @@ -598,6 +601,7 @@ pub static ALL_MODULES: &str = r#" (require-builtin steel/core/types as #%prim.) (require-builtin steel/threads as #%prim.) (require-builtin steel/bytevectors as #%prim.) + (require-builtin steel/capabilities as #%prim.) "#; pub static ALL_MODULES_RESERVED: &str = r#" @@ -626,6 +630,7 @@ pub static ALL_MODULES_RESERVED: &str = r#" (require-builtin steel/core/types as #%prim.) (require-builtin steel/threads as #%prim.) (require-builtin steel/bytevectors as #%prim.) + (require-builtin steel/capabilities as #%prim.) "#; pub static SANDBOXED_MODULES: &str = r#" @@ -646,6 +651,7 @@ pub static SANDBOXED_MODULES: &str = r#" (require-builtin steel/json) (require-builtin steel/constants) (require-builtin steel/syntax) + (require-builtin steel/capabilities) "#; // TODO: Clean this up a lot diff --git a/crates/steel-core/src/steel_vm/vm.rs b/crates/steel-core/src/steel_vm/vm.rs index c8edfea85..cb535bb62 100644 --- a/crates/steel-core/src/steel_vm/vm.rs +++ b/crates/steel-core/src/steel_vm/vm.rs @@ -11,6 +11,8 @@ use crate::rvals::number_equality; use crate::steel_vm::primitives::steel_not; use crate::steel_vm::primitives::steel_set_box_mutable; use crate::steel_vm::primitives::steel_unbox_mutable; +use crate::values::capabilities::install_capability; +use crate::values::capabilities::CapabilityManager; use crate::values::closed::Heap; use crate::values::functions::SerializedLambda; use crate::values::structs::UserDefinedStruct; @@ -126,17 +128,7 @@ impl DehydratedStackTrace { } } -// // Eventually expand this to other kinds of continuations -// #[derive(Debug, Clone, Copy)] -// pub enum ContinuationMark { -// Default, -// Transducer, -// } - -// This should be the go to thing for handling basically everything we need -// Then - do I want to always reference the last one, or just refer to the current one? -// TODO: We'll need to add these functions to the GC as well - +// TODO: Shrink the size of this as much as possible #[derive(Debug, Clone)] pub struct StackFrame { sp: usize, @@ -269,6 +261,8 @@ pub struct SteelThread { pub(crate) stack_frames: Vec, pub(crate) constant_map: ConstantMap, pub(crate) interrupted: Option>, + // Maybe this is... just a reference to a global one? + pub(crate) capabilities: CapabilityManager, } #[derive(Clone)] @@ -334,6 +328,7 @@ impl SteelThread { // with the executables constant_map: DEFAULT_CONSTANT_MAP.with(|x| x.clone()), interrupted: Default::default(), + capabilities: CapabilityManager::fetch_installed(), } } @@ -591,6 +586,7 @@ impl SteelThread { } self.stack.clear(); + self.capabilities.clear(); return Err(e); } else { @@ -600,6 +596,7 @@ impl SteelThread { // Clean up self.stack.clear(); + self.capabilities.clear(); return result; } @@ -746,8 +743,6 @@ impl Continuation { ctx.set_state_from_continuation(definitely_closed); return; - - // println!("CLOSING MARKS WHEN SETTING STATE FROM CONTINUATION: pop count: {}", ctx.pop_count); } } @@ -843,6 +838,8 @@ pub struct ClosedContinuation { sp: usize, pop_count: usize, + capabilities: CapabilityManager, + #[cfg(debug_assertions)] closed_continuation: Option>, } @@ -1145,6 +1142,7 @@ impl<'a> VmCore<'a> { ip: self.ip, sp: self.sp, pop_count: self.pop_count, + capabilities: self.thread.capabilities.clone(), #[cfg(debug_assertions)] closed_continuation: None, } @@ -1167,6 +1165,7 @@ impl<'a> VmCore<'a> { ip: self.ip, sp: self.sp, pop_count: self.pop_count, + capabilities: self.thread.capabilities.clone(), #[cfg(debug_assertions)] closed_continuation: None, } @@ -1243,6 +1242,10 @@ impl<'a> VmCore<'a> { self.pop_count = continuation.pop_count; self.thread.stack_frames = continuation.stack_frames; self.thread.current_frame = continuation.current_frame; + self.thread.capabilities = continuation.capabilities; + + // Set up the capabilities + install_capability(&self.thread.capabilities); } // #[inline(always)] diff --git a/crates/steel-core/src/steel_vm/vm/threads.rs b/crates/steel-core/src/steel_vm/vm/threads.rs index 60d270a0e..45d422f97 100644 --- a/crates/steel-core/src/steel_vm/vm/threads.rs +++ b/crates/steel-core/src/steel_vm/vm/threads.rs @@ -381,6 +381,7 @@ fn spawn_thread_result(ctx: &mut VmCore, args: &[SteelVal]) -> Result stack_frames: Vec::with_capacity(32), constant_map, interrupted: Default::default(), + capabilities: CapabilityManager::fetch_installed(), }; #[cfg(feature = "profiling")] diff --git a/crates/steel-core/src/values/capabilities.rs b/crates/steel-core/src/values/capabilities.rs new file mode 100644 index 000000000..88418ef87 --- /dev/null +++ b/crates/steel-core/src/values/capabilities.rs @@ -0,0 +1,197 @@ +use std::{ + cell::RefCell, + path::{Path, PathBuf}, + rc::Rc, +}; + +use crate::{ + rvals::{as_underlying_type, Custom, Result}, + steel_vm::{builtin::BuiltInModule, register_fn::RegisterFn}, + SteelVal, +}; + +thread_local! { + pub static CURRENT_CAPABILITIES: RefCell = RefCell::new(CapabilityManager::new()); +} + +#[steel_derive::define_module(name = "steel/capabilities")] +pub fn capabilities_module() -> BuiltInModule { + let mut module = BuiltInModule::new("steel/capabilities"); + + module + .register_fn("#%pop-capability", pop_capability) + .register_fn("#%push-capability", push_capability) + .register_native_fn_definition(PUSH_CAPABILITIES_DEFINITION) + .register_fn("#%pop-n-capabilities", pop_n_capabilities) + .register_fn("file-system-access", Capability::new_file_system) + .register_fn("fs/write", || FileSystemAccessKind::Write) + .register_fn("fs/read", || FileSystemAccessKind::Read); + + module +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CapabilityManager { + capabilities: Rc>>, +} + +impl CapabilityManager { + pub fn fetch_installed() -> Self { + CURRENT_CAPABILITIES.with(|x| x.borrow().clone()) + } + + pub fn clear(&mut self) { + self.capabilities.borrow_mut().clear(); + } + + pub fn new() -> Self { + Self { + capabilities: Rc::new(RefCell::new(Vec::new())), + } + } +} + +pub fn install_capability(capability_manager: &CapabilityManager) { + CURRENT_CAPABILITIES.with(|x| *(x.borrow_mut()) = capability_manager.clone()); +} + +pub fn take_current_capability() -> CapabilityManager { + CURRENT_CAPABILITIES.with(|x| { + // Type inference doesn't love this + let inner: CapabilityManager = + std::mem::replace(&mut x.borrow_mut(), CapabilityManager::new()); + + inner + }) +} + +#[steel_derive::native(name = "#%push-capabilities", constant = true, arity = "AtLeast(0)")] +pub fn push_capabilities(args: &[SteelVal]) -> Result { + for arg in args { + if let SteelVal::Custom(c) = arg { + if let Some(underlying) = as_underlying_type::(c.borrow().as_ref()) { + push_capability(underlying) + } + } + } + + Ok(SteelVal::Void) +} + +pub fn push_capability(capability: &Capability) { + CURRENT_CAPABILITIES.with(|x| { + x.borrow_mut() + .capabilities + .borrow_mut() + .push(capability.clone()) + }); +} + +pub fn pop_capability() { + CURRENT_CAPABILITIES.with(|x| x.borrow_mut().capabilities.borrow_mut().pop()); +} + +pub fn pop_n_capabilities(n: usize) { + for _ in 0..n { + pop_capability() + } +} + +pub struct FileSystemAccessRequest<'a> { + pub kind: FileSystemAccessKind, + pub resource: &'a str, +} + +impl<'a> FileSystemAccessRequest<'a> { + pub fn check(&self) -> Result<()> { + CURRENT_CAPABILITIES.with(|x| { + let guard = x.borrow(); + + // TODO: Actually check that this works? + + let capabilities = guard.capabilities.borrow(); + + if capabilities.is_empty() { + return Ok(()); + } + + // The default, is that with no specification, it has + // full access. Otherwise, we check each specification in scope + // for a compatibility - if there aren't any compatibilities, + // we bail out + let mut found_incompatibility = false; + + // Check the access to the resource + for value in capabilities.iter() { + if let Capability::FileSystem(FileSystemCapability { kind, resource }) = value { + let matches_pattern = resource.matches(self.resource); + + if matches_pattern { + // (Asking, Granted) + match (self.kind, kind) { + (FileSystemAccessKind::Write, FileSystemAccessKind::Read) => { + stop!(Generic => "This module has not been given + access to this kind of system resource!") + } + (_, _) => { + found_incompatibility = false; + + continue; + } + } + } else { + found_incompatibility = true; + } + } + } + + if found_incompatibility { + stop!(Generic => "Access denied (capabilities)") + } + + Ok(()) + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Capability { + FileSystem(FileSystemCapability), + Dylib(DylibCapability), + Process(ProcessCapability), +} + +impl Capability { + pub fn new_file_system(kind: FileSystemAccessKind, resource: String) -> Self { + Self::FileSystem(FileSystemCapability { + kind, + resource: glob::Pattern::new(&resource).unwrap(), + }) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum FileSystemAccessKind { + Read, + Write, +} + +impl Custom for FileSystemAccessKind {} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct FileSystemCapability { + kind: FileSystemAccessKind, + resource: glob::Pattern, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +struct DylibCapability; + +impl Custom for Capability {} + +#[derive(Clone, PartialEq, Eq, Debug)] +struct ProcessCapability { + allowed_binary: String, +} + +impl Custom for ProcessCapability {} diff --git a/crates/steel-core/src/values/mod.rs b/crates/steel-core/src/values/mod.rs index 4b6f24e89..e23921f23 100644 --- a/crates/steel-core/src/values/mod.rs +++ b/crates/steel-core/src/values/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod capabilities; #[allow(dead_code)] pub(crate) mod closed; pub(crate) mod contracts; diff --git a/crates/steel-parser/src/ast.rs b/crates/steel-parser/src/ast.rs index 9a01648e0..3bb2b187e 100644 --- a/crates/steel-parser/src/ast.rs +++ b/crates/steel-parser/src/ast.rs @@ -258,6 +258,13 @@ impl ExprKind { } } + pub fn atom_syntax_object_mut(&mut self) -> Option<&mut SyntaxObject> { + match self { + Self::Atom(Atom { syn }) => Some(syn), + _ => None, + } + } + pub fn define_syntax_ident(&self) -> bool { match self { Self::Atom(Atom { diff --git a/crates/steel-parser/src/span.rs b/crates/steel-parser/src/span.rs index a9b596083..d75a0073c 100644 --- a/crates/steel-parser/src/span.rs +++ b/crates/steel-parser/src/span.rs @@ -118,7 +118,7 @@ impl Span { impl fmt::Debug for Span { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}..{}", self.start, self.end) + write!(f, "{}..{} @ {:?}", self.start, self.end, self.source_id) } } diff --git a/src/lib.rs b/src/lib.rs index 5f306e484..30da74faa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,8 +89,9 @@ pub fn run(clap_args: Args) -> Result<(), Box> { let res = vm.compile_and_run_raw_program_with_path(contents.clone(), path.clone()); if let Err(e) = res { - e.emit_result(path.to_str().unwrap(), &contents); + // e.emit_result(path.to_str().unwrap(), &contents); // process::exit(1); + vm.raise_error(e.clone()); return Err(Box::new(e)); } @@ -146,7 +147,9 @@ pub fn run(clap_args: Args) -> Result<(), Box> { vm.debug_print_build(path.to_str().unwrap().to_string(), program) .unwrap(); } - Err(e) => e.emit_result(path.to_str().unwrap(), &contents), + Err(e) => { + vm.raise_error(e); + } // e.emit_result(path.to_str().unwrap(), &contents), } Ok(()) @@ -178,7 +181,9 @@ pub fn run(clap_args: Args) -> Result<(), Box> { match res { Ok(ast) => println!("{ast}"), - Err(e) => e.emit_result(path.to_str().unwrap(), &contents), + Err(e) => { + vm.raise_error(e); + } // e.emit_result(path.to_str().unwrap(), &contents), } Ok(()) @@ -208,7 +213,8 @@ pub fn run(clap_args: Args) -> Result<(), Box> { let res = vm.compile_and_run_raw_program_with_path(contents.clone(), path.clone()); if let Err(e) = res { - e.emit_result(path.to_str().unwrap(), &contents); + // e.emit_result(path.to_str().unwrap(), &contents); + vm.raise_error(e); } run_repl(vm)?;