use std::cell::{Cell, OnceCell, RefCell}; use std::collections::HashMap; use std::rc::Rc; use futures::future::LocalBoxFuture; use crate::Error; /// Shared module text that retains the original String buffer. #[derive(Clone, Debug)] pub struct ModuleSource(Rc); impl From for ModuleSource { fn from(source: String) -> Self { Self(Rc::new(source)) } } impl From<&str> for ModuleSource { fn from(source: &str) -> Self { Self::from(source.to_owned()) } } impl std::ops::Deref for ModuleSource { type Target = str; fn deref(&self) -> &str { &self.0 } } /// Execution format for a prepared module. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum ModuleFormat { Esm, CommonJs, Json, } pub trait ModuleLoader { fn resolve(&self, specifier: &str, referrer: &str) -> Result; fn load(&self, name: &str) -> Result; /// Format metadata for synchronous `require` and ESM interop. /// Defaults to ESM so existing loaders keep working unchanged. fn module_format(&self, _name: &str) -> Result { Ok(ModuleFormat::Esm) } /// Synchronous `require` resolution. Defaults to ESM `resolve`. fn require_resolve(&self, specifier: &str, referrer: &str) -> Result { self.resolve(specifier, referrer) } /// Statically discovered named exports for a CommonJS module. /// Must not evaluate source; defaults to no named exports. fn cjs_named_exports(&self, _name: &str) -> Result, Error> { Ok(Vec::new()) } /// Acquire a module's static closure before V8 links it. Linking itself never awaits. fn prepare( &self, specifier: String, referrer: String, ) -> LocalBoxFuture<'static, Result> { let result = if referrer.is_empty() { Ok(specifier) } else { self.resolve(&specifier, &referrer) }; Box::pin(futures::future::ready(result)) } /// Called before using a loader-backed module, including cached modules and queued imports. fn check_access(&self, _name: &str) -> Result<(), Error> { Ok(()) } /// Resolve synchronously without acquiring or evaluating the target module. fn resolve_import_meta(&self, specifier: &str, referrer: &str) -> Result { let name = self.resolve(specifier, referrer)?; if name.starts_with("ext:") { return Err(Error::Module("Extension modules are private".into())); } Ok(self.module_url(&name).unwrap_or(name)) } /// URL exposed as `import.meta.url`, if this loader has one. fn module_url(&self, _name: &str) -> Option { None } /// Map a generated position to its source; lines and UTF-16 columns are one-based. fn map_source_location( &self, _name: &str, _line: u32, _column: u32, ) -> Option<(String, u32, u32)> { None } } pub(crate) struct ImportPreparation { pub resolver: v8::Global, pub result: Result, } pub(crate) fn prepare_entry(isolate: &mut v8::Isolate, name: &str) -> Result { if name.starts_with("ext:") { return Err(Error::Module("Extension modules are private".into())); } let map = isolate.get_slot::>().unwrap(); if map.is_builtin(name) { return Ok(name.to_owned()); } match &map.loader { Some(loader) => futures::executor::block_on(loader.prepare(name.to_owned(), String::new())), None => Err(Error::Module("No module loader configured".into())), } } type Exports = Vec<(String, v8::Global)>; mod commonjs; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] struct ModuleId(u32); struct Record { name: Rc, module: v8::Global, builtin: Option, dependencies: Vec, visited: u32, cjs_exports: Option>, } struct ModuleMap { loader: Option>, records: RefCell>, index: RefCell, ModuleId>>, // Identity hashes collide, so each bucket holds candidates confirmed by Local == Global. by_identity: RefCell>>, epoch: Cell, main: OnceCell, repl_referrer: RefCell>, // Per-isolate CommonJS state shared by `require` and ESM wrappers. // No global registry: everything lives in the isolate slot. cjs_modules: RefCell, v8::Global>>, cjs_deps: RefCell, Vec>>, json_modules: RefCell, v8::Global>>, } impl ModuleMap { fn id_of(&self, name: &str) -> Option { self.index.borrow().get(name).copied() } fn is_builtin(&self, name: &str) -> bool { let Some(id) = self.id_of(name) else { return false; }; self.records.borrow()[id.0 as usize].builtin.is_some() } fn next_epoch(&self) -> u32 { let mut epoch = self.epoch.get().wrapping_add(1); if epoch == 0 { for record in self.records.borrow_mut().iter_mut() { record.visited = 0; } epoch = 1; } self.epoch.set(epoch); epoch } fn insert( &self, name: Rc, module: v8::Global, builtin: Option, hash: i32, ) -> ModuleId { // Only called on cache miss: register() rejects duplicates and load() // returns early when the name is already indexed. let id = ModuleId(self.records.borrow().len() as u32); self.records.borrow_mut().push(Record { name: name.clone(), module, builtin, dependencies: Vec::new(), visited: 0, cjs_exports: None, }); self.index.borrow_mut().insert(name, id); self.by_identity .borrow_mut() .entry(hash) .or_default() .push(id); id } } pub(crate) fn clear(isolate: &mut v8::Isolate) { isolate.remove_slot::>(); } pub(crate) fn external_references() -> Vec { use v8::MapFnTo; vec![v8::ExternalReference { function: resolve_import_meta.map_fn_to(), }] } pub(crate) fn has_modules(isolate: &v8::Isolate) -> bool { !isolate .get_slot::>() .unwrap() .records .borrow() .is_empty() } pub(crate) fn init(isolate: &mut v8::Isolate, loader: Option>) { isolate.set_slot(Rc::new(ModuleMap { loader, records: RefCell::new(Vec::new()), index: RefCell::new(HashMap::new()), by_identity: RefCell::new(HashMap::new()), epoch: Cell::new(0), main: OnceCell::new(), repl_referrer: RefCell::new(None), cjs_modules: RefCell::new(HashMap::new()), cjs_deps: RefCell::new(HashMap::new()), json_modules: RefCell::new(HashMap::new()), })); isolate.set_host_import_module_dynamically_callback(import_dynamic); isolate.set_host_initialize_import_meta_object_callback(import_meta); } pub(crate) fn set_repl_referrer(scope: &mut v8::PinScope, referrer: Option<&str>) -> Option<()> { let map = scope.get_slot::>()?; *map.repl_referrer.borrow_mut() = referrer.map(str::to_owned); Some(()) } pub(crate) fn register( scope: &mut v8::PinScope, name: &str, exports: v8::Local, ) -> Option<()> { check_guest_name(scope, name)?; let map = scope.get_slot::>().unwrap().clone(); if map.id_of(name).is_some() { return fail(scope, format!("Module already registered: {name}")); } let keys = exports.get_own_property_names(scope, Default::default())?; let mut names = Vec::new(); let mut values = Exports::new(); for index in 0..keys.length() { let key = keys.get_index(scope, index)?; let name = key.to_rust_string_lossy(scope); let value = exports.get(scope, key)?; names.push(v8::String::new(scope, &name)?); values.push((name, v8::Global::new(scope, value))); } let module_name = v8::String::new(scope, name)?; let module = v8::Module::create_synthetic_module(scope, module_name, &names, evaluate_builtin); let hash = module.get_identity_hash().get(); map.insert( name.into(), v8::Global::new(scope, module), Some(values), hash, ); Some(()) } fn evaluate_builtin<'s>( context: v8::Local<'s, v8::Context>, module: v8::Local<'s, v8::Module>, ) -> Option> { // SAFETY: V8 invokes synthetic module evaluation with this context entered. v8::callback_scope!(unsafe scope, context); let id = module_id(scope, module); let map = scope.get_slot::>().unwrap().clone(); // Only synthetic builtin modules carry this evaluation callback, so the // record always has exports; panic like the old builtins-map indexing did. let records = map.records.borrow(); let exports = records[id.0 as usize].builtin.as_ref().unwrap(); for (key, value) in exports { let key = v8::String::new(scope, key)?; let value = v8::Local::new(scope, value); module.set_synthetic_module_export(scope, key, value)?; } let resolver = v8::PromiseResolver::new(scope)?; resolver.resolve(scope, v8::undefined(scope).into())?; Some(resolver.get_promise(scope).into()) } pub(crate) fn map_source_location_op( scope: &mut v8::PinScope, args: v8::FunctionCallbackArguments, mut result: v8::ReturnValue, ) { let name = args.get(0).to_rust_string_lossy(scope); let Some(line) = args.get(1).uint32_value(scope) else { return; }; let Some(column) = args.get(2).uint32_value(scope) else { return; }; if let Some(location) = map_source_location(scope, &name, line, column) { result.set(serde_v8::to_v8(scope, location).unwrap()); } } pub(crate) fn map_source_location( scope: &mut v8::PinScope, name: &str, line: u32, column: u32, ) -> Option<(String, u32, u32)> { scope .get_slot::>()? .loader .as_ref()? .map_source_location(name, line, column) } fn fail_syntax(scope: &mut v8::PinScope, message: impl AsRef) -> Option { let message = v8::String::new(scope, message.as_ref()).unwrap(); let error = v8::Exception::syntax_error(scope, message); scope.throw_exception(error); None } fn fail(scope: &mut v8::PinScope, message: impl AsRef) -> Option { let message = v8::String::new(scope, message.as_ref()).unwrap(); let error = v8::Exception::error(scope, message); scope.throw_exception(error); None } fn check_guest_name(scope: &mut v8::PinScope, name: &str) -> Option<()> { if name.starts_with("ext:") { fail(scope, "Extension modules are private") } else { Some(()) } } fn load<'s>(scope: &mut v8::PinScope<'s, '_>, name: &str) -> Option> { check_guest_name(scope, name)?; let Some(map) = scope.get_slot::>().cloned() else { return fail(scope, "No module loader configured"); }; let is_builtin = map.is_builtin(name); if !is_builtin && let Some(loader) = &map.loader && let Err(error) = loader.check_access(name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } if let Some(id) = map.id_of(name) { // Local::new only creates a handle; no loader or V8 callback runs // while borrowed, so holding the borrow avoids a Global clone. let records = map.records.borrow(); return Some(v8::Local::new(scope, &records[id.0 as usize].module)); } let Some(loader) = map.loader.clone() else { return fail(scope, "No module loader configured"); }; let format = match loader.module_format(name) { Ok(format) => format, Err(Error::Syntax(message)) => return fail_syntax(scope, message), Err(error) => return fail(scope, error.to_string()), }; match format { ModuleFormat::Esm => { let source = match loader.load(name) { Ok(source) => source, Err(Error::Syntax(message)) => return fail_syntax(scope, message), Err(error) => return fail(scope, error.to_string()), }; let mut source = crate::source(scope, name, &source, true, None); let module = v8::script_compiler::compile_module(scope, &mut source)?; let hash = module.get_identity_hash().get(); map.insert(name.into(), v8::Global::new(scope, module), None, hash); Some(module) } ModuleFormat::CommonJs => commonjs::load_synthetic(scope, &map, &loader, name), ModuleFormat::Json => fail( scope, format!("JSON modules are not supported as ESM (use require() instead): {name}"), ), } } fn resolve<'s>( context: v8::Local<'s, v8::Context>, specifier: v8::Local<'s, v8::String>, attributes: v8::Local<'s, v8::FixedArray>, referrer: v8::Local<'s, v8::Module>, ) -> Option> { // SAFETY: V8 invokes this callback with the supplied context entered. v8::callback_scope!(unsafe scope, context); if attributes.length() != 0 { return fail(scope, "Import attributes are not supported yet"); } let referrer_id = module_id(scope, referrer); let referrer = scope.get_slot::>().unwrap().records.borrow() [referrer_id.0 as usize] .name .to_string(); let specifier = specifier.to_rust_string_lossy(scope); let name = resolve_name(scope, &specifier, &referrer)?; let module = load(scope, &name)?; let map = scope.get_slot::>().unwrap().clone(); // load() inserts on cache miss, so the name is always indexed here. let dependency = map.id_of(&name).unwrap(); map.records.borrow_mut()[referrer_id.0 as usize] .dependencies .push(dependency); Some(module) } fn evaluate<'s>( scope: &mut v8::PinScope<'s, '_>, name: &str, ) -> Option<(v8::Local<'s, v8::Module>, v8::Local<'s, v8::Promise>)> { let module = load(scope, name)?; let map = scope.get_slot::>().unwrap().clone(); let root = map.id_of(name).unwrap(); if module.get_status() == v8::ModuleStatus::Uninstantiated { // Clear failed attempts before linking, not on repeated resolution within a diamond or cycle. // Local::new and get_status only inspect handles; no loader or V8 // callback runs while borrowed, so no Global clones are needed. let uninstantiated = { let records = map.records.borrow(); records .iter() .enumerate() .filter_map(|(index, record)| { (v8::Local::new(scope, &record.module).get_status() == v8::ModuleStatus::Uninstantiated) .then_some(ModuleId(index as u32)) }) .collect::>() }; if !uninstantiated.is_empty() { let mut records = map.records.borrow_mut(); for id in uninstantiated { records[id.0 as usize].dependencies.clear(); } } if module.instantiate_module(scope, resolve) != Some(true) { return None; } } // Check the recorded links, not a fresh resolution that could identify different modules. let epoch = map.next_epoch(); let mut pending = vec![root]; while let Some(id) = pending.pop() { let (status, is_builtin, dependencies, record_name) = { let mut records = map.records.borrow_mut(); let record = &mut records[id.0 as usize]; if record.visited == epoch { continue; } record.visited = epoch; // Local::new and get_status only inspect handles; the borrow is // dropped before the loader check below, which may reenter. let status = v8::Local::new(scope, &record.module).get_status(); ( status, record.builtin.is_some(), record.dependencies.clone(), record.name.clone(), ) }; // Cached CJS closures must not bypass a later pledge even when the // synthetic wrapper is already evaluated: check transitively. // Traverse ESM dependencies even for evaluated modules so an ESM // intermediary cannot hide a revoked CJS leaf. if !is_builtin && let Some(loader) = &map.loader && commonjs::check_closure(scope, &map, loader, &record_name).is_none() { return None; } pending.extend(dependencies); if status != v8::ModuleStatus::Instantiated { continue; } if !is_builtin && let Some(loader) = &map.loader && let Err(error) = loader.check_access(&record_name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } } // V8 caches the cycle root's evaluation promise, including pending top-level await. let value = module.evaluate(scope)?; let promise = v8::Local::::try_from(value).unwrap(); promise.mark_as_handled(); Some((module, promise)) } pub(crate) fn execute<'s>( scope: &mut v8::PinScope<'s, '_>, name: &str, ) -> Option> { let Some(map) = scope.get_slot::>().cloned() else { return fail(scope, "No module loader configured"); }; check_guest_name(scope, name)?; map.main.get_or_init(|| name.to_owned()); evaluate(scope, name).map(|(_, promise)| promise) } pub(crate) fn namespace<'s>( scope: &mut v8::PinScope<'s, '_>, name: &str, ) -> Option> { let map = scope.get_slot::>()?; let id = map.id_of(name)?; let records = map.records.borrow(); // Local::new only creates a handle; no loader or V8 callback runs while borrowed. Some(v8::Local::new(scope, &records[id.0 as usize].module).get_module_namespace()) } fn module_id(scope: &mut v8::PinScope, module: v8::Local) -> ModuleId { let map = scope.get_slot::>().unwrap().clone(); let hash = module.get_identity_hash().get(); let by_identity = map.by_identity.borrow(); let records = map.records.borrow(); // Local::new and handle equality only inspect handles; no loader or V8 // callback runs while borrowed, so the bucket and Globals need no clones. if let Some(candidates) = by_identity.get(&hash) { for id in candidates { if v8::Local::new(scope, &records[id.0 as usize].module) == module { return *id; } } } unreachable!("module not in map") } fn module_name(scope: &mut v8::PinScope, module: v8::Local) -> String { let map = scope.get_slot::>().unwrap().clone(); let id = module_id(scope, module); map.records.borrow()[id.0 as usize].name.to_string() } fn resolve_name(scope: &mut v8::PinScope, specifier: &str, referrer: &str) -> Option { check_guest_name(scope, specifier)?; let map = scope.get_slot::>().unwrap().clone(); if map.is_builtin(specifier) { return Some(specifier.to_owned()); } let Some(loader) = map.loader.clone() else { return fail(scope, "No module loader configured"); }; match loader.resolve(specifier, referrer) { Ok(name) => { check_guest_name(scope, &name)?; Some(name) } Err(error) => fail(scope, error.to_string()), } } unsafe extern "C" fn import_meta( context: v8::Local, module: v8::Local, meta: v8::Local, ) { // SAFETY: V8 supplies the module's entered context. v8::callback_scope!(unsafe scope, context); let extension = crate::extensions::module_name(scope, module); let name = extension .clone() .unwrap_or_else(|| module_name(scope, module)); let referrer = v8::String::new(scope, &name).unwrap(); let resolve = v8::Function::builder(resolve_import_meta) .data(referrer.into()) .length(1) .constructor_behavior(v8::ConstructorBehavior::Throw) .build(scope) .unwrap(); let function_name = v8::String::new(scope, "resolve").unwrap(); resolve.set_name(function_name); meta.create_data_property(scope, function_name.into(), resolve.into()) .unwrap(); if extension.is_some() { for (key, value) in [ ("main", v8::Boolean::new(scope, false).into()), ("url", v8::String::new(scope, &name).unwrap().into()), ] { let key = v8::String::new(scope, key).unwrap(); meta.create_data_property(scope, key.into(), value).unwrap(); } return; } let map = scope.get_slot::>().unwrap().clone(); let main = map.main.get() == Some(&name); let key = v8::String::new(scope, "main").unwrap(); let value = v8::Boolean::new(scope, main); meta.create_data_property(scope, key.into(), value.into()) .unwrap(); if let Some(url) = map .loader .as_ref() .and_then(|loader| loader.module_url(&name)) { let key = v8::String::new(scope, "url").unwrap(); let value = v8::String::new(scope, &url).unwrap(); meta.create_data_property(scope, key.into(), value.into()) .unwrap(); if let Ok(url) = url::Url::parse(&url) && let Ok(path) = url.to_file_path() && let Some(filename) = path.to_str() && let Some(dirname) = path.parent().and_then(std::path::Path::to_str) { for (key, value) in [("filename", filename), ("dirname", dirname)] { let key = v8::String::new(scope, key).unwrap(); let value = v8::String::new(scope, value).unwrap(); meta.create_data_property(scope, key.into(), value.into()) .unwrap(); } } } } fn resolve_import_meta( scope: &mut v8::PinScope, args: v8::FunctionCallbackArguments, mut result: v8::ReturnValue, ) { let Some(specifier) = args.get(0).to_string(scope) else { return; }; let specifier = specifier.to_rust_string_lossy(scope); let referrer = args.data().to_rust_string_lossy(scope); if referrer.starts_with("ext:") { if let Some(name) = crate::extensions::resolve_name(scope, &specifier, &referrer) && let Some(value) = v8::String::new(scope, &name) { result.set(value.into()); } return; } let map = scope.get_slot::>().unwrap().clone(); let is_builtin = map.is_builtin(&specifier); let loader = map.loader.clone(); let resolved = if specifier.starts_with("ext:") { Err(Error::Module("Extension modules are private".into())) } else if is_builtin { Ok(specifier) } else if let Some(loader) = &loader { loader.resolve_import_meta(&specifier, &referrer) } else { Err(Error::Module("No module loader configured".into())) }; match resolved { Ok(name) if !name.starts_with("ext:") => { if let Some(value) = v8::String::new(scope, &name) { result.set(value.into()); } } other => { let message = match other { Err(error) => error.to_string(), _ => "Extension modules are private".into(), }; let message = v8::String::new(scope, &message).unwrap(); let error = v8::Exception::type_error(scope, message); scope.throw_exception(error); } } } fn import_dynamic<'s>( scope: &mut v8::PinScope<'s, '_>, options: v8::Local<'s, v8::Data>, referrer: v8::Local<'s, v8::Value>, specifier: v8::Local<'s, v8::String>, attributes: v8::Local<'s, v8::FixedArray>, ) -> Option> { let resolver = v8::PromiseResolver::new(scope)?; let promise = resolver.get_promise(scope); v8::tc_scope!(let scope, scope); let queued = (|| { let specifier = specifier.to_rust_string_lossy(scope); check_guest_name(scope, &specifier)?; // eval inherits host options. This marker only restricts imports; it never grants access. if crate::extensions::dynamic_imports_disabled(scope, options) { return fail( scope, "not yet implemented: dynamic imports from extension modules", ); } if attributes.length() != 0 { return fail(scope, "Import attributes are not supported yet"); } let referrer = referrer.to_rust_string_lossy(scope); let map = scope.get_slot::>().unwrap().clone(); let referrer = if referrer.is_empty() { map.repl_referrer.borrow().clone().unwrap_or(referrer) } else { referrer }; let is_builtin = map.is_builtin(&specifier); let loader = map.loader.clone(); let future = if is_builtin { Box::pin(futures::future::ready(Ok(specifier))) as LocalBoxFuture<'static, Result> } else { let Some(loader) = loader else { return fail(scope, "No module loader configured"); }; loader.prepare(specifier, referrer) }; let resolver = v8::Global::new(scope, resolver); crate::op::enqueue_import( scope, Box::pin(async move { ImportPreparation { resolver, result: future.await } }), ); Some(()) })(); if queued.is_none() { let error = scope.exception()?; scope.reset(); resolver.reject(scope, error); } Some(promise) } pub(crate) fn import_prepared(scope: &mut v8::PinScope, preparation: ImportPreparation) { let resolver = v8::Local::new(scope, preparation.resolver); v8::tc_scope!(let scope, scope); let queued = (|| { let name = match preparation.result { Ok(name) => name, Err(Error::Syntax(message)) => { let message = v8::String::new(scope, &message)?; let error = v8::Exception::syntax_error(scope, message); scope.throw_exception(error); return None; } Err(error) => return fail(scope, error.to_string()), }; check_guest_name(scope, &name)?; let name = v8::String::new(scope, &name)?; let data = v8::Array::new_with_elements(scope, &[name.into(), resolver.into()]); let job = v8::Function::builder(import_job) .data(data.into()) .constructor_behavior(v8::ConstructorBehavior::Throw) .build(scope)?; // Evaluation must not reenter a synchronously evaluating module (including itself). scope.enqueue_microtask(job); Some(()) })(); if queued.is_none() && let Some(error) = scope.exception() { scope.reset(); resolver.reject(scope, error); } } fn import_job<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, _result: v8::ReturnValue, ) { let data = v8::Local::::try_from(args.data()).unwrap(); let name = data .get_index(scope, 0) .unwrap() .to_rust_string_lossy(scope); // SAFETY: Private callback data contains the resolver created by import_dynamic. // import_prepared stores it at index 1; guests cannot change that data. let resolver = unsafe { v8::Local::::cast_unchecked(data.get_index(scope, 1).unwrap()) }; v8::tc_scope!(let scope, scope); let started = (|| { let (module, evaluation) = evaluate(scope, &name)?; let namespace = module.get_module_namespace(); let data = v8::Array::new_with_elements(scope, &[resolver.into(), namespace]); let fulfilled = v8::Function::builder(import_fulfilled) .data(data.into()) .constructor_behavior(v8::ConstructorBehavior::Throw) .build(scope)?; let rejected = v8::Function::builder(import_rejected) .data(resolver.into()) .constructor_behavior(v8::ConstructorBehavior::Throw) .build(scope)?; evaluation .then2(scope, fulfilled, rejected)? .mark_as_handled(); Some(()) })(); if started.is_none() && let Some(error) = scope.exception() { scope.reset(); resolver.reject(scope, error); } } fn import_fulfilled<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, _result: v8::ReturnValue, ) { let data = v8::Local::::try_from(args.data()).unwrap(); // SAFETY: Private callback data contains the original import resolver. // import_job stores it at index 0; guests cannot change that data. let resolver = unsafe { v8::Local::::cast_unchecked(data.get_index(scope, 0).unwrap()) }; let namespace = data.get_index(scope, 1).unwrap(); resolver.resolve(scope, namespace); } fn import_rejected<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, _result: v8::ReturnValue, ) { // SAFETY: Private callback data contains the original import resolver. // import_job installs the PromiseResolver directly; guests cannot change that data. let resolver = unsafe { v8::Local::::cast_unchecked(args.data()) }; resolver.reject(scope, args.get(0)); }