Skip to content

Behavioral difference: dofile() script-dir tracking on WASM vs native #112

@cderv

Description

@cderv

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:

  1. 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.
  2. 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();

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions