A polymorphic variant argument of an external is evaluated twice, or inside a generated function

Offen
#9,215 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Maintainer antworten meist innerhalb von 1 Tag

Ein zugehöriger Pull Request wurde bereits gemerged.

  • #9217 von @mununki — gemerged

Bewertung

Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Anfängerfreundlichkeit
20/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Veraltet
Tech-Stack
javascript, ocaml
Bereich
compilers

Rechercherichtung

Start with compiler/core/js_of_lam_variant.ml and compiler/core/js_exp_make.ml, then inspect how Lam_compile_external_call.ocaml_to_js_eff translates external arguments. Review the existing regression in tests/tests/src/gpr_2633_test.mjs and related issues #2633 and #8932. Done means external arguments with side effects are evaluated once and async arguments compile successfully; an open linked pull request (#9217) is already addressing this issue.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

When an external takes a polymorphic variant with @int, or with @string whose constructors have a payload or an @as, an argument that is not a literal or a variable is evaluated twice, or inside a function the compiler generates. An event-style @string argument, whose constructors have a payload, is evaluated once for the tag and once more for the payload. An @int argument and a @string argument with @as are evaluated inside an arrow function that switches on the value, so an await in them is a syntax error. An event-style argument with @as gets both: the arrow function switches on its tag, and the payload evaluates it again. A @string argument with neither payloads nor @as is passed as it is and is not affected.

type t
@send external on: (t, @string [#click(int => unit) | #keydown(string => unit)]) => unit = "on"
@send external setLevel: (t, @int [#low | #high]) => unit = "setLevel"

@val external nextEvent: unit => [#click(int => unit) | #keydown(string => unit)] = "nextEvent"

let f = t => on(t, nextEvent())
let g = async (t, p) => on(t, await p)
let h = async (t, p) => setLevel(t, await p)
function f(t) {
  t.on(nextEvent().NAME, nextEvent().VAL);
}

async function g(t, p) {
  t.on((await p).NAME, (await p).VAL);
}

async function h(t, p) {
  t.setLevel((() => {
    switch (await p) {
      case "low" :
        return 0;
      case "high" :
        return 1;
    }
  })());
}

f calls nextEvent() twice, so it consumes two events and passes the name of the first with the handler of the second. g awaits p twice. The module does not load because of h: Node.js reports SyntaxError: Unexpected reserved word for the ES module and SyntaxError: Unexpected identifier 'p' for CommonJS. The same holds for the field of an @obj external, as in makeOptions(~level=await p, ()) with ~level: @int [#low | #high]. A @get external passed as an event-style argument runs the getter twice. A literal whose payload has side effects is evaluated twice as well:

@val external wrap: (int => unit) => int => unit = "wrap"

let l = (t, handler) => on(t, #click(wrap(handler)))
function l(t, handler) {
  t.on(({
    NAME: "click",
    VAL: wrap(handler)
  }).NAME, ({
    NAME: "click",
    VAL: wrap(handler)
  }).VAL);
}

Lam_compile_external_call.ocaml_to_js_eff translates the compiled argument with the functions in compiler/core/js_of_lam_variant.ml, which return an expression and cannot bind the argument to a variable first. eval_as_event passes any argument other than a literal with a side-effect-free payload as arg.NAME and arg.VAL, copying the whole expression. eval, eval_as_int and the @as case of eval_as_event switch on the argument in a function built by E.of_block in compiler/core/js_exp_make.ml. The FIXME above eval_as_event, added with the fix for #2633 in 2018, notes the duplicated evaluation, and tests/tests/src/gpr_2633_test.mjs pins it as foo.on(h(event).NAME, h(event).VAL).

Reproduced with 11.1.4, 12.0.0-alpha.1, 12.0.0-alpha.6, 12.3.1, 13.0.0-alpha.6 and master (c10223e80). 11.1.4 and 12.0.0-alpha.1 print (function () { ... })() instead of the arrow function, with the same result.

Related: #2633, #8932.

Vorherrschende Sprache
OCaml
Sterne
7.5k
Forks
485
Ø Merge
17 Std. 9 Min.
Gemergte PRs (30 T.)
264

Entwicklungsumgebung

In Codespaces öffnen

Startet den Dev-Container des Projekts im Browser, mit Ihrem eigenen GitHub-Konto.

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus rescript-lang/rescript

Alle Issues in rescript-lang/rescript

Ähnliche Issues

Weitere Issues zu Compilers

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.