@scriptc/compiler 0.0.28 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +5 -13
  2. package/dist/backend/cc.d.ts +23 -2
  3. package/dist/backend/cc.js +250 -9
  4. package/dist/backend/cc.js.map +1 -1
  5. package/dist/backend/emission/emit-async.d.ts +1 -0
  6. package/dist/backend/emission/emit-async.js +27 -1
  7. package/dist/backend/emission/emit-async.js.map +1 -1
  8. package/dist/backend/emission/emit-exprs.js +109 -18
  9. package/dist/backend/emission/emit-exprs.js.map +1 -1
  10. package/dist/backend/emission/emitter.d.ts +1 -0
  11. package/dist/backend/emission/emitter.js +25 -3
  12. package/dist/backend/emission/emitter.js.map +1 -1
  13. package/dist/backend/llvm/emitter.js +152 -0
  14. package/dist/backend/llvm/emitter.js.map +1 -1
  15. package/dist/backend/mangle.d.ts +1 -0
  16. package/dist/backend/mangle.js +3 -0
  17. package/dist/backend/mangle.js.map +1 -1
  18. package/dist/coverage/surface-manifest.js +1 -0
  19. package/dist/coverage/surface-manifest.js.map +1 -1
  20. package/dist/diagnostics/diagnostic.d.ts +23 -1
  21. package/dist/diagnostics/diagnostic.js +34 -0
  22. package/dist/diagnostics/diagnostic.js.map +1 -1
  23. package/dist/frontend/lowering/lower-calls.d.ts +3 -1
  24. package/dist/frontend/lowering/lower-calls.js +196 -33
  25. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  26. package/dist/frontend/lowering/lower-containers.d.ts +6 -5
  27. package/dist/frontend/lowering/lower-containers.js +15 -11
  28. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  29. package/dist/frontend/lowering/lower-exprs.d.ts +11 -1
  30. package/dist/frontend/lowering/lower-exprs.js +366 -25
  31. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  32. package/dist/frontend/lowering/lower-server.d.ts +5 -0
  33. package/dist/frontend/lowering/lower-server.js +105 -55
  34. package/dist/frontend/lowering/lower-server.js.map +1 -1
  35. package/dist/frontend/lowering/lower-stmts.js +71 -9
  36. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  37. package/dist/frontend/lowering/lowerer.d.ts +24 -5
  38. package/dist/frontend/lowering/lowerer.js +28 -4
  39. package/dist/frontend/lowering/lowerer.js.map +1 -1
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +84 -5
  42. package/dist/index.js.map +1 -1
  43. package/dist/ir/nodes.d.ts +52 -19
  44. package/dist/ir/nodes.js +2 -2
  45. package/dist/ir/nodes.js.map +1 -1
  46. package/dist/ir/validate.js +67 -9
  47. package/dist/ir/validate.js.map +1 -1
  48. package/dist/library/profile.d.ts +33 -0
  49. package/dist/library/profile.js +178 -4
  50. package/dist/library/profile.js.map +1 -1
  51. package/dist/library/sidecar.d.ts +3 -2
  52. package/dist/library/sidecar.js +7 -3
  53. package/dist/library/sidecar.js.map +1 -1
  54. package/package.json +2 -2
  55. package/surface-manifest.json +2 -1
@@ -10,12 +10,12 @@ import { isGenericCallableMemberType, typeKey } from "../types.js";
10
10
  import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx, nodeThrowExpr } from "./lowerer.js";
11
11
  import { enforceLibBoundary } from "./lib-boundary.js";
