gren-lang / gren-lang/compiler

A port's definition can be emitted before the kernel code it calls

Open
#387 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Haskell
Stars
503
Forks
29
PR merge metrics
No merged PRs in 30d

Description

Found against: gren 0.6.6, gren-lang/core 7.4.2, node 22

Summary

A port compiles to a var whose initializer calls _Platform_incomingPort,
_Platform_outgoingPort or _Platform_taskPort. All three read module-level
vars that live in the same kernel chunk — _Platform_effectManagers and
_Platform_taskPorts — so that chunk has to have run first.

Nothing makes it run first. Optimize.Module.addPort records only the
converter's dependencies on the port's graph node:

Can.Incoming _ payloadType _ ->
  let (deps, fields, decoder) = Names.run (Port.toDecoder payloadType)
      node = Opt.PortIncoming (Port.isBytes payloadType) decoder deps
   in addToGraph (Opt.Global home name) node fields graph

deps is what Json.Decode.string and friends need. The kernel Platform
module — which Generate.JavaScript.generatePort is about to emit a call
into — is not in it, so Generate.JavaScript is free to emit the port's var
before the chunk that defines what the call reads.

Whether it does depends on the rest of the program, because the emission order
is a traversal of the dependency graph and the port is being ordered against
edges it has no place in. When it goes the wrong way the program does not
start:

TypeError: Cannot read properties of undefined (reading 'inp')
    at _Platform_checkPortName (min.js:2113:31)
    at _Platform_incomingPort (min.js:2189:3)
    at min.js:1790:32

--optimize makes no difference; the graph is the same.

Reproduction

This example is a browser application depending only on gren-lang/core
7.4.2 — see below for why the platform is browser and not node.

port module Main exposing (main)

import Platform


port inp : (String -> msg) -> Sub msg


main : Program {} {} String
main =
    Platform.worker
        { init = \_ -> { model = {}, command = Cmd.none }
        , update = \_ model -> { model = model, command = Cmd.none }
        , subscriptions = \_ -> inp identity
        }
$ gren make Main --output=min.js
Success! Compiled 1 module.

$ node -e "require('./min.js')"
TypeError: Cannot read properties of undefined (reading 'inp')

Loading the bundle is enough; nothing has to call init. In the emitted file:

$ grep -n "incomingPort('inp'\|^var _Platform_effectManagers" min.js
1790:var $author$project$Main$inp = _Platform_incomingPort('inp', $gren_lang$core$Json$Decode$string, false);
1877:var _Platform_effectManagers = {};

The port's var is 87 lines above the assignment it depends on. The 2113 in
the trace is a third line — if (_Platform_effectManagers[name]) { inside
_Platform_checkPortName, which is where the read happens. The var at 1877 is
hoisted, so the name exists by the time line 1790 runs; it has just not been
assigned, and undefined[name] throws.

The bug is not universal, because the order is a traversal of the whole
program's graph and a bigger program is likely to reach the kernel chunk down
some other edge first. Which way it goes is not the port's to choose either way.

Two programs that do not trip it, for that reason:

  • the same program with an outgoing port instead — port out : String -> Cmd msg, sent from init;
  • the same port on the node platform. Node.defineProgram reaches a great
    deal of core and node before it reaches the port, and the chunk comes out
    first by a wide margin: var _Platform_effectManagers = {} at line 667 and the
    port's var at line 3185.

That last one is why the reproduction is a browser application. It is not a
browser-specific bug — nothing here touches the DOM, and Platform.worker is
gren-lang/core's — it is that a program small enough to trip the ordering is
only reachable on the platform where Platform.worker is the entry point. Two
further reasons the node platform would hide it even if the order went the
other way: gren make on that platform wraps the whole bundle in try { … } catch (e) { console.error(e) } (Generate.Node.sandwich) unless --output
names a .js file, so a load-time throw is printed and the process still exits
0; and the wrapper calls init({}) and discards the result, so app.ports is
unreachable and nobody writes such a program in the first place.

In a browser

Loading the bundle under node is only a convenient way to show the throw; an
ordinary <script> tag fails the same way, at the same line.
For reproducing in a browser, clone the git repo at
https://github.com/gilramir/gren-bug-reports
It's in the 2026-09-06-port-emitted-early directory.
"./run.sh" will build the Gren app and run python as a web server:

% ./run.sh
Info: Running script "build" on /home/gram/prj/gren-bug-reports/2026-09-06-port-emitted-early
Success! Compiled 1 module.

    Main ───> main.js


=== http://localhost:8000/ — the console shows the throw; ctrl-c to stop
Info: Running script "serve" on /home/gram/prj/gren-bug-reports/2026-09-06-port-emitted-early
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Then browse to http://localhost:8000/ and in the javascript console you get:

Image

BTW

Opt.Managerhas the same shape of omission —generateManageremits calls to_Platform_leafand_Platform_createManagerand the node carries no dependency on the kernelPlatformmodule either. It has not been observed to misorder, because a manager is only ever reached throughcore` code that names
that module anyway, but the edge is missing there for the same reason.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the failure with the 2026-09-06-port-emitted-early example and inspect the generated order. Start at Optimize.Module.addPort and Generate.JavaScript.generatePort, then compare the Opt.Manager path and its generateManager calls. Done means the Platform kernel definitions are emitted before any port initializer that reads them, without breaking the existing node and outgoing-port cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell, javascript
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.