use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use super::super::frontends::{is_ident_continue, is_ident_start};
use super::super::{CompileSourceFileOptions, SourceFlavor, SourcePathError};
use super::imports::{
host_namespace_root_from_spec, is_builtin_host_namespace_spec, is_module_specifier,
is_valid_ident, is_virtual_host_namespace_spec, resolve_module_path,
};
use super::model::{ExportedFunctionSignature, ImportClause, ImportRewriteResult, ModuleImport};
struct ImportCallResolution {
alias_calls: HashMap<String, String>,
namespace_calls: HashMap<String, HashSet<String>>,
namespace_prefix_calls: HashMap<String, String>,
}
fn resolve_import_call_paths(
flavor: SourceFlavor,
path: &Path,
imports: &[ModuleImport],
module_exports: &HashMap<PathBuf, HashMap<String, ExportedFunctionSignature>>,
options: &CompileSourceFileOptions,
) -> Result<ImportCallResolution, SourcePathError> {
let mut alias_calls = HashMap::<String, String>::new();
let mut namespace_calls = HashMap::<String, HashSet<String>>::new();
let namespace_prefix_calls = HashMap::<String, String>::new();
for import in imports {
if is_builtin_host_namespace_spec(&import.spec) {
continue;
}
if !is_module_specifier(&import.spec) {
if let Some(host_root) = host_namespace_root_from_spec(&import.spec)
&& is_virtual_host_namespace_spec(&import.spec, options)
&& let Some(host_prefix) = virtual_host_namespace_prefix(flavor, &host_root)
{
match &import.clause {
ImportClause::AllPublic => {}
ImportClause::Named(named) => {
for binding in named {
alias_calls.insert(
binding.local.clone(),
format!("{host_prefix}::{}", binding.imported),
);
}
}
ImportClause::Namespace(_namespace) => {}
ImportClause::Prefix(_) => {}
}
}
continue;
}
let resolved = resolve_module_path(path, &import.spec, options)?;
let Some(exports) = module_exports.get(&resolved) else {
if let Some(host_root) = host_namespace_root_from_spec(&import.spec)
&& is_virtual_host_namespace_spec(&import.spec, options)
&& let Some(host_prefix) = virtual_host_namespace_prefix(flavor, &host_root)
{
match &import.clause {
ImportClause::AllPublic => {}
ImportClause::Named(named) => {
for binding in named {
alias_calls.insert(
binding.local.clone(),
format!("{host_prefix}::{}", binding.imported),
);
}
}
ImportClause::Namespace(_namespace) => {}
ImportClause::Prefix(_) => {}
}
}
continue;
};
match &import.clause {
ImportClause::AllPublic => {
if let Some(namespace) = module_default_namespace(&import.spec) {
let entries = namespace_calls.entry(namespace).or_default();
for name in exports.keys() {
entries.insert(name.clone());
}
}
}
ImportClause::Named(named) => {
for binding in named {
if !exports.contains_key(&binding.imported) {
return Err(SourcePathError::InvalidImportSyntax {
path: path.to_path_buf(),
line: import.line,
message: format!(
"module '{}' has no public function '{}'",
import.spec, binding.imported
),
});
}
if binding.local != binding.imported {
alias_calls.insert(binding.local.clone(), binding.imported.clone());
}
}
}
ImportClause::Namespace(namespace) => {
let entries = namespace_calls.entry(namespace.clone()).or_default();
for name in exports.keys() {
entries.insert(name.clone());
}
}
ImportClause::Prefix(prefix) => {
for name in exports.keys() {
alias_calls.insert(format!("{prefix}{name}"), name.clone());
}
}
}
}
Ok(ImportCallResolution {
alias_calls,
namespace_calls,
namespace_prefix_calls,
})
}
fn virtual_host_namespace_prefix(flavor: SourceFlavor, host_root: &str) -> Option<String> {
match flavor {
SourceFlavor::RustScript => Some(host_root.to_string()),
SourceFlavor::JavaScript | SourceFlavor::Lua => None,
}
}
pub(super) fn rewrite_imported_call_sites(
source: &str,
flavor: SourceFlavor,
path: &Path,
imports: &[ModuleImport],
module_exports: &HashMap<PathBuf, HashMap<String, ExportedFunctionSignature>>,
options: &CompileSourceFileOptions,
) -> Result<ImportRewriteResult, SourcePathError> {
let resolution = resolve_import_call_paths(flavor, path, imports, module_exports, options)?;
let alias_calls = resolution.alias_calls;
let namespace_calls = resolution.namespace_calls;
let namespace_prefix_calls = resolution.namespace_prefix_calls;
let namespace_wildcards = HashSet::<String>::new();
let prefix_aliases = Vec::<String>::new();
let rewritten = rewrite_host_namespace_call_paths(source, flavor, &namespace_prefix_calls);
if alias_calls.is_empty()
&& namespace_calls.is_empty()
&& namespace_wildcards.is_empty()
&& prefix_aliases.is_empty()
{
return Ok(ImportRewriteResult { source: rewritten });
}
Ok(ImportRewriteResult {
source: rewrite_function_call_paths(
&rewritten,
flavor,
&alias_calls,
&namespace_calls,
&namespace_wildcards,
&prefix_aliases,
),
})
}
fn rewrite_host_namespace_call_paths(
source: &str,
flavor: SourceFlavor,
namespace_prefix_calls: &HashMap<String, String>,
) -> String {
if namespace_prefix_calls.is_empty() {
return source.to_string();
}
let bytes = source.as_bytes();
let mut out = String::with_capacity(source.len());
let mut i = 0usize;
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut string_delim: Option<u8> = None;
let mut escaped = false;
while i < bytes.len() {
let b = bytes[i];
if let Some(delim) = string_delim {
out.push(b as char);
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == delim {
string_delim = None;
}
i += 1;
continue;
}
if in_line_comment {
out.push(b as char);
if b == b'\n' {
in_line_comment = false;
}
i += 1;
continue;
}
if in_block_comment {
out.push(b as char);
if b == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
out.push('/');
i += 2;
in_block_comment = false;
continue;
}
i += 1;
continue;
}
if b == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
out.push('/');
out.push('/');
i += 2;
in_line_comment = true;
continue;
}
if b == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
out.push('/');
out.push('*');
i += 2;
in_block_comment = true;
continue;
}
if b == b'"' || b == b'\'' || b == b'`' {
out.push(b as char);
i += 1;
string_delim = Some(b);
escaped = false;
continue;
}
if !is_ident_start(b as char) {
out.push(b as char);
i += 1;
continue;
}
let start = i;
i += 1;
while i < bytes.len() && is_ident_continue(bytes[i] as char) {
i += 1;
}
let ident = &source[start..i];
if let Some(prefix) = namespace_prefix_calls.get(ident)
&& namespace_call_target_is_function(source, i, flavor)
{
out.push_str(prefix);
continue;
}
out.push_str(ident);
}
out
}
fn namespace_call_target_is_function(source: &str, index: usize, flavor: SourceFlavor) -> bool {
let bytes = source.as_bytes();
let mut cursor = index;
if !consume_namespace_separator(bytes, &mut cursor, flavor) {
return false;
}
loop {
while cursor < bytes.len()
&& bytes[cursor].is_ascii_whitespace()
&& bytes[cursor] != b'\n'
&& bytes[cursor] != b'\r'
{
cursor += 1;
}
if cursor >= bytes.len() || !is_ident_start(bytes[cursor] as char) {
return false;
}
cursor += 1;
while cursor < bytes.len() && is_ident_continue(bytes[cursor] as char) {
cursor += 1;
}
while cursor < bytes.len()
&& bytes[cursor].is_ascii_whitespace()
&& bytes[cursor] != b'\n'
&& bytes[cursor] != b'\r'
{
cursor += 1;
}
if cursor < bytes.len() && bytes[cursor] == b'(' {
return true;
}
if !consume_namespace_separator(bytes, &mut cursor, flavor) {
return false;
}
}
}
fn consume_namespace_separator(bytes: &[u8], cursor: &mut usize, flavor: SourceFlavor) -> bool {
if *cursor >= bytes.len() {
return false;
}
if flavor == SourceFlavor::RustScript {
if bytes[*cursor] != b':' {
return false;
}
*cursor += 1;
while *cursor < bytes.len()
&& bytes[*cursor].is_ascii_whitespace()
&& bytes[*cursor] != b'\n'
&& bytes[*cursor] != b'\r'
{
*cursor += 1;
}
if *cursor >= bytes.len() || bytes[*cursor] != b':' {
return false;
}
*cursor += 1;
return true;
}
if bytes[*cursor] == b'.' {
*cursor += 1;
return true;
}
false
}
fn module_default_namespace(spec: &str) -> Option<String> {
let stem = Path::new(spec).file_stem()?.to_str()?;
if is_valid_ident(stem) {
Some(stem.to_string())
} else {
None
}
}
fn skip_inline_whitespace(bytes: &[u8], mut index: usize) -> usize {
while index < bytes.len()
&& bytes[index].is_ascii_whitespace()
&& bytes[index] != b'\n'
&& bytes[index] != b'\r'
{
index += 1;
}
index
}
fn rustscript_turbofish_call_starts(bytes: &[u8], start: usize) -> bool {
let mut index = skip_inline_whitespace(bytes, start);
if index >= bytes.len() || bytes[index] != b':' {
return false;
}
index = skip_inline_whitespace(bytes, index + 1);
if index >= bytes.len() || bytes[index] != b':' {
return false;
}
index = skip_inline_whitespace(bytes, index + 1);
if index >= bytes.len() || bytes[index] != b'<' {
return false;
}
let mut depth = 0usize;
while index < bytes.len() {
match bytes[index] {
b'<' => depth += 1,
b'>' => {
depth = depth.saturating_sub(1);
if depth == 0 {
index += 1;
break;
}
}
b'\n' | b'\r' => return false,
_ => {}
}
index += 1;
}
if depth != 0 {
return false;
}
index = skip_inline_whitespace(bytes, index);
index < bytes.len() && bytes[index] == b'('
}
fn call_starts_after_position(bytes: &[u8], start: usize, flavor: SourceFlavor) -> bool {
let index = skip_inline_whitespace(bytes, start);
if index < bytes.len() && bytes[index] == b'(' {
return true;
}
flavor == SourceFlavor::RustScript && rustscript_turbofish_call_starts(bytes, start)
}
fn rewrite_function_call_paths(
source: &str,
flavor: SourceFlavor,
alias_calls: &HashMap<String, String>,
namespace_calls: &HashMap<String, HashSet<String>>,
namespace_wildcards: &HashSet<String>,
prefix_aliases: &[String],
) -> String {
let bytes = source.as_bytes();
let mut out = String::with_capacity(source.len());
let mut i = 0usize;
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut string_delim: Option<u8> = None;
let mut escaped = false;
while i < bytes.len() {
let b = bytes[i];
if let Some(delim) = string_delim {
out.push(b as char);
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == delim {
string_delim = None;
}
i += 1;
continue;
}
if in_line_comment {
out.push(b as char);
if b == b'\n' {
in_line_comment = false;
}
i += 1;
continue;
}
if in_block_comment {
out.push(b as char);
if b == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
out.push('/');
i += 2;
in_block_comment = false;
continue;
}
i += 1;
continue;
}
if b == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
out.push('/');
out.push('/');
i += 2;
in_line_comment = true;
continue;
}
if b == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
out.push('/');
out.push('*');
i += 2;
in_block_comment = true;
continue;
}
if b == b'"' || b == b'\'' || b == b'`' {
out.push(b as char);
i += 1;
string_delim = Some(b);
escaped = false;
continue;
}
if is_ident_start(b as char) {
let start = i;
i += 1;
while i < bytes.len() && is_ident_continue(bytes[i] as char) {
i += 1;
}
let ident = &source[start..i];
let namespace_methods = namespace_calls.get(ident);
let namespace_wildcard = namespace_wildcards.contains(ident);
if namespace_methods.is_some() || namespace_wildcard {
let mut j = i;
while j < bytes.len()
&& bytes[j].is_ascii_whitespace()
&& bytes[j] != b'\n'
&& bytes[j] != b'\r'
{
j += 1;
}
let mut sep_end = None;
if flavor == SourceFlavor::RustScript {
if j < bytes.len() && bytes[j] == b':' {
let mut k = j + 1;
while k < bytes.len()
&& bytes[k].is_ascii_whitespace()
&& bytes[k] != b'\n'
&& bytes[k] != b'\r'
{
k += 1;
}
if k < bytes.len() && bytes[k] == b':' {
sep_end = Some(k + 1);
}
}
} else if j < bytes.len() && bytes[j] == b'.' {
sep_end = Some(j + 1);
}
if let Some(mut k) = sep_end {
while k < bytes.len()
&& bytes[k].is_ascii_whitespace()
&& bytes[k] != b'\n'
&& bytes[k] != b'\r'
{
k += 1;
}
if k < bytes.len() && is_ident_start(bytes[k] as char) {
let member_start = k;
k += 1;
while k < bytes.len() && is_ident_continue(bytes[k] as char) {
k += 1;
}
let member = &source[member_start..k];
if call_starts_after_position(bytes, k, flavor)
&& (namespace_wildcard
|| namespace_methods
.is_some_and(|methods| methods.contains(member)))
{
out.push_str(member);
i = k;
continue;
}
}
}
}
if let Some(target) = alias_calls.get(ident)
&& call_starts_after_position(bytes, i, flavor)
{
out.push_str(target);
continue;
}
let mut rewritten_by_prefix = false;
for prefix in prefix_aliases {
if !ident.starts_with(prefix) {
continue;
}
let rem = &ident[prefix.len()..];
if rem.is_empty() || !is_valid_ident(rem) {
continue;
}
if call_starts_after_position(bytes, i, flavor) {
out.push_str(rem);
rewritten_by_prefix = true;
break;
}
}
if rewritten_by_prefix {
continue;
}
out.push_str(ident);
continue;
}
out.push(b as char);
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::*;
use crate::compiler::CompileSourceFileOptions;
use crate::compiler::source_loader::model::{
ExportedFunctionSignature, ImportClause, ModuleImport, NamedImport,
};
#[test]
fn rustscript_namespace_import_calls_rewrite_to_direct_calls() {
let source = r#"
string::non_empty("rss");
is_empty("");
"#;
let path = Path::new("tests/main.rss");
let imports = vec![
ModuleImport {
spec: "strings.rss".to_string(),
clause: ImportClause::Namespace("string".to_string()),
line: 1,
},
ModuleImport {
spec: "strings.rss".to_string(),
clause: ImportClause::Named(vec![NamedImport {
imported: "is_empty".to_string(),
local: "is_empty".to_string(),
}]),
line: 2,
},
];
let mut module_exports =
HashMap::<PathBuf, HashMap<String, ExportedFunctionSignature>>::new();
module_exports.insert(
PathBuf::from("tests").join("strings.rss"),
HashMap::from([
(
"is_empty".to_string(),
ExportedFunctionSignature {
arity: 1,
type_params: Vec::new(),
},
),
(
"non_empty".to_string(),
ExportedFunctionSignature {
arity: 1,
type_params: Vec::new(),
},
),
]),
);
let rewritten = rewrite_imported_call_sites(
source,
SourceFlavor::RustScript,
path,
&imports,
&module_exports,
&CompileSourceFileOptions::default(),
)
.expect("rewrite should succeed");
assert_eq!(
rewritten.source.trim(),
r#"
non_empty("rss");
is_empty("");
"#
.trim()
);
}
#[test]
fn rustscript_namespace_import_turbofish_calls_rewrite_to_direct_calls() {
let source = r#"collections::dedup::<string>(["rss", "rss"]);"#;
let path = Path::new("tests/main.rss");
let imports = vec![ModuleImport {
spec: "collections.rss".to_string(),
clause: ImportClause::Namespace("collections".to_string()),
line: 1,
}];
let mut module_exports =
HashMap::<PathBuf, HashMap<String, ExportedFunctionSignature>>::new();
module_exports.insert(
PathBuf::from("tests").join("collections.rss"),
HashMap::from([(
"dedup".to_string(),
ExportedFunctionSignature {
arity: 1,
type_params: vec!["T".to_string()],
},
)]),
);
let rewritten = rewrite_imported_call_sites(
source,
SourceFlavor::RustScript,
path,
&imports,
&module_exports,
&CompileSourceFileOptions::default(),
)
.expect("rewrite should succeed");
assert_eq!(
rewritten.source.trim(),
r#"dedup::<string>(["rss", "rss"]);"#
);
}
#[test]
fn rustscript_named_import_turbofish_calls_rewrite_to_direct_calls() {
let source = r#"dedup_items::<string>(["rss", "rss"]);"#;
let path = Path::new("tests/main.rss");
let imports = vec![ModuleImport {
spec: "collections.rss".to_string(),
clause: ImportClause::Named(vec![NamedImport {
imported: "dedup".to_string(),
local: "dedup_items".to_string(),
}]),
line: 1,
}];
let mut module_exports =
HashMap::<PathBuf, HashMap<String, ExportedFunctionSignature>>::new();
module_exports.insert(
PathBuf::from("tests").join("collections.rss"),
HashMap::from([(
"dedup".to_string(),
ExportedFunctionSignature {
arity: 1,
type_params: vec!["T".to_string()],
},
)]),
);
let rewritten = rewrite_imported_call_sites(
source,
SourceFlavor::RustScript,
path,
&imports,
&module_exports,
&CompileSourceFileOptions::default(),
)
.expect("rewrite should succeed");
assert_eq!(
rewritten.source.trim(),
r#"dedup::<string>(["rss", "rss"]);"#
);
}
#[test]
fn rustscript_all_public_import_namespace_calls_rewrite_to_direct_calls() {
let source = "runtime::sleep(3);\n";
let path = Path::new("tests/main.rss");
let imports = vec![ModuleImport {
spec: "runtime.rss".to_string(),
clause: ImportClause::AllPublic,
line: 1,
}];
let mut module_exports =
HashMap::<PathBuf, HashMap<String, ExportedFunctionSignature>>::new();
module_exports.insert(
PathBuf::from("tests").join("runtime.rss"),
HashMap::from([(
"sleep".to_string(),
ExportedFunctionSignature {
arity: 1,
type_params: Vec::new(),
},
)]),
);
let rewritten = rewrite_imported_call_sites(
source,
SourceFlavor::RustScript,
path,
&imports,
&module_exports,
&CompileSourceFileOptions::default(),
)
.expect("rewrite should succeed");
assert_eq!(rewritten.source.trim(), "sleep(3);");
}
}