12
12
  import { NARROW_FIRST, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js";
13
- import { ffiBindingDiag, ffiSignatureDiag, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
13
+ import { ffiBindingDiag, ffiSignatureDiag, libCallbackDiag, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
14
14
  import { mixinFnShapeOf } from "./lower-mixins.js";
15
15
  import { bufEncoding, dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerRegexMethodCall, lowerStringMethodCall, lowerTupleReadMethodCall } from "./lower-containers.js";
16
16
  import { lowerChildStreamMethodCall, lowerCreateRequireCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerWatcherMethodCall } from "./lower-builtins.js";
17
- import { droppableStatic, lowerPromiseAllTupleCall, lowerPromiseRejectCall, probeLower, templateRawTextOf } from "./lower-exprs.js";
18
- import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerHttpClientFnCall } from "./lower-server.js";
17
+ import { droppableStatic, lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, probeLower, templateRawTextOf } from "./lower-exprs.js";
18
+ import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js";
19
19
  import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall } from "./lower-classes.js";
20
20
  import { emitterRooted, lowerEmitterMethodCall } from "./lower-emitter.js";
21
21
  import { lowerConsoleInspectArg, lowerFormatCall } from "./lower-inspect.js";
@@ -2313,29 +2313,59 @@ function ffiCallbackInputDiagnostic(L, descriptor, callbackType) {
2313
2313
  }
2314
2314
  return null;
2315
2315
  }
2316
+ /** Values returned by native code flow into TypeScript, so a declaration
2317
+ * must admit the whole scalar domain the ABI class can produce. `mapType`
2318
+ * intentionally widens literal and enum types to their storage type; that
2319
+ * is sound for script-owned values but not for an external return contract
2320
+ * (`(): true` cannot describe a bool callback that is free to return false). */
2321
+ function ffiReturnDomainDiagnostic(L, nativeClass, returnType) {
2322
+ if (nativeClass === "void")
2323
+ return null;
2324
+ const domain = nativeClass === "bool" ? "boolean" : "number";
2325
+ const coversDomain = nativeClass === "bool"
2326
+ ? (returnType.flags & ts.TypeFlags.Boolean) !== 0
2327
+ : (returnType.flags & ts.TypeFlags.Number) !== 0;
2328
+ return coversDomain
2329
+ ? null
2330
+ : `the return type is '${L.checker.typeToString(returnType)}', but native class '${nativeClass}' may supply ` +
2331
+ `any ${domain}; declare it as '${domain}'`;
2332
+ }
2333
+ /** The binding surface's diagnostic flavor: native-manifest bindings
2334
+ * speak the SC5002/SC5003 FFI codes; library-mode host callbacks are the
2335
+ * same recognition machinery under the profile's vocabulary — SC4024 for
2336
+ * both the binding and signature halves, with "manifest" respelled
2337
+ * "profile" in the shared detail strings so the teaching names the
2338
+ * document the author actually edits. */
2339
+ function ffiFlavor(L) {
2340
+ if (!L.libraryCallbacks)
2341
+ return { binding: ffiBindingDiag, signature: ffiSignatureDiag };
2342
+ const lib = (name, detail, loc) => libCallbackDiag(name, detail.replaceAll("manifest", "profile"), loc);
2343
+ return { binding: lib, signature: lib };
2344
+ }
2316
2345
  /** The declaration half of an outbound FFI binding. Kept independent of
2317
2346
  * call-site argument checks so the whole manifest can be validated even
2318
2347
  * when a configured function is never called. */
