config: convert the setup argument with FromLua
The serde layer cannot ride mlua conversion automatically, but a
hand-written impl can: it runs inside the callback trampoline where
the main chunk frame is alive, so conversion errors keep the config
call site that the inline from_value call used to add.

Assisted-by: GLM-5.3 via pi
change mxruonozrmnwupsmotxvnsxxmrltotnz
commit bca28d0898a7e7ae1f47c570430ef66e0f09d78a
author Alpha Chen <alpha@kejadlen.dev>
date
parent wwyvxnxo
diff --git a/src/parallax.rs b/src/parallax.rs
index 63c2c52..f6e3333 100644
--- a/src/parallax.rs
+++ b/src/parallax.rs
@@ -4,6 +4,7 @@ use std::mem;
 
 use mlua::AnyUserData;
 use mlua::ErrorContext as _;
+use mlua::FromLua;
 use mlua::Function;
 use mlua::Lua;
 use mlua::LuaSerdeExt as _;
@@ -29,13 +30,10 @@ struct Registrations {
 }
 
 impl Registrations {
-    fn setup(&mut self, lua: &Lua, table: Table) -> Result<(), mlua::Error> {
+    fn setup(&mut self, lua: &Lua, setup: Setup) -> Result<(), mlua::Error> {
         if self.setup.is_some() {
             return Err(error_at(lua, "setup must be called exactly once"));
         }
-        let setup: Setup = lua
-            .from_value(Value::Table(table))
-            .map_err(|error| error_at(lua, &error_message(&error)))?;
         if setup.variants.is_empty() {
             return Err(error_at(lua, "variants must not be empty"));
         }
@@ -65,6 +63,16 @@ impl Registrations {
 /// — with no self to bind a userdata method to.
 impl UserData for Registrations {}
 
+/// Converts `(setup {...})`'s argument, keeping the config call site in
+/// the error: `from_lua` runs inside the callback trampoline, so the
+/// main chunk's frame is still on the Lua stack for caller_position.
+impl FromLua for Setup {
+    fn from_lua(value: Value, lua: &Lua) -> Result<Setup, mlua::Error> {
+        lua.from_value(value)
+            .map_err(|error| error_at(lua, &error_message(&error)))
+    }
+}
+
 /// A loaded parallax: the executable form of a config. The server
 /// function and flow are owned handles into the embedded Lua state,
 /// which stays alive in this struct; fields are ordered so the handles
@@ -156,8 +164,8 @@ fn install_module(lua: &Lua) -> Result<AnyUserData, mlua::Error> {
 
     let setup = {
         let module = module.clone();
-        lua.create_function(move |lua, table: Table| {
-            module.borrow_mut::<Registrations>()?.setup(lua, table)
+        lua.create_function(move |lua, setup: Setup| {
+            module.borrow_mut::<Registrations>()?.setup(lua, setup)
         })?
     };
     let server = {
diff --git a/tests/cli.rs b/tests/cli.rs
index 38ab3b9..66f26c5 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -99,6 +99,25 @@ fn setup_errors_point_at_the_setup_call() {
         );
 }
 
+#[test]
+fn setup_conversion_errors_point_at_the_setup_call() {
+    let (_dir, path) = config_file(concat!(
+        "(local {: setup : flow : server} (require :parallax))\n",
+        "\n",
+        "(setup {:variants {:desktop {:viewport [1280 900]}} :extra 1})\n",
+        "\n",
+        "(server (fn [] {:cmd [\"true\"]}))\n",
+        "(flow (fn [] nil))\n",
+    ));
+    cargo_bin_cmd!("parallax")
+        .arg("list")
+        .arg("--config")
+        .arg(&path)
+        .assert()
+        .failure()
+        .stderr(predicate::str::contains("config.fnl:3:").and(predicate::str::contains("unknown")));
+}
+
 #[test]
 fn missing_setup_registration_fails() {
     let (_dir, path) = config_file(concat!(