@nudojs/core 1.2.2 → 1.4.0

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.
@@ -21,6 +21,7 @@ import {
21
21
  getFnImpl,
22
22
  getSlot,
23
23
  gt,
24
+ identBoundaryRegex,
24
25
  joinAbs,
25
26
  le,
26
27
  lit,
@@ -36,12 +37,704 @@ import {
36
37
  ptypeof,
37
38
  runTranspiledOptionsMemoKey,
38
39
  snapshotAbs,
40
+ stripStringsKeepComments,
39
41
  termToString,
40
42
  tryRunTranspiled,
41
43
  unknown,
42
44
  v,
43
- withExecPhi
44
- } from "./chunk-4V3CRVSD.js";
45
+ withEvalBudgetOverride,
46
+ withExecPhi,
47
+ withFnBudgetOverride
48
+ } from "./chunk-LFCF6BLE.js";
49
+
50
+ // src/algebra/directive-scan.ts
51
+ function cleanDirectiveLine(raw, kind) {
52
+ let s = raw;
53
+ if (kind === "block") {
54
+ s = s.replace(/^\s*\*\s?/, "");
55
+ } else {
56
+ s = s.replace(/^\//, "");
57
+ }
58
+ return s.trim();
59
+ }
60
+ function commentTextToLines(text, kind) {
61
+ return text.split("\n").map((line) => cleanDirectiveLine(line, kind));
62
+ }
63
+ function commentKind(c) {
64
+ return c.type === "CommentLine" ? "line" : "block";
65
+ }
66
+ function buildScope(name, node, wrapper, comments, wrapperComments) {
67
+ const seen = /* @__PURE__ */ new Set();
68
+ const texts = [];
69
+ const startLines = [];
70
+ const lines = [];
71
+ for (const c of [...wrapperComments ?? [], ...comments ?? []]) {
72
+ if (seen.has(c)) continue;
73
+ seen.add(c);
74
+ texts.push(c.value);
75
+ startLines.push(c.loc?.start?.line ?? 0);
76
+ for (const line of commentTextToLines(c.value, commentKind(c))) lines.push(line);
77
+ }
78
+ const loc = node.loc;
79
+ return {
80
+ name,
81
+ node,
82
+ commentTexts: texts,
83
+ commentLines: lines,
84
+ commentStartLines: startLines,
85
+ startLine: loc?.start?.line ?? 0
86
+ };
87
+ }
88
+ function isFnExpr(n) {
89
+ return !!n && (n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression" || n.type === "FunctionDeclaration");
90
+ }
91
+ function keyNameOf(key) {
92
+ if (!key) return void 0;
93
+ const k = key;
94
+ if (k.type === "Identifier") return k.name;
95
+ if (k.type === "StringLiteral" || k.type === "NumericLiteral") return String(k.value);
96
+ return void 0;
97
+ }
98
+ function listFnDirectiveScopes(file) {
99
+ const out = [];
100
+ const pushNamedFn = (name, fnNode, commentNode, wrapper) => {
101
+ out.push(
102
+ buildScope(
103
+ name,
104
+ fnNode,
105
+ wrapper,
106
+ commentNode?.leadingComments,
107
+ wrapper?.leadingComments
108
+ )
109
+ );
110
+ };
111
+ const visit2 = (node, ctx) => {
112
+ if (!node || typeof node !== "object") return;
113
+ const n = node;
114
+ switch (n.type) {
115
+ case "File":
116
+ visit2(n.program, ctx);
117
+ return;
118
+ case "Program": {
119
+ for (const s of n.body ?? []) visit2(s, ctx);
120
+ return;
121
+ }
122
+ case "ExportNamedDeclaration":
123
+ case "ExportDefaultDeclaration": {
124
+ const decl = n.declaration;
125
+ if (!decl) return;
126
+ const wrapCtx = { ...ctx, wrapperNode: node };
127
+ visitWithWrapper(decl, node, wrapCtx);
128
+ return;
129
+ }
130
+ case "FunctionDeclaration": {
131
+ const name = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
132
+ pushNamedFn(name, node, node, ctx.wrapperNode);
133
+ visit2(n.body, { owner: name });
134
+ return;
135
+ }
136
+ case "FunctionExpression": {
137
+ const name = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
138
+ pushNamedFn(name, node, node, ctx.wrapperNode);
139
+ visit2(n.body, { owner: name });
140
+ return;
141
+ }
142
+ case "ArrowFunctionExpression": {
143
+ const name = ctx.fallbackName ?? "<anonymous>";
144
+ pushNamedFn(name, node, ctx.commentHost ?? node, ctx.wrapperNode);
145
+ visit2(n.body, { owner: name });
146
+ return;
147
+ }
148
+ case "VariableDeclaration": {
149
+ for (const d of n.declarations ?? []) {
150
+ const decl = d;
151
+ const idName = decl.id?.type === "Identifier" ? decl.id.name : void 0;
152
+ if (isFnExpr(decl.init)) {
153
+ const fn2 = decl.init;
154
+ const scope = buildScope(
155
+ idName ?? "<anonymous>",
156
+ fn2,
157
+ ctx.wrapperNode,
158
+ node.leadingComments,
159
+ ctx.wrapperNode?.leadingComments
160
+ );
161
+ out.push(scope);
162
+ visit2(fn2, { owner: idName });
163
+ } else {
164
+ visit2(decl.init, { owner: idName, commentHost: node, wrapperNode: ctx.wrapperNode });
165
+ }
166
+ }
167
+ return;
168
+ }
169
+ case "ClassDeclaration":
170
+ case "ClassExpression": {
171
+ const cname = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
172
+ if (n.type === "ClassDeclaration") {
173
+ }
174
+ const body = n.body?.body ?? [];
175
+ for (const m of body) visit2(m, { owner: cname });
176
+ return;
177
+ }
178
+ case "ClassMethod":
179
+ case "ClassPrivateMethod":
180
+ case "ObjectMethod": {
181
+ const key = keyNameOf(n.key);
182
+ const owner = ctx.owner;
183
+ const name = key ? owner ? `${owner}.${key}` : key : "<anonymous>";
184
+ pushNamedFn(name, node, node, void 0);
185
+ visit2(n.body, { owner: name });
186
+ return;
187
+ }
188
+ case "ObjectProperty":
189
+ case "ClassProperty":
190
+ case "ClassPrivateProperty": {
191
+ const key = keyNameOf(n.key);
192
+ const val = n.value ?? n.init;
193
+ if (isFnExpr(val)) {
194
+ const owner = ctx.owner;
195
+ const name = key ? owner ? `${owner}.${key}` : key : "<anonymous>";
196
+ out.push(
197
+ buildScope(
198
+ name,
199
+ val,
200
+ void 0,
201
+ val.leadingComments ?? node.leadingComments,
202
+ void 0
203
+ )
204
+ );
205
+ visit2(val, { owner: name });
206
+ return;
207
+ }
208
+ visit2(val, ctx);
209
+ return;
210
+ }
211
+ case "ReturnStatement":
212
+ case "ThrowStatement":
213
+ case "AwaitExpression":
214
+ case "SpreadElement":
215
+ case "RestElement":
216
+ visit2(n.argument, ctx);
217
+ return;
218
+ case "UnaryExpression":
219
+ case "UpdateExpression":
220
+ visit2(n.argument, ctx);
221
+ return;
222
+ case "BinaryExpression":
223
+ case "LogicalExpression":
224
+ case "AssignmentExpression":
225
+ visit2(n.left, ctx);
226
+ visit2(n.right, ctx);
227
+ return;
228
+ case "MemberExpression":
229
+ case "OptionalMemberExpression":
230
+ visit2(n.object, ctx);
231
+ visit2(n.property, ctx);
232
+ return;
233
+ case "CallExpression":
234
+ case "OptionalCallExpression":
235
+ case "NewExpression": {
236
+ visit2(n.callee, ctx);
237
+ for (const a of n.arguments ?? []) visit2(a, ctx);
238
+ return;
239
+ }
240
+ case "ConditionalExpression":
241
+ visit2(n.test, ctx);
242
+ visit2(n.consequent, ctx);
243
+ visit2(n.alternate, ctx);
244
+ return;
245
+ case "ArrayExpression": {
246
+ for (const e of n.elements ?? []) visit2(e, ctx);
247
+ return;
248
+ }
249
+ case "ObjectExpression": {
250
+ for (const p of n.properties ?? []) visit2(p, ctx);
251
+ return;
252
+ }
253
+ case "SequenceExpression": {
254
+ for (const e of n.expressions ?? []) visit2(e, ctx);
255
+ return;
256
+ }
257
+ case "ParenthesizedExpression":
258
+ visit2(n.expression, ctx);
259
+ return;
260
+ case "ExpressionStatement":
261
+ visit2(n.expression, ctx);
262
+ return;
263
+ case "BlockStatement": {
264
+ for (const s of n.body ?? []) visit2(s, ctx);
265
+ return;
266
+ }
267
+ case "IfStatement":
268
+ visit2(n.test, ctx);
269
+ visit2(n.consequent, ctx);
270
+ visit2(n.alternate, ctx);
271
+ return;
272
+ case "ForStatement":
273
+ visit2(n.init, ctx);
274
+ visit2(n.test, ctx);
275
+ visit2(n.update, ctx);
276
+ visit2(n.body, ctx);
277
+ return;
278
+ case "ForInStatement":
279
+ case "ForOfStatement":
280
+ visit2(n.left, ctx);
281
+ visit2(n.right, ctx);
282
+ visit2(n.body, ctx);
283
+ return;
284
+ case "WhileStatement":
285
+ case "DoWhileStatement":
286
+ visit2(n.test, ctx);
287
+ visit2(n.body, ctx);
288
+ return;
289
+ case "TryStatement":
290
+ visit2(n.block, ctx);
291
+ visit2(n.handler, ctx);
292
+ visit2(n.finalizer, ctx);
293
+ return;
294
+ case "CatchClause":
295
+ visit2(n.body, ctx);
296
+ return;
297
+ case "SwitchStatement":
298
+ visit2(n.discriminant, ctx);
299
+ for (const c of n.cases ?? []) visit2(c, ctx);
300
+ return;
301
+ case "SwitchCase":
302
+ visit2(n.test, ctx);
303
+ for (const s of n.consequent ?? []) visit2(s, ctx);
304
+ return;
305
+ case "LabeledStatement":
306
+ visit2(n.body, ctx);
307
+ return;
308
+ default: {
309
+ for (const key of [
310
+ "body",
311
+ "expression",
312
+ "init",
313
+ "value",
314
+ "argument",
315
+ "declaration",
316
+ "left",
317
+ "right",
318
+ "test",
319
+ "consequent",
320
+ "alternate",
321
+ "block",
322
+ "handler",
323
+ "finalizer",
324
+ "callee",
325
+ "object",
326
+ "property",
327
+ "argument"
328
+ ]) {
329
+ const v2 = n[key];
330
+ if (Array.isArray(v2)) v2.forEach((x) => visit2(x, ctx));
331
+ else if (v2 && typeof v2 === "object" && "type" in v2) visit2(v2, ctx);
332
+ }
333
+ for (const key of ["declarations", "properties", "elements", "arguments", "cases", "params"]) {
334
+ const v2 = n[key];
335
+ if (Array.isArray(v2)) v2.forEach((x) => visit2(x, ctx));
336
+ }
337
+ return;
338
+ }
339
+ }
340
+ };
341
+ const visitWithWrapper = (decl, wrapper, ctx) => {
342
+ const d = decl;
343
+ if (d.type === "FunctionDeclaration" || d.type === "FunctionExpression") {
344
+ const name = d.id?.name ?? "default";
345
+ pushNamedFn(name, decl, decl, wrapper);
346
+ visit2(d.body, { owner: name });
347
+ return;
348
+ }
349
+ if (d.type === "ClassDeclaration" || d.type === "ClassExpression") {
350
+ const cname = d.id?.name ?? "<anonymous>";
351
+ const body = d.body?.body ?? [];
352
+ for (const m of body) visit2(m, { owner: cname });
353
+ return;
354
+ }
355
+ if (d.type === "VariableDeclaration") {
356
+ for (const raw of d.declarations ?? []) {
357
+ const vd = raw;
358
+ const idName = vd.id?.type === "Identifier" ? vd.id.name : void 0;
359
+ if (isFnExpr(vd.init)) {
360
+ const fn2 = vd.init;
361
+ out.push(
362
+ buildScope(
363
+ idName ?? "default",
364
+ fn2,
365
+ wrapper,
366
+ d.leadingComments,
367
+ wrapper.leadingComments
368
+ )
369
+ );
370
+ visit2(fn2, { owner: idName });
371
+ } else {
372
+ visit2(vd.init, {
373
+ owner: idName,
374
+ commentHost: decl,
375
+ wrapperNode: wrapper
376
+ });
377
+ }
378
+ }
379
+ return;
380
+ }
381
+ if (d.type === "ArrowFunctionExpression" || d.type === "FunctionExpression") {
382
+ const name = "default";
383
+ out.push(
384
+ buildScope(
385
+ name,
386
+ decl,
387
+ wrapper,
388
+ d.leadingComments,
389
+ wrapper.leadingComments
390
+ )
391
+ );
392
+ visit2(d.body, { owner: name });
393
+ return;
394
+ }
395
+ visit2(decl, { ...ctx, wrapperNode: wrapper });
396
+ };
397
+ for (const stmt of file.program.body) {
398
+ visit2(stmt, {});
399
+ }
400
+ return out;
401
+ }
402
+ function findFnDirectiveScope(file, fnName) {
403
+ return listFnDirectiveScopes(file).find((s) => s.name === fnName);
404
+ }
405
+ function fnDirectiveCommentLines(source, fnName) {
406
+ let file;
407
+ try {
408
+ file = parseSource(source);
409
+ } catch {
410
+ return [];
411
+ }
412
+ return findFnDirectiveScope(file, fnName)?.commentLines ?? [];
413
+ }
414
+ var isPathLikeEnv = (s) => /^[^\s]+$/.test(s) && (s.startsWith("./") || s.startsWith("../") || s.startsWith("/") || /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(s));
415
+ function parseEnvPayload(payload) {
416
+ return payload.split(",").map((e) => e.trim().replace(/^['"]|['"]$/g, "")).filter((e) => e && (/^\w+$/.test(e) || isPathLikeEnv(e)));
417
+ }
418
+ function parseMockModulePayload(payload) {
419
+ const partial2 = payload.match(/^\s*"([^"]+)"\s*\{([^}]+)\}\s*from\s*"([^"]+)"\s*$/);
420
+ if (partial2) {
421
+ const names = partial2[2].split(",").map((n) => n.trim()).filter(Boolean);
422
+ return { source: partial2[1], names, fromPath: partial2[3] };
423
+ }
424
+ const full = payload.match(/^\s*"([^"]+)"\s+from\s*"([^"]+)"\s*$/);
425
+ if (full) {
426
+ return { source: full[1], fromPath: full[2] };
427
+ }
428
+ return void 0;
429
+ }
430
+ function parseNudoImportPayload(payload) {
431
+ const s = payload.trim();
432
+ if (s === "") return void 0;
433
+ const named = s.match(/^\{([^}]+)\}\s*from\s*["']([^"']+)["']$/);
434
+ if (named) {
435
+ const names = named[1].split("\n").map((line) => line.replace(/^\s*\/\/\/?\s?/, "").trim()).join("\n").split(",").map((x) => x.trim()).filter(Boolean).map((x) => x.split(/\s+as\s+/).pop().trim());
436
+ return { names, spec: named[2] };
437
+ }
438
+ const ns = s.match(/^\*\s+as\s+(\w+)\s+from\s*["']([^"']+)["']$/);
439
+ if (ns) {
440
+ return { names: [`*${ns[1]}`], spec: ns[2] };
441
+ }
442
+ if (/^[\w$]+\s+from\b/.test(s) || /^[\w$]+\s*,/.test(s)) return "malformed-default";
443
+ return "malformed";
444
+ }
445
+ var LINE_COMMENT_PREFIX = "(?:^|[^/:])\\/\\/\\/?";
446
+ function fileDirectiveVisibleText(source) {
447
+ return stripStringsKeepComments(source).replace(/\/\*[\s\S]*?\*\//g, " ");
448
+ }
449
+ function extractFileEnvNames(source) {
450
+ const text = fileDirectiveVisibleText(source);
451
+ const re = new RegExp(`${LINE_COMMENT_PREFIX}\\s*@nudo:env\\s+([^\\n*]+)`, "g");
452
+ const out = [];
453
+ let m;
454
+ while (m = re.exec(text)) {
455
+ out.push(...parseEnvPayload(m[1]));
456
+ }
457
+ return out;
458
+ }
459
+ function extractMockModuleRecords(source) {
460
+ const text = fileDirectiveVisibleText(source);
461
+ const re = new RegExp(
462
+ `${LINE_COMMENT_PREFIX}\\s*@nudo:mock-module\\s+([^\\n]+)`,
463
+ "g"
464
+ );
465
+ const out = [];
466
+ let m;
467
+ while (m = re.exec(text)) {
468
+ const rec = parseMockModulePayload(m[1]);
469
+ if (rec) out.push(rec);
470
+ }
471
+ return out;
472
+ }
473
+ function directiveCommentLinesRaw(source) {
474
+ const src = stripStringsKeepComments(source);
475
+ const out = [];
476
+ const lineRe = /(?:^|[^/:])\/\/\/?([^\n]*)/g;
477
+ let m;
478
+ while (m = lineRe.exec(src)) {
479
+ out.push(cleanDirectiveLine(m[1], "line"));
480
+ }
481
+ const blockRe = /\/\*([\s\S]*?)\*\//g;
482
+ while (m = blockRe.exec(src)) {
483
+ out.push(...commentTextToLines(m[1], "block"));
484
+ }
485
+ return out;
486
+ }
487
+ function nudoImportPayloads(source) {
488
+ const lines = directiveCommentLinesRaw(source);
489
+ const out = [];
490
+ for (let i = 0; i < lines.length; i++) {
491
+ const line = lines[i];
492
+ if (!/^@nudo:import\s/.test(line)) continue;
493
+ let payload = line.replace(/^@nudo:import\s+/, "");
494
+ if (payload.startsWith("{")) {
495
+ let depth = 0;
496
+ for (const ch of payload) {
497
+ if (ch === "{") depth++;
498
+ else if (ch === "}") depth--;
499
+ }
500
+ while (depth > 0 && i + 1 < lines.length) {
501
+ i += 1;
502
+ const cont = lines[i];
503
+ if (/^@nudo:/.test(cont)) {
504
+ i -= 1;
505
+ break;
506
+ }
507
+ payload += `
508
+ ${cont}`;
509
+ for (const ch of cont) {
510
+ if (ch === "{") depth++;
511
+ else if (ch === "}") depth--;
512
+ }
513
+ }
514
+ }
515
+ out.push(payload);
516
+ }
517
+ return out;
518
+ }
519
+ function extractNudoImportRecords(source) {
520
+ const out = [];
521
+ for (const payload of nudoImportPayloads(source)) {
522
+ const rec = parseNudoImportPayload(payload);
523
+ if (rec && rec !== "malformed" && rec !== "malformed-default") out.push(rec);
524
+ }
525
+ return out;
526
+ }
527
+ function scanMalformedNudoImports(source) {
528
+ const out = [];
529
+ for (const payload of nudoImportPayloads(source)) {
530
+ const rec = parseNudoImportPayload(payload);
531
+ if (rec === "malformed-default" || rec === "malformed") out.push(rec);
532
+ }
533
+ return out;
534
+ }
535
+ function extractBalancedParens(text, startIdx) {
536
+ if (text[startIdx] !== "(") return null;
537
+ let depth = 0;
538
+ let inString = null;
539
+ for (let i = startIdx; i < text.length; i++) {
540
+ const ch = text[i];
541
+ if (inString) {
542
+ if (ch === inString) inString = null;
543
+ continue;
544
+ }
545
+ if (ch === '"' || ch === "'") {
546
+ inString = ch;
547
+ continue;
548
+ }
549
+ if (ch === "(") depth++;
550
+ if (ch === ")") depth--;
551
+ if (depth === 0) return text.slice(startIdx + 1, i);
552
+ }
553
+ return null;
554
+ }
555
+ var CASE_NAME_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/\/\/?\s*)?@nudo:case\s+"([^"]+)"\s*\(/g;
556
+ function extractCaseExpectedExpr(text, afterParen) {
557
+ const lines = text.slice(afterParen).split("\n");
558
+ for (let i = 0; i < lines.length; i++) {
559
+ let line = lines[i];
560
+ if (i > 0) line = line.replace(/^\s*\*\s?/, "").replace(/^\s*\/\/\/?\s?/, "");
561
+ if (/^\s*@nudo:/.test(line)) return void 0;
562
+ const arrow = line.match(/^\s*=>\s*(\S.*)$/);
563
+ if (arrow) return arrow[1].trim();
564
+ if (line.trim() !== "") return void 0;
565
+ }
566
+ return void 0;
567
+ }
568
+ function scanCaseTags(text) {
569
+ const caseArgSpans = [];
570
+ const out = [];
571
+ CASE_NAME_REGEX.lastIndex = 0;
572
+ let m;
573
+ while ((m = CASE_NAME_REGEX.exec(text)) !== null) {
574
+ const parenStart = m.index + m[0].length - 1;
575
+ const argsStr = extractBalancedParens(text, parenStart);
576
+ if (argsStr === null) continue;
577
+ const afterParen = parenStart + argsStr.length + 2;
578
+ if (caseArgSpans.some(([s, e]) => m.index >= s && m.index < e)) continue;
579
+ caseArgSpans.push([m.index, afterParen]);
580
+ const tagOffset = m.index + m[0].indexOf("@nudo:case");
581
+ const expectedText = extractCaseExpectedExpr(text, afterParen);
582
+ out.push({ name: m[1], argsText: argsStr, expectedText, tagOffset });
583
+ }
584
+ return out;
585
+ }
586
+ function scanCaseArgSpans(text) {
587
+ const spans = [];
588
+ CASE_NAME_REGEX.lastIndex = 0;
589
+ let m;
590
+ while ((m = CASE_NAME_REGEX.exec(text)) !== null) {
591
+ const parenStart = m.index + m[0].length - 1;
592
+ const argsStr = extractBalancedParens(text, parenStart);
593
+ if (argsStr === null) continue;
594
+ const afterParen = parenStart + argsStr.length + 2;
595
+ if (spans.some(([s, e]) => m.index >= s && m.index < e)) continue;
596
+ spans.push([m.index, afterParen]);
597
+ }
598
+ return spans;
599
+ }
600
+ function scanContractSegments(lines) {
601
+ const reqs = [];
602
+ for (const line of lines) {
603
+ const m = line.match(/^@nudo:contract\s+(.+)$/);
604
+ if (m) reqs.push(m[1].trim().replace(/\*\/$/, "").trim());
605
+ }
606
+ return reqs;
607
+ }
608
+ function scanThrowsDecl(lines) {
609
+ const kinds = /* @__PURE__ */ new Set();
610
+ let any2 = false;
611
+ for (const line of lines) {
612
+ const th = line.match(/^@nudo:throws\s+(.+)$/i);
613
+ if (th) {
614
+ const spec = th[1].trim();
615
+ if (spec === "*") any2 = true;
616
+ else {
617
+ for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
618
+ if (k === "*") any2 = true;
619
+ else kinds.add(k);
620
+ }
621
+ }
622
+ }
623
+ const cs = line.match(/!!\s*throws(?:\s+([A-Za-z*][\w*|,\s]*))?/i);
624
+ if (cs) {
625
+ const spec = (cs[1] ?? "*").trim();
626
+ if (spec === "" || spec === "*") any2 = true;
627
+ else {
628
+ for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
629
+ if (k === "*") any2 = true;
630
+ else kinds.add(k);
631
+ }
632
+ }
633
+ }
634
+ }
635
+ if (any2) return "*";
636
+ if (kinds.size > 0) return [...kinds].join(",");
637
+ return void 0;
638
+ }
639
+ function scanBudgetDecl(lines) {
640
+ const out = {};
641
+ for (const line of lines) {
642
+ const bd = line.match(/^@nudo:budget\s+(.+)$/i);
643
+ if (!bd) continue;
644
+ for (const part of bd[1].split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
645
+ const kv = part.match(/^(forks|calls|depth)\s*=\s*(\d+)$/i);
646
+ if (kv) {
647
+ const num = Number(kv[2]);
648
+ if (Number.isFinite(num) && num >= 1) {
649
+ out[kv[1].toLowerCase()] = Math.floor(num);
650
+ }
651
+ }
652
+ }
653
+ }
654
+ return Object.keys(out).length > 0 ? out : void 0;
655
+ }
656
+
657
+ // src/algebra/hash-source.ts
658
+ var lastHashSource;
659
+ var lastHashOut;
660
+ function hashSource(s) {
661
+ if (s === lastHashSource && lastHashOut !== void 0) return lastHashOut;
662
+ let h1 = 2166136261;
663
+ let h2 = 2654435769;
664
+ for (let i = 0; i < s.length; i++) {
665
+ const c = s.charCodeAt(i);
666
+ h1 ^= c;
667
+ h1 = Math.imul(h1, 16777619);
668
+ h2 = Math.imul(h2 ^ c, 16777619);
669
+ }
670
+ lastHashSource = s;
671
+ lastHashOut = `${(h1 >>> 0).toString(36)}-${(h2 >>> 0).toString(36)}_${s.length.toString(36)}`;
672
+ return lastHashOut;
673
+ }
674
+ function resetHashSourceCache() {
675
+ lastHashSource = void 0;
676
+ lastHashOut = void 0;
677
+ }
678
+
679
+ // src/algebra/sidecar-path.ts
680
+ function normPath(p) {
681
+ return p.replace(/\\/g, "/");
682
+ }
683
+ function stablePathKey(p) {
684
+ let fp = p;
685
+ if (fp.startsWith("file://")) {
686
+ fp = fp.slice("file://".length);
687
+ try {
688
+ fp = decodeURIComponent(fp);
689
+ } catch {
690
+ }
691
+ }
692
+ fp = normPath(fp);
693
+ const drive = /^\/([A-Za-z]:)(.*)$/.exec(fp);
694
+ if (drive) fp = `${drive[1]}${drive[2]}`;
695
+ if (/^[A-Za-z]:\//.test(fp)) return fp[0].toLowerCase() + fp.slice(1);
696
+ return fp;
697
+ }
698
+ function stablePathKeyGraph(m) {
699
+ const out = /* @__PURE__ */ new Map();
700
+ for (const [k, vs] of m) {
701
+ const key = stablePathKey(k);
702
+ let set = out.get(key);
703
+ if (!set) {
704
+ set = /* @__PURE__ */ new Set();
705
+ out.set(key, set);
706
+ }
707
+ for (const v2 of vs) set.add(stablePathKey(v2));
708
+ }
709
+ return out;
710
+ }
711
+ function resolveDepPath(fromFile, spec) {
712
+ const s = normPath(spec);
713
+ if (!s.startsWith(".")) return s;
714
+ const from = normPath(fromFile);
715
+ const i = from.lastIndexOf("/");
716
+ const base = i >= 0 ? from.slice(0, i) : "";
717
+ const parts = base ? base.split("/") : [];
718
+ for (const seg of s.split("/")) {
719
+ if (seg === "" || seg === ".") continue;
720
+ if (seg === "..") parts.pop();
721
+ else parts.push(seg);
722
+ }
723
+ return parts.join("/");
724
+ }
725
+ function isNodeModulesPath(p) {
726
+ const f = normPath(p);
727
+ if (f === "node_modules" || f.startsWith("node_modules/")) return true;
728
+ return f.includes("/node_modules/") || f.endsWith("/node_modules");
729
+ }
730
+ function sidecarPathOf(file) {
731
+ const f = normPath(file);
732
+ if (f.endsWith(".mjs")) return f.slice(0, -4) + ".nudo.js";
733
+ if (f.endsWith(".js")) return f.slice(0, -3) + ".nudo.js";
734
+ if (f.endsWith(".mts")) return f.slice(0, -4) + ".nudo.ts";
735
+ if (f.endsWith(".ts")) return f.slice(0, -3) + ".nudo.ts";
736
+ return `${f}.nudo.js`;
737
+ }
45
738
 
46
739
  // src/algebra/constraint.ts
47
740
  var SELF = "__nudo_self__";
@@ -190,6 +883,28 @@ function union(...cs) {
190
883
  members: cs.map((c) => asNestedConstraint(c, "union member"))
191
884
  });
192
885
  }
886
+ function nullable(c) {
887
+ return makeBuilder(void 0, [], {
888
+ members: [
889
+ asNestedConstraint(c, "nullable()"),
890
+ toPlainConstraint(litC(null)),
891
+ toPlainConstraint(litC(void 0))
892
+ ]
893
+ });
894
+ }
895
+ function constraintAdmitsNullish(c) {
896
+ if (c.members) return c.members.some((m) => constraintAdmitsNullish(m));
897
+ if (!c.prim && c.preds.length === 0 && !c.fields && !c.element && !c.fn) return true;
898
+ for (const p of c.preds) {
899
+ for (const flat of p.op === "and" ? p.args : [p]) {
900
+ if (flat.op !== "eq") continue;
901
+ for (const side of [flat.a, flat.b]) {
902
+ if (side.op === "lit" && (side.value === null || side.value === void 0)) return true;
903
+ }
904
+ }
905
+ }
906
+ return false;
907
+ }
193
908
  function fn(params, returns, opts) {
194
909
  const normalized = {};
195
910
  for (const [k, v2] of Object.entries(params)) {
@@ -261,6 +976,28 @@ function omit(c, keys) {
261
976
  }
262
977
  return makeBuilder(c.prim, c.preds, { fields });
263
978
  }
979
+ var CONSTRAINT_BUILDERS = {
980
+ number,
981
+ string,
982
+ boolean,
983
+ any,
984
+ array,
985
+ shape,
986
+ lit: litC,
987
+ union,
988
+ nullable,
989
+ fn,
990
+ and: andC,
991
+ partial,
992
+ pick,
993
+ omit
994
+ };
995
+ var CONSTRAINT_BUILDER_NAMES = Object.keys(
996
+ CONSTRAINT_BUILDERS
997
+ );
998
+ var CONSTRAINT_EXPR_RE = new RegExp(
999
+ `^(${CONSTRAINT_BUILDER_NAMES.join("|")})\\s*\\(`
1000
+ );
264
1001
  function substTerm(t, paramName) {
265
1002
  if (t.op === "var" && t.id === SELF) return v(paramName);
266
1003
  if (t.op === "app" && t.fn === "get" && t.args.length === 2) {
@@ -388,21 +1125,26 @@ function memberLitValue(m) {
388
1125
  leaves.push(p2);
389
1126
  };
390
1127
  m.preds.forEach(visit2);
391
- if (leaves.length !== 1) return void 0;
1128
+ if (leaves.length !== 1) return { ok: false };
392
1129
  const p = leaves[0];
393
- if (p.op !== "eq") return void 0;
394
- if (p.a.op === "var" && p.b.op === "lit") return p.b.value;
395
- if (p.b.op === "var" && p.a.op === "lit") return p.a.value;
396
- return void 0;
1130
+ if (p.op !== "eq") return { ok: false };
1131
+ if (p.a.op === "var" && p.b.op === "lit") {
1132
+ return { ok: true, v: p.b.value };
1133
+ }
1134
+ if (p.b.op === "var" && p.a.op === "lit") {
1135
+ return { ok: true, v: p.a.value };
1136
+ }
1137
+ return { ok: false };
397
1138
  }
398
1139
  function samePrimLiteralUnionAbs(members, t) {
399
1140
  if (members.length === 0) return void 0;
400
1141
  const values = [];
401
1142
  let prim;
402
1143
  for (const m of members) {
403
- const v2 = memberLitValue(m);
404
- if (v2 === void 0) return void 0;
405
- if (v2 === null) return void 0;
1144
+ const mk = memberLitValue(m);
1145
+ if (!mk.ok) return void 0;
1146
+ const v2 = mk.v;
1147
+ if (v2 === void 0 || v2 === null) return void 0;
406
1148
  const mp = m.prim ?? (typeof v2 === "number" ? "number" : typeof v2 === "string" ? "string" : "boolean");
407
1149
  if (prim === void 0) prim = mp;
408
1150
  else if (prim !== mp) return void 0;
@@ -461,10 +1203,10 @@ function constraintOnTermAbs(c, t) {
461
1203
  if (c.prim) {
462
1204
  return abs({ k: "prim", type: c.prim }, t, predOut, "path");
463
1205
  }
464
- const allEqNull = c.preds.length > 0 && c.preds.every(
465
- (p) => p.op === "eq" && (p.b.op === "lit" && p.b.value === null || p.a.op === "lit" && p.a.value === null)
1206
+ const allEqNullish = c.preds.length > 0 && c.preds.every(
1207
+ (p) => p.op === "eq" && (p.b.op === "lit" && (p.b.value === null || p.b.value === void 0) || p.a.op === "lit" && (p.a.value === null || p.a.value === void 0))
466
1208
  );
467
- if (allEqNull) {
1209
+ if (allEqNullish) {
468
1210
  return abs({ k: "unknown" }, t, predOut, "path");
469
1211
  }
470
1212
  if (c.preds.length > 0) {
@@ -505,72 +1247,24 @@ function throwConstraintToKinds(c) {
505
1247
  return void 0;
506
1248
  }
507
1249
 
508
- // src/algebra/hash-source.ts
509
- var lastHashSource;
510
- var lastHashOut;
511
- function hashSource(s) {
512
- if (s === lastHashSource && lastHashOut !== void 0) return lastHashOut;
513
- let h1 = 2166136261;
514
- let h2 = 2654435769;
515
- for (let i = 0; i < s.length; i++) {
516
- const c = s.charCodeAt(i);
517
- h1 ^= c;
518
- h1 = Math.imul(h1, 16777619);
519
- h2 = Math.imul(h2 ^ c, 16777619);
520
- }
521
- lastHashSource = s;
522
- lastHashOut = `${(h1 >>> 0).toString(36)}-${(h2 >>> 0).toString(36)}_${s.length.toString(36)}`;
523
- return lastHashOut;
524
- }
525
- function resetHashSourceCache() {
526
- lastHashSource = void 0;
527
- lastHashOut = void 0;
528
- }
529
-
530
- // src/algebra/sidecar-path.ts
531
- function normPath(p) {
532
- return p.replace(/\\/g, "/");
533
- }
534
- function resolveDepPath(fromFile, spec) {
535
- const s = normPath(spec);
536
- if (!s.startsWith(".")) return s;
537
- const from = normPath(fromFile);
538
- const i = from.lastIndexOf("/");
539
- const base = i >= 0 ? from.slice(0, i) : "";
540
- const parts = base ? base.split("/") : [];
541
- for (const seg of s.split("/")) {
542
- if (seg === "" || seg === ".") continue;
543
- if (seg === "..") parts.pop();
544
- else parts.push(seg);
545
- }
546
- return parts.join("/");
547
- }
548
- function isNodeModulesPath(p) {
549
- const f = normPath(p);
550
- if (f === "node_modules" || f.startsWith("node_modules/")) return true;
551
- return f.includes("/node_modules/") || f.endsWith("/node_modules");
552
- }
553
- function sidecarPathOf(file) {
554
- const f = normPath(file);
555
- if (f.endsWith(".mjs")) return f.slice(0, -4) + ".nudo.js";
556
- if (f.endsWith(".js")) return f.slice(0, -3) + ".nudo.js";
557
- if (f.endsWith(".mts")) return f.slice(0, -4) + ".nudo.ts";
558
- if (f.endsWith(".ts")) return f.slice(0, -3) + ".nudo.ts";
559
- return `${f}.nudo.js`;
560
- }
561
-
562
1250
  // src/algebra/refine.ts
563
1251
  function extractNudoImports(source) {
564
1252
  const out = [];
565
- const named = /@nudo:import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
566
- let m;
567
- while (m = named.exec(source)) {
568
- const names = m[1].split(",").map((s) => s.trim()).filter(Boolean).map((s) => s.split(/\s+as\s+/).pop().trim());
569
- out.push({ names, spec: m[2] });
1253
+ for (const rec of extractNudoImportRecords(source)) {
1254
+ out.push({ names: rec.names, spec: rec.spec });
570
1255
  }
571
- const ns = /@nudo:import\s+\*\s+as\s+(\w+)\s+from\s*["']([^"']+)["']/g;
572
- while (m = ns.exec(source)) {
573
- out.push({ names: [`*${m[1]}`], spec: m[2] });
1256
+ for (const bad of scanMalformedNudoImports(source)) {
1257
+ if (bad === "malformed-default") {
1258
+ collectDiag({
1259
+ code: "nudo:contract-syntax",
1260
+ message: `Default @nudo:import is not supported (use named { a, b } or namespace * as ns)`
1261
+ });
1262
+ } else {
1263
+ collectDiag({
1264
+ code: "nudo:contract-syntax",
1265
+ message: `Unrecognized @nudo:import form (expected { names } from "path" or * as ns from "path")`
1266
+ });
1267
+ }
574
1268
  }
575
1269
  return out;
576
1270
  }
@@ -612,20 +1306,7 @@ var NudoSidecarError = class extends Error {
612
1306
  this.code = code;
613
1307
  }
614
1308
  };
615
- var sidecarInjects = {
616
- number,
617
- string,
618
- boolean,
619
- shape,
620
- array,
621
- lit: litC,
622
- union,
623
- fn,
624
- and: andC,
625
- partial,
626
- pick,
627
- omit
628
- };
1309
+ var sidecarInjects = CONSTRAINT_BUILDERS;
629
1310
  function isSidecarSpec(spec) {
630
1311
  return spec.endsWith(".nudo.js") || spec.endsWith(".nudo.ts");
631
1312
  }
@@ -974,6 +1655,7 @@ function execNudoModule(src, opts) {
974
1655
  return execNudoModuleCached(src, opts ?? {});
975
1656
  }
976
1657
  var nudoModuleExecCache = /* @__PURE__ */ new Map();
1658
+ var nudoModuleFailCache = /* @__PURE__ */ new Map();
977
1659
  var MAX_NUDO_MODULE_EXEC = 64;
978
1660
  var MAX_SIDECAR_CLOSURE = 64;
979
1661
  function sidecarDepClosure(src, fromFile, loadModule, out) {
@@ -991,7 +1673,12 @@ function sidecarDepClosure(src, fromFile, loadModule, out) {
991
1673
  if (!isSidecarSpec(spec)) continue;
992
1674
  const canonical = resolveDepPath(fromFile, spec);
993
1675
  if (out.has(canonical)) continue;
994
- const depSrc = loadModule(spec, fromFile);
1676
+ let depSrc;
1677
+ try {
1678
+ depSrc = loadModule(spec, fromFile);
1679
+ } catch {
1680
+ depSrc = void 0;
1681
+ }
995
1682
  out.set(canonical, depSrc);
996
1683
  if (depSrc === void 0) continue;
997
1684
  const r = sidecarDepClosure(depSrc, canonical, loadModule, out);
@@ -1020,19 +1707,34 @@ function execNudoModuleCached(src, opts) {
1020
1707
  nudoModuleExecCache.set(key, hit);
1021
1708
  return hit;
1022
1709
  }
1710
+ if (nudoModuleFailCache.has(key)) {
1711
+ throw nudoModuleFailCache.get(key);
1712
+ }
1023
1713
  }
1024
- const out = execSidecar(src, opts, opts.fromFile ? [opts.fromFile] : [], preloaded);
1025
- if (key !== void 0) {
1026
- if (nudoModuleExecCache.size >= MAX_NUDO_MODULE_EXEC) {
1027
- const oldest = nudoModuleExecCache.keys().next().value;
1028
- if (oldest !== void 0) nudoModuleExecCache.delete(oldest);
1714
+ try {
1715
+ const out = execSidecar(src, opts, opts.fromFile ? [opts.fromFile] : [], preloaded);
1716
+ if (key !== void 0) {
1717
+ if (nudoModuleExecCache.size >= MAX_NUDO_MODULE_EXEC) {
1718
+ const oldest = nudoModuleExecCache.keys().next().value;
1719
+ if (oldest !== void 0) nudoModuleExecCache.delete(oldest);
1720
+ }
1721
+ nudoModuleExecCache.set(key, out);
1029
1722
  }
1030
- nudoModuleExecCache.set(key, out);
1723
+ return out;
1724
+ } catch (e) {
1725
+ if (key !== void 0) {
1726
+ if (nudoModuleFailCache.size >= MAX_NUDO_MODULE_EXEC) {
1727
+ const oldest = nudoModuleFailCache.keys().next().value;
1728
+ if (oldest !== void 0) nudoModuleFailCache.delete(oldest);
1729
+ }
1730
+ nudoModuleFailCache.set(key, e);
1731
+ }
1732
+ throw e;
1031
1733
  }
1032
- return out;
1033
1734
  }
1034
1735
  function resetNudoModuleExecCache() {
1035
1736
  nudoModuleExecCache.clear();
1737
+ nudoModuleFailCache.clear();
1036
1738
  }
1037
1739
  function collectConstraints(source, opts) {
1038
1740
  const map = /* @__PURE__ */ new Map();
@@ -1046,7 +1748,17 @@ function collectConstraints(source, opts) {
1046
1748
  });
1047
1749
  continue;
1048
1750
  }
1049
- const src = opts.loadModule(imp.spec, opts.fromFile);
1751
+ let src;
1752
+ try {
1753
+ src = opts.loadModule(imp.spec, opts.fromFile);
1754
+ } catch (e) {
1755
+ collectDiag({
1756
+ code: "nudo:interface-load",
1757
+ message: `sidecar '${imp.spec}' failed to load from '${opts.fromFile}': ${e instanceof Error ? e.message : String(e)}`,
1758
+ file: opts.fromFile
1759
+ });
1760
+ continue;
1761
+ }
1050
1762
  if (src === void 0) {
1051
1763
  collectDiag({
1052
1764
  code: "nudo:interface-load",
@@ -1102,36 +1814,25 @@ function collectConstraints(source, opts) {
1102
1814
  return map;
1103
1815
  }
1104
1816
  function extractRefineLines(source, fnName) {
1105
- const escaped = fnName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1106
- const fnRe = new RegExp(
1107
- `(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?(?:function\\s+${escaped}\\b|const\\s+${escaped}\\s*=)`
1108
- );
1109
- const m = source.match(fnRe);
1110
- if (!m || m.index === void 0) return [];
1111
- const before = source.slice(0, m.index);
1112
- const lines = before.split("\n");
1113
- const reqs = [];
1114
- for (let i = lines.length - 1; i >= 0; i--) {
1115
- const line = lines[i].trim();
1116
- if (line === "" || line === "*/") continue;
1117
- if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
1118
- const rm = line.match(/@nudo:contract\s+(.+)$/);
1119
- if (rm) reqs.unshift(rm[1].trim().replace(/\*\/$/, "").trim());
1120
- continue;
1121
- }
1122
- break;
1123
- }
1124
- return reqs;
1817
+ const lines = fnDirectiveCommentLines(source, fnName);
1818
+ return scanContractSegments(lines);
1125
1819
  }
1126
1820
  function extractRefinesFromSource(source, fnName, opts = {}) {
1127
- if (!source.includes("@nudo:contract")) return [];
1821
+ if (!stripStringsKeepComments(source).includes("@nudo:contract")) return [];
1128
1822
  const constraints = collectConstraints(source, opts);
1129
1823
  const out = [];
1130
1824
  for (const line of extractRefineLines(source, fnName)) {
1131
1825
  const parts = line.split(/&&|,/).map((s) => s.trim()).filter(Boolean);
1132
1826
  for (const part of parts) {
1133
1827
  const m = part.match(/^(\w+)\s+([\w.]+)$/);
1134
- if (!m) continue;
1828
+ if (!m) {
1829
+ if (/^return\b/.test(part)) continue;
1830
+ collectDiag({
1831
+ code: "nudo:contract-syntax",
1832
+ message: `Malformed @nudo:contract segment '${part}' (expected <param> <constraintName> or return <constraintName>; e.g. @nudo:contract x positive)`
1833
+ });
1834
+ continue;
1835
+ }
1135
1836
  const [, param, cName] = m;
1136
1837
  if (param === "return") continue;
1137
1838
  const c = constraints.get(cName);
@@ -1155,13 +1856,21 @@ function refineToIndexedFull(source, fnName, paramNames, opts = {}) {
1155
1856
  return out;
1156
1857
  }
1157
1858
  function extractRefineReturnFromSource(source, fnName, opts = {}) {
1158
- if (!source.includes("@nudo:contract")) return void 0;
1859
+ if (!stripStringsKeepComments(source).includes("@nudo:contract")) return void 0;
1159
1860
  const constraints = collectConstraints(source, opts);
1160
1861
  for (const line of extractRefineLines(source, fnName)) {
1161
1862
  const parts = line.split(/&&|,/).map((s) => s.trim()).filter(Boolean);
1162
1863
  for (const part of parts) {
1163
1864
  const m = part.match(/^return\s+([\w.]+)$/);
1164
- if (!m) continue;
1865
+ if (!m) {
1866
+ if (/^return\b/.test(part) && !/^return\s+[\w.]+$/.test(part)) {
1867
+ collectDiag({
1868
+ code: "nudo:contract-syntax",
1869
+ message: `Malformed @nudo:contract return segment '${part}' (expected return <constraintName>)`
1870
+ });
1871
+ }
1872
+ continue;
1873
+ }
1165
1874
  const cName = m[1];
1166
1875
  const c = constraints.get(cName);
1167
1876
  if (!c) continue;
@@ -1171,59 +1880,21 @@ function extractRefineReturnFromSource(source, fnName, opts = {}) {
1171
1880
  return void 0;
1172
1881
  }
1173
1882
  function extractDeclaredThrows(source, fnName) {
1174
- if (!source.includes("@nudo:throws") && !source.includes("!! throws")) {
1883
+ const directiveSrc = stripStringsKeepComments(source);
1884
+ if (!directiveSrc.includes("@nudo:throws") && !directiveSrc.includes("!! throws")) {
1175
1885
  return void 0;
1176
1886
  }
1177
- const kinds = /* @__PURE__ */ new Set();
1178
- let any2 = false;
1179
- const escaped = fnName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1180
- const fnRe = new RegExp(
1181
- `(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?(?:function\\s+${escaped}\\b|const\\s+${escaped}\\s*=)`
1182
- );
1183
- const m = source.match(fnRe);
1184
- if (!m || m.index === void 0) return void 0;
1185
- const before = source.slice(0, m.index);
1186
- const lines = before.split("\n");
1187
- let seenComment = false;
1188
- for (let i = lines.length - 1; i >= 0; i--) {
1189
- const line = lines[i].trim();
1190
- if (line === "") {
1191
- if (seenComment) break;
1192
- continue;
1193
- }
1194
- if (line === "*/") continue;
1195
- if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
1196
- seenComment = true;
1197
- const body = line.replace(/^[*/\s]+/, "").replace(/\*\/$/, "").trim();
1198
- const th = body.match(/@nudo:throws\s+(.+)$/i);
1199
- if (th) {
1200
- const spec = th[1].trim();
1201
- if (spec === "*") any2 = true;
1202
- else {
1203
- for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
1204
- if (k === "*") any2 = true;
1205
- else kinds.add(k);
1206
- }
1207
- }
1208
- }
1209
- const cs = body.match(/!!\s*throws(?:\s+([A-Za-z*][\w*|,\s]*))?/i);
1210
- if (cs) {
1211
- const spec = (cs[1] ?? "*").trim();
1212
- if (spec === "" || spec === "*") any2 = true;
1213
- else {
1214
- for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
1215
- if (k === "*") any2 = true;
1216
- else kinds.add(k);
1217
- }
1218
- }
1219
- }
1220
- continue;
1221
- }
1222
- break;
1223
- }
1224
- if (any2) return "*";
1225
- if (kinds.size > 0) return [...kinds];
1226
- return void 0;
1887
+ const lines = fnDirectiveCommentLines(source, fnName);
1888
+ const spec = scanThrowsDecl(lines);
1889
+ if (spec === void 0) return void 0;
1890
+ if (spec === "*") return "*";
1891
+ return spec.split(",").filter(Boolean);
1892
+ }
1893
+ function extractFnBudget(source, fnName) {
1894
+ const directiveSrc = stripStringsKeepComments(source);
1895
+ if (!directiveSrc.includes("@nudo:budget")) return void 0;
1896
+ const lines = fnDirectiveCommentLines(source, fnName);
1897
+ return scanBudgetDecl(lines);
1227
1898
  }
1228
1899
 
1229
1900
  // src/algebra/interface.ts
@@ -1321,11 +1992,24 @@ function localNamedExports(source) {
1321
1992
  for (const stmt of ast.program.body) {
1322
1993
  const s = stmt;
1323
1994
  if (s.type === "ExportNamedDeclaration") {
1324
- if (s.source) continue;
1995
+ if (s.source) {
1996
+ const specs = s.specifiers ?? [];
1997
+ for (const spec of specs) {
1998
+ if (spec.type !== "ExportNamespaceSpecifier") continue;
1999
+ const name = identName(spec.exported);
2000
+ if (name) out.add(name);
2001
+ }
2002
+ continue;
2003
+ }
1325
2004
  const d = s.declaration;
1326
2005
  if (!d) {
1327
2006
  const specs = s.specifiers ?? [];
1328
2007
  for (const spec of specs) {
2008
+ if (spec.type === "ExportNamespaceSpecifier") {
2009
+ const name2 = identName(spec.exported);
2010
+ if (name2) out.add(name2);
2011
+ continue;
2012
+ }
1329
2013
  if (spec.type !== "ExportSpecifier") continue;
1330
2014
  const name = identName(spec.exported);
1331
2015
  if (!name) continue;
@@ -1473,17 +2157,53 @@ function sidecarAutoBindAllowed(sidecarPath, autoBind, projectDir) {
1473
2157
  return false;
1474
2158
  }
1475
2159
  function isDefaultExportLocal(source, localName) {
2160
+ const name = identBoundaryRegex(localName);
1476
2161
  const reList = new RegExp(
1477
- `export\\s*\\{[^}]*\\b${localName}\\s+as\\s+default\\b[^}]*\\}`,
2162
+ `export\\s*\\{[^}]*${name}\\s+as\\s+default(?![\\w$])[^}]*\\}`,
1478
2163
  "m"
1479
2164
  );
1480
2165
  const reDefaultFn = new RegExp(
1481
- `export\\s+default\\s+(?:async\\s+)?function\\s+${localName}\\b`,
2166
+ `export\\s+default\\s+(?:async\\s+)?function\\s+${name}`,
2167
+ "m"
2168
+ );
2169
+ const reDefaultId = new RegExp(
2170
+ `export\\s+default\\s+${name}`,
1482
2171
  "m"
1483
2172
  );
1484
- const reDefaultId = new RegExp(`export\\s+default\\s+${localName}\\b`, "m");
1485
2173
  return reList.test(source) || reDefaultFn.test(source) || reDefaultId.test(source);
1486
2174
  }
2175
+ function sidecarExportNames(sidecarSrc) {
2176
+ try {
2177
+ return localNamedExports(sidecarSrc);
2178
+ } catch {
2179
+ return void 0;
2180
+ }
2181
+ }
2182
+ function sidecarMayBind(sidecarNames, fnName, source) {
2183
+ if (!sidecarNames) return true;
2184
+ if (sidecarNames.size === 0) return true;
2185
+ if (sidecarNames.has(fnName)) return true;
2186
+ if (sidecarNames.has("default") && isDefaultExportLocal(source, fnName)) return true;
2187
+ if (fnName.includes(".")) {
2188
+ const [cls, method] = fnName.split(".", 2);
2189
+ if (cls && method) {
2190
+ if (sidecarNames.has(`${cls}_${method}`)) return true;
2191
+ if (sidecarNames.has(cls)) return true;
2192
+ if (sidecarNames.has(method)) return true;
2193
+ }
2194
+ }
2195
+ return false;
2196
+ }
2197
+ var sidecarLoadFailures = /* @__PURE__ */ new Map();
2198
+ function noteSidecarLoadFailure(sidecarPath, reason) {
2199
+ const key = `${sidecarPath}\0${reason}`;
2200
+ if (sidecarLoadFailures.has(key)) return false;
2201
+ sidecarLoadFailures.set(key, reason);
2202
+ return true;
2203
+ }
2204
+ function resetSidecarLoadFailureCache() {
2205
+ sidecarLoadFailures.clear();
2206
+ }
1487
2207
  function loadSidecarBinding(source, fnName, opts) {
1488
2208
  const { loadModule, fromFile, autoBind, projectDir } = opts;
1489
2209
  if (!loadModule || !fromFile) return { ok: false };
@@ -1491,18 +2211,46 @@ function loadSidecarBinding(source, fnName, opts) {
1491
2211
  if (!sidecarAutoBindAllowed(sidecarPath, autoBind, projectDir)) return { ok: false };
1492
2212
  if (!localNamedExports(source).has(fnName)) return { ok: false };
1493
2213
  const spec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
1494
- const sidecarSrc = loadModule(spec, fromFile);
2214
+ let sidecarSrc;
2215
+ try {
2216
+ sidecarSrc = loadModule(spec, fromFile);
2217
+ } catch (e) {
2218
+ const reason = e instanceof Error ? e.message : String(e);
2219
+ if (noteSidecarLoadFailure(sidecarPath, reason)) {
2220
+ collectDiag2({
2221
+ code: "nudo:interface-load",
2222
+ message: `sidecar '${spec}' failed to load: ${reason}`,
2223
+ file: sidecarPath
2224
+ });
2225
+ }
2226
+ return { ok: false };
2227
+ }
1495
2228
  if (sidecarSrc === void 0) return { ok: false };
1496
2229
  if (sidecarSrc === source) return { ok: false };
2230
+ const scNames = sidecarExportNames(sidecarSrc);
2231
+ if (!sidecarMayBind(scNames, fnName, source)) return { ok: false };
1497
2232
  let exports;
1498
2233
  try {
1499
2234
  exports = execNudoModule(sidecarSrc, { loadModule, fromFile: sidecarPath });
1500
2235
  } catch (e) {
1501
- collectDiag2({
1502
- code: e instanceof NudoSidecarError ? e.code : "nudo:interface-load",
1503
- message: `sidecar '${spec}' for '${fnName}' failed: ${e instanceof Error ? e.message : String(e)}`,
1504
- file: sidecarPath
1505
- });
2236
+ const reason = e instanceof Error ? e.message : String(e);
2237
+ const code = e instanceof NudoSidecarError ? e.code : "nudo:interface-load";
2238
+ if (noteSidecarLoadFailure(sidecarPath, reason)) {
2239
+ let affected = 1;
2240
+ if (scNames && scNames.size > 0) {
2241
+ const srcExports = localNamedExports(source);
2242
+ let n = 0;
2243
+ for (const name of srcExports) {
2244
+ if (sidecarMayBind(scNames, name, source)) n++;
2245
+ }
2246
+ if (n > 0) affected = n;
2247
+ }
2248
+ collectDiag2({
2249
+ code,
2250
+ message: affected > 1 ? `sidecar '${spec}' failed to load: ${reason} (affects ${affected} bindings)` : `sidecar '${spec}' failed to load: ${reason}`,
2251
+ file: sidecarPath
2252
+ });
2253
+ }
1506
2254
  return { ok: false };
1507
2255
  }
1508
2256
  let binding = exports[fnName];
@@ -1585,14 +2333,17 @@ function eqLitsOf(c) {
1585
2333
  if (p.op !== "eq") return;
1586
2334
  let term;
1587
2335
  let value;
2336
+ let hasLit = false;
1588
2337
  if (p.a.op === "var" && p.b.op === "lit") {
1589
2338
  term = p.a;
1590
2339
  value = p.b.value;
2340
+ hasLit = true;
1591
2341
  } else if (p.b.op === "var" && p.a.op === "lit") {
1592
2342
  term = p.b;
1593
2343
  value = p.a.value;
2344
+ hasLit = true;
1594
2345
  }
1595
- if (term !== void 0 && value !== void 0) {
2346
+ if (term !== void 0 && hasLit) {
1596
2347
  out.push({ term: termToString(term), value });
1597
2348
  }
1598
2349
  };
@@ -1758,7 +2509,12 @@ function sidecarClosureFingerprint(fromFile, opts) {
1758
2509
  const sidecarPath = sidecarPathOf(fromFile);
1759
2510
  if (!sidecarAutoBindAllowed(sidecarPath, autoBind, projectDir)) return void 0;
1760
2511
  const spec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
1761
- const sidecarSrc = loadModule(spec, fromFile);
2512
+ let sidecarSrc;
2513
+ try {
2514
+ sidecarSrc = loadModule(spec, fromFile);
2515
+ } catch {
2516
+ return void 0;
2517
+ }
1762
2518
  if (sidecarSrc === void 0) return void 0;
1763
2519
  const parts = [];
1764
2520
  const seen = /* @__PURE__ */ new Set([sidecarPath]);
@@ -1772,7 +2528,12 @@ function sidecarClosureFingerprint(fromFile, opts) {
1772
2528
  }
1773
2529
  const { spec: s, from } = queue.shift();
1774
2530
  const depPath = resolveDepPath(from, s);
1775
- const depSrc = loadModule(s, from);
2531
+ let depSrc;
2532
+ try {
2533
+ depSrc = loadModule(s, from);
2534
+ } catch {
2535
+ depSrc = void 0;
2536
+ }
1776
2537
  parts.push(`${depPath}=${depSrc === void 0 ? "miss" : hashSource(depSrc)}`);
1777
2538
  if (depSrc !== void 0) enqueueSidecarDeps(depSrc, depPath, seen, queue);
1778
2539
  }
@@ -2055,7 +2816,7 @@ function hasModuleSyntax(file) {
2055
2816
  return false;
2056
2817
  }
2057
2818
  function canSkipLiteralCallScan(source, file) {
2058
- if (source.includes("@nudo:contract")) return false;
2819
+ if (stripStringsKeepComments(source).includes("@nudo:contract")) return false;
2059
2820
  if (hasModuleSyntax(file)) return false;
2060
2821
  if (hasAnyCallLike(file)) return false;
2061
2822
  const fps = fnFingerprints(source, file);
@@ -2071,17 +2832,12 @@ function extractAllLoadSpecs(source) {
2071
2832
  const specs = /* @__PURE__ */ new Set();
2072
2833
  for (const imp of extractNudoImports(source)) specs.add(imp.spec);
2073
2834
  const isPathLikeSpec = (spec) => spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(spec);
2074
- const envRe = /@nudo:env\s+([^\n*]+)/g;
2075
- let envM;
2076
- while (envM = envRe.exec(source)) {
2077
- for (const part of envM[1].split(",")) {
2078
- const spec = part.trim().replace(/^['"]|['"]$/g, "");
2079
- if (spec && isPathLikeSpec(spec)) specs.add(spec);
2080
- }
2835
+ for (const name of extractFileEnvNames(source)) {
2836
+ if (isPathLikeSpec(name)) specs.add(name);
2837
+ }
2838
+ for (const rec of extractMockModuleRecords(source)) {
2839
+ if (rec.fromPath) specs.add(rec.fromPath);
2081
2840
  }
2082
- const mockFromRe = /@nudo:mock-module\s+"[^"]+"\s+(?:\{[^}]*\}\s+)?from\s+["']([^"']+)["']/g;
2083
- let mockM;
2084
- while (mockM = mockFromRe.exec(source)) specs.add(mockM[1]);
2085
2841
  const patterns = [
2086
2842
  /\bfrom\s*['"]([^'"]+)['"]/g,
2087
2843
  /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
@@ -2098,6 +2854,13 @@ function sidecarSpecsOf(source) {
2098
2854
  (spec) => (spec.startsWith(".") || spec.startsWith("/")) && (spec.endsWith(".nudo.js") || spec.endsWith(".nudo.ts"))
2099
2855
  );
2100
2856
  }
2857
+ function loadOrMiss(loadModule, spec, from) {
2858
+ try {
2859
+ return loadModule(spec, from);
2860
+ } catch {
2861
+ return void 0;
2862
+ }
2863
+ }
2101
2864
  function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2102
2865
  const parts = [];
2103
2866
  const paths = [];
@@ -2116,7 +2879,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2116
2879
  const sidecarPath = sidecarPathOf(entryFile);
2117
2880
  if (seen.has(sidecarPath)) return;
2118
2881
  const sidecarSpec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
2119
- const sidecarSrc = loadModule(sidecarSpec, entryFile);
2882
+ const sidecarSrc = loadOrMiss(loadModule, sidecarSpec, entryFile);
2120
2883
  if (sidecarSrc === void 0) return;
2121
2884
  if (n >= MAX_LOAD_DEP_NODES) {
2122
2885
  truncated = true;
@@ -2140,7 +2903,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2140
2903
  if (seen.has(path)) continue;
2141
2904
  seen.add(path);
2142
2905
  n++;
2143
- const src = loadModule(cur.spec, cur.from);
2906
+ const src = loadOrMiss(loadModule, cur.spec, cur.from);
2144
2907
  parts.push(`sidecar:${path}=${src === void 0 ? "miss" : hashSource(src)}`);
2145
2908
  paths.push(path);
2146
2909
  contents.push({ path, content: src ?? null });
@@ -2161,7 +2924,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2161
2924
  if (seen.has(path)) continue;
2162
2925
  seen.add(path);
2163
2926
  n++;
2164
- const src = loadModule(cur.spec, cur.from);
2927
+ const src = loadOrMiss(loadModule, cur.spec, cur.from);
2165
2928
  parts.push(`${path}=${src === void 0 ? "miss" : hashSource(src)}`);
2166
2929
  paths.push(path);
2167
2930
  contents.push({ path, content: src ?? null });
@@ -2209,6 +2972,8 @@ function predKey(p, rename) {
2209
2972
  return `not(${predKey(p.arg, rename)})`;
2210
2973
  case "typeof":
2211
2974
  return `typeof(${termKey(p.t, rename)},${p.type})`;
2975
+ case "assumeFinite":
2976
+ return `assumeFinite(${termKey(p.t, rename)})`;
2212
2977
  }
2213
2978
  }
2214
2979
  function shapeKey(s, seen, rename) {
@@ -2359,6 +3124,8 @@ function renamePred(p, map) {
2359
3124
  return { op: "not", arg: renamePred(p.arg, map) };
2360
3125
  case "typeof":
2361
3126
  return { op: "typeof", t: renameTerm(p.t, map), type: p.type };
3127
+ case "assumeFinite":
3128
+ return { op: "assumeFinite", t: renameTerm(p.t, map) };
2362
3129
  }
2363
3130
  }
2364
3131
  function renameAbs(a, map) {
@@ -2767,7 +3534,7 @@ function unindexMemoKey(key) {
2767
3534
  function evictGeneralizeMemoForPaths(paths) {
2768
3535
  let n = 0;
2769
3536
  for (const raw of paths) {
2770
- const p = normPath(raw);
3537
+ const p = stablePathKey(raw);
2771
3538
  const keys = memoDepIndex.get(p);
2772
3539
  if (!keys) continue;
2773
3540
  for (const key of [...keys]) {
@@ -2809,7 +3576,7 @@ function generalizeMemoKey(fnName, source, opts) {
2809
3576
  const budget = opts.budget ?? defaultLeakBudget;
2810
3577
  const deps = opts.depsFp ?? (r ? refineDepsFingerprint(source, r) : { fp: "-", paths: [], contents: [], truncated: false });
2811
3578
  const sc = opts.sidecarFp ?? (r?.loadModule && r.fromFile ? sidecarClosureFingerprint(r.fromFile, r) : void 0);
2812
- const scPath = sc !== void 0 && !sc.startsWith("trunc:") && r?.fromFile ? normPath(sidecarPathOf(r.fromFile)) : void 0;
3579
+ const scPath = sc !== void 0 && !sc.startsWith("trunc:") && r?.fromFile ? sidecarPathOf(r.fromFile) : void 0;
2813
3580
  const srcPart = generalizeSourceKeyPart(source, fnName, opts.file);
2814
3581
  const key = [
2815
3582
  srcPart,
@@ -2845,8 +3612,9 @@ function generalizeMemoSet(key, value, depPaths) {
2845
3612
  }
2846
3613
  generalizeMemo.set(key, value);
2847
3614
  if (depPaths.length > 0) {
2848
- memoKeyDeps.set(key, depPaths);
2849
- for (const p of depPaths) {
3615
+ const normDeps = [...new Set(depPaths.map(stablePathKey))];
3616
+ memoKeyDeps.set(key, normDeps);
3617
+ for (const p of normDeps) {
2850
3618
  let set = memoDepIndex.get(p);
2851
3619
  if (!set) {
2852
3620
  set = /* @__PURE__ */ new Set();
@@ -2993,6 +3761,40 @@ function extractFn(source, fnName, fileAst) {
2993
3761
  registerFnNode(propName, right);
2994
3762
  }
2995
3763
  }
3764
+ for (const stmt of file.program.body) {
3765
+ if (stmt.type !== "ExportNamedDeclaration" || stmt.declaration) continue;
3766
+ if (stmt.source) continue;
3767
+ const specs = stmt.specifiers ?? [];
3768
+ for (const spec of specs) {
3769
+ if (spec.type !== "ExportSpecifier") continue;
3770
+ const localName = spec.local?.name;
3771
+ const exportedName = spec.exported?.name ?? spec.exported?.value;
3772
+ if (!localName || exportedName == null) continue;
3773
+ const exp = String(exportedName);
3774
+ if (exp === localName || env.fns.has(exp)) continue;
3775
+ const target = env.fns.get(localName);
3776
+ if (!target) continue;
3777
+ env.fns.set(exp, target);
3778
+ formalsByName.set(exp, formalsByName.get(localName) ?? []);
3779
+ }
3780
+ }
3781
+ for (const scope of listFnDirectiveScopes(file)) {
3782
+ if (env.fns.has(scope.name)) continue;
3783
+ if (scope.name === "<anonymous>") continue;
3784
+ const n = scope.node;
3785
+ const isFnLike = n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" || n.type === "ClassMethod" || n.type === "ClassPrivateMethod" || n.type === "ObjectMethod";
3786
+ if (!isFnLike || !n.body) continue;
3787
+ if ((n.type === "ClassMethod" || n.type === "ClassPrivateMethod" || n.type === "ObjectMethod") && n.kind && n.kind !== "method") {
3788
+ continue;
3789
+ }
3790
+ const formals = formalParamsFromNodes(n.params ?? []);
3791
+ env.fns.set(scope.name, {
3792
+ params: formalParamDisplayNames(formals),
3793
+ body: n.body,
3794
+ async: n.async === true
3795
+ });
3796
+ formalsByName.set(scope.name, formals);
3797
+ }
2996
3798
  const fn2 = env.fns.get(fnName);
2997
3799
  if (!fn2) return void 0;
2998
3800
  return { params: fn2.params, body: fn2.body, env, formals: formalsByName.get(fnName) ?? [] };
@@ -3027,7 +3829,7 @@ function generalizeFromAstUncached(fnName, source, opts = {}) {
3027
3829
  if (opts.refine) {
3028
3830
  try {
3029
3831
  const r = opts.refine;
3030
- const hasDirective = source.includes("@nudo:contract") || source.includes("@nudo:contract");
3832
+ const hasDirective = stripStringsKeepComments(source).includes("@nudo:contract");
3031
3833
  const sc = opts.sidecarFp ?? (r.loadModule && r.fromFile ? sidecarClosureFingerprint(r.fromFile, r) : void 0);
3032
3834
  const eff = hasDirective || sc !== void 0 ? effectiveInterface(source, fnName, r) : void 0;
3033
3835
  const reqs = eff?.params ?? [];
@@ -3036,6 +3838,7 @@ function generalizeFromAstUncached(fnName, source, opts = {}) {
3036
3838
  param: e.param,
3037
3839
  pred: instantiateConstraint(e.constraint, e.param)
3038
3840
  }));
3841
+ const patternFields = /* @__PURE__ */ new Map();
3039
3842
  for (const e of reqs) {
3040
3843
  const idx = params.indexOf(e.param);
3041
3844
  if (idx >= 0) {
@@ -3048,7 +3851,54 @@ function generalizeFromAstUncached(fnName, source, opts = {}) {
3048
3851
  abs: snapshotAbs(entryAbs),
3049
3852
  source: "refine"
3050
3853
  });
3854
+ continue;
3855
+ }
3856
+ const hit = formals.length > 0 ? locateContractParam(formals, e.param) : void 0;
3857
+ if (hit && hit.field !== void 0) {
3858
+ let bag = patternFields.get(hit.index);
3859
+ if (!bag) {
3860
+ bag = {};
3861
+ patternFields.set(hit.index, bag);
3862
+ }
3863
+ const fieldAbs = constraintToEntryAbs(e.constraint, e.param);
3864
+ bag[hit.field] = {
3865
+ abs: fieldAbs,
3866
+ ...e.constraint.isOptional ? { optional: true } : {}
3867
+ };
3868
+ } else if (hit) {
3869
+ const entryAbs = constraintToEntryAbs(e.constraint, e.param);
3870
+ typeParams[hit.index] = {
3871
+ id: typeParams[hit.index].id,
3872
+ value: entryAbs
3873
+ };
3874
+ refineEntryShapes.set(e.param, {
3875
+ abs: snapshotAbs(entryAbs),
3876
+ source: "refine"
3877
+ });
3878
+ }
3879
+ }
3880
+ for (const [pIdx, bag] of patternFields) {
3881
+ const slots = {};
3882
+ for (const [key, rec] of Object.entries(bag)) {
3883
+ slots[key] = {
3884
+ value: rec.abs,
3885
+ ...rec.optional ? { optional: true } : {}
3886
+ };
3051
3887
  }
3888
+ const objAbs = abs(
3889
+ { k: "obj", slots },
3890
+ v(params[pIdx] ?? `_p${pIdx}`),
3891
+ void 0,
3892
+ "path"
3893
+ );
3894
+ typeParams[pIdx] = {
3895
+ id: typeParams[pIdx].id,
3896
+ value: objAbs
3897
+ };
3898
+ refineEntryShapes.set(params[pIdx] ?? `_p${pIdx}`, {
3899
+ abs: snapshotAbs(objAbs),
3900
+ source: "refine"
3901
+ });
3052
3902
  }
3053
3903
  }
3054
3904
  } catch {
@@ -3080,17 +3930,49 @@ function generalizeFromAstUncached(fnName, source, opts = {}) {
3080
3930
  const hasMocks = inject && Object.keys(inject.mocks ?? {}).length > 0;
3081
3931
  const hasEnv = inject && Object.keys(inject.envGlobals ?? {}).length > 0;
3082
3932
  const hasReps = inject && Object.keys(inject.replacements ?? {}).length > 0;
3083
- const hasFnMockDirective = /@nudo:mock\s+\w+\s*(?:=|from\b)/.test(source);
3084
- const hasModMockDirective = /@nudo:mock-module\b/.test(source);
3933
+ const directiveSrc = stripStringsKeepComments(source);
3934
+ const hasFnMockDirective = /@nudo:mock\s+\w+\s*(?:=|from\b)/.test(directiveSrc);
3935
+ const hasModMockDirective = /@nudo:mock-module\b/.test(directiveSrc);
3085
3936
  const hasModInject = inject && inject.modules && Object.keys(inject.modules).length > 0;
3086
3937
  const mockGated = hasFnMockDirective && !hasMocks || hasModMockDirective && !hasModInject;
3087
- const envGatedSource = /@nudo:env\b/.test(source) && !hasEnv;
3938
+ const envGatedSource = /@nudo:env\b/.test(directiveSrc) && !hasEnv;
3088
3939
  const boundNames = formals.flatMap(
3089
3940
  (f) => f.kind === "pattern" ? f.bound : [f.name]
3090
3941
  );
3091
3942
  const envGated = envGatedSource && freeIdentifiers(body, boundNames).size > 0;
3092
- const replaceGated = /@nudo:replace\b/.test(source) && !hasReps;
3943
+ const replaceGated = /@nudo:replace\b/.test(directiveSrc) && !hasReps;
3093
3944
  const evalEligible = !unresolvableImports && !mockGated && !envGated && !replaceGated;
3945
+ const fnBudget = extractFnBudget(source, fnName);
3946
+ const runCore = (args, phi) => {
3947
+ if (!evalEligible) return void 0;
3948
+ try {
3949
+ const evalRun = evalRunOf(source, opts.modules, opts.inject);
3950
+ if (!evalRun) {
3951
+ return void 0;
3952
+ } else if (fnName.includes(".")) {
3953
+ const [clsName, methodName] = fnName.split(".", 2);
3954
+ const clsAbs = evalRun[clsName ?? ""];
3955
+ if (clsAbs && typeof clsAbs === "object" && "shape" in clsAbs) {
3956
+ const nCtor = ctorParamCountOf(fileAst, clsName ?? "");
3957
+ const inst = $new(clsAbs, Array.from({ length: nCtor }, () => abs({ k: "any" }, void 0, pTrue, "path")));
3958
+ return withExecPhi(phi, () => $invoke(inst, methodName ?? "", args));
3959
+ }
3960
+ return void 0;
3961
+ } else if (fnName in evalRun) {
3962
+ const evalArgs = args.map((a, i) => {
3963
+ const shape2 = promoteScan.promotedShapes.get(params[i]);
3964
+ if (shape2 && (a.shape.k === "any" || a.shape.k === "unknown")) {
3965
+ return { shape: shape2, term: a.term, pred: a.pred, conf: "path" };
3966
+ }
3967
+ return a;
3968
+ });
3969
+ return callTranspiledExportFull(evalRun, fnName, evalArgs, { phi }).result;
3970
+ }
3971
+ return void 0;
3972
+ } catch {
3973
+ return void 0;
3974
+ }
3975
+ };
3094
3976
  const run = (args, phi = pTrue) => {
3095
3977
  const { key, varOrder } = instantiateMemoKey(args, phi);
3096
3978
  const hit = instMemo.get(key);
@@ -3098,30 +3980,14 @@ function generalizeFromAstUncached(fnName, source, opts = {}) {
3098
3980
  return alphaRenameResult(hit.result, hit.varOrder, varOrder);
3099
3981
  }
3100
3982
  let result;
3101
- if (evalEligible) {
3102
- try {
3103
- const evalRun = evalRunOf(source, opts.modules, opts.inject);
3104
- if (!evalRun) {
3105
- } else if (fnName.includes(".")) {
3106
- const [clsName, methodName] = fnName.split(".", 2);
3107
- const clsAbs = evalRun[clsName ?? ""];
3108
- if (clsAbs && typeof clsAbs === "object" && "shape" in clsAbs) {
3109
- const nCtor = ctorParamCountOf(fileAst, clsName ?? "");
3110
- const inst = $new(clsAbs, Array.from({ length: nCtor }, () => abs({ k: "any" }, void 0, pTrue, "path")));
3111
- result = withExecPhi(phi, () => $invoke(inst, methodName ?? "", args));
3112
- }
3113
- } else if (fnName in evalRun) {
3114
- const evalArgs = args.map((a, i) => {
3115
- const shape2 = promoteScan.promotedShapes.get(params[i]);
3116
- if (shape2 && (a.shape.k === "any" || a.shape.k === "unknown")) {
3117
- return { shape: shape2, term: a.term, pred: a.pred, conf: "path" };
3118
- }
3119
- return a;
3120
- });
3121
- result = callTranspiledExportFull(evalRun, fnName, evalArgs, { phi }).result;
3122
- }
3123
- } catch {
3124
- }
3983
+ if (fnBudget) {
3984
+ withFnBudgetOverride(fnBudget, () => {
3985
+ withEvalBudgetOverride(fnBudget, () => {
3986
+ result = runCore(args, phi);
3987
+ });
3988
+ });
3989
+ } else {
3990
+ result = runCore(args, phi);
3125
3991
  }
3126
3992
  if (result === void 0) {
3127
3993
  result = abs({ k: "unknown" }, void 0, void 0, "opaque");
@@ -3310,6 +4176,8 @@ function primMatches(prim, lv) {
3310
4176
  return typeof lv === "boolean";
3311
4177
  case "object":
3312
4178
  return lv === null;
4179
+ case "undefined":
4180
+ return lv === void 0;
3313
4181
  default:
3314
4182
  return false;
3315
4183
  }
@@ -3317,8 +4185,9 @@ function primMatches(prim, lv) {
3317
4185
  function predHolds(lv, p) {
3318
4186
  switch (p.op) {
3319
4187
  case "eq": {
3320
- const v2 = selfEqLiteral(p);
3321
- return v2 !== void 0 && lv === v2;
4188
+ const litT = isSelfVar(p.a) && p.b.op === "lit" ? p.b : isSelfVar(p.b) && p.a.op === "lit" ? p.a : void 0;
4189
+ if (!litT) return false;
4190
+ return Object.is(lv, litT.value);
3322
4191
  }
3323
4192
  case "gt":
3324
4193
  case "ge":
@@ -3336,11 +4205,6 @@ function predHolds(lv, p) {
3336
4205
  return false;
3337
4206
  }
3338
4207
  }
3339
- function selfEqLiteral(p) {
3340
- if (isSelfVar(p.a) && p.b.op === "lit") return p.b.value;
3341
- if (isSelfVar(p.b) && p.a.op === "lit") return p.a.value;
3342
- return void 0;
3343
- }
3344
4208
  function cmpHolds(x, op, n) {
3345
4209
  switch (op) {
3346
4210
  case "gt":
@@ -3629,7 +4493,8 @@ function extractLiteralEvidence(rec, idx) {
3629
4493
  const absArg = rec.argAbs?.[idx];
3630
4494
  if (!absArg) return void 0;
3631
4495
  if (absArg.conf !== "exact" && absArg.conf !== "path") return void 0;
3632
- const lv = litValue(absArg);
4496
+ const lvR = litValue(absArg);
4497
+ const lv = lvR.ok ? lvR.value : void 0;
3633
4498
  if (typeof lv === "number" || typeof lv === "string" || typeof lv === "boolean") {
3634
4499
  return lv;
3635
4500
  }
@@ -3689,6 +4554,7 @@ function resolveExportSource(modSrc, fnName, loadSpecFrom, depth = 0, baseFromFi
3689
4554
  }
3690
4555
  const file = parseSource(modSrc);
3691
4556
  let nextSpec;
4557
+ let nextFnName = fnName;
3692
4558
  for (const stmt of file.program.body) {
3693
4559
  if (stmt.type !== "ExportNamedDeclaration" && stmt.type !== "ExportAllDeclaration") continue;
3694
4560
  const src = stmt.source;
@@ -3701,8 +4567,11 @@ function resolveExportSource(modSrc, fnName, loadSpecFrom, depth = 0, baseFromFi
3701
4567
  for (const sp of clause) {
3702
4568
  if (sp.type !== "ExportSpecifier") continue;
3703
4569
  const local = sp.local;
3704
- if (local?.type === "Identifier" && local.name === fnName) {
4570
+ const exported = sp.exported ?? sp.local;
4571
+ const exportedName = exported?.type === "Identifier" ? exported.name : exported && "value" in exported ? String(exported.value ?? "") : void 0;
4572
+ if (exportedName === fnName) {
3705
4573
  nextSpec = String(src.value);
4574
+ nextFnName = local?.type === "Identifier" && local.name ? local.name : fnName;
3706
4575
  break;
3707
4576
  }
3708
4577
  }
@@ -3715,7 +4584,7 @@ function resolveExportSource(modSrc, fnName, loadSpecFrom, depth = 0, baseFromFi
3715
4584
  const nextFromFile = hopFrom ? resolveDepPath(hopFrom, nextSpec) : "";
3716
4585
  return resolveExportSource(
3717
4586
  next,
3718
- fnName,
4587
+ nextFnName,
3719
4588
  loadSpecFrom,
3720
4589
  depth + 1,
3721
4590
  baseFromFile,
@@ -4143,7 +5012,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4143
5012
  }
4144
5013
  arg = projected;
4145
5014
  }
4146
- const lv = litValue(arg);
5015
+ const lvR = litValue(arg);
5016
+ const lv = lvR.ok ? lvR.value : void 0;
4147
5017
  const isStr = arg.shape.k === "prim" && arg.shape.type === "string";
4148
5018
  const strLen = typeof lv === "string" ? lv.length : void 0;
4149
5019
  const argIsUndef = isExactUndef(arg);
@@ -4352,7 +5222,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4352
5222
  }
4353
5223
  }
4354
5224
  if (isIntFlag(constraint)) {
4355
- const iv = litValue(fieldAbs);
5225
+ const ivR = litValue(fieldAbs);
5226
+ const iv = ivR.ok ? ivR.value : void 0;
4356
5227
  if (typeof iv === "number" && !Number.isInteger(iv)) {
4357
5228
  out.push({
4358
5229
  severity: "error",
@@ -4394,7 +5265,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4394
5265
  });
4395
5266
  }
4396
5267
  }
4397
- const lv = litValue(fieldAbs);
5268
+ const lvR = litValue(fieldAbs);
5269
+ const lv = lvR.ok ? lvR.value : void 0;
4398
5270
  const sv = typeof lv === "string" ? lv.length : void 0;
4399
5271
  const isStr = fieldAbs.shape.k === "prim" && fieldAbs.shape.type === "string";
4400
5272
  for (const p of constraint.preds) {
@@ -4447,7 +5319,9 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4447
5319
  }
4448
5320
  }
4449
5321
  }
4450
- if (lv !== void 0 && (typeof lv === "number" || typeof lv === "string" || typeof lv === "boolean") && ((constraint.members?.length ?? 0) > 0 || constraint.preds.some((p) => p.op === "eq")) && !literalMeetsConstraint(lv, constraint)) {
5322
+ const fieldLit = fieldAbs.term?.op === "lit" ? fieldAbs.term.value : void 0;
5323
+ const fieldIsLit = fieldAbs.term?.op === "lit";
5324
+ if (fieldIsLit && (typeof fieldLit === "number" || typeof fieldLit === "string" || typeof fieldLit === "boolean" || fieldLit === null || fieldLit === void 0) && ((constraint.members?.length ?? 0) > 0 || constraint.preds.some((p) => p.op === "eq")) && !literalMeetsConstraint(fieldLit, constraint)) {
4451
5325
  out.push({
4452
5326
  severity: "error",
4453
5327
  code: "nudo:constraint-violated",
@@ -4531,7 +5405,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4531
5405
  }
4532
5406
  }
4533
5407
  if (isIntFlag(c)) {
4534
- const iv = litValue(arg);
5408
+ const ivR = litValue(arg);
5409
+ const iv = ivR.ok ? ivR.value : void 0;
4535
5410
  if (typeof iv === "number" && !Number.isInteger(iv)) {
4536
5411
  out.push({
4537
5412
  severity: "error",
@@ -4845,6 +5720,29 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4845
5720
  }
4846
5721
 
4847
5722
  export {
5723
+ listFnDirectiveScopes,
5724
+ findFnDirectiveScope,
5725
+ fnDirectiveCommentLines,
5726
+ parseEnvPayload,
5727
+ parseMockModulePayload,
5728
+ parseNudoImportPayload,
5729
+ extractFileEnvNames,
5730
+ extractMockModuleRecords,
5731
+ extractNudoImportRecords,
5732
+ extractBalancedParens,
5733
+ scanCaseTags,
5734
+ scanCaseArgSpans,
5735
+ scanContractSegments,
5736
+ scanThrowsDecl,
5737
+ scanBudgetDecl,
5738
+ hashSource,
5739
+ resetHashSourceCache,
5740
+ normPath,
5741
+ stablePathKey,
5742
+ stablePathKeyGraph,
5743
+ resolveDepPath,
5744
+ isNodeModulesPath,
5745
+ sidecarPathOf,
4848
5746
  SELF,
4849
5747
  getTerm,
4850
5748
  lenTerm,
@@ -4858,21 +5756,21 @@ export {
4858
5756
  shape,
4859
5757
  litC,
4860
5758
  union,
5759
+ nullable,
5760
+ constraintAdmitsNullish,
4861
5761
  fn,
4862
5762
  andC,
4863
5763
  partial,
4864
5764
  pick,
4865
5765
  omit,
5766
+ CONSTRAINT_BUILDERS,
5767
+ CONSTRAINT_BUILDER_NAMES,
5768
+ CONSTRAINT_EXPR_RE,
4866
5769
  instantiateConstraint,
5770
+ instantiateOnTerm,
4867
5771
  constraintToEntryAbs,
4868
5772
  fnConstraintToEntryReqs,
4869
5773
  throwConstraintToKinds,
4870
- hashSource,
4871
- resetHashSourceCache,
4872
- normPath,
4873
- resolveDepPath,
4874
- isNodeModulesPath,
4875
- sidecarPathOf,
4876
5774
  extractNudoImports,
4877
5775
  setRefineDiagCollector,
4878
5776
  refineDiagCount,
@@ -4885,12 +5783,14 @@ export {
4885
5783
  refineToIndexedFull,
4886
5784
  extractRefineReturnFromSource,
4887
5785
  extractDeclaredThrows,
5786
+ extractFnBudget,
4888
5787
  setInterfaceDiagCollector,
4889
5788
  interfaceDiagCount,
4890
5789
  takeInterfaceDiags,
4891
5790
  takeInterfaceDiagsSince,
4892
5791
  localNamedExports,
4893
5792
  generatedExportNames,
5793
+ resetSidecarLoadFailureCache,
4894
5794
  effectiveInterface,
4895
5795
  sidecarClosureFingerprint,
4896
5796
  formatConstraint,