Reporting a behavioral difference between the WASM and native Lua paths for dofile() and quarto.utils.resolve_path(). This came up during the WASM testing cleanup in #109 — sharing for discussion.
Observation
On WASM, register_wasm_dofile overrides Lua's built-in dofile and adds push/pop of the script-dir stack around each call. On native, Lua's C dofile is used and doesn't interact with the stack.
This means the same Lua filter produces different results:
-- filter.lua (in /ext/)
local resolved = dofile("helpers/ui.lua")
-- helpers/ui.lua
return quarto.utils.resolve_path("style.css")
-- WASM result: /ext/helpers/style.css
-- Native result: /ext/style.css
How this was discovered
While removing the #[cfg(any(target_arch = "wasm32", test))] proxy in #109, the test_dofile_script_dir_stack test started failing on native. The proxy had been routing native tests through register_wasm_dofile, masking the difference. In #109 we resolved this by marking the test as #[ignore] on non-wasm32 targets.
Context
The original design (commit f87b11b) intentionally limited the dofile override to WASM because native C fopen works fine. The script-dir push/pop was bundled with the file I/O override since both were needed on WASM.
Neither Pandoc nor Quarto CLI provides script-dir tracking for raw dofile():
- Pandoc:
dofile resolves relative to CWD. PANDOC_SCRIPT_FILE is set once for the top-level filter.
- Quarto CLI: has a
scriptFile stack via _quarto.withScriptFile, but only for shortcodes and wrapped filters. Raw dofile() uses standard Lua CWD-relative behavior.
Open question
Should these behave the same? Two directions are possible:
- Native gains the push/pop behavior (wrap C
dofile to add stack tracking without replacing file I/O). This would be an improvement over both Pandoc and Quarto CLI.
- WASM drops the push/pop behavior (only override file I/O via SystemRuntime, don't add stack tracking). This would align WASM with native and with Pandoc/Quarto CLI behavior.
Relevant code
|
/// Register `dofile` and `loadfile` overrides for the restricted Lua environment. |
|
pub fn register_wasm_dofile(lua: &Lua, runtime: Arc<dyn SystemRuntime>) -> Result<()> { |
|
// dofile(path) — read, compile, push script dir, execute, pop, return results |
|
let rt = runtime.clone(); |
|
lua.globals().set( |
|
"dofile", |
|
lua.create_function(move |lua, path: String| { |
|
let resolved = resolve_dofile_path(lua, &path)?; |
|
|
|
let content = rt.file_read_string(Path::new(&resolved)).map_err(|e| { |
|
mlua::Error::runtime(format!("dofile: cannot read '{}': {}", path, e)) |
|
})?; |
|
|
|
let chunk = lua.load(&content).set_name(&path); |
|
|
|
// Push the loaded file's directory onto the script-dir stack |
|
let file_dir = Path::new(&resolved) |
|
.parent() |
|
.unwrap_or(Path::new("")) |
|
.to_string_lossy() |
|
.to_string(); |
|
push_script_dir(lua, &file_dir)?; |
|
|
|
let result = chunk.eval::<MultiValue>(); |
|
|
|
pop_script_dir(lua)?; |
|
|
|
result |
|
})?, |
|
)?; |
|
|
|
// loadfile(path) — read and compile only, return chunk (or nil + error) |
|
lua.globals().set( |
|
"loadfile", |
|
lua.create_function(move |lua, path: String| { |
|
let resolved = resolve_dofile_path(lua, &path)?; |
|
|
|
let content = match runtime.file_read_string(Path::new(&resolved)) { |
|
Ok(c) => c, |
|
Err(e) => { |
|
// Lua semantics: loadfile returns (nil, error_message) on failure |
|
return Ok(MultiValue::from_iter([ |
|
Value::Nil, |
|
Value::String(lua.create_string(format!("cannot read '{}': {}", path, e))?), |
|
])); |
|
} |
|
}; |
|
|
|
match lua.load(&content).set_name(&path).into_function() { |
|
Ok(func) => Ok(MultiValue::from_iter([Value::Function(func)])), |
|
Err(e) => Ok(MultiValue::from_iter([ |
|
Value::Nil, |
|
Value::String(lua.create_string(e.to_string())?), |
|
])), |
|
} |
|
})?, |
|
)?; |
|
|
|
Ok(()) |
|
} |
|
#[cfg(any(target_arch = "wasm32", test))] |
|
let lua = { |
|
use mlua::StdLib; |
|
let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH; |
|
let lua = Lua::new_with(libs, mlua::LuaOptions::default()) |
|
.map_err(|e| LuaFilterError::LuaError(e))?; |
|
super::os_wasm::register_wasm_os(&lua, runtime.clone())?; |
|
super::io_wasm::register_wasm_io(&lua, runtime.clone())?; |
|
super::dofile_wasm::register_wasm_dofile(&lua, runtime.clone())?; |
|
lua |
|
}; |
|
#[cfg(not(any(target_arch = "wasm32", test)))] |
|
let lua = Lua::new(); |
Reporting a behavioral difference between the WASM and native Lua paths for
dofile()andquarto.utils.resolve_path(). This came up during the WASM testing cleanup in #109 — sharing for discussion.Observation
On WASM,
register_wasm_dofileoverrides Lua's built-indofileand adds push/pop of the script-dir stack around each call. On native, Lua's Cdofileis used and doesn't interact with the stack.This means the same Lua filter produces different results:
How this was discovered
While removing the
#[cfg(any(target_arch = "wasm32", test))]proxy in #109, thetest_dofile_script_dir_stacktest started failing on native. The proxy had been routing native tests throughregister_wasm_dofile, masking the difference. In #109 we resolved this by marking the test as#[ignore]on non-wasm32 targets.Context
The original design (commit f87b11b) intentionally limited the dofile override to WASM because native C
fopenworks fine. The script-dir push/pop was bundled with the file I/O override since both were needed on WASM.Neither Pandoc nor Quarto CLI provides script-dir tracking for raw
dofile():dofileresolves relative to CWD.PANDOC_SCRIPT_FILEis set once for the top-level filter.scriptFilestack via_quarto.withScriptFile, but only for shortcodes and wrapped filters. Rawdofile()uses standard Lua CWD-relative behavior.Open question
Should these behave the same? Two directions are possible:
dofileto add stack tracking without replacing file I/O). This would be an improvement over both Pandoc and Quarto CLI.Relevant code
q2/crates/pampa/src/lua/dofile_wasm.rs
Lines 47 to 106 in dd1cdd4
q2/crates/pampa/src/lua/filter.rs
Lines 135 to 147 in dd1cdd4