diff --git a/fish-rust/src/common.rs b/fish-rust/src/common.rs index 20f4d1a95..2d80ca512 100644 --- a/fish-rust/src/common.rs +++ b/fish-rust/src/common.rs @@ -30,10 +30,9 @@ use std::os::fd::{AsRawFd, RawFd}; use std::os::unix::prelude::OsStringExt; use std::path::PathBuf; -use std::rc::Rc; use std::str::FromStr; use std::sync::atomic::{AtomicI32, AtomicU32, Ordering}; -use std::sync::{Mutex, TryLockError}; +use std::sync::{Arc, Mutex, TryLockError}; use std::time; use widestring::Utf32String; use widestring_suffix::widestrs; @@ -1376,7 +1375,7 @@ fn extract_most_significant_digit(xp: &mut u64) -> u8 { } /// Stored in blocks to reference the file which created the block. -pub type FilenameRef = Rc; +pub type FilenameRef = Arc; /// This function should be called after calling `setlocale()` to perform fish specific locale /// initialization. diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index 95f4f567c..df8d6ca22 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -18,6 +18,7 @@ pub type wchar_t = u32; include_cpp! { + #include "autoload.h" #include "builtin.h" #include "color.h" #include "common.h" @@ -155,6 +156,10 @@ generate!("function_invalidate_path") generate!("complete_invalidate_path") generate!("update_wait_on_escape_ms_ffi") + generate!("autoload_t") + generate!("make_autoload_ffi") + generate!("perform_autoload_ffi") + generate!("complete_get_wrap_targets_ffi") } impl parser_t { @@ -337,6 +342,7 @@ fn unpin(self: Pin<&mut Self>) -> &mut Self { } // Implement Repin for our types. +impl Repin for autoload_t {} impl Repin for block_t {} impl Repin for env_stack_t {} impl Repin for env_universal_t {} diff --git a/fish-rust/src/function.rs b/fish-rust/src/function.rs new file mode 100644 index 000000000..8dcef01c1 --- /dev/null +++ b/fish-rust/src/function.rs @@ -0,0 +1,710 @@ +// Functions for storing and retrieving function information. These functions also take care of +// autoloading functions in the $fish_function_path. Actual function evaluation is taken care of by +// the parser and to some degree the builtin handling library. + +use crate::ast::{self, Node}; +use crate::common::{escape, valid_func_name, FilenameRef}; +use crate::env::{EnvStack, Environment}; +use crate::event::{self, EventDescription}; +use crate::ffi::{self, parser_t, Repin}; +use crate::global_safety::RelaxedAtomicBool; +use crate::parse_tree::{NodeRef, ParsedSourceRefFFI}; +use crate::parser_keywords::parser_keywords_is_reserved; +use crate::wchar::{wstr, WString, L}; +use crate::wchar_ext::WExt; +use crate::wchar_ffi::wcstring_list_ffi_t; +use crate::wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI}; +use crate::wutil::{dir_iter::DirIter, gettext::wgettext_expr, sprintf}; +use cxx::{CxxWString, UniquePtr}; +use once_cell::sync::Lazy; +use std::collections::{HashMap, HashSet}; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct FunctionProperties { + /// Reference to the node, along with the parsed source. + pub func_node: NodeRef, + + /// List of all named arguments for this function. + pub named_arguments: Vec, + + /// Description of the function. + pub description: WString, + + /// Mapping of all variables that were inherited from the function definition scope to their + /// values. + pub inherit_vars: HashMap>, + + /// Set to true if invoking this function shadows the variables of the underlying function. + pub shadow_scope: bool, + + /// Whether the function was autoloaded. + /// This is the only field which is mutated after the properties are created. + pub is_autoload: RelaxedAtomicBool, + + /// The file from which the function was created, or None if not from a file. + pub definition_file: Option, + + /// Whether the function was copied. + pub is_copy: bool, + + /// The file from which the function was copied, or None if not from a file. + pub copy_definition_file: Option, + + /// The line number where the specified function was copied. + pub copy_definition_lineno: i32, +} + +pub type FunctionPropertiesRef = Arc; + +/// Type wrapping up the set of all functions. +/// There's only one of these; it's managed by a lock. +struct FunctionSet { + /// The map of all functions by name. + funcs: HashMap, + + /// Tombstones for functions that should no longer be autoloaded. + autoload_tombstones: HashSet, + + /// The autoloader for our functions. + autoloader: cxx::UniquePtr, +} + +impl FunctionSet { + /// Remove a function. + /// \return true if successful, false if it doesn't exist. + fn remove(&mut self, name: &wstr) -> bool { + if self.funcs.remove(name).is_some() { + event::remove_function_handlers(name); + true + } else { + false + } + } + + /// Get the properties for a function, or None if none. + fn get_props(&self, name: &wstr) -> Option { + self.funcs.get(name).cloned() + } + + /// \return true if we should allow autoloading a given function. + fn allow_autoload(&self, name: &wstr) -> bool { + // Prohibit autoloading if we have a non-autoload (explicit) function, or if the function is + // tombstoned. + let props = self.get_props(name); + let has_explicit_func = + props.map_or(false, |p: Arc| !p.is_autoload.load()); + let tombstoned = self.autoload_tombstones.contains(name); + !has_explicit_func && !tombstoned + } +} + +/// The big set of all functions. +static FUNCTION_SET: Lazy> = Lazy::new(|| { + Mutex::new(FunctionSet { + funcs: HashMap::new(), + autoload_tombstones: HashSet::new(), + autoloader: ffi::make_autoload_ffi(L!("fish_function_path").to_ffi()), + }) +}); + +/// Necessary until autoloader has been ported to Rust. +unsafe impl Send for FunctionSet {} + +/// Make sure that if the specified function is a dynamically loaded function, it has been fully +/// loaded. Note this executes fish script code. +fn load(name: &wstr, parser: &mut parser_t) -> bool { + parser.assert_can_execute(); + let mut path_to_autoload: Option = None; + // Note we can't autoload while holding the funcset lock. + // Lock around a local region. + { + let mut funcset: std::sync::MutexGuard = FUNCTION_SET.lock().unwrap(); + if funcset.allow_autoload(name) { + let path = funcset + .autoloader + .as_mut() + .unwrap() + .resolve_command_ffi(&name.to_ffi() /* Environment::globals() */) + .from_ffi(); + if !path.is_empty() { + path_to_autoload = Some(path); + } + } + } + + // Release the lock and perform any autoload, then reacquire the lock and clean up. + if let Some(path_to_autoload) = path_to_autoload.as_ref() { + // Crucially, the lock is acquired after perform_autoload(). + ffi::perform_autoload_ffi(&path_to_autoload.to_ffi(), parser.pin()); + FUNCTION_SET + .lock() + .unwrap() + .autoloader + .as_mut() + .unwrap() + .mark_autoload_finished(&name.to_ffi()); + } + path_to_autoload.is_some() +} + +/// Insert a list of all dynamically loaded functions into the specified list. +fn autoload_names(names: &mut HashSet, get_hidden: bool) { + // TODO: justify this. + let vars = EnvStack::principal(); + let Some(path_var) = vars.get_unless_empty(L!("fish_function_path")) else { + return; + }; + let path_list = path_var.as_list(); + for ndir_str in path_list { + let Ok(mut dir) = DirIter::new(ndir_str) else { + continue; + }; + while let Some(entry) = dir.next() { + let Ok(entry) = entry else { + continue; + }; + let func: &WString = &entry.name; + if !get_hidden && func.char_at(0) == '_' { + continue; + } + let suffix: Option = func.chars().rposition(|x| x == '.'); + // We need a ".fish" *suffix*, it can't be the entire name. + if let Some(suffix) = suffix { + if suffix > 0 && entry.name.slice_from(suffix) == ".fish" { + // Also ignore directories. + if !entry.is_dir() { + let name = entry.name.slice_to(suffix).to_owned(); + names.insert(name); + } + } + } + } + } +} + +/// Add a function. This may mutate \p props to set is_autoload. +pub fn add(name: WString, props: FunctionPropertiesRef) { + let mut funcset = FUNCTION_SET.lock().unwrap(); + + // Historical check. TODO: rationalize this. + if name.is_empty() { + return; + } + + // Remove the old function. + funcset.remove(&name); + + // Check if this is a function that we are autoloading. + props + .is_autoload + .store(funcset.autoloader.autoload_in_progress(&name.to_ffi())); + + // Create and store a new function. + let existing = funcset.funcs.insert(name, props); + assert!( + existing.is_none(), + "Function should not already be present in the table" + ); +} + +/// \return the properties for a function, or None. This does not trigger autoloading. +pub fn get_props(name: &wstr) -> Option { + if parser_keywords_is_reserved(name) { + None + } else { + FUNCTION_SET.lock().unwrap().get_props(name) + } +} + +/// \return the properties for a function, or None, perhaps triggering autoloading. +pub fn get_props_autoload(name: &wstr, parser: &mut parser_t) -> Option { + parser.assert_can_execute(); + if parser_keywords_is_reserved(name) { + return None; + } + load(name, parser); + get_props(name) +} + +/// Returns true if the function named \p cmd exists. +/// This may autoload. +pub fn exists(cmd: &wstr, parser: &mut parser_t) -> bool { + parser.assert_can_execute(); + if !valid_func_name(cmd) { + return false; + } + get_props_autoload(cmd, parser).is_some() +} + +/// Returns true if the function \p cmd either is loaded, or exists on disk in an autoload +/// directory. +pub fn exists_no_autoload(cmd: &wstr) -> bool { + if !valid_func_name(cmd) { + return false; + } + if parser_keywords_is_reserved(cmd) { + return false; + } + let mut funcset = FUNCTION_SET.lock().unwrap(); + // Check if we either have the function, or it could be autoloaded. + funcset.get_props(cmd).is_some() + || funcset + .autoloader + .as_mut() + .unwrap() + .can_autoload(&cmd.to_ffi()) +} + +/// Remove the function with the specified name. +pub fn remove(name: &wstr) { + let mut funcset = FUNCTION_SET.lock().unwrap(); + funcset.remove(name); + // Prevent (re-)autoloading this function. + funcset.autoload_tombstones.insert(name.to_owned()); +} + +// \return the body of a function (everything after the header, up to but not including the 'end'). +fn get_function_body_source(props: &FunctionProperties) -> &wstr { + // We want to preserve comments that the AST attaches to the header (#5285). + // Take everything from the end of the header to the 'end' keyword. + let Some(header_source) = props.func_node.header.try_source_range() else { + return L!(""); + }; + let Some(end_kw_source) = props.func_node.end.try_source_range() else { + return L!(""); + }; + let body_start = header_source.start as usize + header_source.length as usize; + let body_end = end_kw_source.start as usize; + assert!( + body_start <= body_end, + "end keyword should come after header" + ); + props + .func_node + .parsed_source() + .src + .slice_to(body_end) + .slice_from(body_start) +} + +/// Sets the description of the function with the name \c name. +/// This triggers autoloading. +fn set_desc(name: &wstr, desc: WString, parser: &mut parser_t) { + parser.assert_can_execute(); + load(name, parser); + let mut funcset = FUNCTION_SET.lock().unwrap(); + if let Some(props) = funcset.funcs.get(name) { + // Note the description is immutable, as it may be accessed on another thread, so we copy + // the properties to modify it. + let mut new_props = props.as_ref().clone(); + new_props.description = desc; + funcset.funcs.insert(name.to_owned(), Arc::new(new_props)); + } +} + +/// Creates a new function using the same definition as the specified function. Returns true if copy +/// is successful. +pub fn copy(name: &wstr, new_name: WString, parser: &parser_t) -> bool { + let filename = parser.current_filename_ffi().from_ffi(); + let lineno = parser.get_lineno(); + + let mut funcset = FUNCTION_SET.lock().unwrap(); + let Some(props) = funcset.get_props(name) else { + // No such function. + return false; + }; + // Copy the function's props. + let mut new_props = props.as_ref().clone(); + new_props.is_autoload.store(false); + new_props.is_copy = true; + new_props.copy_definition_file = Some(Arc::new(filename)); + new_props.copy_definition_lineno = lineno.into(); + + // Note this will NOT overwrite an existing function with the new name. + // TODO: rationalize if this behavior is desired. + funcset.funcs.entry(new_name).or_insert(Arc::new(new_props)); + return true; +} + +/// Returns all function names. +/// +/// \param get_hidden whether to include hidden functions, i.e. ones starting with an underscore. +pub fn get_names(get_hidden: bool) -> Vec { + let mut names = HashSet::::new(); + let funcset = FUNCTION_SET.lock().unwrap(); + autoload_names(&mut names, get_hidden); + for name in funcset.funcs.keys() { + // Maybe skip hidden. + if !get_hidden && (name.is_empty() || name.char_at(0) == '_') { + continue; + } + names.insert(name.clone()); + } + names.into_iter().collect() +} + +/// Observes that fish_function_path has changed. +pub fn invalidate_path() { + // Remove all autoloaded functions and update the autoload path. + let mut funcset = FUNCTION_SET.lock().unwrap(); + funcset.funcs.retain(|_, props| !props.is_autoload.load()); + funcset.autoloader.as_mut().unwrap().clear(); +} + +impl FunctionProperties { + /// Return the description, localized via wgettext. + pub fn localized_description(&self) -> &'static wstr { + if self.description.is_empty() { + L!("") + } else { + wgettext_expr!(&self.description) + } + } + + /// Return true if this function is a copy. + pub fn is_copy(&self) -> bool { + self.is_copy + } + + /// Return a reference to the function's definition file, or None if it was defined interactively or copied. + pub fn definition_file(&self) -> Option<&wstr> { + self.definition_file.as_ref().map(|f| f.as_utfstr()) + } + + /// Return a reference to the vars that this function has inherited from its definition scope. + pub fn inherit_vars(&self) -> &HashMap> { + &self.inherit_vars + } + + /// If this function is a copy, return a reference to the original definition file, or None if it was defined interactively or copied. + pub fn copy_definition_file(&self) -> Option<&wstr> { + self.copy_definition_file.as_ref().map(|f| f.as_utfstr()) + } + + /// Return the 1-based line number of the function's definition. + pub fn definition_lineno(&self) -> i32 { + // Return one plus the number of newlines at offsets less than the start of our function's + // statement (which includes the header). + // TODO: merge with line_offset_of_character_at_offset? + let Some(source_range) = self.func_node.try_source_range() else { + panic!("Function has no source range"); + }; + let func_start = source_range.start as usize; + let source = &self.func_node.parsed_source().src; + assert!( + func_start <= source.char_count(), + "function start out of bounds" + ); + 1 + source + .slice_to(func_start) + .chars() + .filter(|&c| c == '\n') + .count() as i32 + } + + /// If this function is a copy, return the original line number, else 0. + pub fn copy_definition_lineno(&self) -> i32 { + self.copy_definition_lineno + } + + /// Return a definition of the function, annotated with properties like event handlers and wrap + /// targets. This is to support the 'functions' builtin. + /// Note callers must provide the function name, since the function does not know its own name. + pub fn annotated_definition(&self, name: &wstr) -> WString { + let mut out = WString::new(); + let desc = self.localized_description(); + let def = get_function_body_source(self); + let handlers = event::get_function_handlers(name); + + out.push_str("function "); + // Typically we prefer to specify the function name first, e.g. "function foo --description bar" + // But if the function name starts with a -, we'll need to output it after all the options. + let defer_function_name = name.char_at(0) == '-'; + if !defer_function_name { + out.push_utfstr(&escape(name)); + } + + // Output wrap targets. + for wrap in ffi::complete_get_wrap_targets_ffi(&name.to_ffi()).from_ffi() { + out.push_str(" --wraps="); + out.push_utfstr(&escape(&wrap)); + } + + if !desc.is_empty() { + out.push_str(" --description "); + out.push_utfstr(&escape(desc)); + } + + if !self.shadow_scope { + out.push_str(" --no-scope-shadowing"); + } + + for handler in handlers { + let d = &handler.desc; + match d { + EventDescription::Signal { signal } => { + sprintf!(=> &mut out, " --on-signal %ls", signal.name()); + } + EventDescription::Variable { name } => { + sprintf!(=> &mut out, " --on-variable %ls", name); + } + EventDescription::ProcessExit { pid } => { + sprintf!(=> &mut out, " --on-process-exit %d", pid); + } + EventDescription::JobExit { pid, .. } => { + sprintf!(=> &mut out, " --on-job-exit %d", pid); + } + EventDescription::CallerExit { .. } => { + out.push_str(" --on-job-exit caller"); + } + EventDescription::Generic { param } => { + sprintf!(=> &mut out, " --on-event %ls", param); + } + EventDescription::Any => { + panic!("Unexpected event handler type"); + } + }; + } + + let named = &self.named_arguments; + if !named.is_empty() { + out.push_str(" --argument"); + for name in named { + // TODO: should these names be escaped? + sprintf!(=> &mut out, " %ls", name); + } + } + + // Output the function name if we deferred it. + if defer_function_name { + out.push_str(" -- "); + out.push_utfstr(&escape(name)); + } + + // Output any inherited variables as `set -l` lines. + for (name, values) in &self.inherit_vars { + // We don't know what indentation style the function uses, + // so we do what fish_indent would. + sprintf!(=> &mut out, "\n set -l %ls", name); + for arg in values { + out.push(' '); + out.push_utfstr(&escape(arg)); + } + } + out.push('\n'); + out.push_utfstr(def); + + // Append a newline before the 'end', unless there already is one there. + if !def.ends_with('\n') { + out.push('\n'); + } + out.push_str("end\n"); + out + } +} + +pub struct FunctionPropertiesRefFFI(pub FunctionPropertiesRef); + +impl FunctionPropertiesRefFFI { + fn definition_file(&self) -> UniquePtr { + if let Some(file) = self.0.definition_file() { + file.to_ffi() + } else { + UniquePtr::null() + } + } + + fn definition_lineno(&self) -> i32 { + self.0.definition_lineno() + } + + fn copy_definition_lineno(&self) -> i32 { + self.0.copy_definition_lineno() + } + + fn shadow_scope(&self) -> bool { + self.0.shadow_scope + } + + fn named_arguments(&self) -> UniquePtr { + self.0.named_arguments.to_ffi() + } + + fn get_description(&self) -> UniquePtr { + self.0.description.to_ffi() + } + + fn annotated_definition(&self, name: &CxxWString) -> UniquePtr { + self.0.annotated_definition(name.as_wstr()).to_ffi() + } + + fn is_autoload(&self) -> bool { + self.0.is_autoload.load() + } + + fn is_copy(&self) -> bool { + self.0.is_copy + } + + fn get_block_statement_node_ffi(&self) -> *const u8 { + let stmt: &ast::BlockStatement = &self.0.func_node; + stmt as *const ast::BlockStatement as *const u8 + } + + fn parsed_source_ffi(&self) -> *mut u8 { + let source = self.0.func_node.parsed_source_ref(); + let res = Box::new(ParsedSourceRefFFI(Some(source))); + Box::into_raw(res) as *mut u8 + } + + fn copy_definition_file_ffi(&self) -> UniquePtr { + if let Some(file) = self.0.copy_definition_file() { + file.to_ffi() + } else { + UniquePtr::null() + } + } +} + +#[allow(clippy::boxed_local)] +fn function_add_ffi(name: &CxxWString, props: Box) { + add(name.from_ffi(), props.0); +} + +fn function_remove_ffi(name: &CxxWString) { + remove(name.as_wstr()); +} + +fn function_get_props_ffi(name: &CxxWString) -> *mut FunctionPropertiesRefFFI { + let props = get_props(name.as_wstr()); + if let Some(props) = props { + Box::into_raw(Box::new(FunctionPropertiesRefFFI(props))) + } else { + std::ptr::null_mut() + } +} + +fn function_get_props_autoload_ffi( + name: &CxxWString, + parser: Pin<&mut parser_t>, +) -> *mut FunctionPropertiesRefFFI { + let props = get_props_autoload(name.as_wstr(), parser.unpin()); + if let Some(props) = props { + Box::into_raw(Box::new(FunctionPropertiesRefFFI(props))) + } else { + std::ptr::null_mut() + } +} + +fn function_load_ffi(name: &CxxWString, parser: Pin<&mut parser_t>) -> bool { + load(name.as_wstr(), parser.unpin()) +} + +fn function_set_desc_ffi(name: &CxxWString, desc: &CxxWString, parser: Pin<&mut parser_t>) { + set_desc(name.as_wstr(), desc.from_ffi(), parser.unpin()); +} + +fn function_exists_ffi(cmd: &CxxWString, parser: Pin<&mut parser_t>) -> bool { + exists(cmd.as_wstr(), parser.unpin()) +} + +fn function_exists_no_autoload_ffi(cmd: &CxxWString) -> bool { + exists_no_autoload(cmd.as_wstr()) +} + +fn function_get_names_ffi(get_hidden: bool, mut out: Pin<&mut wcstring_list_ffi_t>) { + let names = get_names(get_hidden); + for name in names { + out.as_mut().push(name.to_ffi()); + } +} + +fn function_copy_ffi(name: &CxxWString, new_name: &CxxWString, parser: Pin<&mut parser_t>) -> bool { + copy(name.as_wstr(), new_name.from_ffi(), parser.unpin()) +} + +#[cxx::bridge] +mod function_ffi { + extern "C++" { + include!("ast.h"); + include!("parse_tree.h"); + include!("parser.h"); + include!("wutil.h"); + type parser_t = crate::ffi::parser_t; + type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; + } + + extern "Rust" { + #[cxx_name = "function_properties_t"] + type FunctionPropertiesRefFFI; + + fn definition_file(&self) -> UniquePtr; + fn copy_definition_lineno(&self) -> i32; + fn shadow_scope(&self) -> bool; + fn named_arguments(&self) -> UniquePtr; + fn get_description(&self) -> UniquePtr; + fn annotated_definition(&self, name: &CxxWString) -> UniquePtr; + fn is_autoload(&self) -> bool; + fn is_copy(&self) -> bool; + + #[cxx_name = "copy_definition_file"] + fn copy_definition_file_ffi(&self) -> UniquePtr; + + /// Returns unowned pointer to BlockStatement, cast to a u8. + #[cxx_name = "get_block_statement_node"] + fn get_block_statement_node_ffi(&self) -> *const u8; + + /// Returns rust::Box::into_raw(), cast to a u8. + #[cxx_name = "parsed_source"] + fn parsed_source_ffi(self: &FunctionPropertiesRefFFI) -> *mut u8; + + #[cxx_name = "function_add"] + fn function_add_ffi(name: &CxxWString, props: Box); + + #[cxx_name = "function_remove"] + fn function_remove_ffi(name: &CxxWString); + + /// Returns a Box::into_raw(), or nullptr if None. + #[cxx_name = "function_get_props_raw"] + fn function_get_props_ffi(name: &CxxWString) -> *mut FunctionPropertiesRefFFI; + + /// Returns a Box::into_raw(), or nullptr if None. + #[cxx_name = "function_get_props_autoload_raw"] + fn function_get_props_autoload_ffi( + name: &CxxWString, + parser: Pin<&mut parser_t>, + ) -> *mut FunctionPropertiesRefFFI; + + #[cxx_name = "function_load"] + fn function_load_ffi(name: &CxxWString, parser: Pin<&mut parser_t>) -> bool; + + #[cxx_name = "function_set_desc"] + fn function_set_desc_ffi(name: &CxxWString, desc: &CxxWString, parser: Pin<&mut parser_t>); + + #[cxx_name = "function_exists"] + fn function_exists_ffi(cmd: &CxxWString, parser: Pin<&mut parser_t>) -> bool; + + #[cxx_name = "function_exists_no_autoload"] + fn function_exists_no_autoload_ffi(cmd: &CxxWString) -> bool; + + #[cxx_name = "function_get_names"] + fn function_get_names_ffi(get_hidden: bool, out: Pin<&mut wcstring_list_ffi_t>); + + #[cxx_name = "function_copy"] + fn function_copy_ffi( + name: &CxxWString, + new_name: &CxxWString, + parser: Pin<&mut parser_t>, + ) -> bool; + + #[cxx_name = "function_invalidate_path"] + fn invalidate_path(); + } +} + +unsafe impl cxx::ExternType for FunctionPropertiesRefFFI { + type Id = cxx::type_id!("function_properties_t"); + type Kind = cxx::kind::Opaque; +} diff --git a/fish-rust/src/global_safety.rs b/fish-rust/src/global_safety.rs index e2707c267..3d93f0a55 100644 --- a/fish-rust/src/global_safety.rs +++ b/fish-rust/src/global_safety.rs @@ -16,3 +16,9 @@ pub fn swap(&self, value: bool) -> bool { self.0.swap(value, Ordering::Relaxed) } } + +impl Clone for RelaxedAtomicBool { + fn clone(&self) -> Self { + Self(AtomicBool::new(self.load())) + } +} diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index bad730bf8..16d6c5a4e 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -37,6 +37,7 @@ mod ffi_tests; mod fish_indent; mod flog; +mod function; mod future_feature_flags; mod global_safety; mod highlight; diff --git a/fish-rust/src/parse_tree.rs b/fish-rust/src/parse_tree.rs index 512589382..4eeda9fa4 100644 --- a/fish-rust/src/parse_tree.rs +++ b/fish-rust/src/parse_tree.rs @@ -1,9 +1,10 @@ //! Programmatic representation of fish code. +use std::ops::Deref; use std::pin::Pin; use std::sync::Arc; -use crate::ast::Ast; +use crate::ast::{Ast, Node}; use crate::parse_constants::{ token_type_user_presentable_description, ParseErrorCode, ParseErrorList, ParseErrorListFfi, ParseKeyword, ParseTokenType, ParseTreeFlags, SourceOffset, SourceRange, SOURCE_OFFSET_INVALID, @@ -114,6 +115,55 @@ fn new(src: WString, ast: Ast) -> Self { pub type ParsedSourceRef = Arc; +/// A reference to a node within a parse tree. +pub struct NodeRef { + /// The parse tree containing the node. + /// This is pinned because we hold a pointer into it. + parsed_source: Pin>, + + /// The node itself. This points into the parsed source. + node: *const NodeType, +} + +impl Clone for NodeRef { + fn clone(&self) -> Self { + NodeRef { + parsed_source: self.parsed_source.clone(), + node: self.node, + } + } +} + +impl Deref for NodeRef { + type Target = NodeType; + fn deref(&self) -> &Self::Target { + // Safety: the node is valid for the lifetime of the source. + unsafe { &*self.node } + } +} + +impl NodeRef { + pub fn parsed_source(&self) -> &ParsedSource { + &self.parsed_source + } + + pub fn parsed_source_ref(&self) -> ParsedSourceRef { + Pin::into_inner(self.parsed_source.clone()) + } + + /// Construct a NodeRef from ParsedSource and a node, which must point into that parsed source. + pub unsafe fn from_parts(parsed_source: ParsedSourceRef, node: &NodeType) -> Self { + NodeRef { + parsed_source: Pin::new(parsed_source), + node: node as *const NodeType, + } + } +} + +// Safety: NodeRef is Send and Sync because it's just a pointer into a parse tree, which is pinned. +unsafe impl Send for NodeRef {} +unsafe impl Sync for NodeRef {} + /// Return a shared pointer to ParsedSource, or null on failure. /// If parse_flag_continue_after_error is not set, this will return null on any error. pub fn parse_source( @@ -129,7 +179,7 @@ pub fn parse_source( } } -struct ParsedSourceRefFFI(pub Option); +pub struct ParsedSourceRefFFI(pub Option); #[cxx::bridge] mod parse_tree_ffi { diff --git a/fish-rust/src/wutil/gettext.rs b/fish-rust/src/wutil/gettext.rs index fd48eef65..50bae82fe 100644 --- a/fish-rust/src/wutil/gettext.rs +++ b/fish-rust/src/wutil/gettext.rs @@ -30,6 +30,16 @@ macro_rules! wgettext { } pub(crate) use wgettext; +/// Like wgettext, but for non-literals. +macro_rules! wgettext_expr { + ($string:expr) => { + crate::wutil::gettext::wgettext_impl_do_not_use_directly( + widestring::U32CString::from_ustr_truncate($string).as_slice_with_nul(), + ) + }; +} +pub(crate) use wgettext_expr; + /// Like wgettext, but applies a sprintf format string. /// The result is a WString. macro_rules! wgettext_fmt { diff --git a/src/autoload.cpp b/src/autoload.cpp index 55d49724e..9656c3141 100644 --- a/src/autoload.cpp +++ b/src/autoload.cpp @@ -189,6 +189,14 @@ maybe_t autoload_t::resolve_command(const wcstring &cmd, const environ } } +wcstring autoload_t::resolve_command_ffi(const wcstring &cmd) { + if (auto res = resolve_command(cmd, env_stack_t::globals())) { + return std::move(*res); + } else { + return wcstring(); + } +} + maybe_t autoload_t::resolve_command(const wcstring &cmd, const std::vector &paths) { // Are we currently in the process of autoloading this? @@ -228,3 +236,11 @@ void autoload_t::perform_autoload(const wcstring &path, parser_t &parser) { const cleanup_t put_back([&] { parser.set_last_statuses(prev_statuses); }); parser.eval(script_source, io_chain_t{}); } + +std::unique_ptr make_autoload_ffi(wcstring env_var_name) { + return make_unique(std::move(env_var_name)); +} + +void perform_autoload_ffi(const wcstring &path, parser_t &parser) { + autoload_t::perform_autoload(path, parser); +} diff --git a/src/autoload.h b/src/autoload.h index 6fc7c7c5d..d0967c2b3 100644 --- a/src/autoload.h +++ b/src/autoload.h @@ -68,6 +68,9 @@ class autoload_t { /// code; it is the caller's responsibility to load the file. maybe_t resolve_command(const wcstring &cmd, const environment_t &env); + /// FFI cover. This always uses globals, and returns an empty string instead of None. + wcstring resolve_command_ffi(const wcstring &cmd); + /// Helper to actually perform an autoload. /// This is a static function because it executes fish script, and so must be called without /// holding any particular locks. @@ -104,4 +107,8 @@ class autoload_t { } }; +/// FFI helpers. +std::unique_ptr make_autoload_ffi(wcstring env_var_name); +void perform_autoload_ffi(const wcstring &path, parser_t &parser); + #endif diff --git a/src/complete.cpp b/src/complete.cpp index e1237a8df..0603ce3d4 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -1959,3 +1959,7 @@ std::vector complete_get_wrap_targets(const wcstring &command) { if (iter == wraps.end()) return {}; return iter->second; } + +wcstring_list_ffi_t complete_get_wrap_targets_ffi(const wcstring &command) { + return complete_get_wrap_targets(command); +} diff --git a/src/complete.h b/src/complete.h index 7a725b0c8..301cb0d1d 100644 --- a/src/complete.h +++ b/src/complete.h @@ -284,6 +284,7 @@ bool complete_remove_wrapper(const wcstring &command, const wcstring &target_to_ /// Returns a list of wrap targets for a given command. std::vector complete_get_wrap_targets(const wcstring &command); +wcstring_list_ffi_t complete_get_wrap_targets_ffi(const wcstring &command); // Observes that fish_complete_path has changed. void complete_invalidate_path();