mod cache;
mod config;
mod context;
mod discovery;
mod error;
mod extract;
pub use cache::{CURRENT_VERSION, CacheManager};
pub use config::{
DebugFileConfig, GhostConfig, LoaderConfig, LoggingConfig, LuaConfig, PastaConfig,
PersistenceConfig, TalkConfig, default_debug_port, default_hour_margin, default_libs,
default_log_file_path, default_lua_search_paths, default_spot_newlines,
default_talk_interval_max, default_talk_interval_min,
};
pub use context::LoaderContext;
pub use error::{LoaderError, TranspileFailure};
use crate::context::TranspileContext;
use crate::debug::source_map::{MapBuilderSink, SourceMap};
use crate::runtime::{PastaLuaRuntime, RuntimeConfig};
use crate::transpiler::LuaTranspiler;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, error, info, warn};
struct ProcessStats {
transpiled: usize,
skipped: usize,
failed: usize,
copied: usize,
}
pub struct PastaLoader;
impl PastaLoader {
pub fn load(base_dir: impl AsRef<Path>) -> Result<PastaLuaRuntime, LoaderError> {
Self::load_with_config(base_dir, RuntimeConfig::new())
}
pub fn load_with_config(
base_dir: impl AsRef<Path>,
runtime_config: RuntimeConfig,
) -> Result<PastaLuaRuntime, LoaderError> {
let base_dir = base_dir.as_ref();
if !base_dir.exists() {
return Err(LoaderError::DirectoryNotFound(base_dir.to_path_buf()));
}
info!(path = %base_dir.display(), "Starting pasta loader");
debug!("Phase 1: Loading configuration");
let config = PastaConfig::load(base_dir)?;
debug!("Stage 1.5: Applying logging configuration");
let logger = Self::create_and_register_logger(base_dir, &config)?;
debug!("Phase 2: Preparing directories and cache");
Self::prepare_directories(base_dir, &config.loader)?;
let cache_manager =
CacheManager::new(base_dir.to_path_buf(), &config.loader.transpiled_output_dir);
cache_manager.prepare_cache_dir()?;
debug!("Phase 2.5: Self-deploying framework scripts");
match extract::sync_pasta_scripts(base_dir) {
Ok(_outcome) => {
}
Err(e) => {
error!(
path = %base_dir.join("profile/pasta/pasta_scripts").display(),
error = %e,
"Self-deploy failed; continuing startup with existing scripts (version drift unresolved)"
);
}
}
debug!("Phase 3: Discovering pasta and lua files");
let (pasta_files, lua_files) =
Self::discover_all_files(base_dir, &config.loader.pasta_patterns)?;
let total_files = pasta_files.len() + lua_files.len();
if total_files == 0 {
warn!(path = %base_dir.display(), "No .pasta or .lua files found");
} else {
info!(
pasta = pasta_files.len(),
lua = lua_files.len(),
"Found files"
);
}
debug!("Phase 4: Incremental processing");
let (context, module_names, stats) =
Self::process_incremental(&pasta_files, &lua_files, &cache_manager)?;
if config.loader.debug_mode {
info!(
transpiled = stats.transpiled,
skipped = stats.skipped,
failed = stats.failed,
copied = stats.copied,
"Processing statistics"
);
}
let all_source_files: Vec<_> = pasta_files
.iter()
.chain(lua_files.iter())
.cloned()
.collect();
let orphans = cache_manager.find_orphaned_caches(&all_source_files);
if !orphans.is_empty() && config.loader.debug_mode {
for orphan in &orphans {
warn!(path = %orphan.display(), "Orphaned cache file detected");
}
}
debug!("Phase 5: Generating scene_dic.lua");
let scene_dic_path = cache_manager.generate_scene_dic(&module_names)?;
let debug_file = config.debug();
let resolved_debug = crate::debug::DebugConfig::from_env(debug_file.as_ref());
let debug_enabled = resolved_debug.enabled;
let source_map = if debug_enabled {
let map = Self::build_source_map(
&pasta_files,
&cache_manager,
resolved_debug.source_map_sidecar,
);
debug!("Phase 5.5: Built source map for debug session");
Some(map)
} else {
None
};
debug!("Phase 6: Initializing runtime");
let loader_context = LoaderContext::from_config(base_dir, &config);
let runtime = PastaLuaRuntime::from_loader_with_scene_dic(
context,
loader_context,
runtime_config,
Some(config),
logger,
&scene_dic_path,
source_map,
)?;
info!(path = %base_dir.display(), "Startup sequence completed");
Ok(runtime)
}
fn create_and_register_logger(
base_dir: &Path,
config: &PastaConfig,
) -> Result<Option<std::sync::Arc<crate::logging::PastaLogger>>, LoaderError> {
let logging_config = config.logging();
match crate::logging::PastaLogger::new(base_dir, logging_config.as_ref()) {
Ok(logger) => {
let logger = std::sync::Arc::new(logger);
info!(path = %logger.log_path().display(), "Created instance logger");
crate::logging::GlobalLoggerRegistry::instance()
.register(base_dir.to_path_buf(), logger.clone());
if let Some(ref cfg) = logging_config {
crate::logging::update_tracing_filter(cfg);
}
Ok(Some(logger))
}
Err(e) => {
warn!(error = %e, "Failed to create instance logger, logging disabled");
Ok(None)
}
}
}
fn prepare_directories(base_dir: &Path, config: &LoaderConfig) -> Result<(), LoaderError> {
let dirs = [
"profile/pasta/save",
"profile/pasta/save/lua",
"profile/pasta/cache",
&config.transpiled_output_dir,
];
for dir in &dirs {
let path = base_dir.join(dir);
if !path.exists() {
fs::create_dir_all(&path).map_err(|e| LoaderError::io(&path, e))?;
debug!(path = %path.display(), "Created directory");
}
}
Ok(())
}
fn discover_all_files(
base_dir: &Path,
pasta_patterns: &[String],
) -> Result<(Vec<std::path::PathBuf>, Vec<std::path::PathBuf>), LoaderError> {
let pasta_files = discovery::discover_files(base_dir, pasta_patterns)?;
let lua_patterns: Vec<String> = pasta_patterns
.iter()
.filter_map(|p| {
if let Some(stem) = p.strip_suffix(".pasta") {
Some(format!("{}.lua", stem))
} else {
warn!(pattern = %p, "Cannot convert pattern to .lua, skipping");
None
}
})
.collect();
let lua_files = if lua_patterns.is_empty() {
Vec::new()
} else {
discovery::discover_files(base_dir, &lua_patterns).unwrap_or_else(|e| {
warn!(error = %e, "Failed to discover .lua files, skipping");
Vec::new()
})
};
for file in pasta_files.iter().chain(lua_files.iter()) {
if let Some(file_name) = file.file_name().and_then(|n| n.to_str())
&& (file_name == "init.lua" || file_name == "init.pasta")
{
return Err(LoaderError::invalid_file_name(file));
}
}
let pasta_module_names: std::collections::HashSet<String> = pasta_files
.iter()
.map(|f| module_key(base_dir, f))
.collect();
let mut filtered_lua_files = Vec::new();
for lua_file in &lua_files {
let module_key = module_key(base_dir, lua_file);
if pasta_module_names.contains(&module_key) {
warn!(
lua_file = %lua_file.display(),
module_name = %format!("pasta.scene.{}", module_key),
"Module name conflict: .pasta file takes priority, .lua file ignored"
);
} else {
filtered_lua_files.push(lua_file.clone());
}
}
Ok((pasta_files, filtered_lua_files))
}
fn process_incremental(
pasta_files: &[std::path::PathBuf],
lua_files: &[std::path::PathBuf],
cache_manager: &CacheManager,
) -> Result<(TranspileContext, Vec<String>, ProcessStats), LoaderError> {
let transpiler = LuaTranspiler::default();
let mut combined_context = TranspileContext::new();
let total_count = pasta_files.len() + lua_files.len();
let mut module_names = Vec::with_capacity(total_count);
let mut failures = Vec::new();
let mut stats = ProcessStats {
transpiled: 0,
skipped: 0,
failed: 0,
copied: 0,
};
for file_path in pasta_files {
let needs_transpile = cache_manager.needs_transpile(file_path).unwrap_or(true);
let module_name = cache_manager.source_to_module_name(file_path);
module_names.push(module_name.clone());
if !needs_transpile {
stats.skipped += 1;
debug!(file = %file_path.display(), "Skipped (cache up-to-date)");
continue;
}
let content = match fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Read error: {}", e),
});
stats.failed += 1;
continue;
}
};
let filename = file_path.to_string_lossy().to_string();
let pasta_file = match pasta_dsl::parse_str(&content, &filename) {
Ok(pf) => pf,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Parse error: {}", e),
});
stats.failed += 1;
continue;
}
};
let mut output = Vec::new();
let file_context = match transpiler.transpile(&pasta_file, &mut output) {
Ok(ctx) => ctx,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Transpile error: {}", e),
});
stats.failed += 1;
continue;
}
};
combined_context.merge_from(file_context);
let lua_code = match String::from_utf8(output) {
Ok(s) => s,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("UTF-8 error: {}", e),
});
stats.failed += 1;
continue;
}
};
if let Err(e) = cache_manager.save_cache(file_path, &lua_code) {
warn!(file = %file_path.display(), error = %e, "Failed to save cache");
}
stats.transpiled += 1;
debug!(file = %file_path.display(), module = %module_name, "Transpiled");
}
for file_path in lua_files {
let needs_copy = cache_manager.needs_transpile(file_path).unwrap_or(true);
let module_name = cache_manager.source_to_module_name(file_path);
module_names.push(module_name.clone());
if !needs_copy {
stats.skipped += 1;
debug!(file = %file_path.display(), "Skipped .lua (cache up-to-date)");
continue;
}
let content = match fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
error!(
file = %file_path.display(),
error = %e,
"Failed to read .lua file"
);
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Read error: {}", e),
});
stats.failed += 1;
continue;
}
};
if let Err(e) = cache_manager.save_cache(file_path, &content) {
error!(
file = %file_path.display(),
error = %e,
"Failed to copy .lua file to cache"
);
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Cache write error: {}", e),
});
stats.failed += 1;
continue;
}
stats.copied += 1;
debug!(file = %file_path.display(), module = %module_name, "Copied .lua");
}
if !failures.is_empty() {
for failure in &failures {
error!(
path = %failure.source_path.display(),
error = %failure.error,
"Process failure"
);
}
let succeeded = stats.transpiled + stats.skipped + stats.copied;
return Err(LoaderError::partial_transpile(
succeeded,
stats.failed,
failures,
));
}
Ok((combined_context, module_names, stats))
}
pub fn build_source_map(
pasta_files: &[std::path::PathBuf],
cache_manager: &CacheManager,
sidecar: bool,
) -> Arc<SourceMap> {
Arc::new(build_source_map_inner(pasta_files, cache_manager, sidecar))
}
}
fn module_key(base_dir: &Path, file: &Path) -> String {
let relative = file
.strip_prefix(base_dir)
.unwrap_or(file)
.to_string_lossy()
.to_string();
let without_prefix = relative
.strip_prefix("dic")
.unwrap_or(&relative)
.trim_start_matches(['/', '\\'])
.to_string();
let stem = std::path::Path::new(&without_prefix)
.with_extension("")
.to_string_lossy()
.to_string();
stem.replace(['/', '\\'], ".").replace('-', "_")
}
fn build_source_map_inner(
pasta_files: &[std::path::PathBuf],
cache_manager: &CacheManager,
sidecar: bool,
) -> SourceMap {
let transpiler = LuaTranspiler::default();
let mut source_map = SourceMap::new();
let mut chunk_count = 0usize;
for file_path in pasta_files {
let content = match fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
warn!(file = %file_path.display(), error = %e, "source-map: skip (read error)");
continue;
}
};
let filename = file_path.to_string_lossy().to_string();
let pasta_file = match pasta_dsl::parse_str(&content, &filename) {
Ok(pf) => pf,
Err(e) => {
warn!(file = %file_path.display(), error = %e, "source-map: skip (parse error)");
continue;
}
};
let chunk_name = cache_manager
.source_to_cache_path(file_path)
.to_string_lossy()
.to_string();
let mut sink = MapBuilderSink::new(filename.clone(), chunk_name.clone());
let mut output = Vec::new();
let shift = match transpiler.transpile_with_source_map(
&pasta_file,
&mut output,
Some(&mut sink),
) {
Ok((_ctx, shift)) => shift,
Err(e) => {
warn!(file = %file_path.display(), error = %e, "source-map: skip (transpile error)");
continue;
}
};
let chunk_map = sink.finish(&shift);
if sidecar {
let lua_path = cache_manager.source_to_cache_path(file_path);
if let Err(e) =
crate::debug::source_map::write_sidecar(&lua_path, &filename, &chunk_map)
{
warn!(
file = %lua_path.display(),
error = %e,
"source-map: sidecar write failed; continuing with in-memory map (non-fatal, 3.2/3.1)"
);
} else {
debug!(file = %lua_path.display(), "source-map: wrote sidecar (.lua.map)");
}
}
source_map.insert_chunk(chunk_name, filename, chunk_map);
chunk_count += 1;
}
debug!(
chunks = chunk_count,
files = pasta_files.len(),
"source-map: built aggregate map"
);
source_map
}
#[derive(Debug, Clone)]
pub struct TranspileResult {
pub module_name: String,
pub lua_code: String,
pub source_path: std::path::PathBuf,
}