rip-lang 3.15.4 → 3.16.1
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.
- package/README.md +6 -4
- package/bin/rip +167 -12
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +808 -0
- package/docs/RIP-DUCKDB.md +477 -0
- package/docs/RIP-INTRO.md +396 -0
- package/docs/RIP-LANG.md +59 -5
- package/docs/RIP-SCHEMA.md +191 -8
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +3627 -1470
- package/docs/dist/rip.min.js +671 -244
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/vscode/print/index.html +2 -1
- package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/ui/bundle.json +61 -0
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +0 -7
- package/docs/ui/index.css +66 -23
- package/docs/ui/index.html +6 -6
- package/package.json +9 -3
- package/rip-loader.js +64 -2
- package/src/AGENTS.md +63 -36
- package/src/browser.js +96 -14
- package/src/compiler.js +960 -143
- package/src/components.js +794 -88
- package/src/{types-emit.js → dts.js} +181 -71
- package/src/grammar/README.md +1 -1
- package/src/grammar/grammar.rip +111 -97
- package/src/lexer.js +132 -18
- package/src/parser.js +203 -205
- package/src/repl.js +74 -6
- package/src/schema/runtime-orm.js +168 -4
- package/src/schema/runtime-validate.js +146 -2
- package/src/schema/runtime.generated.js +314 -6
- package/src/schema/schema.js +5 -5
- package/src/sourcemaps.js +277 -1
- package/src/stdlib.js +253 -0
- package/src/typecheck.js +2023 -106
- package/src/types.js +127 -7
- package/docs/ui/accordion.rip +0 -103
- package/docs/ui/alert-dialog.rip +0 -53
- package/docs/ui/autocomplete.rip +0 -115
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -47
- package/docs/ui/button-group.rip +0 -26
- package/docs/ui/button.rip +0 -23
- package/docs/ui/card.rip +0 -25
- package/docs/ui/carousel.rip +0 -110
- package/docs/ui/checkbox-group.rip +0 -61
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -130
- package/docs/ui/context-menu.rip +0 -88
- package/docs/ui/date-picker.rip +0 -206
- package/docs/ui/dialog.rip +0 -60
- package/docs/ui/drawer.rip +0 -58
- package/docs/ui/editable-value.rip +0 -82
- package/docs/ui/field.rip +0 -53
- package/docs/ui/fieldset.rip +0 -22
- package/docs/ui/form.rip +0 -39
- package/docs/ui/grid.rip +0 -901
- package/docs/ui/input-group.rip +0 -28
- package/docs/ui/input.rip +0 -36
- package/docs/ui/label.rip +0 -16
- package/docs/ui/menu.rip +0 -134
- package/docs/ui/menubar.rip +0 -151
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -203
- package/docs/ui/native-select.rip +0 -33
- package/docs/ui/nav-menu.rip +0 -126
- package/docs/ui/number-field.rip +0 -162
- package/docs/ui/otp-field.rip +0 -89
- package/docs/ui/pagination.rip +0 -123
- package/docs/ui/popover.rip +0 -93
- package/docs/ui/preview-card.rip +0 -75
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -57
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -151
- package/docs/ui/separator.rip +0 -17
- package/docs/ui/skeleton.rip +0 -22
- package/docs/ui/slider.rip +0 -165
- package/docs/ui/spinner.rip +0 -17
- package/docs/ui/table.rip +0 -27
- package/docs/ui/tabs.rip +0 -113
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -71
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -38
- package/docs/ui/tooltip.rip +0 -85
- package/src/app.rip +0 -1571
- package/src/sourcemap-merge.js +0 -287
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
- /package/src/schema/{dts-emit.js → dts.js} +0 -0
package/src/compiler.js
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
import { Lexer } from './lexer.js';
|
|
12
12
|
import { parser } from './parser.js';
|
|
13
13
|
import { installComponentSupport } from './components.js';
|
|
14
|
-
// Type emission is CLI/editor-only.
|
|
15
|
-
// setTypesEmitter() at module load. The browser never imports
|
|
14
|
+
// Type emission is CLI/editor-only. dts.js registers itself via
|
|
15
|
+
// setTypesEmitter() at module load. The browser never imports dts.js,
|
|
16
16
|
// so _typesEmitter stays null and .d.ts output is silently skipped.
|
|
17
17
|
let _typesEmitter = null;
|
|
18
18
|
export function setTypesEmitter(fn) { _typesEmitter = fn; }
|
|
19
19
|
import { installSchemaSupport } from './schema/schema.js';
|
|
20
20
|
import { SourceMapGenerator } from './sourcemaps.js';
|
|
21
|
+
import { stringify, getStdlibCode } from './stdlib.js';
|
|
21
22
|
import { RipError, toRipError } from './error.js';
|
|
22
23
|
|
|
23
24
|
// =============================================================================
|
|
@@ -321,6 +322,20 @@ export class CodeEmitter {
|
|
|
321
322
|
// Each entry pairs a statement's generated code with its source loc.
|
|
322
323
|
// Output line positions are computed by exact arithmetic — no heuristics.
|
|
323
324
|
buildMappings() {
|
|
325
|
+
// Imports are emitted at the top of the file (before the preamble),
|
|
326
|
+
// starting at line 0. Process them first with their own line offset.
|
|
327
|
+
if (this._importEntries) {
|
|
328
|
+
let importLineOffset = 0;
|
|
329
|
+
for (let entry of this._importEntries) {
|
|
330
|
+
if (entry.loc) {
|
|
331
|
+
this.sourceMap.addMapping(importLineOffset, 0, entry.loc.r, entry.loc.c);
|
|
332
|
+
}
|
|
333
|
+
if (entry.sexpr && entry.loc) {
|
|
334
|
+
this.recordSubMappings(entry.code, entry.sexpr, importLineOffset);
|
|
335
|
+
}
|
|
336
|
+
importLineOffset += entry.code.split('\n').length;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
324
339
|
if (!this._stmtEntries) return;
|
|
325
340
|
let lineOffset = this._preambleLines;
|
|
326
341
|
for (let entry of this._stmtEntries) {
|
|
@@ -340,11 +355,30 @@ export class CodeEmitter {
|
|
|
340
355
|
// matches (e.g. identifiers appearing as values inside union type strings).
|
|
341
356
|
static _isColInsideString(line, col) {
|
|
342
357
|
let inStr = false, quote = '';
|
|
358
|
+
// When inside a template literal (`...`), `${...}` opens an interpolation
|
|
359
|
+
// expression that is NOT part of the string. Track interp depth so
|
|
360
|
+
// identifiers inside `${expr}` aren't incorrectly classified as strings.
|
|
361
|
+
let interpDepth = 0;
|
|
343
362
|
for (let i = 0; i < line.length && i < col; i++) {
|
|
344
363
|
let ch = line[i];
|
|
345
364
|
if (inStr) {
|
|
346
365
|
if (ch === '\\') { i++; continue; }
|
|
366
|
+
if (quote === '`' && ch === '$' && line[i + 1] === '{') {
|
|
367
|
+
interpDepth = 1;
|
|
368
|
+
inStr = false;
|
|
369
|
+
i++; // skip `{`
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
347
372
|
if (ch === quote) inStr = false;
|
|
373
|
+
} else if (interpDepth > 0) {
|
|
374
|
+
if (ch === '{') interpDepth++;
|
|
375
|
+
else if (ch === '}') {
|
|
376
|
+
interpDepth--;
|
|
377
|
+
if (interpDepth === 0) { inStr = true; quote = '`'; }
|
|
378
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
379
|
+
// Nested string inside interpolation — recurse via inStr handling
|
|
380
|
+
inStr = true; quote = ch;
|
|
381
|
+
}
|
|
348
382
|
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
349
383
|
inStr = true; quote = ch;
|
|
350
384
|
}
|
|
@@ -385,26 +419,106 @@ export class CodeEmitter {
|
|
|
385
419
|
}
|
|
386
420
|
return hi;
|
|
387
421
|
};
|
|
422
|
+
// Track generated positions already claimed by an earlier sub-expression
|
|
423
|
+
// in this statement, so distinct source positions (e.g. `a` in two
|
|
424
|
+
// adjacent arrow functions) can't all collapse onto the first match.
|
|
425
|
+
// Without this, identical identifiers in repeated structures get
|
|
426
|
+
// mis-mapped — e.g. `else if` branches inheriting the `if` branch's
|
|
427
|
+
// generated coordinates, leaving the real branch unmapped.
|
|
428
|
+
const usedGenPositions = new Set();
|
|
429
|
+
// Cache `// @rip-src:N` annotations per generated line so render-block
|
|
430
|
+
// stub mappings can be honored over unrelated heuristic matches.
|
|
431
|
+
const ripSrcCache = new Map();
|
|
432
|
+
const getRipSrcAnnot = (genLineInStmt) => {
|
|
433
|
+
if (ripSrcCache.has(genLineInStmt)) return ripSrcCache.get(genLineInStmt);
|
|
434
|
+
const lt = codeLines[genLineInStmt];
|
|
435
|
+
const m = lt && lt.match(/\/\/ @rip-src:(\d+)\s*$/);
|
|
436
|
+
const v = m ? parseInt(m[1], 10) : null;
|
|
437
|
+
ripSrcCache.set(genLineInStmt, v);
|
|
438
|
+
return v;
|
|
439
|
+
};
|
|
440
|
+
// Inline type annotations (emitted when `inlineTypes: true`) inject
|
|
441
|
+
// identifiers into function-signature lines that have no source
|
|
442
|
+
// counterpart — e.g. `header(name, value, opts: { append?: boolean })`.
|
|
443
|
+
// Their identifiers (`append`, `boolean`, etc.) would otherwise compete
|
|
444
|
+
// with real body identifiers in the regex matcher below, mis-mapping
|
|
445
|
+
// source positions onto the type literal. Detect those brace ranges
|
|
446
|
+
// per line and skip matches inside them.
|
|
447
|
+
const inlineTypeRangesCache = new Map();
|
|
448
|
+
const getInlineTypeRanges = (genLineInStmt) => {
|
|
449
|
+
if (inlineTypeRangesCache.has(genLineInStmt)) return inlineTypeRangesCache.get(genLineInStmt);
|
|
450
|
+
const lt = codeLines[genLineInStmt];
|
|
451
|
+
const ranges = [];
|
|
452
|
+
// Only look at function-signature shaped lines: contain `(...) {` or `(...) =>`.
|
|
453
|
+
if (lt && /\)\s*(\{|=>)\s*$/.test(lt)) {
|
|
454
|
+
// Find `: {` after an identifier (with optional `?` and whitespace).
|
|
455
|
+
const annotRe = /\b[a-zA-Z_$][\w$]*\??\s*:\s*\{/g;
|
|
456
|
+
let am;
|
|
457
|
+
while ((am = annotRe.exec(lt)) !== null) {
|
|
458
|
+
const braceStart = am.index + am[0].length - 1; // position of `{`
|
|
459
|
+
// Skip inside strings (defensive — unlikely here)
|
|
460
|
+
if (CodeEmitter._isColInsideString(lt, braceStart)) continue;
|
|
461
|
+
// Walk to matching `}` honoring brace depth and strings
|
|
462
|
+
let depth = 1, j = braceStart + 1, inStr = false, quote = '';
|
|
463
|
+
while (j < lt.length && depth > 0) {
|
|
464
|
+
const ch = lt[j];
|
|
465
|
+
if (inStr) {
|
|
466
|
+
if (ch === '\\') { j += 2; continue; }
|
|
467
|
+
if (ch === quote) inStr = false;
|
|
468
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
469
|
+
inStr = true; quote = ch;
|
|
470
|
+
} else if (ch === '{') depth++;
|
|
471
|
+
else if (ch === '}') depth--;
|
|
472
|
+
j++;
|
|
473
|
+
}
|
|
474
|
+
ranges.push([braceStart, j]); // exclude positions [braceStart, j)
|
|
475
|
+
annotRe.lastIndex = j;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
inlineTypeRangesCache.set(genLineInStmt, ranges);
|
|
479
|
+
return ranges;
|
|
480
|
+
};
|
|
388
481
|
for (let { name, origLine, origCol } of subs) {
|
|
389
482
|
let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
390
483
|
let re = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
391
|
-
let m
|
|
484
|
+
let m;
|
|
485
|
+
let bestMatch = null, bestDist = Infinity;
|
|
486
|
+
let bestUnused = null, bestUnusedDist = Infinity;
|
|
392
487
|
let origLineInStmt = origLine - stmtOrigLine;
|
|
393
488
|
while ((m = re.exec(code)) !== null) {
|
|
394
489
|
const genLineInStmt = offsetToLine(m.index);
|
|
395
490
|
const genCol = m.index - lineStarts[genLineInStmt];
|
|
396
|
-
// Skip matches inside string literals — prevents false mappings when
|
|
397
|
-
// an identifier also appears as a string value (e.g. union type member)
|
|
398
491
|
let lineText = codeLines[genLineInStmt];
|
|
399
|
-
|
|
492
|
+
// A `// @rip-src:N` annotation tags the generated line as derived
|
|
493
|
+
// from source line N (used by render-block stubs). When N matches
|
|
494
|
+
// the sub-expression's origLine, accept matches here even if they
|
|
495
|
+
// fall inside string literals — render stubs emit tag names as
|
|
496
|
+
// `'p'` and interpolations as `${...}` inside template strings.
|
|
497
|
+
const annotSrc = getRipSrcAnnot(genLineInStmt);
|
|
498
|
+
const annotMatches = annotSrc != null && annotSrc === origLine;
|
|
499
|
+
if (lineText && CodeEmitter._isColInsideString(lineText, genCol) && !annotMatches) continue;
|
|
500
|
+
// Skip matches inside inline type annotation brace ranges (e.g.
|
|
501
|
+
// `opts: { append?: boolean }`) — those identifiers are emitted
|
|
502
|
+
// for TS type-checking only and have no real source counterpart.
|
|
503
|
+
const itRanges = getInlineTypeRanges(genLineInStmt);
|
|
504
|
+
if (itRanges.length && itRanges.some(([s, e]) => genCol >= s && genCol < e)) continue;
|
|
400
505
|
let genLine = lineOffset + genLineInStmt;
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
506
|
+
// Annotation-matched lines are the authoritative gen position for
|
|
507
|
+
// their source line — score them as a perfect line match so they
|
|
508
|
+
// beat any heuristic match elsewhere in the statement.
|
|
509
|
+
let dist = annotMatches
|
|
510
|
+
? Math.abs(genCol - origCol)
|
|
511
|
+
: Math.abs(genLineInStmt - origLineInStmt) * 10000 + Math.abs(genCol - origCol);
|
|
404
512
|
if (dist < bestDist) { bestDist = dist; bestMatch = { genLine, genCol }; }
|
|
513
|
+
const key = genLine + ':' + genCol;
|
|
514
|
+
if (!usedGenPositions.has(key) && dist < bestUnusedDist) {
|
|
515
|
+
bestUnusedDist = dist; bestUnused = { genLine, genCol };
|
|
516
|
+
}
|
|
405
517
|
}
|
|
406
|
-
|
|
407
|
-
|
|
518
|
+
const chosen = bestUnused || bestMatch;
|
|
519
|
+
if (chosen) {
|
|
520
|
+
this.sourceMap.addMapping(chosen.genLine, chosen.genCol, origLine, origCol);
|
|
521
|
+
usedGenPositions.add(chosen.genLine + ':' + chosen.genCol);
|
|
408
522
|
}
|
|
409
523
|
}
|
|
410
524
|
}
|
|
@@ -419,25 +533,73 @@ export class CodeEmitter {
|
|
|
419
533
|
for (let i = 0; i < node.length; i++) {
|
|
420
534
|
if (Array.isArray(node[i])) this.collectSubExprs(node[i], result);
|
|
421
535
|
}
|
|
536
|
+
// Also honor side-channel anchors attached to nodes whose head is an
|
|
537
|
+
// array (e.g. tag-shorthand `[(. p error), error]` where the tag
|
|
538
|
+
// node's head is itself an access expression). Without this, anchors
|
|
539
|
+
// attached by walkRender to such nodes are silently dropped.
|
|
540
|
+
if (Array.isArray(node._anchors)) {
|
|
541
|
+
for (const a of node._anchors) result.push(a);
|
|
542
|
+
}
|
|
422
543
|
return;
|
|
423
544
|
}
|
|
424
545
|
if (node.loc) {
|
|
425
546
|
head = str(head);
|
|
426
547
|
let ident = null;
|
|
548
|
+
let identCol = node.loc.c;
|
|
427
549
|
// Property access: anchor is the property name (check BEFORE operators
|
|
428
550
|
// because the operator regex also matches '.' via ^\.\.?$)
|
|
429
551
|
if (head === '.') {
|
|
430
|
-
if (typeof node[2] === 'string')
|
|
552
|
+
if (typeof node[2] === 'string') {
|
|
553
|
+
ident = node[2];
|
|
554
|
+
// Adjust origCol to point at the property name itself, not the start
|
|
555
|
+
// of the access expression. node.loc.c marks the object start;
|
|
556
|
+
// shift past it and the `.` so source-mapping anchors land on the
|
|
557
|
+
// property identifier (e.g. `image` in `product.image`).
|
|
558
|
+
if (typeof node[1] === 'string') {
|
|
559
|
+
identCol = node.loc.c + node[1].length + 1;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
431
562
|
}
|
|
432
563
|
// Operators/keywords: anchor is the subject at index 1
|
|
433
|
-
else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]
|
|
434
|
-
if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1]))
|
|
564
|
+
else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]|^\.{1,3}$|^def$|^class$|^state$|^computed$|^readonly$|^for-/.test(head)) {
|
|
565
|
+
if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1])) {
|
|
566
|
+
ident = node[1];
|
|
567
|
+
// Spread `...x`: node.loc.c marks the `...` start; shift past the
|
|
568
|
+
// operator so the anchor lands on the operand identifier.
|
|
569
|
+
if (head === '...') identCol = node.loc.c + 3;
|
|
570
|
+
}
|
|
435
571
|
}
|
|
436
572
|
// Function call (head is identifier)
|
|
437
573
|
else if (typeof head === 'string' && /^[a-zA-Z_$]/.test(head)) {
|
|
438
574
|
ident = head;
|
|
439
575
|
}
|
|
440
|
-
if (ident) result.push({ name: ident, origLine: node.loc.r, origCol:
|
|
576
|
+
if (ident) result.push({ name: ident, origLine: node.loc.r, origCol: identCol });
|
|
577
|
+
|
|
578
|
+
// Arrow body bare-identifier anchor: a single-expression arrow body
|
|
579
|
+
// like `-> products` parses as `['->', [], ['block', 'products']]`.
|
|
580
|
+
// The body atom has no .loc (parser only attaches loc to arrays), and
|
|
581
|
+
// the `block` wrapper has bogus `loc=0:0`, so the identifier reference
|
|
582
|
+
// is invisible to the heuristic mapping. Synthesize an anchor by
|
|
583
|
+
// scanning source forward from the arrow's location.
|
|
584
|
+
if ((head === '->' || head === '=>') && Array.isArray(node[2]) && str(node[2][0]) === 'block') {
|
|
585
|
+
const body = node[2];
|
|
586
|
+
for (let i = 1; i < body.length; i++) {
|
|
587
|
+
const leaf = body[i];
|
|
588
|
+
const leafStr = typeof leaf === 'string' || leaf instanceof String ? str(leaf) : null;
|
|
589
|
+
if (leafStr && /^[a-zA-Z_$][\w$]*$/.test(leafStr) && !leaf.loc) {
|
|
590
|
+
const anchor = this._scanForIdentAfter(leafStr, node.loc);
|
|
591
|
+
if (anchor) result.push(anchor);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Side-channel anchors attached by walkRender for bare-identifier
|
|
597
|
+
// children of template-tag nodes (e.g. `error` in `p.error error`).
|
|
598
|
+
// The s-expression child is a plain string atom with no .loc, so the
|
|
599
|
+
// walk would otherwise miss it; the anchor carries the source position
|
|
600
|
+
// computed from the parent's loc plus a regex scan over source lines.
|
|
601
|
+
if (Array.isArray(node._anchors)) {
|
|
602
|
+
for (const a of node._anchors) result.push(a);
|
|
441
603
|
}
|
|
442
604
|
// Recurse into children (skip head at index 0 — already processed via parent).
|
|
443
605
|
// For arrow functions (-> / =>), skip index 1 (params array) — parameter
|
|
@@ -449,6 +611,30 @@ export class CodeEmitter {
|
|
|
449
611
|
}
|
|
450
612
|
}
|
|
451
613
|
|
|
614
|
+
// Scan original source for the first occurrence of `ident` after the
|
|
615
|
+
// given start location (typically an arrow's `->` loc). Skips matches
|
|
616
|
+
// inside string/comment regions. Returns a sub-expression anchor or null.
|
|
617
|
+
_scanForIdentAfter(ident, startLoc) {
|
|
618
|
+
const source = this.options && this.options.source;
|
|
619
|
+
if (!source || !startLoc) return null;
|
|
620
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
621
|
+
const re = new RegExp('\\b' + ident.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
|
|
622
|
+
const startRow = startLoc.r;
|
|
623
|
+
const startCol = startLoc.c;
|
|
624
|
+
// Search current line from startCol forward, then up to 20 subsequent lines.
|
|
625
|
+
for (let r = startRow; r < Math.min(lines.length, startRow + 20); r++) {
|
|
626
|
+
const line = lines[r];
|
|
627
|
+
if (!line) continue;
|
|
628
|
+
re.lastIndex = r === startRow ? startCol : 0;
|
|
629
|
+
let m;
|
|
630
|
+
while ((m = re.exec(line)) !== null) {
|
|
631
|
+
if (CodeEmitter._isColInsideString(line, m.index)) continue;
|
|
632
|
+
return { name: ident, origLine: r, origCol: m.index };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
452
638
|
// ---------------------------------------------------------------------------
|
|
453
639
|
// Variable collection
|
|
454
640
|
// ---------------------------------------------------------------------------
|
|
@@ -498,7 +684,12 @@ export class CodeEmitter {
|
|
|
498
684
|
}
|
|
499
685
|
|
|
500
686
|
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
501
|
-
|
|
687
|
+
// Don't hoist loop vars: emitForIn/emitForOf/emitForAs already
|
|
688
|
+
// emit `for (let x of ...)`, which is block-scoped AND gives JS's
|
|
689
|
+
// per-iteration binding semantics (critical for closures captured
|
|
690
|
+
// inside the loop). Hoisting `let x` to the surrounding scope
|
|
691
|
+
// would shadow that as dead code (TS6133 "declared but never
|
|
692
|
+
// read") without changing runtime behavior.
|
|
502
693
|
rest.slice(1).forEach(item => this.collectProgramVariables(item));
|
|
503
694
|
return;
|
|
504
695
|
}
|
|
@@ -549,14 +740,22 @@ export class CodeEmitter {
|
|
|
549
740
|
if (Array.isArray(head)) { sexpr.forEach(item => collect(item)); return; }
|
|
550
741
|
if (CodeEmitter.ASSIGNMENT_OPS.has(head)) {
|
|
551
742
|
let [target, value] = rest;
|
|
552
|
-
|
|
743
|
+
// Match collectProgramVariables: identifier targets may arrive as
|
|
744
|
+
// String wrappers when types.js attaches `.data.type` metadata
|
|
745
|
+
// to a typed local. Without unwrapping the wrapper, typed locals
|
|
746
|
+
// skip the function-top `let` declaration and silently leak to
|
|
747
|
+
// the global scope.
|
|
748
|
+
if (typeof target === 'string' || target instanceof String) vars.add(str(target));
|
|
553
749
|
else if (this.is(target, 'array')) this.collectVarsFromArray(target, vars);
|
|
554
750
|
else if (this.is(target, 'object')) this.collectVarsFromObject(target, vars);
|
|
555
751
|
collect(value);
|
|
556
752
|
return;
|
|
557
753
|
}
|
|
558
754
|
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
559
|
-
|
|
755
|
+
// See collectProgramVariables for-loop branch: loop vars are
|
|
756
|
+
// block-scoped via the for-header's `let`; hoisting them here
|
|
757
|
+
// would only produce a redundant outer `let x` flagged as
|
|
758
|
+
// unused.
|
|
560
759
|
rest.slice(1).forEach(collect);
|
|
561
760
|
return;
|
|
562
761
|
}
|
|
@@ -584,20 +783,55 @@ export class CodeEmitter {
|
|
|
584
783
|
return vars;
|
|
585
784
|
}
|
|
586
785
|
|
|
786
|
+
// Walk a function body and collect typed local assignments. Returns a
|
|
787
|
+
// Map<name, typeString> for every `name:: T = value` whose target is a
|
|
788
|
+
// String-wrapped identifier carrying `data.type` (attached by types.js).
|
|
789
|
+
//
|
|
790
|
+
// Used by `emitBodyWithReturns` in `inlineTypes` mode to annotate the
|
|
791
|
+
// function-top hoist (`let a, y: boolean, b;`) so shadow-TS sees the
|
|
792
|
+
// intended type instead of inferring a literal from the first RHS. Stops
|
|
793
|
+
// at nested function boundaries — each function owns its own typed locals.
|
|
794
|
+
//
|
|
795
|
+
// Conflict policy: first annotation wins. Same-name re-annotations are
|
|
796
|
+
// silently ignored; mixing different types on the same local is an
|
|
797
|
+
// unusual pattern best surfaced by TS itself once the hoist carries the
|
|
798
|
+
// first annotation.
|
|
799
|
+
collectTypedLocals(body) {
|
|
800
|
+
let typed = new Map();
|
|
801
|
+
let walk = (sexpr) => {
|
|
802
|
+
if (!Array.isArray(sexpr)) return;
|
|
803
|
+
let [head, ...rest] = sexpr;
|
|
804
|
+
head = str(head);
|
|
805
|
+
if (Array.isArray(head)) { sexpr.forEach(walk); return; }
|
|
806
|
+
if (CodeEmitter.ASSIGNMENT_OPS.has(head)) {
|
|
807
|
+
let [target, value] = rest;
|
|
808
|
+
if (target instanceof String && target.type && !typed.has(str(target))) {
|
|
809
|
+
typed.set(str(target), target.type);
|
|
810
|
+
}
|
|
811
|
+
walk(value);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (head === 'def' || head === '->' || head === '=>' || head === 'effect') return;
|
|
815
|
+
rest.forEach(walk);
|
|
816
|
+
};
|
|
817
|
+
walk(body);
|
|
818
|
+
return typed;
|
|
819
|
+
}
|
|
820
|
+
|
|
587
821
|
// ---------------------------------------------------------------------------
|
|
588
822
|
// Main dispatch
|
|
589
823
|
// ---------------------------------------------------------------------------
|
|
590
824
|
|
|
591
825
|
emit(sexpr, context = 'statement') {
|
|
592
|
-
// String object with metadata (quote,
|
|
826
|
+
// String object with metadata (quote, bang, optional, heregex, etc.)
|
|
593
827
|
if (sexpr instanceof String) {
|
|
594
828
|
// Dammit operator (!)
|
|
595
|
-
if (meta(sexpr, '
|
|
829
|
+
if (meta(sexpr, 'bang') === true) {
|
|
596
830
|
return `await ${str(sexpr)}()`;
|
|
597
831
|
}
|
|
598
832
|
|
|
599
833
|
// Existence check (?)
|
|
600
|
-
if (meta(sexpr, '
|
|
834
|
+
if (meta(sexpr, 'optional')) {
|
|
601
835
|
return `(${str(sexpr)} != null)`;
|
|
602
836
|
}
|
|
603
837
|
|
|
@@ -653,8 +887,8 @@ export class CodeEmitter {
|
|
|
653
887
|
|
|
654
888
|
let [head, ...rest] = sexpr;
|
|
655
889
|
|
|
656
|
-
// Preserve
|
|
657
|
-
let
|
|
890
|
+
// Preserve bang metadata before converting head to primitive
|
|
891
|
+
let headBangMeta = meta(head, 'bang');
|
|
658
892
|
head = str(head);
|
|
659
893
|
|
|
660
894
|
// Dispatch table
|
|
@@ -674,7 +908,7 @@ export class CodeEmitter {
|
|
|
674
908
|
let postfix = this._tryPostfixCall(head, rest, context);
|
|
675
909
|
if (postfix) return postfix;
|
|
676
910
|
|
|
677
|
-
let needsAwait =
|
|
911
|
+
let needsAwait = headBangMeta === true;
|
|
678
912
|
let callStr = `${this.emit(head, 'value')}(${this._emitArgs(rest)})`;
|
|
679
913
|
return needsAwait ? `await ${callStr}` : callStr;
|
|
680
914
|
}
|
|
@@ -700,15 +934,16 @@ export class CodeEmitter {
|
|
|
700
934
|
let postfix = this._tryPostfixCall(head, rest, context);
|
|
701
935
|
if (postfix) return postfix;
|
|
702
936
|
|
|
703
|
-
// Property access with
|
|
937
|
+
// Property access with bang sigil on property
|
|
704
938
|
let needsAwait = false;
|
|
705
939
|
let calleeCode;
|
|
706
|
-
if (head[0] === '.' && meta(head[2], '
|
|
940
|
+
if (head[0] === '.' && meta(head[2], 'bang') === true) {
|
|
707
941
|
needsAwait = true;
|
|
708
942
|
let [obj, prop] = head.slice(1);
|
|
709
943
|
let objCode = this.emit(obj, 'value');
|
|
710
944
|
let needsParens = CodeEmitter.NUMBER_LITERAL_RE.test(objCode) ||
|
|
711
|
-
((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield')))
|
|
945
|
+
((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield'))) ||
|
|
946
|
+
/^(await|yield)\s/.test(objCode);
|
|
712
947
|
let base = needsParens ? `(${objCode})` : objCode;
|
|
713
948
|
calleeCode = `${base}.${str(prop)}`;
|
|
714
949
|
} else {
|
|
@@ -766,7 +1001,12 @@ export class CodeEmitter {
|
|
|
766
1001
|
let needsBlank = false;
|
|
767
1002
|
|
|
768
1003
|
if (imports.length > 0) {
|
|
769
|
-
|
|
1004
|
+
let importEntries = imports.map(s => {
|
|
1005
|
+
let generated = this.addSemicolon(s, this.emit(s, 'statement'));
|
|
1006
|
+
return { code: generated, loc: Array.isArray(s) ? s.loc : null, sexpr: Array.isArray(s) ? s : null };
|
|
1007
|
+
});
|
|
1008
|
+
this._importEntries = importEntries;
|
|
1009
|
+
code += importEntries.map(e => e.code).join('\n');
|
|
770
1010
|
needsBlank = true;
|
|
771
1011
|
}
|
|
772
1012
|
|
|
@@ -936,6 +1176,15 @@ export class CodeEmitter {
|
|
|
936
1176
|
let [target, value] = rest;
|
|
937
1177
|
let op = head === '?=' ? '??=' : head;
|
|
938
1178
|
|
|
1179
|
+
// Reject destructuring shapes that aren't valid binding patterns. The
|
|
1180
|
+
// grammar accepts the full Array / Object expression on the LHS of `=`,
|
|
1181
|
+
// so things like `[a + b] = src` and `{x: y for x in arr} = src` parse
|
|
1182
|
+
// and used to either silently produce broken JS or crash the compiler
|
|
1183
|
+
// with an obscure error. This walk catches them with a clear message.
|
|
1184
|
+
if (this.is(target, 'array') || this.is(target, 'object')) {
|
|
1185
|
+
this._validateBindingPattern(target, sexpr);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
939
1188
|
// Optional chain assignment: x?.prop = val → if (x != null) x.prop = val
|
|
940
1189
|
let optInfo = this._findOptionalInTarget(target);
|
|
941
1190
|
if (optInfo) {
|
|
@@ -949,15 +1198,7 @@ export class CodeEmitter {
|
|
|
949
1198
|
}
|
|
950
1199
|
|
|
951
1200
|
// Validate: no sigils in assignment targets (except void function syntax)
|
|
952
|
-
|
|
953
|
-
if (target instanceof String && meta(target, 'await') !== undefined && !isFnValue) {
|
|
954
|
-
let sigil = meta(target, 'await') === true ? '!' : '&';
|
|
955
|
-
this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (target instanceof String && meta(target, 'await') === true && isFnValue) {
|
|
959
|
-
this.nextFunctionIsVoid = true;
|
|
960
|
-
}
|
|
1201
|
+
this.applyVoidMarker(target, value, sexpr);
|
|
961
1202
|
|
|
962
1203
|
// Empty destructuring — just evaluate RHS
|
|
963
1204
|
let isEmptyArr = this.is(target, 'array', 0);
|
|
@@ -1046,7 +1287,7 @@ export class CodeEmitter {
|
|
|
1046
1287
|
|
|
1047
1288
|
// Generate target (handle reactive, sigils)
|
|
1048
1289
|
let targetCode;
|
|
1049
|
-
if (target instanceof String && meta(target, '
|
|
1290
|
+
if (target instanceof String && meta(target, 'bang') !== undefined) {
|
|
1050
1291
|
targetCode = str(target);
|
|
1051
1292
|
} else if (typeof target === 'string' && this.reactiveVars?.has(target)) {
|
|
1052
1293
|
targetCode = `${target}.value`;
|
|
@@ -1093,8 +1334,8 @@ export class CodeEmitter {
|
|
|
1093
1334
|
objCode.startsWith('await ') ||
|
|
1094
1335
|
((this.is(obj, 'object') || this.is(obj, 'yield')));
|
|
1095
1336
|
let base = needsParens ? `(${objCode})` : objCode;
|
|
1096
|
-
if (meta(prop, '
|
|
1097
|
-
if (meta(prop, '
|
|
1337
|
+
if (meta(prop, 'bang') === true) return `await ${base}.${str(prop)}()`;
|
|
1338
|
+
if (meta(prop, 'optional')) return `(${base}.${str(prop)} != null)`;
|
|
1098
1339
|
return `${base}.${str(prop)}`;
|
|
1099
1340
|
}
|
|
1100
1341
|
|
|
@@ -1260,7 +1501,7 @@ export class CodeEmitter {
|
|
|
1260
1501
|
|
|
1261
1502
|
emitDef(head, rest, context, sexpr) {
|
|
1262
1503
|
let [name, params, body] = rest;
|
|
1263
|
-
let sideEffectOnly = meta(name, '
|
|
1504
|
+
let sideEffectOnly = meta(name, 'bang') === true;
|
|
1264
1505
|
let cleanName = str(name);
|
|
1265
1506
|
let paramList = this.emitParamList(params);
|
|
1266
1507
|
let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
|
|
@@ -1272,8 +1513,7 @@ export class CodeEmitter {
|
|
|
1272
1513
|
emitThinArrow(head, rest, context, sexpr) {
|
|
1273
1514
|
let [params, body] = rest;
|
|
1274
1515
|
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
|
|
1275
|
-
let sideEffectOnly =
|
|
1276
|
-
this.nextFunctionIsVoid = false;
|
|
1516
|
+
let sideEffectOnly = sexpr.isVoid || false;
|
|
1277
1517
|
let paramList = this.emitParamList(params);
|
|
1278
1518
|
let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
|
|
1279
1519
|
let isAsync = this.containsAwait(body);
|
|
@@ -1285,8 +1525,7 @@ export class CodeEmitter {
|
|
|
1285
1525
|
emitFatArrow(head, rest, context, sexpr) {
|
|
1286
1526
|
let [params, body] = rest;
|
|
1287
1527
|
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
|
|
1288
|
-
let sideEffectOnly =
|
|
1289
|
-
this.nextFunctionIsVoid = false;
|
|
1528
|
+
let sideEffectOnly = sexpr.isVoid || false;
|
|
1290
1529
|
let paramList = this.emitParamList(params);
|
|
1291
1530
|
let isSingle = params.length === 1 && typeof params[0] === 'string' &&
|
|
1292
1531
|
!paramList.includes('=') && !paramList.includes('...') &&
|
|
@@ -1405,9 +1644,13 @@ export class CodeEmitter {
|
|
|
1405
1644
|
let [cond, then_, else_] = rest;
|
|
1406
1645
|
|
|
1407
1646
|
// Hoist assignment: (cond ? (x = a) : b) → x = (cond ? a : b)
|
|
1408
|
-
// Enables
|
|
1647
|
+
// Enables the Python-style postfix-ternary idiom:
|
|
1648
|
+
// x = "admin" if cond else "member" → x = (cond ? "admin" : "member")
|
|
1649
|
+
// Skip when the assignment is parenthesized — the parser tags those via
|
|
1650
|
+
// `.parenthesized = true` so we can preserve "only assign when cond"
|
|
1651
|
+
// semantics for the explicit form `(x = "a") if cond else "b"`.
|
|
1409
1652
|
let thenHead = then_?.[0]?.valueOf?.() ?? then_?.[0];
|
|
1410
|
-
if (thenHead === '=' && Array.isArray(then_)) {
|
|
1653
|
+
if (thenHead === '=' && Array.isArray(then_) && !then_.parenthesized) {
|
|
1411
1654
|
let target = this.emit(then_[1], 'value');
|
|
1412
1655
|
let thenVal = this.emit(then_[2], 'value');
|
|
1413
1656
|
let elseVal = this.emit(else_, 'value');
|
|
@@ -1799,7 +2042,20 @@ export class CodeEmitter {
|
|
|
1799
2042
|
// Symbol literals
|
|
1800
2043
|
// ---------------------------------------------------------------------------
|
|
1801
2044
|
|
|
1802
|
-
emitSymbol(head, rest
|
|
2045
|
+
emitSymbol(head, rest, context, sexpr) {
|
|
2046
|
+
// Anchor the symbol name's source position to the generated `Symbol`
|
|
2047
|
+
// identifier so hover on `:foo` shows `SymbolConstructor` (the
|
|
2048
|
+
// generated `"foo"` is a string literal that TS has no hover for).
|
|
2049
|
+
// sexpr.loc points at the `:`; the name starts at col + 1.
|
|
2050
|
+
if (sexpr && sexpr.loc && typeof rest[0] === 'string') {
|
|
2051
|
+
sexpr._anchors = (sexpr._anchors || []).concat([{
|
|
2052
|
+
name: 'Symbol',
|
|
2053
|
+
origLine: sexpr.loc.r,
|
|
2054
|
+
origCol: sexpr.loc.c + 1,
|
|
2055
|
+
}]);
|
|
2056
|
+
}
|
|
2057
|
+
return `Symbol.for(${JSON.stringify(rest[0])})`;
|
|
2058
|
+
}
|
|
1803
2059
|
|
|
1804
2060
|
// ---------------------------------------------------------------------------
|
|
1805
2061
|
// Data structures
|
|
@@ -1824,24 +2080,180 @@ export class CodeEmitter {
|
|
|
1824
2080
|
return this.emit(['object-comprehension', keyVar, valueExpr, iterators, guards], context);
|
|
1825
2081
|
}
|
|
1826
2082
|
|
|
1827
|
-
|
|
2083
|
+
// Helper: scan source line for an identifier name within [fromCol, toCol).
|
|
2084
|
+
// Used to attach _anchors so identifiers that the parser dropped position
|
|
2085
|
+
// info from (method-shorthand params, property shorthand keys) can still
|
|
2086
|
+
// produce source-map mappings.
|
|
2087
|
+
const scanIdentCol = (srcRow, name, fromCol = 0, toCol = Infinity) => {
|
|
2088
|
+
const source = this.options && this.options.source;
|
|
2089
|
+
if (!source || typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) return -1;
|
|
2090
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
2091
|
+
const line = lines[srcRow];
|
|
2092
|
+
if (!line) return -1;
|
|
2093
|
+
const re = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
|
|
2094
|
+
re.lastIndex = Math.max(0, fromCol);
|
|
2095
|
+
let m;
|
|
2096
|
+
while ((m = re.exec(line)) !== null) {
|
|
2097
|
+
if (m.index >= toCol) return -1;
|
|
2098
|
+
return m.index;
|
|
2099
|
+
}
|
|
2100
|
+
return -1;
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
// Same as scanIdentCol but returns the LAST match in [fromCol, toCol).
|
|
2104
|
+
// Property-shorthand pairs only carry .loc for the *end* of the pair,
|
|
2105
|
+
// so to find the key's actual column we walk all occurrences and take
|
|
2106
|
+
// the rightmost one (the param with the same name appears earlier on
|
|
2107
|
+
// the line and would otherwise win).
|
|
2108
|
+
const scanIdentColLast = (srcRow, name, fromCol = 0, toCol = Infinity) => {
|
|
2109
|
+
const source = this.options && this.options.source;
|
|
2110
|
+
if (!source || typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) return -1;
|
|
2111
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
2112
|
+
const line = lines[srcRow];
|
|
2113
|
+
if (!line) return -1;
|
|
2114
|
+
const re = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
|
|
2115
|
+
re.lastIndex = Math.max(0, fromCol);
|
|
2116
|
+
let m, last = -1;
|
|
2117
|
+
while ((m = re.exec(line)) !== null) {
|
|
2118
|
+
if (m.index >= toCol) break;
|
|
2119
|
+
last = m.index;
|
|
2120
|
+
}
|
|
2121
|
+
return last;
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
// Track which pairs emit as method-shorthand so we can format the
|
|
2125
|
+
// object multi-line. Joining sibling methods with `, ` collapses
|
|
2126
|
+
// `}, name(args) {` onto the same generated line as the previous
|
|
2127
|
+
// method's closing brace, which makes per-line source-map mappings
|
|
2128
|
+
// ambiguous (the LSP can't tell which source line owns that gen line,
|
|
2129
|
+
// so parameter classifications get attributed to the wrong source
|
|
2130
|
+
// position). Putting each method on its own gen line keeps the
|
|
2131
|
+
// line→source mapping one-to-one.
|
|
2132
|
+
let hasMethod = false;
|
|
2133
|
+
let isMethod = new Array(pairs.length).fill(false);
|
|
2134
|
+
|
|
2135
|
+
let codes = pairs.map((pair, idx) => {
|
|
1828
2136
|
if (this.is(pair, '...')) return `...${this.emit(pair[1], 'value')}`;
|
|
1829
2137
|
let [operator, key, value] = pair;
|
|
1830
2138
|
let keyCode;
|
|
2139
|
+
let isSimpleKey = false;
|
|
1831
2140
|
if (this.is(key, 'dynamicKey')) keyCode = `[${this.emit(key[1], 'value')}]`;
|
|
1832
2141
|
else if (this.is(key, 'str')) keyCode = `[${this.emit(key, 'value')}]`;
|
|
1833
2142
|
else {
|
|
1834
2143
|
this.suppressReactiveUnwrap = true;
|
|
1835
2144
|
keyCode = this.emit(key, 'value');
|
|
1836
2145
|
this.suppressReactiveUnwrap = false;
|
|
2146
|
+
isSimpleKey = !Array.isArray(key) && typeof keyCode === 'string'
|
|
2147
|
+
&& /^[A-Za-z_$][\w$]*$/.test(keyCode);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// Method-shorthand: `key: -> body` → `key(args) { body }`. Enables
|
|
2151
|
+
// TypeScript contextual `this` binding when the object is assigned to
|
|
2152
|
+
// a method-shorthand-typed slot, and produces cleaner JS output.
|
|
2153
|
+
// Only for thin-arrow (`->`) — fat arrow has lexical `this` semantics.
|
|
2154
|
+
// Skip when the value carries side-effect (`!`) or non-trivial meta.
|
|
2155
|
+
if (operator === ':' && isSimpleKey && this.is(value, '->')) {
|
|
2156
|
+
let [, mParams, mBody] = value;
|
|
2157
|
+
if ((!mParams || (Array.isArray(mParams) && mParams.length === 0)) && this.containsIt(mBody)) mParams = ['it'];
|
|
2158
|
+
let mSideEffect = value.isVoid || false;
|
|
2159
|
+
let mParamList = this.emitParamList(mParams);
|
|
2160
|
+
let mBodyCode = this.emitFunctionBody(mBody, mParams, mSideEffect);
|
|
2161
|
+
let mIsAsync = this.containsAwait(mBody);
|
|
2162
|
+
let mIsGen = this.containsYield(mBody);
|
|
2163
|
+
let prefix = mIsAsync ? 'async ' : '';
|
|
2164
|
+
let star = mIsGen ? '*' : '';
|
|
2165
|
+
// Inject source-map anchors for plain-string params (e.g. `quantity`
|
|
2166
|
+
// in `(product, quantity) ->`). The parser drops .loc from non-data
|
|
2167
|
+
// identifier tokens and collectSubExprs intentionally skips the
|
|
2168
|
+
// params slot of arrow nodes, so without these the param positions
|
|
2169
|
+
// produce no source mapping and the LSP's gen→src lookup falls back
|
|
2170
|
+
// to a nearby unrelated identifier — typically classifying the
|
|
2171
|
+
// param as `property`.
|
|
2172
|
+
if (value && value.loc && Array.isArray(mParams) && mParams.length) {
|
|
2173
|
+
const srcRow = value.loc.r;
|
|
2174
|
+
// value.loc.c marks the start of the params group (the `(` for
|
|
2175
|
+
// parenthesized params, or the first param's column for bare
|
|
2176
|
+
// single-param arrows). Scan forward from there for each param
|
|
2177
|
+
// in declaration order.
|
|
2178
|
+
const paramsStart = value.loc.c;
|
|
2179
|
+
const anchors = [];
|
|
2180
|
+
let cursor = paramsStart;
|
|
2181
|
+
for (const p of mParams) {
|
|
2182
|
+
if (typeof p !== 'string') continue;
|
|
2183
|
+
const col = scanIdentCol(srcRow, p, cursor);
|
|
2184
|
+
if (col >= 0) {
|
|
2185
|
+
anchors.push({ name: p, origLine: srcRow, origCol: col });
|
|
2186
|
+
cursor = col + p.length;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
if (anchors.length) {
|
|
2190
|
+
value._anchors = (value._anchors || []).concat(anchors);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
hasMethod = true;
|
|
2194
|
+
isMethod[idx] = true;
|
|
2195
|
+
return `${prefix}${star}${keyCode}(${mParamList}) ${mBodyCode}`;
|
|
1837
2196
|
}
|
|
2197
|
+
|
|
1838
2198
|
let valCode = this.emit(value, 'value');
|
|
2199
|
+
// Anchor the key for simple `key: value` pairs so the source-map
|
|
2200
|
+
// heuristic has one anchor per source occurrence (otherwise pairs
|
|
2201
|
+
// like `quantity: 1` are silent and downstream same-named anchors
|
|
2202
|
+
// get stolen by the wrong gen position).
|
|
2203
|
+
if (operator === ':' && isSimpleKey && Array.isArray(pair) && pair.loc &&
|
|
2204
|
+
typeof key === 'string') {
|
|
2205
|
+
const col = scanIdentCol(pair.loc.r, key, 0, pair.loc.c + key.length + 1);
|
|
2206
|
+
if (col >= 0) {
|
|
2207
|
+
pair._anchors = (pair._anchors || []).concat([
|
|
2208
|
+
{ name: key, origLine: pair.loc.r, origCol: col }
|
|
2209
|
+
]);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
1839
2212
|
if (operator === '=') return `${keyCode} = ${valCode}`;
|
|
1840
2213
|
if (operator === ':') return `${keyCode}: ${valCode}`;
|
|
1841
|
-
if (keyCode === valCode && !Array.isArray(key))
|
|
2214
|
+
if (keyCode === valCode && !Array.isArray(key)) {
|
|
2215
|
+
// Property shorthand `{ ...i, quantity }` → pair is [null, "quantity",
|
|
2216
|
+
// "quantity"] with the *pair* carrying .loc but the key/value strings
|
|
2217
|
+
// having none. Without an anchor the heuristic gives the gen position
|
|
2218
|
+
// to whichever same-named identifier in the statement happens to be
|
|
2219
|
+
// closest by line distance.
|
|
2220
|
+
if (Array.isArray(pair) && pair.loc && typeof key === 'string') {
|
|
2221
|
+
const col = scanIdentColLast(pair.loc.r, key, 0, pair.loc.c + 1);
|
|
2222
|
+
if (col >= 0) {
|
|
2223
|
+
pair._anchors = (pair._anchors || []).concat([
|
|
2224
|
+
{ name: key, origLine: pair.loc.r, origCol: col }
|
|
2225
|
+
]);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return keyCode;
|
|
2229
|
+
}
|
|
1842
2230
|
return `${keyCode}: ${valCode}`;
|
|
1843
|
-
})
|
|
1844
|
-
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
if (!hasMethod) return `{${codes.join(', ')}}`;
|
|
2234
|
+
|
|
2235
|
+
// Multi-line output when any pair is a method. Pairs preceding the
|
|
2236
|
+
// first method may stay on the opening-brace line, then each method
|
|
2237
|
+
// (and any pair following a method) starts on its own line.
|
|
2238
|
+
let parts = [];
|
|
2239
|
+
let onOpenLine = [];
|
|
2240
|
+
for (let i = 0; i < codes.length; i++) {
|
|
2241
|
+
if (isMethod[i] || (i > 0 && isMethod[i - 1])) {
|
|
2242
|
+
if (onOpenLine.length && parts.length === 0) {
|
|
2243
|
+
parts.push(onOpenLine.join(', '));
|
|
2244
|
+
onOpenLine = [];
|
|
2245
|
+
}
|
|
2246
|
+
parts.push(codes[i]);
|
|
2247
|
+
} else if (parts.length === 0) {
|
|
2248
|
+
onOpenLine.push(codes[i]);
|
|
2249
|
+
} else {
|
|
2250
|
+
parts.push(codes[i]);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (onOpenLine.length && parts.length === 0) parts.push(onOpenLine.join(', '));
|
|
2254
|
+
let head0 = parts[0];
|
|
2255
|
+
let rest = parts.slice(1);
|
|
2256
|
+
return rest.length === 0 ? `{${head0}}` : `{${head0},\n${rest.join(',\n')}}`;
|
|
1845
2257
|
}
|
|
1846
2258
|
|
|
1847
2259
|
emitMap(head, pairs, context) {
|
|
@@ -2266,14 +2678,18 @@ export class CodeEmitter {
|
|
|
2266
2678
|
let atParamMap = isSubclass ? new Map() : null;
|
|
2267
2679
|
cleanParams = params.map(p => {
|
|
2268
2680
|
if (this.is(p, '.') && p[1] === 'this') {
|
|
2269
|
-
|
|
2681
|
+
// Unwrap String-wrapper identifiers — typed @field params
|
|
2682
|
+
// arrive with their type metadata attached as `.data.type`
|
|
2683
|
+
// and would otherwise miss the atParamMap key match (the
|
|
2684
|
+
// map is queried with primitive strings via `str(prop)`).
|
|
2685
|
+
let name = str(p[2]);
|
|
2270
2686
|
let param = isSubclass ? `_${name}` : name;
|
|
2271
2687
|
autoAssign.push(`this.${name} = ${param}`);
|
|
2272
2688
|
if (isSubclass) atParamMap.set(name, param);
|
|
2273
2689
|
return param;
|
|
2274
2690
|
}
|
|
2275
2691
|
if (this.is(p, 'default') && this.is(p[1], '.') && p[1][1] === 'this') {
|
|
2276
|
-
let name = p[1][2];
|
|
2692
|
+
let name = str(p[1][2]);
|
|
2277
2693
|
let param = isSubclass ? `_${name}` : name;
|
|
2278
2694
|
autoAssign.push(`this.${name} = ${param}`);
|
|
2279
2695
|
if (isSubclass) atParamMap.set(name, param);
|
|
@@ -2320,7 +2736,7 @@ export class CodeEmitter {
|
|
|
2320
2736
|
emitImport(head, rest, context, sexpr) {
|
|
2321
2737
|
if (rest.length === 1) {
|
|
2322
2738
|
let importExpr = `import(${this.emit(rest[0], 'value')})`;
|
|
2323
|
-
if (meta(sexpr[0], '
|
|
2739
|
+
if (meta(sexpr[0], 'bang') === true) return `(await ${importExpr})`;
|
|
2324
2740
|
return importExpr;
|
|
2325
2741
|
}
|
|
2326
2742
|
if (this.options.skipImports) return '';
|
|
@@ -2329,6 +2745,7 @@ export class CodeEmitter {
|
|
|
2329
2745
|
let fixedSource = this.addJsExtensionAndAssertions(source);
|
|
2330
2746
|
if (named[0] === '*' && named.length === 2) return `import ${def}, * as ${named[1]} from ${fixedSource}`;
|
|
2331
2747
|
let names = named.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
|
|
2748
|
+
this._attachImportSpecifierAnchors(sexpr, [def, ...named.flatMap(i => Array.isArray(i) ? i : [i])]);
|
|
2332
2749
|
return `import ${def}, { ${names} } from ${fixedSource}`;
|
|
2333
2750
|
}
|
|
2334
2751
|
let [specifier, source] = rest;
|
|
@@ -2337,11 +2754,70 @@ export class CodeEmitter {
|
|
|
2337
2754
|
if (Array.isArray(specifier)) {
|
|
2338
2755
|
if (specifier[0] === '*' && specifier.length === 2) return `import * as ${specifier[1]} from ${fixedSource}`;
|
|
2339
2756
|
let names = specifier.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
|
|
2757
|
+
this._attachImportSpecifierAnchors(sexpr, specifier.flatMap(i => Array.isArray(i) ? i : [i]));
|
|
2340
2758
|
return `import { ${names} } from ${fixedSource}`;
|
|
2341
2759
|
}
|
|
2342
2760
|
return `import ${this.emit(specifier, 'value')} from ${fixedSource}`;
|
|
2343
2761
|
}
|
|
2344
2762
|
|
|
2763
|
+
// Attach source-map anchors for each named import specifier so hover /
|
|
2764
|
+
// go-to-def works on individual names in `import { foo, bar } from '...'`.
|
|
2765
|
+
// The parser drops .loc from specifier strings, so we recover positions
|
|
2766
|
+
// by scanning the source forward from the `import` keyword.
|
|
2767
|
+
_attachImportSpecifierAnchors(sexpr, names) {
|
|
2768
|
+
if (!sexpr || !sexpr.loc) return;
|
|
2769
|
+
const source = this.options && this.options.source;
|
|
2770
|
+
if (!source || !names || !names.length) return;
|
|
2771
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
2772
|
+
let row = sexpr.loc.r;
|
|
2773
|
+
let col = sexpr.loc.c;
|
|
2774
|
+
const anchors = [];
|
|
2775
|
+
for (const name of names) {
|
|
2776
|
+
if (typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) continue;
|
|
2777
|
+
const re = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
|
|
2778
|
+
let found = false;
|
|
2779
|
+
while (row < lines.length) {
|
|
2780
|
+
const line = lines[row] || '';
|
|
2781
|
+
// Strip trailing line comment so '# foo' doesn't match
|
|
2782
|
+
const codePart = line.replace(/#.*$/, '');
|
|
2783
|
+
re.lastIndex = 0;
|
|
2784
|
+
const slice = codePart.slice(col);
|
|
2785
|
+
const m = re.exec(slice);
|
|
2786
|
+
if (m) {
|
|
2787
|
+
const c = col + m.index;
|
|
2788
|
+
anchors.push({ name, origLine: row, origCol: c });
|
|
2789
|
+
col = c + name.length;
|
|
2790
|
+
found = true;
|
|
2791
|
+
break;
|
|
2792
|
+
}
|
|
2793
|
+
row++;
|
|
2794
|
+
col = 0;
|
|
2795
|
+
}
|
|
2796
|
+
if (!found) break;
|
|
2797
|
+
}
|
|
2798
|
+
if (anchors.length) sexpr._anchors = (sexpr._anchors || []).concat(anchors);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// Propagate the void marker from a `name! = fn` LHS onto the function node.
|
|
2802
|
+
// The `!` suffix is recorded as `.bang === true` metadata on the target
|
|
2803
|
+
// identifier; when the value is a function (`->`/`=>`/`def`) the bang means
|
|
2804
|
+
// the function is void (no implicit return). We stamp `isVoid` directly on
|
|
2805
|
+
// the function node so the arrow emitters read it locally — the same way
|
|
2806
|
+
// `emitDef` reads `meta(name, 'bang')` off its own node. Used by assignment
|
|
2807
|
+
// and export declaration paths so `export name! = ->` matches the bare
|
|
2808
|
+
// `name! = ->` semantics. Rejects `!`/`&` sigils on non-function values,
|
|
2809
|
+
// exactly like a plain assignment.
|
|
2810
|
+
applyVoidMarker(target, value, sexpr) {
|
|
2811
|
+
let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
|
|
2812
|
+
if (target instanceof String && meta(target, 'bang') !== undefined && !isFnValue) {
|
|
2813
|
+
let sigil = meta(target, 'bang') === true ? '!' : '&';
|
|
2814
|
+
this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
|
|
2815
|
+
}
|
|
2816
|
+
if (target instanceof String && meta(target, 'bang') === true && isFnValue) {
|
|
2817
|
+
value.isVoid = true;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2345
2821
|
emitExport(head, rest) {
|
|
2346
2822
|
let [decl] = rest;
|
|
2347
2823
|
if (this.options.skipExports) {
|
|
@@ -2354,6 +2830,7 @@ export class CodeEmitter {
|
|
|
2354
2830
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2355
2831
|
}
|
|
2356
2832
|
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2833
|
+
this.applyVoidMarker(decl[1], decl[2], decl);
|
|
2357
2834
|
const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2358
2835
|
this._componentName = prev;
|
|
2359
2836
|
this._componentTypeParams = prevTP;
|
|
@@ -2372,6 +2849,7 @@ export class CodeEmitter {
|
|
|
2372
2849
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2373
2850
|
}
|
|
2374
2851
|
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2852
|
+
this.applyVoidMarker(decl[1], decl[2], decl);
|
|
2375
2853
|
const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2376
2854
|
this._componentName = prev;
|
|
2377
2855
|
this._componentTypeParams = prevTP;
|
|
@@ -2385,10 +2863,14 @@ export class CodeEmitter {
|
|
|
2385
2863
|
emitExportDefault(head, rest) {
|
|
2386
2864
|
let [expr] = rest;
|
|
2387
2865
|
if (this.options.skipExports) {
|
|
2388
|
-
if (this.is(expr, '='))
|
|
2866
|
+
if (this.is(expr, '=')) {
|
|
2867
|
+
this.applyVoidMarker(expr[1], expr[2], expr);
|
|
2868
|
+
return `const ${expr[1]} = ${this.emit(expr[2], 'value')}`;
|
|
2869
|
+
}
|
|
2389
2870
|
return this.emit(expr, 'statement');
|
|
2390
2871
|
}
|
|
2391
2872
|
if (this.is(expr, '=')) {
|
|
2873
|
+
this.applyVoidMarker(expr[1], expr[2], expr);
|
|
2392
2874
|
return `const ${expr[1]} = ${this.emit(expr[2], 'value')};\nexport default ${expr[1]}`;
|
|
2393
2875
|
}
|
|
2394
2876
|
return `export default ${this.emit(expr, 'statement')}`;
|
|
@@ -2518,14 +3000,36 @@ export class CodeEmitter {
|
|
|
2518
3000
|
|
|
2519
3001
|
formatParam(param) {
|
|
2520
3002
|
if (typeof param === 'string') return param;
|
|
2521
|
-
if (param instanceof String)
|
|
2522
|
-
|
|
3003
|
+
if (param instanceof String) {
|
|
3004
|
+
// In `inlineTypes` mode (set by typecheck.compileForCheck), emit the
|
|
3005
|
+
// type annotation inline so shadow TS sees `name: T` for typed params
|
|
3006
|
+
// in every function-like position — top-level arrows, class methods,
|
|
3007
|
+
// object-literal method shorthand, nested functions, etc. The user-
|
|
3008
|
+
// facing `-c` output stays untouched because this flag is off by
|
|
3009
|
+
// default. `.type` carries the raw Rip type string (with `::`),
|
|
3010
|
+
// which converts to TS form by swapping `::` → `:`.
|
|
3011
|
+
if (this.options.inlineTypes && param.type) {
|
|
3012
|
+
return `${param.valueOf()}: ${param.type.replace(/::/g, ':')}`;
|
|
3013
|
+
}
|
|
3014
|
+
return param.valueOf();
|
|
3015
|
+
}
|
|
3016
|
+
if (this.is(param, 'rest')) {
|
|
3017
|
+
// Rest param: `...name`. When the name is a String wrapper carrying
|
|
3018
|
+
// a type, emit `...name: T` so shadow TS sees the rest tuple/array.
|
|
3019
|
+
let restName = param[1];
|
|
3020
|
+
if (this.options.inlineTypes && restName instanceof String && restName.type) {
|
|
3021
|
+
return `...${restName.valueOf()}: ${restName.type.replace(/::/g, ':')}`;
|
|
3022
|
+
}
|
|
3023
|
+
return `...${restName}`;
|
|
3024
|
+
}
|
|
2523
3025
|
if (this.is(param, 'default')) {
|
|
2524
3026
|
// `param[1]` is either a plain identifier string (e.g. `x = 5`) or a
|
|
2525
3027
|
// destructuring pattern AST node (e.g. `{a, b} = {}`). Recurse via
|
|
2526
3028
|
// `formatParam` so patterns emit as `{a, b}` / `[x, y]` instead of
|
|
2527
3029
|
// being coerced to a string via `Array.prototype.toString`, which
|
|
2528
3030
|
// produced the famous `(object,,a,a,,b,b = {})` mis-rendering.
|
|
3031
|
+
// The recursion also picks up any inline type annotation on the
|
|
3032
|
+
// name in `inlineTypes` mode, yielding `name: T = default`.
|
|
2529
3033
|
return `${this.formatParam(param[1])} = ${this.emit(param[2], 'value')}`;
|
|
2530
3034
|
}
|
|
2531
3035
|
if (this.is(param, '.') && param[1] === 'this') return param[2];
|
|
@@ -2565,10 +3069,14 @@ export class CodeEmitter {
|
|
|
2565
3069
|
|
|
2566
3070
|
let paramNames = new Set();
|
|
2567
3071
|
let extractPN = (p) => {
|
|
2568
|
-
|
|
3072
|
+
// Unwrap String wrappers — typed params arrive as `new String('name')`
|
|
3073
|
+
// with `.data.type` metadata. Without unwrapping, `paramNames.has('name')`
|
|
3074
|
+
// misses (Set compares wrappers by identity), causing the param name to
|
|
3075
|
+
// be re-hoisted as a local `let`, producing duplicate-declaration errors.
|
|
3076
|
+
if (typeof p === 'string' || p instanceof String) paramNames.add(str(p));
|
|
2569
3077
|
else if (Array.isArray(p)) {
|
|
2570
|
-
if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string') paramNames.add(p[1]); }
|
|
2571
|
-
else if (p[0] === 'default') { if (typeof p[1] === 'string') paramNames.add(p[1]); }
|
|
3078
|
+
if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
|
|
3079
|
+
else if (p[0] === 'default') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
|
|
2572
3080
|
else if (p[0] === 'array' || p[0] === 'object') this.collectVarsFromArray(p, paramNames);
|
|
2573
3081
|
}
|
|
2574
3082
|
};
|
|
@@ -2612,7 +3120,20 @@ export class CodeEmitter {
|
|
|
2612
3120
|
|
|
2613
3121
|
this.indentLevel++;
|
|
2614
3122
|
let code = '{\n';
|
|
2615
|
-
if (newVars.size > 0)
|
|
3123
|
+
if (newVars.size > 0) {
|
|
3124
|
+
// In `inlineTypes` mode, propagate `name:: T = value` annotations from
|
|
3125
|
+
// body-level typed assignments onto the hoisted `let`. Without this,
|
|
3126
|
+
// the hoist emits `let y;` (no type), shadow-TS infers from the first
|
|
3127
|
+
// RHS literal (`y = true` → `y: true`), and any later `y = false`
|
|
3128
|
+
// fails TS2322. With this, the hoist emits `let y: boolean;` and the
|
|
3129
|
+
// body's `y = true` / `y = false` both check cleanly.
|
|
3130
|
+
let typedLocals = this.options.inlineTypes ? this.collectTypedLocals(body) : null;
|
|
3131
|
+
let names = Array.from(newVars).sort().map(n => {
|
|
3132
|
+
let t = typedLocals?.get(n);
|
|
3133
|
+
return t ? `${n}: ${t}` : n;
|
|
3134
|
+
});
|
|
3135
|
+
code += this.indent() + `let ${names.join(', ')};\n`;
|
|
3136
|
+
}
|
|
2616
3137
|
|
|
2617
3138
|
let firstIsSuper = autoAssignments.length > 0 && statements.length > 0 &&
|
|
2618
3139
|
Array.isArray(statements[0]) && statements[0][0] === 'super';
|
|
@@ -2936,20 +3457,21 @@ export class CodeEmitter {
|
|
|
2936
3457
|
// ---------------------------------------------------------------------------
|
|
2937
3458
|
|
|
2938
3459
|
emitIfElseWithEarlyReturns(ifStmt) {
|
|
2939
|
-
let [
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3460
|
+
let [, condition, thenBranch, ...elseBranches] = ifStmt;
|
|
3461
|
+
return this.indent() + this.emitIfElseEarlyReturnsChain(condition, thenBranch, elseBranches);
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// Recursive companion to emitIfElseWithEarlyReturns. Each branch body
|
|
3465
|
+
// is wrapped in braces with the last expression promoted to a return.
|
|
3466
|
+
emitIfElseEarlyReturnsChain(condition, thenBranch, elseBranches) {
|
|
3467
|
+
let code = `if (${this.emit(condition, 'value')}) {\n`;
|
|
2943
3468
|
code += this.withIndent(() => this.emitBranchWithReturn(thenBranch));
|
|
2944
3469
|
code += this.indent() + '}';
|
|
2945
3470
|
for (let branch of elseBranches) {
|
|
2946
3471
|
code += ' else ';
|
|
2947
3472
|
if (this.is(branch, 'if')) {
|
|
2948
3473
|
let [, nc, nt, ...ne] = branch;
|
|
2949
|
-
code +=
|
|
2950
|
-
code += this.withIndent(() => this.emitBranchWithReturn(nt));
|
|
2951
|
-
code += this.indent() + '}';
|
|
2952
|
-
for (let rb of ne) { code += ' else {\n'; code += this.withIndent(() => this.emitBranchWithReturn(rb)); code += this.indent() + '}'; }
|
|
3474
|
+
code += this.emitIfElseEarlyReturnsChain(nc, nt, ne);
|
|
2953
3475
|
} else {
|
|
2954
3476
|
code += '{\n';
|
|
2955
3477
|
code += this.withIndent(() => this.emitBranchWithReturn(branch));
|
|
@@ -2979,31 +3501,7 @@ export class CodeEmitter {
|
|
|
2979
3501
|
if (needsIIFE) {
|
|
2980
3502
|
// Enclosed: condition, thenBranch, elseBranches
|
|
2981
3503
|
let hasAwait = this.containsAwait(condition) || this.containsAwait(thenBranch) || elseBranches.some(b => this.containsAwait(b));
|
|
2982
|
-
|
|
2983
|
-
code += `if (${this.emit(condition, 'value')}) `;
|
|
2984
|
-
code += this.emitBlockWithReturns(thenBranch);
|
|
2985
|
-
for (let branch of elseBranches) {
|
|
2986
|
-
code += ' else ';
|
|
2987
|
-
if (this.is(branch, 'if')) {
|
|
2988
|
-
let [_, nc, nt, ...ne] = branch;
|
|
2989
|
-
code += `if (${this.emit(nc, 'value')}) `;
|
|
2990
|
-
code += this.emitBlockWithReturns(nt);
|
|
2991
|
-
for (let nb of ne) {
|
|
2992
|
-
code += ' else ';
|
|
2993
|
-
if (this.is(nb, 'if')) {
|
|
2994
|
-
let [__, nnc, nnt, ...nne] = nb;
|
|
2995
|
-
code += `if (${this.emit(nnc, 'value')}) `;
|
|
2996
|
-
code += this.emitBlockWithReturns(nnt);
|
|
2997
|
-
elseBranches.push(...nne);
|
|
2998
|
-
} else {
|
|
2999
|
-
code += this.emitBlockWithReturns(nb);
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3002
|
-
} else {
|
|
3003
|
-
code += this.emitBlockWithReturns(branch);
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
return code + ' })()';
|
|
3504
|
+
return this.asyncIIFEOpen(hasAwait) + ' ' + this.emitIfChain(condition, thenBranch, elseBranches) + ' })()';
|
|
3007
3505
|
}
|
|
3008
3506
|
let thenExpr = this.extractExpression(this.unwrapIfBranch(thenBranch));
|
|
3009
3507
|
let elseExpr = this.buildTernaryChain(elseBranches);
|
|
@@ -3012,6 +3510,23 @@ export class CodeEmitter {
|
|
|
3012
3510
|
return `(${condCode} ? ${thenExpr} : ${elseExpr})`;
|
|
3013
3511
|
}
|
|
3014
3512
|
|
|
3513
|
+
// Recursive emitter for `if / else if / else` chains in IIFE/value contexts.
|
|
3514
|
+
// Walks the right-recursive AST: ["if", cond, then, else?] where `else` may
|
|
3515
|
+
// itself be another `["if", ...]` for an elseif chain.
|
|
3516
|
+
emitIfChain(condition, thenBranch, elseBranches) {
|
|
3517
|
+
let code = `if (${this.emit(condition, 'value')}) ` + this.emitBlockWithReturns(thenBranch);
|
|
3518
|
+
for (let branch of elseBranches) {
|
|
3519
|
+
code += ' else ';
|
|
3520
|
+
if (this.is(branch, 'if')) {
|
|
3521
|
+
let [, nc, nt, ...ne] = branch;
|
|
3522
|
+
code += this.emitIfChain(nc, nt, ne);
|
|
3523
|
+
} else {
|
|
3524
|
+
code += this.emitBlockWithReturns(branch);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
return code;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3015
3530
|
emitIfAsStatement(condition, thenBranch, elseBranches) {
|
|
3016
3531
|
let code = `if (${this.unwrap(this.emit(condition, 'value'))}) `;
|
|
3017
3532
|
code += this.emit(this.unwrapIfBranch(thenBranch), 'statement');
|
|
@@ -3210,6 +3725,67 @@ export class CodeEmitter {
|
|
|
3210
3725
|
return null;
|
|
3211
3726
|
}
|
|
3212
3727
|
|
|
3728
|
+
// Walk a destructuring LHS and reject shapes that aren't valid binding
|
|
3729
|
+
// patterns. `null` array elements are elision (valid). Identifiers are
|
|
3730
|
+
// strings and parse as leaves. Member-access (`obj.x`) and bracket-index
|
|
3731
|
+
// (`arr[i]`) shapes are valid as assignment targets. Optional chains
|
|
3732
|
+
// (`obj?.x`, `arr?.[i]`) are NOT valid in JS destructuring assignment
|
|
3733
|
+
// targets, so we reject them. Comprehensions, arithmetic, calls, etc.
|
|
3734
|
+
// are unconditionally rejected.
|
|
3735
|
+
_validateBindingPattern(node, sexpr) {
|
|
3736
|
+
if (!Array.isArray(node)) return;
|
|
3737
|
+
let head = node[0];
|
|
3738
|
+
if (head === 'comprehension' || head === 'object-comprehension') {
|
|
3739
|
+
this.error(`Cannot use ${head} as a destructuring target`, sexpr);
|
|
3740
|
+
}
|
|
3741
|
+
if (head === 'array') {
|
|
3742
|
+
for (let elem of node.slice(1)) this._validateBindingPattern(elem, sexpr);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
if (head === 'object') {
|
|
3746
|
+
// Object entry shapes used by the grammar:
|
|
3747
|
+
// [null, k, k] — shorthand {x}; both slots are the same identifier
|
|
3748
|
+
// [":", k, target] — rename {a: target}; recurse into target
|
|
3749
|
+
// ["=", k, default] — default {a = 5}; key is identifier, default is RHS
|
|
3750
|
+
// ["...", target] — rest {...rest}; recurse into target
|
|
3751
|
+
// Anything else is unexpected; fail loudly so future grammar additions
|
|
3752
|
+
// can't silently pass through unvalidated.
|
|
3753
|
+
for (let entry of node.slice(1)) {
|
|
3754
|
+
if (!Array.isArray(entry)) continue;
|
|
3755
|
+
let h = entry[0];
|
|
3756
|
+
if (h === null) {
|
|
3757
|
+
// Shorthand {x}: target is entry[2], which must be a bare identifier
|
|
3758
|
+
// (validated by the array-leaf rules below).
|
|
3759
|
+
this._validateBindingPattern(entry[2], sexpr);
|
|
3760
|
+
} else if (h === ':') {
|
|
3761
|
+
this._validateBindingPattern(entry[2], sexpr);
|
|
3762
|
+
} else if (h === '=') {
|
|
3763
|
+
// Default {a = 5}: key (entry[1]) is the binding target.
|
|
3764
|
+
this._validateBindingPattern(entry[1], sexpr);
|
|
3765
|
+
} else if (h === '...') {
|
|
3766
|
+
this._validateBindingPattern(entry[1], sexpr);
|
|
3767
|
+
} else {
|
|
3768
|
+
this.error(`Unexpected object entry '${h}' in destructuring target`, sexpr);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
// Wrapper shapes that contain a nested target.
|
|
3774
|
+
if (head === '...' || head === 'default' || head === '=') {
|
|
3775
|
+
this._validateBindingPattern(node[1], sexpr);
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
// Member access and bracket index are valid assignment targets.
|
|
3779
|
+
if (head === '.' || head === '[]') return;
|
|
3780
|
+
// Optional chains are NOT valid in JS destructuring-assignment context
|
|
3781
|
+
// (only in optional-chain GET expressions). Reject explicitly.
|
|
3782
|
+
if (head === '?.' || head === 'optindex' || head === 'optcall') {
|
|
3783
|
+
this.error(`Cannot use optional chain as a destructuring target`, sexpr);
|
|
3784
|
+
}
|
|
3785
|
+
// Anything else is an expression that isn't a destructuring shape.
|
|
3786
|
+
this.error(`Cannot use '${head}' expression as a destructuring target`, sexpr);
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3213
3789
|
unwrapLogical(code) {
|
|
3214
3790
|
if (typeof code !== 'string') return code;
|
|
3215
3791
|
while (code.startsWith('(') && code.endsWith(')')) {
|
|
@@ -3261,17 +3837,19 @@ export class CodeEmitter {
|
|
|
3261
3837
|
return false;
|
|
3262
3838
|
}
|
|
3263
3839
|
|
|
3840
|
+
// Walk the right-recursive AST emitting nested ternaries. `branches` is the
|
|
3841
|
+
// tail of an `if` s-expression — either empty (no else) or one element
|
|
3842
|
+
// (the else, which may itself be another `["if", ...]`).
|
|
3264
3843
|
buildTernaryChain(branches) {
|
|
3265
3844
|
if (branches.length === 0) return 'undefined';
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
let [_, cond, then_, ...rest] = first;
|
|
3845
|
+
let branch = this.unwrapIfBranch(branches[0]);
|
|
3846
|
+
if (this.is(branch, 'if')) {
|
|
3847
|
+
let [, cond, then_, ...rest] = branch;
|
|
3270
3848
|
let thenPart = this.extractExpression(this.unwrapIfBranch(then_));
|
|
3271
|
-
let elsePart = this.buildTernaryChain(
|
|
3849
|
+
let elsePart = this.buildTernaryChain(rest);
|
|
3272
3850
|
return `(${this.emit(cond, 'value')} ? ${thenPart} : ${elsePart})`;
|
|
3273
3851
|
}
|
|
3274
|
-
return this.extractExpression(
|
|
3852
|
+
return this.extractExpression(branch);
|
|
3275
3853
|
}
|
|
3276
3854
|
|
|
3277
3855
|
// ---------------------------------------------------------------------------
|
|
@@ -3392,7 +3970,7 @@ export class CodeEmitter {
|
|
|
3392
3970
|
|
|
3393
3971
|
containsAwait(sexpr) {
|
|
3394
3972
|
if (!sexpr) return false;
|
|
3395
|
-
if (sexpr instanceof String && meta(sexpr, '
|
|
3973
|
+
if (sexpr instanceof String && meta(sexpr, 'bang') === true) return true;
|
|
3396
3974
|
if (typeof sexpr !== 'object') return false;
|
|
3397
3975
|
if (this.is(sexpr, 'await')) return true;
|
|
3398
3976
|
if (this.is(sexpr, 'for-as') && sexpr[3] === true) return true;
|
|
@@ -3449,10 +4027,15 @@ let __pendingEffects = new Set(); // Effects queued to run
|
|
|
3449
4027
|
let __batching = false; // Are we inside a batch()?
|
|
3450
4028
|
|
|
3451
4029
|
// Flush all pending effects (called after state updates, or at end of batch)
|
|
4030
|
+
// Defense in depth: skip disposed effects. The primary guard is in
|
|
4031
|
+
// effect.run() itself — but filtering here avoids even calling .run() on
|
|
4032
|
+
// a known-dead effect, which is faster and clearer in stack traces.
|
|
3452
4033
|
function __flushEffects() {
|
|
3453
4034
|
const effects = [...__pendingEffects];
|
|
3454
4035
|
__pendingEffects.clear();
|
|
3455
|
-
for (const effect of effects)
|
|
4036
|
+
for (const effect of effects) {
|
|
4037
|
+
if (!effect._disposed) effect.run();
|
|
4038
|
+
}
|
|
3456
4039
|
}
|
|
3457
4040
|
|
|
3458
4041
|
// Shared primitive coercion (used by state and computed)
|
|
@@ -3568,11 +4151,46 @@ function __computed(fn) {
|
|
|
3568
4151
|
return computed;
|
|
3569
4152
|
}
|
|
3570
4153
|
|
|
3571
|
-
function __effect(fn) {
|
|
4154
|
+
function __effect(fn, opts) {
|
|
4155
|
+
let controller = null;
|
|
4156
|
+
let runId = 0; // increments per run; async resolutions check this to drop stale results
|
|
3572
4157
|
const effect = {
|
|
3573
4158
|
dependencies: new Set(),
|
|
4159
|
+
_disposed: false,
|
|
4160
|
+
signal: null, // AbortSignal for the current run; aborts on re-run / dispose
|
|
3574
4161
|
|
|
3575
4162
|
run() {
|
|
4163
|
+
// Zombie-run guard. An effect can be queued in __pendingEffects
|
|
4164
|
+
// (when a signal it subscribes to changes) and then disposed
|
|
4165
|
+
// before the flush reaches it — e.g. its parent block was
|
|
4166
|
+
// destroyed by an earlier effect in the same flush, and that
|
|
4167
|
+
// destruction's disposers ran effect.dispose(). Without this
|
|
4168
|
+
// guard, run() would execute fn() with __currentEffect = effect,
|
|
4169
|
+
// and any signal .value read inside fn() would re-subscribe the
|
|
4170
|
+
// effect by adding it back to the signal's subscribers Set —
|
|
4171
|
+
// accumulating one leaked subscriber per flush cycle that hits
|
|
4172
|
+
// this race. Symptom: each navigation creates one more component
|
|
4173
|
+
// instance than the previous (N visits => N synchronous mounts
|
|
4174
|
+
// on visit N).
|
|
4175
|
+
if (effect._disposed) return;
|
|
4176
|
+
// Abort the previous run's signal before allocating a new one.
|
|
4177
|
+
// Any async work from the previous run that's still mid-flight
|
|
4178
|
+
// (an await fetch with the signal, for example) sees its signal
|
|
4179
|
+
// go aborted and can bail. This also fires the 'abort' event on
|
|
4180
|
+
// the previous signal so user code subscribed via
|
|
4181
|
+
// signal.addEventListener can run.
|
|
4182
|
+
if (controller) {
|
|
4183
|
+
try { controller.abort(); } catch {}
|
|
4184
|
+
}
|
|
4185
|
+
controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
4186
|
+
effect.signal = controller ? controller.signal : null;
|
|
4187
|
+
// Per-run id captured by the closures below. When the effect
|
|
4188
|
+
// re-runs (signal changed) while a prior async body is still
|
|
4189
|
+
// awaiting, the prior body's eventual resolution sees myRun !==
|
|
4190
|
+
// runId and bails — preventing stale cleanup from overwriting
|
|
4191
|
+
// the current run's cleanup.
|
|
4192
|
+
const myRun = ++runId;
|
|
4193
|
+
|
|
3576
4194
|
if (effect._cleanup) { effect._cleanup(); effect._cleanup = null; }
|
|
3577
4195
|
for (const dep of effect.dependencies) dep.delete(effect);
|
|
3578
4196
|
effect.dependencies.clear();
|
|
@@ -3580,11 +4198,63 @@ function __effect(fn) {
|
|
|
3580
4198
|
__currentEffect = effect;
|
|
3581
4199
|
try {
|
|
3582
4200
|
const result = fn();
|
|
3583
|
-
if (typeof result === 'function')
|
|
4201
|
+
if (typeof result === 'function') {
|
|
4202
|
+
effect._cleanup = result;
|
|
4203
|
+
} else if (result && typeof result.then === 'function') {
|
|
4204
|
+
// Async effect body. We can't unwind a pending await, but we
|
|
4205
|
+
// CAN intercept the eventual resolution and decide whether
|
|
4206
|
+
// to honor any cleanup it returned. Two failure modes are
|
|
4207
|
+
// handled:
|
|
4208
|
+
// - Effect disposed while body was awaiting: run cleanup
|
|
4209
|
+
// immediately so resources release, but don't store it.
|
|
4210
|
+
// - Effect re-ran (newer run superseded this one): same.
|
|
4211
|
+
// In both cases we do NOT touch effect._cleanup, which now
|
|
4212
|
+
// belongs to a different run.
|
|
4213
|
+
result.then(
|
|
4214
|
+
(cleanup) => {
|
|
4215
|
+
if (myRun !== runId || effect._disposed) {
|
|
4216
|
+
if (typeof cleanup === 'function') {
|
|
4217
|
+
try { cleanup(); }
|
|
4218
|
+
catch (e) { console.error('[Rip] superseded async cleanup error:', e); }
|
|
4219
|
+
}
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
if (typeof cleanup === 'function') effect._cleanup = cleanup;
|
|
4223
|
+
},
|
|
4224
|
+
(err) => {
|
|
4225
|
+
// AbortError from a dispose or supersede is expected
|
|
4226
|
+
// (the user passed our signal to fetch/etc. and it
|
|
4227
|
+
// aborted). Swallow silently.
|
|
4228
|
+
if (err && err.name === 'AbortError') return;
|
|
4229
|
+
// Stale rejection from a superseded run: caller has
|
|
4230
|
+
// already moved on, no point surfacing.
|
|
4231
|
+
if (myRun !== runId || effect._disposed) return;
|
|
4232
|
+
console.error('[Rip] async effect error:', err);
|
|
4233
|
+
}
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
3584
4236
|
} finally { __currentEffect = prev; }
|
|
3585
4237
|
},
|
|
3586
4238
|
|
|
3587
4239
|
dispose() {
|
|
4240
|
+
// Idempotent: a parent disposer chain may legitimately reach the
|
|
4241
|
+
// same effect twice in tangled cleanup paths. Quick exit avoids
|
|
4242
|
+
// re-running cleanup and re-walking already-empty dependencies.
|
|
4243
|
+
if (effect._disposed) return;
|
|
4244
|
+
effect._disposed = true;
|
|
4245
|
+
// Proactive pending-set eviction. The flush-time guard in
|
|
4246
|
+
// __flushEffects also handles this, but pulling the effect out
|
|
4247
|
+
// of __pendingEffects here keeps the set bounded across long
|
|
4248
|
+
// batched cycles and makes disposal semantics direct rather
|
|
4249
|
+
// than dependent on flush ordering.
|
|
4250
|
+
__pendingEffects.delete(effect);
|
|
4251
|
+
// Abort the current signal so any in-flight async work (a
|
|
4252
|
+
// user's fetch with the signal, a setTimeout-via-signal, etc.)
|
|
4253
|
+
// unwinds via AbortError and the body can bail without mutating
|
|
4254
|
+
// signals on a destroyed component.
|
|
4255
|
+
if (controller) {
|
|
4256
|
+
try { controller.abort(); } catch {}
|
|
4257
|
+
}
|
|
3588
4258
|
if (effect._cleanup) { effect._cleanup(); effect._cleanup = null; }
|
|
3589
4259
|
for (const dep of effect.dependencies) dep.delete(effect);
|
|
3590
4260
|
effect.dependencies.clear();
|
|
@@ -3592,7 +4262,29 @@ function __effect(fn) {
|
|
|
3592
4262
|
};
|
|
3593
4263
|
|
|
3594
4264
|
effect.run();
|
|
3595
|
-
|
|
4265
|
+
const dispose = () => effect.dispose();
|
|
4266
|
+
// Auto-register with the current component (if any) so disposers fire
|
|
4267
|
+
// on component unmount. Without this, every __effect created inside a
|
|
4268
|
+
// component's _init / _setup / _create lived forever — its callback
|
|
4269
|
+
// stayed subscribed to its signals, the closure pinned the component,
|
|
4270
|
+
// and any DOM/event-listener cleanup the effect had returned never
|
|
4271
|
+
// fired. The bridge is intentionally cross-module: the reactive
|
|
4272
|
+
// runtime (this file) doesn't depend on components.js, but components.js
|
|
4273
|
+
// exposes a getter on globalThis.__ripComponent at registration time
|
|
4274
|
+
// and we read it lazily so module-load order is irrelevant.
|
|
4275
|
+
//
|
|
4276
|
+
// {skipRegister: true} opts out of auto-registration. Used by factory
|
|
4277
|
+
// blocks (for-loops, if-blocks in render) that maintain their own
|
|
4278
|
+
// local disposers array and call them via the d(detaching) hook.
|
|
4279
|
+
// Without skipRegister, those effects would be registered TWICE — once
|
|
4280
|
+
// in the local factory disposers and again on the parent component's
|
|
4281
|
+
// _disposers — leaking stale disposer references on every block
|
|
4282
|
+
// re-render until the parent itself unmounts.
|
|
4283
|
+
if (!opts || !opts.skipRegister) {
|
|
4284
|
+
const cur = globalThis.__ripComponent?.__getCurrentComponent?.();
|
|
4285
|
+
if (cur) (cur._disposers ??= []).push(dispose);
|
|
4286
|
+
}
|
|
4287
|
+
return dispose;
|
|
3596
4288
|
}
|
|
3597
4289
|
|
|
3598
4290
|
function __batch(fn) {
|
|
@@ -3606,6 +4298,24 @@ function __batch(fn) {
|
|
|
3606
4298
|
}
|
|
3607
4299
|
}
|
|
3608
4300
|
|
|
4301
|
+
// Returns the AbortSignal of the currently-running effect, or null if
|
|
4302
|
+
// called outside an effect or before AbortController is available.
|
|
4303
|
+
// Designed for async-aware effect bodies — capture the signal BEFORE
|
|
4304
|
+
// any await so it stays valid for the duration of the body:
|
|
4305
|
+
//
|
|
4306
|
+
// (in Rip source)
|
|
4307
|
+
// ~>
|
|
4308
|
+
// signal = getEffectSignal()
|
|
4309
|
+
// data = fetch! url, {signal}
|
|
4310
|
+
// this.data = data
|
|
4311
|
+
//
|
|
4312
|
+
// On effect re-run or component unmount, the signal aborts; the
|
|
4313
|
+
// fetch rejects with AbortError; the body unwinds without touching
|
|
4314
|
+
// signals on a destroyed component.
|
|
4315
|
+
function __getEffectSignal() {
|
|
4316
|
+
return __currentEffect ? __currentEffect.signal : null;
|
|
4317
|
+
}
|
|
4318
|
+
|
|
3609
4319
|
function __readonly(value) {
|
|
3610
4320
|
return Object.freeze({ value });
|
|
3611
4321
|
}
|
|
@@ -3647,7 +4357,11 @@ function __catchErrors(fn) {
|
|
|
3647
4357
|
|
|
3648
4358
|
// Register on globalThis for runtime deduplication
|
|
3649
4359
|
if (typeof globalThis !== 'undefined') {
|
|
3650
|
-
globalThis.__rip = { __state, __computed, __effect, __batch, __readonly, __setErrorHandler, __handleError, __catchErrors };
|
|
4360
|
+
globalThis.__rip = { __state, __computed, __effect, __batch, __readonly, __setErrorHandler, __handleError, __catchErrors, __getEffectSignal };
|
|
4361
|
+
// Stdlib-style global so user code can call getEffectSignal() in a
|
|
4362
|
+
// ~> body without importing or destructuring. Mirrors how p, pp,
|
|
4363
|
+
// assert, etc. are registered for ergonomic use.
|
|
4364
|
+
globalThis.getEffectSignal ??= __getEffectSignal;
|
|
3651
4365
|
}
|
|
3652
4366
|
|
|
3653
4367
|
// === End Reactive Runtime ===
|
|
@@ -3702,9 +4416,26 @@ export class Compiler {
|
|
|
3702
4416
|
|
|
3703
4417
|
// Elide type-only imports — after type stripping, imported names that were
|
|
3704
4418
|
// only used in type annotations no longer appear in the token stream.
|
|
3705
|
-
//
|
|
3706
|
-
|
|
3707
|
-
|
|
4419
|
+
// The elision is per-specifier, not per-import: a single declaration like
|
|
4420
|
+
// `import { ApiErrors, parseError } from 'm'` where only `ApiErrors` is
|
|
4421
|
+
// type-only becomes `import { parseError } from 'm'`. If all named
|
|
4422
|
+
// specifiers drop and no default/namespace specifier remains, the whole
|
|
4423
|
+
// import is removed.
|
|
4424
|
+
//
|
|
4425
|
+
// A named specifier is "type-only" from the importing file's point of view
|
|
4426
|
+
// when its local binding is unused at runtime. That catches both
|
|
4427
|
+
// (a) names referenced solely in this file's type annotations (now in
|
|
4428
|
+
// typeRefNames) and (b) names imported only as types but never referenced
|
|
4429
|
+
// in this file at all — the exporting module strips its `export type` to
|
|
4430
|
+
// nothing, so leaving the specifier in place would cause a runtime
|
|
4431
|
+
// "module does not provide an export named X" error.
|
|
4432
|
+
//
|
|
4433
|
+
// Skip elision in `inlineTypes` mode (set by typecheck.compileForCheck).
|
|
4434
|
+
// The shadow-TS output is fed to the TypeScript language service, not
|
|
4435
|
+
// executed — so keeping the specifiers preserves go-to-definition and
|
|
4436
|
+
// hover for type-only imports like `RetryConfig` that resolve to
|
|
4437
|
+
// `export type` declarations in the target module.
|
|
4438
|
+
if (lexer.typeRefNames?.size > 0 && !this.options.inlineTypes) {
|
|
3708
4439
|
let usedNames = new Set();
|
|
3709
4440
|
let inImport = false;
|
|
3710
4441
|
for (let t of tokens) {
|
|
@@ -3713,6 +4444,7 @@ export class Compiler {
|
|
|
3713
4444
|
if (inImport) continue;
|
|
3714
4445
|
if (t[0] === 'IDENTIFIER') usedNames.add(t[1]);
|
|
3715
4446
|
}
|
|
4447
|
+
let isTypeOnly = (local) => !usedNames.has(local);
|
|
3716
4448
|
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
3717
4449
|
if (tokens[i][0] !== 'IMPORT') continue;
|
|
3718
4450
|
let j = i + 1;
|
|
@@ -3721,22 +4453,119 @@ export class Compiler {
|
|
|
3721
4453
|
if (tokens[j][0] === 'CALL_START' || tokens[j][0] === '(') continue;
|
|
3722
4454
|
// Skip side-effect imports: import 'module'
|
|
3723
4455
|
if (tokens[j][0] === 'STRING') continue;
|
|
3724
|
-
//
|
|
3725
|
-
let
|
|
3726
|
-
while (
|
|
3727
|
-
if (tokens[
|
|
3728
|
-
|
|
4456
|
+
// Find FROM / TERMINATOR bounds
|
|
4457
|
+
let fromIdx = -1, endIdx = j;
|
|
4458
|
+
while (endIdx < tokens.length && tokens[endIdx][0] !== 'TERMINATOR') {
|
|
4459
|
+
if (fromIdx === -1 && tokens[endIdx][0] === 'FROM') fromIdx = endIdx;
|
|
4460
|
+
endIdx++;
|
|
4461
|
+
}
|
|
4462
|
+
if (fromIdx === -1) continue;
|
|
4463
|
+
// Locate `{` / `}` for named specifiers
|
|
4464
|
+
let lbIdx = -1, rbIdx = -1;
|
|
4465
|
+
for (let k = i + 1; k < fromIdx; k++) {
|
|
4466
|
+
if (tokens[k][0] === '{') { lbIdx = k; break; }
|
|
4467
|
+
}
|
|
4468
|
+
if (lbIdx !== -1) {
|
|
4469
|
+
for (let k = lbIdx + 1; k < fromIdx; k++) {
|
|
4470
|
+
if (tokens[k][0] === '}') { rbIdx = k; break; }
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
// Determine whether a default or namespace specifier exists outside the braces
|
|
4474
|
+
let hasOtherSpec = false;
|
|
4475
|
+
let scanEnd = lbIdx !== -1 ? lbIdx : fromIdx;
|
|
4476
|
+
for (let k = i + 1; k < scanEnd; k++) {
|
|
4477
|
+
let tag = tokens[k][0];
|
|
4478
|
+
if (tag === 'IDENTIFIER' || tag === '*') { hasOtherSpec = true; break; }
|
|
4479
|
+
}
|
|
4480
|
+
// No named specifiers — fall back to whole-import elision
|
|
4481
|
+
if (lbIdx === -1 || rbIdx === -1) {
|
|
4482
|
+
if (hasOtherSpec) {
|
|
4483
|
+
// Default / namespace import: collect outer local names
|
|
4484
|
+
let names = [];
|
|
4485
|
+
let k = i + 1;
|
|
4486
|
+
while (k < fromIdx) {
|
|
4487
|
+
if (tokens[k][0] === 'AS' && k + 1 < fromIdx && tokens[k + 1][0] === 'IDENTIFIER') {
|
|
4488
|
+
names.push(tokens[k + 1][1]); k += 2;
|
|
4489
|
+
} else if (tokens[k][0] === 'IDENTIFIER') {
|
|
4490
|
+
names.push(tokens[k][1]); k++;
|
|
4491
|
+
} else { k++; }
|
|
4492
|
+
}
|
|
4493
|
+
if (names.length === 0 || !names.every(isTypeOnly)) continue;
|
|
4494
|
+
let end = endIdx < tokens.length ? endIdx + 1 : endIdx;
|
|
4495
|
+
tokens.splice(i, end - i);
|
|
4496
|
+
}
|
|
4497
|
+
continue;
|
|
4498
|
+
}
|
|
4499
|
+
// Split brace contents into specifier ranges (start inclusive, end exclusive)
|
|
4500
|
+
// Each specifier is delimited by `,` at depth 0 (the `{}` themselves).
|
|
4501
|
+
let specs = [];
|
|
4502
|
+
let s = lbIdx + 1;
|
|
4503
|
+
while (s < rbIdx) {
|
|
4504
|
+
let e = s;
|
|
4505
|
+
while (e < rbIdx && tokens[e][0] !== ',') e++;
|
|
4506
|
+
if (e > s) specs.push({ start: s, end: e });
|
|
4507
|
+
s = e + 1;
|
|
4508
|
+
}
|
|
4509
|
+
// Determine local name per specifier (after `as` if present)
|
|
4510
|
+
for (let spec of specs) {
|
|
4511
|
+
let local = null;
|
|
4512
|
+
for (let k = spec.start; k < spec.end; k++) {
|
|
4513
|
+
if (tokens[k][0] !== 'IDENTIFIER') continue;
|
|
4514
|
+
if (local === null) local = tokens[k][1];
|
|
4515
|
+
else if (tokens[k - 1]?.[0] === 'AS') local = tokens[k][1];
|
|
4516
|
+
}
|
|
4517
|
+
spec.local = local;
|
|
4518
|
+
spec.drop = local != null && isTypeOnly(local);
|
|
4519
|
+
}
|
|
4520
|
+
let droppedAny = specs.some(s => s.drop);
|
|
4521
|
+
if (!droppedAny) continue;
|
|
4522
|
+
let allDropped = specs.every(s => s.drop);
|
|
4523
|
+
// Outer (default / namespace) local names — needed when all named drop
|
|
4524
|
+
let outerNames = [];
|
|
4525
|
+
if (allDropped && hasOtherSpec) {
|
|
4526
|
+
let k = i + 1;
|
|
4527
|
+
while (k < lbIdx) {
|
|
4528
|
+
if (tokens[k][0] === 'AS' && k + 1 < lbIdx && tokens[k + 1][0] === 'IDENTIFIER') {
|
|
4529
|
+
outerNames.push(tokens[k + 1][1]); k += 2;
|
|
4530
|
+
} else if (tokens[k][0] === 'IDENTIFIER') {
|
|
4531
|
+
outerNames.push(tokens[k][1]); k++;
|
|
4532
|
+
} else { k++; }
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
// Splice from right to left to preserve indices
|
|
4536
|
+
if (allDropped) {
|
|
4537
|
+
if (!hasOtherSpec) {
|
|
4538
|
+
// Drop entire import
|
|
4539
|
+
let end = endIdx < tokens.length ? endIdx + 1 : endIdx;
|
|
4540
|
+
tokens.splice(i, end - i);
|
|
4541
|
+
} else if (outerNames.length > 0 && outerNames.every(isTypeOnly)) {
|
|
4542
|
+
// Outer specifiers are also type-only → drop whole import
|
|
4543
|
+
let end = endIdx < tokens.length ? endIdx + 1 : endIdx;
|
|
4544
|
+
tokens.splice(i, end - i);
|
|
4545
|
+
} else {
|
|
4546
|
+
// Remove `{ ... }` and the comma between default and `{`
|
|
4547
|
+
let removeStart = lbIdx, removeEnd = rbIdx + 1;
|
|
4548
|
+
// Strip a leading comma that separated default from `{`
|
|
4549
|
+
let k = lbIdx - 1;
|
|
4550
|
+
while (k > i && tokens[k][0] !== ',' && tokens[k][0] !== 'IDENTIFIER' && tokens[k][0] !== '*') k--;
|
|
4551
|
+
if (k > i && tokens[k][0] === ',') removeStart = k;
|
|
4552
|
+
tokens.splice(removeStart, removeEnd - removeStart);
|
|
4553
|
+
}
|
|
4554
|
+
} else {
|
|
4555
|
+
// Remove dropped specifiers individually, right-to-left, taking one adjacent comma
|
|
4556
|
+
for (let idx = specs.length - 1; idx >= 0; idx--) {
|
|
4557
|
+
let spec = specs[idx];
|
|
4558
|
+
if (!spec.drop) continue;
|
|
4559
|
+
let removeStart = spec.start, removeEnd = spec.end;
|
|
4560
|
+
// Prefer trailing comma; otherwise take leading comma
|
|
4561
|
+
if (removeEnd < rbIdx && tokens[removeEnd][0] === ',') {
|
|
4562
|
+
removeEnd++;
|
|
4563
|
+
} else if (removeStart > lbIdx + 1 && tokens[removeStart - 1][0] === ',') {
|
|
4564
|
+
removeStart--;
|
|
4565
|
+
}
|
|
4566
|
+
tokens.splice(removeStart, removeEnd - removeStart);
|
|
4567
|
+
}
|
|
3729
4568
|
}
|
|
3730
|
-
if (names.length === 0) continue;
|
|
3731
|
-
// Keep if any name is used at runtime
|
|
3732
|
-
if (names.some(n => usedNames.has(n))) continue;
|
|
3733
|
-
// Only elide if at least one name was used in a type annotation
|
|
3734
|
-
if (!names.some(n => typeRefNames.has(n))) continue;
|
|
3735
|
-
// All imported names are type-only — remove IMPORT through TERMINATOR
|
|
3736
|
-
let end = j;
|
|
3737
|
-
while (end < tokens.length && tokens[end][0] !== 'TERMINATOR') end++;
|
|
3738
|
-
if (end < tokens.length) end++; // include TERMINATOR
|
|
3739
|
-
tokens.splice(i, end - i);
|
|
3740
4569
|
}
|
|
3741
4570
|
}
|
|
3742
4571
|
|
|
@@ -3817,6 +4646,11 @@ export class Compiler {
|
|
|
3817
4646
|
skipDataPart: this.options.skipDataPart,
|
|
3818
4647
|
stubComponents: this.options.stubComponents,
|
|
3819
4648
|
reactiveVars: this.options.reactiveVars,
|
|
4649
|
+
// Emit `name: T` inline on typed params so shadow TS in compileForCheck
|
|
4650
|
+
// sees annotations on every function-like position (top-level arrows,
|
|
4651
|
+
// class methods, object-literal method shorthand, nested functions).
|
|
4652
|
+
// Off by default — only set when producing input for the shadow TS pass.
|
|
4653
|
+
inlineTypes: this.options.inlineTypes,
|
|
3820
4654
|
// Schema runtime mode: 'browser' / 'validate' / 'server' / 'migration'.
|
|
3821
4655
|
// Default 'migration' covers the common case (CLI, server, tests) where
|
|
3822
4656
|
// the user might call any schema feature including .toSQL(). The browser
|
|
@@ -3920,24 +4754,6 @@ export function emit(sexpr, options = {}) {
|
|
|
3920
4754
|
return new CodeEmitter(options).compile(sexpr);
|
|
3921
4755
|
}
|
|
3922
4756
|
|
|
3923
|
-
export function getStdlibCode() {
|
|
3924
|
-
return `\
|
|
3925
|
-
globalThis.abort ??= (msg) => { if (msg) console.error(msg); process.exit(1); };
|
|
3926
|
-
globalThis.assert ??= (v, msg) => { if (!v) throw new Error(msg || "Assertion failed"); };
|
|
3927
|
-
globalThis.exit ??= (code) => process.exit(code || 0);
|
|
3928
|
-
globalThis.kind ??= (v) => v != null ? (v.constructor?.name || Object.prototype.toString.call(v).slice(8, -1)).toLowerCase() : String(v);
|
|
3929
|
-
globalThis.noop ??= () => {};
|
|
3930
|
-
globalThis.p ??= console.log;
|
|
3931
|
-
globalThis.pp ??= (v) => { console.log(JSON.stringify(v, null, 2)); return v; };
|
|
3932
|
-
globalThis.raise ??= (a, b) => { throw (b !== undefined ? new a(b) : new Error(a)); };
|
|
3933
|
-
globalThis.rand ??= (a, b) => b !== undefined ? (a > b && ([a, b] = [b, a]), Math.floor(Math.random() * (b - a + 1) + a)) : a ? Math.floor(Math.random() * a) : Math.random();
|
|
3934
|
-
globalThis.sleep ??= (ms) => new Promise(r => setTimeout(r, ms));
|
|
3935
|
-
globalThis.todo ??= (msg) => { throw new Error(msg || "Not implemented"); };
|
|
3936
|
-
globalThis.warn ??= console.warn;
|
|
3937
|
-
globalThis.zip ??= (...a) => a[0].map((_, i) => a.map(b => b[i]));
|
|
3938
|
-
`;
|
|
3939
|
-
}
|
|
3940
|
-
|
|
3941
4757
|
export function getReactiveRuntime() {
|
|
3942
4758
|
return new CodeEmitter({}).getReactiveRuntime();
|
|
3943
4759
|
}
|
|
@@ -3947,4 +4763,5 @@ export function getComponentRuntime() {
|
|
|
3947
4763
|
}
|
|
3948
4764
|
|
|
3949
4765
|
export { formatSExpr };
|
|
4766
|
+
export { stringify, getStdlibCode };
|
|
3950
4767
|
export { RipError, toRipError, formatError, formatErrorHTML } from './error.js';
|