gren-lang / gren-lang/compiler
A port's definition can be emitted before the kernel code it calls
Nobody has claimed this yet.
- 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 frominit; - the same port on the
nodeplatform.Node.defineProgramreaches a great
deal ofcoreandnodebefore it reaches the port, and the chunk comes out
first by a wide margin:var _Platform_effectManagers = {}at line 667 and the
port'svarat 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:
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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