A dynamic import `module M = await List` fails with `Unbound module type __List__` after a statement, in a callback and in other positions

オープン
#9,230 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
55/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
ocaml
領域
compilers

調査の方向性

Start by reading compiler/frontend/bs_builtin_ppx.ml, especially the Pexp_letmodule case in expr_mapper and aux in spelunk_vbs; then trace Ast_await.create_await_module_expression and structure_mapper's Pstr_value case. Reproduce the listed failing and working positions, including a Pstr_eval expression, and inspect existing coverage around #6431 and #7783. Done means every import rewritten by expr_mapper has its module type declared, with regression tests covering the reported positions.

索引モデルが issue の本文から書いたものです。

説明

A dynamic import of a module, module M = await List, is rejected with Unbound module type __List__ depending on where it appears in the async function. A statement before it is enough:

let f = async () => {
  Console.log("start")
  module M = await List
  M.length(list{1, 2})
}
f()->Promise.thenResolve(Console.log)->ignore
  We've found a bug for you!
  Repro.res:3:14-23

  1 │ let f = async () => {
  2 │   Console.log("start")
  3 │   module M = await List
  4 │   M.length(list{1, 2})
  5 │ }

  Unbound module type __List__

The source is valid and should log start and then 2. __List__ is a name generated by the compiler, not one the source uses. Without the Console.log("start") line, the same function compiles to a dynamic import and logs 2:

async function f() {
  let M = await import("@rescript/runtime/lib/js/Stdlib_List.cjs");
  return M.length({hd: 1, tl: {hd: 2, tl: /* [] */0}});
}

The same error (__List__, or __Array__ for await Array) comes from an import

  • after any statement (e; module M = await List), also in a switch arm,
  • in an async function passed as an argument, e.g. [1]->Array.map(async _ => {...}) or p->Promise.then(async _ => {...}), or written in a record field, an array or a tuple,
  • in the body of a try, a for or a while, or in an if without else,
  • after open U, a local exception E, a plain local module module L = List, or a constrained import module L = await (List: ListT),
  • in an expression at the top level of a file, e.g. run(async () => { module M = await List; ... })->ignore, or of a nested module.

It compiles directly in the body of a function bound by let, after a let, in a nested block, in both branches of if/else and of a ternary, as the first expression of a switch arm, under a return type annotation, in an async function bound by let inside another function, in Some(async () => ...), and after another unconstrained import.

Whether an import compiles also depends on the rest of the file. The function from the first example compiles when an earlier binding imports List in one of the positions that work:

let g = async () => {
  module M = await List
  M.length(list{1})
}
let f = async () => {
  Console.log("start")
  module M = await List
  M.length(list{1, 2})
}
f()->Promise.thenResolve(Console.log)->ignore
async function f() {
  console.log("start");
  let M = await import("@rescript/runtime/lib/js/Stdlib_List.cjs");
  return M.length({hd: 1, tl: {hd: 2, tl: /* [] */0}});
}

This logs start and 2. With g placed after f, f fails again with Unbound module type __List__.

In compiler/frontend/bs_builtin_ppx.ml, the Pexp_letmodule case of expr_mapper rewrites every module M = await List into an unpack of import at the package type module(__List__) (Ast_await.create_await_module_expression), wherever the expression is. The declaration module type __List__ = module type of List is added separately, before the binding, by the Pstr_value case of structure_mapper. To find the imports, aux in spelunk_vbs follows only Pexp_letmodule, Pexp_let, Pexp_braces, Pexp_ifthenelse with an else, Pexp_ternary, Pexp_construct with one argument, Pexp_fun, Pexp_constraint and Pexp_match. Every other expression ends the search (| _ -> acc), among them Pexp_sequence, Pexp_apply, Pexp_try, Pexp_for, Pexp_while, Pexp_open, Pexp_letexception, Pexp_record, Pexp_array and Pexp_tuple. A Pexp_letmodule that is not an unconstrained import ends it too, without its body being searched. A Pstr_eval item, an expression at the top level of a file or of a module, is not searched at all. local_module_type_decl declares each name once per structure, so a declaration found for an earlier binding also serves every later import of the same module, which is why f compiles when g comes before it. Searching every subexpression, and Pstr_eval items too, would declare the module type for each import that expr_mapper rewrites.

Writing the module type out avoids the hoisting: with module type ListT = module type of List at the top level, module M = await (List: ListT) compiles to await import(...) in all of the positions above.

Reproduced with 11.1.4 (with Js.log for Console.log), 12.0.0-alpha.6, 12.3.1, 13.0.0-alpha.6 and master (fc7444266). The search has followed a fixed list of expression shapes since #6431 (11.0.0-rc.5). #7783, the fix for #7173, added Pexp_constraint and Pexp_match to it in 12.0.0-beta.7, so 12.0.0-alpha.6 also fails in a switch arm and under a return type annotation.

Related: #7173, #7783.

主要言語
OCaml
スター
7.5k
フォーク
485
平均マージ
17時間 9分
マージ済み PR(30日)
264

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

rescript-lang/rescript のほかの issue

rescript-lang/rescript の issue をすべて見る

似ている issue

Compilers の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。