Rust in Everything
基于 topcoat-runtime 0.10.0

Failed to compile event @click:$(...) 里的 return、break 与 while

主线位置:交互主线的前半段。$(...) 由 topcoat-runtime-grammar 同时生成 Rust 和 JavaScript($(...) 表达式);事件处理器的闭包体在服务端不执行,只在浏览器里跑,所以 JS 一侧的问题编译期和服务端渲染都看不出来。

flowchart TB
  src["$(|_e| { ... })"] --> grammar["runtime-grammar:逐个语法节点生成 JS"]
  grammar --> ifn["if 包成立即调用的箭头函数"]
  grammar --> tail["块的最后一个表达式前加 return"]
  ifn --> r1["return 只退出 if:结果和 Rust 不同"]
  ifn --> r2["break 越过函数边界:SyntaxError"]
  tail --> r3["return while (...):SyntaxError"]
  r2 --> fail["浏览器:Failed to compile event @click"]
  r3 --> fail

现象

依赖:topcoat =0.10.0 的 full、tokio =1.54.1;页面挂了运行时(.assets(...)、.runtime()、script()),一个驱动脚本加载 500 毫秒后点按钮、把结果写进 <pre>,无头 Chrome --dump-dom 读出来。

let flag = signal(cx, || true);
let a = signal(cx, || 0.0f64);
// 1. if 里的 return
<button id="ret" @click=$(|_e| { if flag.get() { return a.set(1.0); } a.set(2.0) })>"ret"</button>
// 2. if 里的 break
<button id="brk" @click=$(|_e| { loop { if true { break; } break; } n.set(3.0) })>"brk"</button>
// 3. 块末尾的 while
<button id="brk-fixed" @click=$(|_e| { while m.get() < 3.0 { m.set(m.get() + 1.0); } })>"brk-fixed"</button>

三个都能编译,服务端照常渲染出页面。浏览器里:

ret:  a=2                       (按 Rust 语义应该是 1)
brk:  n=0                       (处理器没挂上,点击无反应)
      "Uncaught Error: Failed to compile event @click", source: .../_topcoat/assets/topcoat-a85ca5b0b8314af5.js (3)
while: m=0                      (同上)
      "Uncaught Error: Failed to compile event @click", source: .../_topcoat/assets/topcoat-a85ca5b0b8314af5.js (3)

控制台只有「Failed to compile event @click」,真正的 SyntaxError 在错误的 cause 里,要展开才看得到;是哪个元素的处理器,消息里也没有。

原因

if 被包成函数。 expr_if 为了让 if 在表达式和语句位置都能用,把 JS 一侧写成 (() => { if (...) ... })()。函数边界挡住了 return(只退出这个箭头函数,后面的 a.set(2.0) 照样执行)、break 和 continue(JS 里越过函数的 break 是语法错误「Illegal break statement」)。这三条已经登记在 coherence 的 known-mismatches.toml(return_inside_if、break_inside_if、continue_inside_if)。

块末尾的 while 被加了 return。 这一条没有登记。stmt 把块里最后一个、没有分号的表达式当成块的值,在 JS 里前面写 return :

// A trailing expression (no semicolon) is the block's value, so
// it becomes the JavaScript `return`.
let returns = is_last && semi.is_none();
if returns {
    js.push_str("return ");
}

Rust 里 while 是值为 () 的表达式,放在块末尾、不加分号完全合法;JS 里 while 是语句,生成出来的是:

(__local0) => { return while ((cx.hydrate({"t":"Signal","id":"a1bf..."})).get().lt(cx.hydrate(3.0)).dehydrate()) { ... }; }

return while 是语法错误。coherence 测试里有 while,但写的是 { while false {} 3.0 },while 不在末尾,所以没测到。tag 之后到 2026-10-14,expr/ 目录没有新提交。

改写

写法改成实测
{ if c { return x.set(1.0); } x.set(2.0) }if c { x.set(1.0) } else { x.set(2.0) }:把提前返回改成 if / else 两个分支b=1,和 Rust 一致
loop { if c { break; } ... }把条件提到循环头:while !c { ... },并在 while 后面加分号—
{ while c { ... } }{ while c { ... }; }:末尾加一个分号m=3,处理器正常

加了分号之后生成的 JS 是 { while (...) { ... }; },没有 return。Rust 一侧语义不变,块的值还是 ()。

经验规则:

  • $(...) 的事件处理器里不要写提前 return、不要在 if 里 break / continue。
  • 块的最后一个表达式如果是 while、loop 这类在 JS 里只能当语句的结构,后面加分号。(本页只实测了 while。)
  • 逻辑复杂到需要这些控制流时,把它放进 #[procedure] 在服务端算,或者用 raw! 直接写 JS。

怎么尽早发现

  • 开发时开着浏览器控制台;看到「Failed to compile event」就展开 cause。
  • 在测试里用无头浏览器打开每个页面,收集控制台的 Uncaught:服务端测试和 cargo build 都抓不到这一类错误。
  • coherence 只比较「表达式的结果」,不覆盖你的页面;本页的 while 就是它没覆盖到的写法。

小结

  • 浏览器报「Failed to compile event @click」而服务端一切正常,多半是 $(...) 生成的 JS 有语法错误;真正的 SyntaxError 在 cause 里。
  • if 被包成立即调用的函数:return 只退出 if(实测得 2 不得 1),break / continue 是语法错误;三条都在 coherence 的已知不一致里。
  • 块末尾不带分号的 while 被生成为 return while (...),同样编译失败;这一条没有登记,加一个分号就好。

💬 关联讨论

发起讨论
加载中...