Skip to content

codegen(deno-esm): Deno externs missing from codegen lookup → ReferenceError at runtime (endsWith / stripSuffix / numToFixed2) #470

@hyperpolymath

Description

@hyperpolymath

Summary

Three pub extern fn declarations in stdlib/Deno.affine (endsWith, stripSuffix, numToFixed2) have no entry in lib/codegen_deno.ml's lookup table. The codegen happily emits call sites for them, but never defines them in the compiled output. At runtime: ReferenceError: endsWith is not defined.

Repro

A minimal port (hand-trimmed from the live standards/scripts/check-ts-allowlist.affine port that hit this):

use Deno::{ endsWith };

pub fn main() -> Int {
  if endsWith("foo.ts", ".ts") { 0 } else { 1 }
}
$ affinescript check minimal.affine
Type checking passed

$ affinescript compile --deno-esm -o /tmp/min.deno.js minimal.affine
Compiled minimal.affine -> /tmp/min.deno.js (Deno-ESM)

$ deno run /tmp/min.deno.js
error: Uncaught (in promise) ReferenceError: endsWith is not defined
    at main (file:///tmp/min.deno.js:N:M)

Root cause

lib/codegen_deno.ml has a comment at ~line 541-542:

// not externs — endsWith/stripSuffix/pathJoin/etc. are NOT here:
// they are real AffineScript built on `ends_with`/`substring`/`++`.

The intent is clear: these externs are supposed to be backed by real AS bodies in stdlib/string.affine (e.g. fn ends_with). But the Deno-ESM backend doesn't link stdlib bodies for extern fn declarations — only for pub fn definitions actually imported via use.

So we get a worst-of-both-worlds:

  • The codegen lookup table (b "..." lines) covers pathJoin, walkRecursive, regexMatch, etc. — those get inline __as_* JS shims.
  • Real stdlib pub fns (e.g. string::split) get their bodies emitted when imported.
  • But pub extern fn declarations whose "real implementation" lives elsewhere fall in a gap — neither pathway covers them.

endsWith, stripSuffix, numToFixed2 are the three that fall in the gap today (per comm -23 of the two name lists).

Workaround (used in standards#310)

Drop the use Deno::{ endsWith }, inline a string_sub-backed helper:

fn ends_with(s: String, suffix: String) -> Bool {
  let slen = len(s);
  let sfxlen = len(suffix);
  if sfxlen > slen { false }
  else { string_sub(s, slen - sfxlen, sfxlen) == suffix }
}

This is exactly the defensive pattern the merged seed in standards#283 used; my rewrite removed it on the assumption the extern would link. It didn't.

Proposed fix (pick one)

  1. Add the missing lookup entries to codegen_deno.ml — pure mechanical fix, mirrors what's done for pathJoin:

    b "endsWith"     (fun a -> Printf.sprintf "String(%s).endsWith(%s)" (arg 0 a) (arg 1 a));
    b "stripSuffix"  (fun a -> Printf.sprintf "((s,x) => s.endsWith(x) ? s.slice(0, -x.length) : s)(%s, %s)" (arg 0 a) (arg 1 a));
    b "numToFixed2"  (fun a -> Printf.sprintf "Number(%s).toFixed(2)" (arg 0 a));

    Cheap; ships with the next compiler bump; no stdlib linkage required.

  2. Compile-in the real AS bodies when a use Deno::{ endsWith } is encountered. More general; would also unblock similar gaps in other stdlib modules. Larger change.

Option 1 is the right STEP. Option 2 is the right LONG-TERM. Recommend doing option 1 now and tracking option 2 as a separate stdlib-linkage issue.

Related

  • standards#283 — seed PR (used defensive inline helpers; merged but didn't compile)
  • standards#310 — compile + runtime fix that re-introduced the workaround
  • standards#311 — workflow swap that ships the compiled output

Acceptance

  • codegen_deno.ml gains lookup entries for endsWith, stripSuffix, numToFixed2
  • Minimal repro above compiles and runs (deno run returns 0)
  • A test in tests/codegen-deno/ covers each (probably extends deno_scripting.affine)
  • Follow-up issue filed for general extern→stdlib-body linkage (option 2)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions