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)
-
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.
-
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
Summary
Three
pub extern fndeclarations instdlib/Deno.affine(endsWith,stripSuffix,numToFixed2) have no entry inlib/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.affineport that hit this):Root cause
lib/codegen_deno.mlhas a comment at ~line 541-542: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 forextern fndeclarations — only forpub fndefinitions actually imported viause.So we get a worst-of-both-worlds:
b "..."lines) coverspathJoin,walkRecursive,regexMatch, etc. — those get inline__as_*JS shims.pub fns (e.g.string::split) get their bodies emitted when imported.pub extern fndeclarations whose "real implementation" lives elsewhere fall in a gap — neither pathway covers them.endsWith,stripSuffix,numToFixed2are the three that fall in the gap today (percomm -23of the two name lists).Workaround (used in standards#310)
Drop the
use Deno::{ endsWith }, inline astring_sub-backed helper: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)
Add the missing lookup entries to
codegen_deno.ml— pure mechanical fix, mirrors what's done forpathJoin:Cheap; ships with the next compiler bump; no stdlib linkage required.
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
Acceptance
codegen_deno.mlgains lookup entries forendsWith,stripSuffix,numToFixed2deno runreturns 0)tests/codegen-deno/covers each (probably extendsdeno_scripting.affine)