2319
2348
  function ffiDeclarationDiagnostic(L, binding, symbol, loc) {
2349
+ const { binding: bindingDiag, signature: signatureDiag } = ffiFlavor(L);
2320
2350
  const declarations = L.checker.declarationsOf(symbol);
2321
2351
  const functionDecls = declarations.filter(ts.isFunctionDeclaration);
2322
2352
  if (functionDecls.length === 0 ||
2323
2353
  declarations.some((decl) => !ts.isFunctionDeclaration(decl)) ||
2324
2354
  functionDecls.some((decl) => decl.body !== undefined)) {
2325
- return ffiBindingDiag(binding.name, "the configured name does not resolve exclusively to signature-only function declarations", loc);
2355
+ return bindingDiag(binding.name, "the configured name does not resolve exclusively to signature-only function declarations", loc);
2326
2356
  }
2327
2357
  if (functionDecls.some((decl) => (decl.typeParameters?.length ?? 0) > 0)) {
2328
- return ffiSignatureDiag(binding.name, "generic ambient declarations cannot describe one fixed C ABI", loc);
2358
+ return signatureDiag(binding.name, "generic ambient declarations cannot describe one fixed C ABI", loc);
2329
2359
  }
2330
2360
  const signatures = L.checker.getCallSignatures(L.checker.getTypeOfSymbol(symbol));
2331
2361
  if (signatures.length !== 1) {
2332
- return ffiSignatureDiag(binding.name, `the ambient binding has ${signatures.length} call signatures; exactly one non-overloaded signature is required`, loc);
2362
+ return signatureDiag(binding.name, `the ambient binding has ${signatures.length} call signatures; exactly one non-overloaded signature is required`, loc);
2333
2363
  }
2334
2364
  const signature = signatures[0];
2335
2365
  const params = signature.getParameters();
2336
2366
  const sourceParams = ffiSourceParams(binding);
2337
2367
  if (params.length !== sourceParams.length) {
2338
- return ffiSignatureDiag(binding.name, `the TypeScript declaration has ${params.length} parameter(s), but the manifest declares ${sourceParams.length} source parameter(s) ` +
2368
+ return signatureDiag(binding.name, `the TypeScript declaration has ${params.length} parameter(s), but the manifest declares ${sourceParams.length} source parameter(s) ` +
2339
2369
  `(${binding.params.length - sourceParams.length} additional native context slot(s) are compiler-supplied)`, loc);
2340
2370
  }
2341
2371
  const expectedParams = ffiSourceParamTypes(binding.params);
@@ -2347,18 +2377,18 @@ function ffiDeclarationDiagnostic(L, binding, symbol, loc) {
2347
2377
  // declaration is different: it is a callable external contract, so a
2348
2378
  // `never` slot cannot truthfully describe any native parameter.
2349
2379
  if ((paramType.flags & ts.TypeFlags.Never) !== 0) {
2350
- return ffiSignatureDiag(binding.name, `parameter ${i + 1} is 'never', an uninhabited TypeScript type that cannot describe a native ABI parameter`, loc);
2380
+ return signatureDiag(binding.name, `parameter ${i + 1} is 'never', an uninhabited TypeScript type that cannot describe a native ABI parameter`, loc);
2351
2381
  }
2352
2382
  if (isFfiCallbackParam(sourceParam)) {
2353
2383
  const callbackDiagnostic = ffiCallbackInputDiagnostic(L, sourceParam, paramType);
2354
2384
  if (callbackDiagnostic !== null) {
2355
- return ffiSignatureDiag(binding.name, callbackDiagnostic, loc);
2385
+ return signatureDiag(binding.name, callbackDiagnostic, loc);
2356
2386
  }
2357
2387
  }
2358
2388
  const mapped = L.mapTypeOf(paramType);
2359
2389
  const expected = expectedParams[i];
2360
2390
  if (mapped === null || !typeEquals(mapped, expected)) {
2361
- return ffiSignatureDiag(binding.name, `parameter ${i + 1} maps to '${mapped === null ? L.checker.typeToString(paramType) : L.fmt(mapped)}', ` +
2391
+ return signatureDiag(binding.name, `parameter ${i + 1} maps to '${mapped === null ? L.checker.typeToString(paramType) : L.fmt(mapped)}', ` +
2362
2392
  `which does not fit manifest ${ffiParamDisplay(sourceParam)}`, loc);
2363
2393
  }
2364
2394
  }
@@ -2367,21 +2397,36 @@ function ffiDeclarationDiagnostic(L, binding, symbol, loc) {
2367
2397
  // let tsc erase all control flow after the call while the linked function
2368
2398
  // continues, making the generated program disagree with TypeScript.
2369
2399
  if ((returnType.flags & ts.TypeFlags.Never) !== 0) {
2370
- return ffiSignatureDiag(binding.name, "the return type is 'never', but a native ABI return cannot uphold TypeScript's non-returning contract", loc);
2400
+ return signatureDiag(binding.name, "the return type is 'never', but a native ABI return cannot uphold TypeScript's non-returning contract", loc);
2371
2401
  }
2372
2402
  const declaredReturn = L.mapTypeOf(returnType);
2373
2403
  const expectedReturn = ffiClassType(binding.returns);
2374
2404
  if (declaredReturn === null || !typeEquals(declaredReturn, expectedReturn)) {
2375
- return ffiSignatureDiag(binding.name, `the return maps to '${declaredReturn === null ? L.checker.typeToString(returnType) : L.fmt(declaredReturn)}', ` +
2405
+ return signatureDiag(binding.name, `the return maps to '${declaredReturn === null ? L.checker.typeToString(returnType) : L.fmt(declaredReturn)}', ` +
2376
2406
  `which does not fit manifest class '${binding.returns}'`, loc);
2377
2407
  }
2408
+ const returnDomainDiagnostic = ffiReturnDomainDiagnostic(L, binding.returns, returnType);
2409
+ if (returnDomainDiagnostic !== null) {
2410
+ return signatureDiag(binding.name, returnDomainDiagnostic, loc);
2411
+ }
2378
2412
  return null;
2379
2413
  }
2414
+ /** True when a library callback may claim declarations from this file.
2415
+ * Ordinary program source is always eligible. Declaration files are
2416
+ * eligible only when they are project-owned: the standard library and
2417
+ * installed/workspace package declarations are existing ambient surfaces,
2418
+ * never callback declarations merely because a profile channel shares their
2419
+ * spelling. */
2420
+ function libraryCallbackOwnsFile(L, file) {
2421
+ return !file.isDeclarationFile || (!L.isStdlibFile(file) && !L.isNpmFile(file));
2422
+ }
2380
2423
  /** Resolve and validate every configured outbound binding before emit.
2381
2424
  * Candidate declarations are signature-only functions bearing the manifest
2382
2425
  * name anywhere in the program. Multiple scoped declarations are all native
2383
2426
  * bindings under the existing name-based call surface, so every candidate
2384
- * must fit the one manifest ABI. */
2427
+ * must fit the one manifest ABI. Library callbacks additionally exclude
2428
+ * standard-library and package declaration files while retaining project
2429
+ * declaration files as authored callback surface. */
2385
2430
  export function validateFfiImports(L) {
2386
2431
  const diagnostics = [];
2387
2432
  const symbolsByName = new Map();
@@ -2390,6 +2435,8 @@ export function validateFfiImports(L) {
2390
2435
  if (configuredNames.size === 0)
2391
2436
  return { diagnostics, symbolsByName };
2392
2437
  for (const file of L.program.getSourceFiles()) {
2438
+ if (L.libraryCallbacks && !libraryCallbackOwnsFile(L, file))
2439
+ continue;
2393
2440
  ts.walkPreorder(file, (node) => {
2394
2441
  if (ts.isFunctionDeclaration(node)) {
2395
2442
  if (node.body === undefined &&
@@ -2415,7 +2462,23 @@ export function validateFfiImports(L) {
2415
2462
  for (const binding of L.ffiImports) {
2416
2463
  const bySymbol = candidates.get(binding.name);
2417
2464
  if (bySymbol === undefined || bySymbol.size === 0) {
2418
- diagnostics.push(ffiBindingDiag(binding.name, "the program has no signature-only function declaration with this name", { file: L.entry.fileName, start: 0, end: 0 }));
2465
+ // A native-manifest binding with no declaration is a broken build
2466
+ // input. A library callback channel with no declaration is unused
2467
+ // CAPACITY: the registration symbol still dispatches it, nothing
2468
+ // calls it, and a later program revision may (a program that calls
2469
+ // the name without declaring it fails ordinary typechecking first).
2470
+ if (!L.libraryCallbacks) {
2471
+ diagnostics.push(ffiBindingDiag(binding.name, "the program has no signature-only function declaration with this name", { file: L.entry.fileName, start: 0, end: 0 }));
2472
+ }
2473
+ else {
2474
+ // Preserve the distinction between unused capacity and a binding
2475
+ // whose program declaration was found but failed validation. The
2476
+ // latter deliberately leaves no map entry so calls poison without
2477
+ // duplicating the program-level diagnostic; the empty set lets call
2478
+ // lowering inspect same-named builtins, implementations, and
2479
+ // unsupported ambient declaration shapes individually.
2480
+ symbolsByName.set(binding.name, new Set());
2481
+ }
2419
2482
  continue;
2420
2483
  }
2421
2484
  const validSymbols = new Set();
@@ -2444,15 +2507,37 @@ export function lowerFfiCall(L, expr) {
2444
2507
  if (!ts.isIdentifier(expr.expression))
2445
2508
  return null;
2446
2509
  const binding = L.ffiImportsByName.get(expr.expression.text);
2447
- if (binding === undefined)
2510
+ if (binding === undefined) {
2511
+ // LIBRARY mode with a declared callback surface: a CALL of a
2512
+ // program-authored signature-only ambient function that names no
2513
+ // channel is the author reaching for the host seam the profile does
2514
+ // not provide — refuse with the callback teaching instead of the
2515
+ // ambient ReferenceError lowering. Scoped to project-owned source and
2516
+ // declaration files so lib.d.ts/@types/package ambients (parseInt,
2517
+ // setTimeout, …) keep every existing lowering; callback-free profiles
2518
+ // are untouched.
2519
+ if (L.libraryCallbacks) {
2520
+ const symbol = L.resolveValueSymbol(expr.expression);
2521
+ const decls = symbol === null ? [] : L.checker.declarationsOf(symbol);
2522
+ const callbackShaped = decls.length > 0 &&
2523
+ decls.every((decl) => ts.isFunctionDeclaration(decl) &&
2524
+ decl.body === undefined &&
2525
+ libraryCallbackOwnsFile(L, decl.getSourceFile()));
2526
+ if (callbackShaped) {
2527
+ L.pushDiag(libCallbackDiag(expr.expression.text, "the profile declares no callback channel with this name", locOf(expr)));
2528
+ throw new PoisonError();
2529
+ }
2530
+ }
2448
2531
  return null;
2532
+ }
2449
2533
  const loc = locOf(expr);
2534
+ const { binding: bindingFlavor, signature: signatureFlavor } = ffiFlavor(L);
2450
2535
  const bindingError = (detail) => {
2451
- L.pushDiag(ffiBindingDiag(binding.name, detail, loc));
2536
+ L.pushDiag(bindingFlavor(binding.name, detail, loc));
2452
2537
  throw new PoisonError();
2453
2538
  };
2454
2539
  const signatureError = (detail) => {
2455
- L.pushDiag(ffiSignatureDiag(binding.name, detail, loc));
2540
+ L.pushDiag(signatureFlavor(binding.name, detail, loc));
2456
2541
  throw new PoisonError();
2457
2542
  };
2458
2543
  const symbol = L.resolveValueSymbol(expr.expression) ??
@@ -2464,10 +2549,39 @@ export function lowerFfiCall(L, expr) {
2464
2549
  if (validSymbols === undefined)
2465
2550
  throw new PoisonError();
2466
2551
  if (!validSymbols.has(symbol)) {
2467
- // TypeScript resolved this call to a distinct local declaration.
2468
- // The manifest owns only the exact validated ambient binding; a
2469
- // same-named function with a body remains ordinary scriptc code.
2470
- return null;
2552
+ if (L.libraryCallbacks) {
2553
+ const declarations = L.checker.declarationsOf(symbol);
2554
+ const programDeclarations = declarations.filter((decl) => libraryCallbackOwnsFile(L, decl.getSourceFile()));
2555
+ const programAmbient = programDeclarations.length > 0 &&
2556
+ programDeclarations.every((decl) => decl.getSourceFile().isDeclarationFile ||
2557
+ (ts.getCombinedModifierFlags(decl) &
2558
+ ts.ModifierFlags.Ambient) !== 0);
2559
+ if (programAmbient) {
2560
+ // A called, program-authored ambient with a configured channel
2561
+ // name is not unused capacity. Validate the resolved symbol now
2562
+ // so unsupported declaration forms (`declare const cb: ...`)
2563
+ // refuse SC4024 instead of silently dropping the call. A valid
2564
+ // declaration missed by the up-front syntax walk may proceed as
2565
+ // the callback binding after this exact-symbol check.
2566
+ const diagnostic = ffiDeclarationDiagnostic(L, binding, symbol, loc);
2567
+ if (diagnostic !== null) {
2568
+ L.pushDiag(diagnostic);
2569
+ throw new PoisonError();
2570
+ }
2571
+ }
2572
+ else {
2573
+ // Standard-library/package ambients and same-named program
2574
+ // implementations remain their ordinary TypeScript bindings;
2575
+ // the profile does not claim them.
2576
+ return null;
2577
+ }
2578
+ }
2579
+ else {
2580
+ // TypeScript resolved this call to a distinct local declaration.
2581
+ // The manifest owns only the exact validated ambient binding; a
2582
+ // same-named function with a body remains ordinary scriptc code.
2583
+ return null;
2584
+ }
2471
2585
  }
2472
2586
  }
2473
2587
  else {
@@ -2558,6 +2672,9 @@ export function lowerCall(L, expr) {
2558
2672
  const processOptional = L.lowerProcessOptionalMethodCall(expr);
2559
2673
  if (processOptional)
2560
2674
  return processOptional;
2675
+ const reqStreamOptional = lowerCompatReqStreamOptionalCall(L, expr);
2676
+ if (reqStreamOptional)
2677
+ return reqStreamOptional;
2561
2678
  // `t.unref?.()` on a Timeout handle — same story: the method always
2562
2679
  // exists, so the optional call is the call.
2563
2680
  if (expr.questionDotToken && ts.isPropertyAccessExpression(expr.expression)) {
@@ -2565,20 +2682,13 @@ export function lowerCall(L, expr) {
2565
2682
  if (timeoutOptional)
2566
2683
  return timeoutOptional;
2567
2684
  }
2568
- // `req.stream?.on(...)` — the http2 compatibility request's h2-only
2569
- // stream member, guarded. The allowHTTP1 lowering serves every
2570
- // connection as HTTP/1.1, where Node answers undefined for req.stream:
2571
- // the optional chain short-circuits, the arguments never evaluate, and
2572
- // the whole statement is a no-op — exactly what lowers here (a VOID
2573
- // no-op the emitter drops). The receiver is restricted to an
2574
- // identifier so no evaluation is skipped, and to statement position so
2575
- // no value is consumed (an unguarded or computed use meets the pointed
2576
- // per-member fence in lower-server.ts instead).
2685
+ // `req.session?.m(...)` remains absent on compatibility requests. Live
2686
+ // `req.stream` calls now pass through the normal optional-chain and h2
2687
+ // stream lowering using lowerServerProperty's checked getter.
2577
2688
  if (ts.isPropertyAccessExpression(expr.expression) &&
2578
2689
  ts.isPropertyAccessExpression(expr.expression.expression) &&
2579
2690
  !expr.expression.expression.questionDotToken &&
2580
- (expr.expression.expression.name.text === "stream" ||
2581
- expr.expression.expression.name.text === "session") &&
2691
+ expr.expression.expression.name.text === "session" &&
2582
2692
  ts.isIdentifier(expr.expression.expression.expression) &&
2583
2693
  L.mapTypeOf(L.typeOf(expr.expression.expression.expression))?.kind === "httpReq" &&
2584
2694
  L.isStdlibMember(expr.expression.expression)) {
@@ -3203,10 +3313,63 @@ export function lowerCall(L, expr) {
3203
3313
  const x = L.lowerExprExpecting(expr.arguments[0], F64);
3204
3314
  return { kind: "libCall", fn: "num.isNaN", args: [x], type: BOOL, loc };
3205
3315
  }
3206
- const s = L.lowerExprExpecting(expr.arguments[0], STRING);
3207
3316
  const radix = expr.arguments[1]
3208
3317
  ? L.lowerExprExpecting(expr.arguments[1], F64)
3209
3318
  : { kind: "numLit", value: 0, type: F64, loc };
3319
+ const optional = lowerAbsenceProbe(L, expr.arguments[0]);
3320
+ if (optional?.type.kind === "union") {
3321
+ const def = L.unions.get(optional.type.unionId);
3322
+ const stringTag = def?.arms.findIndex((arm) => arm.kind === "string") ?? -1;
3323
+ if (def &&
3324
+ stringTag >= 0 &&
3325
+ def.arms.every((arm) => arm.kind === "string" || isUnitType(arm))) {
3326
+ const unionT = optional.type;
3327
+ const key = `parseInt.optional:${unionT.unionId}`;
3328
+ let helper = L.widthHelpers.get(key);
3329
+ if (!helper) {
3330
+ helper = `%parseInt.optional.${L.widthHelpers.size}`;
3331
+ L.widthHelpers.set(key, helper);
3332
+ const value = { kind: "varRef", localId: "value.0", type: unionT, loc };
3333
+ const radixRef = { kind: "varRef", localId: "radix.0", type: F64, loc };
3334
+ const body = def.arms.flatMap((arm, tag) => isUnitType(arm)
3335
+ ? [{
3336
+ kind: "if",
3337
+ cond: { kind: "unionIsTag", unionId: unionT.unionId, tag, negated: false, value, type: BOOL, loc },
3338
+ then: [{ kind: "return", value: { kind: "numLit", value: NaN, type: F64, loc }, loc }],
3339
+ else_: null,
3340
+ loc,
3341
+ }]
3342
+ : []);
3343
+ body.push({
3344
+ kind: "return",
3345
+ value: {
3346
+ kind: "libCall",
3347
+ fn: "num.parseInt",
3348
+ args: [{ kind: "unionNarrow", unionId: unionT.unionId, tag: stringTag, value, type: STRING, loc }, radixRef],
3349
+ type: F64,
3350
+ loc,
3351
+ },
3352
+ loc,
3353
+ });
3354
+ L.liftedFns.push({
3355
+ name: helper,
3356
+ params: [
3357
+ { localId: "value.0", name: "value", type: unionT },
3358
+ { localId: "radix.0", name: "radix", type: F64 },
3359
+ ],
3360
+ returnType: F64,
3361
+ locals: [
3362
+ { id: "value.0", name: "value", type: unionT, mutable: true },
3363
+ { id: "radix.0", name: "radix", type: F64, mutable: true },
3364
+ ],
3365
+ body,
3366
+ loc,
3367
+ });
3368
+ }
3369
+ return { kind: "call", callee: helper, args: [optional, radix], type: F64, loc };
3370
+ }
3371
+ }
3372
+ const s = L.lowerExprExpecting(expr.arguments[0], STRING);
3210
3373
  return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc };
3211
3374
  }
3212
3375
  // STATIC parseFloat/isFinite over exactly-typed arguments —