可能的 OCaml 代码生成错误

Possible OCaml code generation bug

以下自包含代码突出了 OCaml 中的一个问题,可能与代码生成有关。 数组 x 具有 [0..9] 中节点的连接信息。函数 init_graph 最初为每个节点构造了传入节点的显式数组。下面显示的简化版本仅打印两个连接的节点。

函数 init_graph2 与 init_graph 相同,除了 "useless" else 分支。但是这两个函数产生的输出是完全不同的。您可以 运行 它并看到 init_graph 在某些情况下会跳过第二个 if-then-else!

我们在版本 3.12.1(适当替换了 make_matrix)、4.03.0 和 4.03.0+flambda 上有 运行 这个程序。他们都有同样的问题。

我一直在处理这个问题以及 OCaml 神秘地跳过分支或者在某些情况下同时使用两个分支的相关问题。感谢合作者,我们能够将实际代码缩减为一个小的独立示例。

对这里发生的事情有什么想法吗?有没有办法避免这个和相关问题?

let x =
   let arr = Array.make_matrix 10 10 false in
     begin
      arr.( 6).( 4) <- true;
      arr.( 2).( 9) <- true;
     end;
     arr

let init_graph () =
   for i = 0 to 9 do
     for j = 0 to (i-1) do
       begin
         if x.(i).(j) then
           let (i_inarr, _) = ([||],[||]) in
           begin
             Format.printf "updateA: %d %d \n" i j;
           end
         (* else () *)
        ;
       if x.(j).(i) then
         let (j_inarr, _) = ([||],[||]) in
         begin
           Format.printf "updateB: %d %d \n" i j;
         end
       end
    done
 done;
 Format.printf "init_graph: num nodes is %i\n" 10

let init_graph2 () =
  for i = 0 to 9 do
    for j = 0 to (i-1) do
      begin
        if x.(i).(j) then
          let (i_inarr, _) = ([||],[||]) in
          begin
            Format.printf "updateA: %d %d \n" i j;
          end
        else ()
        ;
        if x.(j).(i) then
          let (j_inarr, _) = ([||],[||]) in
          begin
            Format.printf "updateB: %d %d \n" i j;
          end
        end
      done
   done;
   Format.printf "init_graph: num nodes is %i\n" 10

 let test1 = init_graph ()

 let test2 = init_graph2 ()

更新: Ocamllint 将 init_graph2 中的 else 分支标记为 "useless",这显然是错误的。

其次,在这种情况下,camlspotter 建议的缩进方法可能会产生误导。我们遵循 Ocamllint 的建议并注释掉 else 分支。具有 taureg 模式的 Emacs 不会重新缩进此代码,除非明确要求让我们相信一切都很好。

我们需要的是一种类似 lint 的工具,可以在这些情况下发出警告。我正在等待这方面的好建议。

谢谢。

您的问题似乎与 let ... in 的处理有关。此构造引入了一系列以分号分隔的表达式,而不是单个表达式。所以这段代码:

   if x.(i).(j) then
     let (i_inarr, _) = ([||],[||]) in
     begin
       Format.printf "updateA: %d %d \n" i j;
     end
   (* else () *)
   ;
   if x.(j).(i) then
     let (j_inarr, _) = ([||],[||]) in
     begin
       Format.printf "updateB: %d %d \n" i j;
     end

实际上是这样解析的:

     if x.(i).(j) then
       let (i_inarr, _) = ([||],[||]) in
       begin
         Format.printf "updateA: %d %d \n" i j;
       end
           (* else () *)
       ;
       if x.(j).(i) then
         let (j_inarr, _) = ([||],[||]) in
         begin
           Format.printf "updateB: %d %d \n" i j;
         end

也就是说第一个begin/end和第二个if/then都被第一个if/then控制了。

另一种说法是 ; 的优先级高于 let ... in。所以 let x = y in a ; b 被解析为 let x = y in (a; b),而不是 (let x = y in a); b.

当您包含 "useless" else 时,事情会按照您认为应该的方式进行解析。

确实如此,在 OCaml 中将 if/thenlet 混合使用时必须非常小心。我遇到过这样的问题。 if/thenelse 控制单个表达式的一般直觉虽然正确,但当其中一个表达式是 let.

时很容易出错

正如 Jeffrey 所回答的,从代码缩进中可读的意图与代码的实际解析方式大不相同。

您可以通过使用适当的自动缩进工具来避免此类错误,例如 caml-mode、tuareg-mode、ocp-indent 和 OCaml 的 vim 插件。

通过自动缩进 init_graph 的第二个 if,您可以立即发现它在第一个 ifthen 子句下。