rip-lang 3.13.134 → 3.13.135
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/CHANGELOG.md +0 -1
- package/README.md +4 -7
- package/bin/rip +16 -4
- package/docs/RIP-LANG.md +0 -42
- package/docs/RIP-TYPES.md +47 -52
- package/docs/demo.html +2 -2
- package/docs/dist/rip.js +1585 -826
- package/docs/dist/rip.min.js +149 -137
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/rip-loader.js +2 -2
- package/src/AGENTS.md +1 -2
- package/src/browser.js +5 -5
- package/src/compiler.js +351 -30
- package/src/components.js +176 -11
- package/src/error.js +250 -0
- package/src/grammar/grammar.rip +2 -12
- package/src/lexer.js +15 -11
- package/src/parser.js +220 -223
- package/src/repl.js +3 -2
- package/src/sourcemap-utils.js +39 -6
- package/src/typecheck.js +312 -80
- package/src/types.js +226 -51
- package/src/ui.rip +4 -0
package/src/compiler.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parser } from './parser.js';
|
|
|
13
13
|
import { installComponentSupport } from './components.js';
|
|
14
14
|
import { emitTypes, emitEnum } from './types.js';
|
|
15
15
|
import { SourceMapGenerator } from './sourcemaps.js';
|
|
16
|
+
import { RipError, toRipError } from './error.js';
|
|
16
17
|
|
|
17
18
|
// =============================================================================
|
|
18
19
|
// Metadata helpers — isolate all new String() awareness here
|
|
@@ -28,7 +29,7 @@ let str = (node) => node instanceof String ? node.valueOf() : node;
|
|
|
28
29
|
let INLINE_FORMS = new Set([
|
|
29
30
|
'+', '-', '*', '/', '%', '//', '%%', '**',
|
|
30
31
|
'==', '!=', '<', '>', '<=', '>=', '===', '!==',
|
|
31
|
-
'&&', '||', '??', '
|
|
32
|
+
'&&', '||', '??', 'not',
|
|
32
33
|
'&', '|', '^', '<<', '>>', '>>>',
|
|
33
34
|
'=', '.', '?.', '[]',
|
|
34
35
|
'!', 'typeof', 'void', 'delete', 'new',
|
|
@@ -149,7 +150,7 @@ export class CodeEmitter {
|
|
|
149
150
|
'==': 'emitBinaryOp', '===': 'emitBinaryOp', '!=': 'emitBinaryOp',
|
|
150
151
|
'!==': 'emitBinaryOp', '<': 'emitBinaryOp', '>': 'emitBinaryOp',
|
|
151
152
|
'<=': 'emitBinaryOp', '>=': 'emitBinaryOp', '??': 'emitBinaryOp',
|
|
152
|
-
'
|
|
153
|
+
'&': 'emitBinaryOp', '|': 'emitBinaryOp',
|
|
153
154
|
'^': 'emitBinaryOp', '<<': 'emitBinaryOp', '>>': 'emitBinaryOp',
|
|
154
155
|
'>>>': 'emitBinaryOp',
|
|
155
156
|
|
|
@@ -212,7 +213,6 @@ export class CodeEmitter {
|
|
|
212
213
|
'break': 'emitBreak',
|
|
213
214
|
'continue': 'emitContinue',
|
|
214
215
|
'?': 'emitExistential',
|
|
215
|
-
'defined': 'emitDefined',
|
|
216
216
|
'presence': 'emitPresence',
|
|
217
217
|
'?:': 'emitTernary',
|
|
218
218
|
'|>': 'emitPipe',
|
|
@@ -277,6 +277,11 @@ export class CodeEmitter {
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
// Throw a RipError with source location from the nearest s-expression node
|
|
281
|
+
error(message, sexpr, { suggestion } = {}) {
|
|
282
|
+
throw RipError.fromSExpr(message, sexpr, this.options.source, this.options.filename, suggestion);
|
|
283
|
+
}
|
|
284
|
+
|
|
280
285
|
// ---------------------------------------------------------------------------
|
|
281
286
|
// Entry point
|
|
282
287
|
// ---------------------------------------------------------------------------
|
|
@@ -371,8 +376,12 @@ export class CodeEmitter {
|
|
|
371
376
|
}
|
|
372
377
|
if (ident) result.push({ name: ident, origLine: node.loc.r, origCol: node.loc.c });
|
|
373
378
|
}
|
|
374
|
-
// Recurse into children (skip head at index 0 — already processed via parent)
|
|
375
|
-
|
|
379
|
+
// Recurse into children (skip head at index 0 — already processed via parent).
|
|
380
|
+
// For arrow functions (-> / =>), skip index 1 (params array) — parameter
|
|
381
|
+
// names are not call expressions and would produce incorrect mappings via
|
|
382
|
+
// the distance heuristic when the same identifier appears in other methods.
|
|
383
|
+
let start = (head === '->' || head === '=>') ? 2 : 1;
|
|
384
|
+
for (let i = start; i < node.length; i++) {
|
|
376
385
|
if (Array.isArray(node[i])) this.collectSubExprs(node[i], result);
|
|
377
386
|
}
|
|
378
387
|
}
|
|
@@ -566,7 +575,7 @@ export class CodeEmitter {
|
|
|
566
575
|
|
|
567
576
|
if (typeof sexpr === 'number') return String(sexpr);
|
|
568
577
|
if (sexpr === null || sexpr === undefined) return 'null';
|
|
569
|
-
if (!Array.isArray(sexpr))
|
|
578
|
+
if (!Array.isArray(sexpr)) this.error(`Invalid s-expression: ${JSON.stringify(sexpr)}`, sexpr);
|
|
570
579
|
|
|
571
580
|
let [head, ...rest] = sexpr;
|
|
572
581
|
|
|
@@ -636,7 +645,7 @@ export class CodeEmitter {
|
|
|
636
645
|
return needsAwait ? `await ${callStr}` : callStr;
|
|
637
646
|
}
|
|
638
647
|
|
|
639
|
-
|
|
648
|
+
this.error(`Unknown s-expression type: ${head}`, sexpr);
|
|
640
649
|
}
|
|
641
650
|
|
|
642
651
|
// ---------------------------------------------------------------------------
|
|
@@ -654,6 +663,17 @@ export class CodeEmitter {
|
|
|
654
663
|
else body.push(stmt);
|
|
655
664
|
}
|
|
656
665
|
|
|
666
|
+
// Classify program-level variables for inline declarations
|
|
667
|
+
let prevInlinePending = this._inlineVarsPending;
|
|
668
|
+
let programInlineVars = new Set();
|
|
669
|
+
if (this.programVars.size > 0 && body.length > 0) {
|
|
670
|
+
let classified = this.classifyVarsForInlining(body, this.programVars);
|
|
671
|
+
programInlineVars = classified.inlineVars;
|
|
672
|
+
// '_' is always emitted as 'var _' (not 'let'), so must not be inlined
|
|
673
|
+
programInlineVars.delete('_');
|
|
674
|
+
if (programInlineVars.size > 0) this._inlineVarsPending = new Set(programInlineVars);
|
|
675
|
+
}
|
|
676
|
+
|
|
657
677
|
// Generate body first to detect needed helpers
|
|
658
678
|
let blockStmts = ['def', 'class', 'if', 'for-in', 'for-of', 'for-as', 'while', 'loop', 'switch', 'try'];
|
|
659
679
|
let stmtEntries = body.map((stmt, index) => {
|
|
@@ -680,6 +700,8 @@ export class CodeEmitter {
|
|
|
680
700
|
});
|
|
681
701
|
let statementsCode = stmtEntries.map(e => e.code).join('\n');
|
|
682
702
|
|
|
703
|
+
this._inlineVarsPending = prevInlinePending;
|
|
704
|
+
|
|
683
705
|
let needsBlank = false;
|
|
684
706
|
|
|
685
707
|
if (imports.length > 0) {
|
|
@@ -691,10 +713,12 @@ export class CodeEmitter {
|
|
|
691
713
|
let hasUnderscore = this.programVars.has('_');
|
|
692
714
|
if (hasUnderscore) this.programVars.delete('_');
|
|
693
715
|
if (this.programVars.size > 0) {
|
|
694
|
-
let vars = Array.from(this.programVars).sort().join(', ');
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
|
|
716
|
+
let vars = Array.from(this.programVars).filter(v => !programInlineVars.has(v)).sort().join(', ');
|
|
717
|
+
if (vars) {
|
|
718
|
+
if (needsBlank) code += '\n';
|
|
719
|
+
code += `let ${vars};\n`;
|
|
720
|
+
needsBlank = true;
|
|
721
|
+
}
|
|
698
722
|
}
|
|
699
723
|
if (hasUnderscore) {
|
|
700
724
|
if (needsBlank) code += '\n';
|
|
@@ -803,10 +827,6 @@ export class CodeEmitter {
|
|
|
803
827
|
return `((${a} ${leftOp} ${b}) && (${b} ${op} ${c}))`;
|
|
804
828
|
}
|
|
805
829
|
}
|
|
806
|
-
if (op === '!?') {
|
|
807
|
-
let l = this.emit(left, 'value'), r = this.emit(right, 'value');
|
|
808
|
-
return `(${l} !== undefined ? ${l} : ${r})`;
|
|
809
|
-
}
|
|
810
830
|
if (op === '==') op = '===';
|
|
811
831
|
if (op === '!=') op = '!==';
|
|
812
832
|
return `(${this.emit(left, 'value')} ${op} ${this.emit(right, 'value')})`;
|
|
@@ -860,7 +880,7 @@ export class CodeEmitter {
|
|
|
860
880
|
let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
|
|
861
881
|
if (target instanceof String && meta(target, 'await') !== undefined && !isFnValue) {
|
|
862
882
|
let sigil = meta(target, 'await') === true ? '!' : '&';
|
|
863
|
-
|
|
883
|
+
this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
|
|
864
884
|
}
|
|
865
885
|
|
|
866
886
|
if (target instanceof String && meta(target, 'await') === true && isFnValue) {
|
|
@@ -891,6 +911,14 @@ export class CodeEmitter {
|
|
|
891
911
|
if (ctrlOp === '||') return `(() => { const __v = ${exprCode}; if (!__v) ${ctrlCode}; return (${targetCode} = __v); })()`;
|
|
892
912
|
return `(() => { const __v = ${exprCode}; if (__v) ${ctrlCode}; return (${targetCode} = __v); })()`;
|
|
893
913
|
}
|
|
914
|
+
// Inline `let` for control flow — split into declaration + guard
|
|
915
|
+
let tgtName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
|
|
916
|
+
if (tgtName && context === 'statement' && this._inlineVarsPending?.delete(tgtName)) {
|
|
917
|
+
let ind = this.indent();
|
|
918
|
+
if (ctrlOp === '??') return `let ${targetCode} = ${exprCode};\n${ind}if (${targetCode} == null) ${ctrlCode}`;
|
|
919
|
+
if (ctrlOp === '||') return `let ${targetCode} = ${exprCode};\n${ind}if (!${targetCode}) ${ctrlCode}`;
|
|
920
|
+
return `let ${targetCode} = ${exprCode};\n${ind}if (${targetCode}) ${ctrlCode}`;
|
|
921
|
+
}
|
|
894
922
|
if (ctrlOp === '??') return `if ((${targetCode} = ${exprCode}) == null) ${ctrlCode}`;
|
|
895
923
|
if (ctrlOp === '||') return `if (!(${targetCode} = ${exprCode})) ${ctrlCode}`;
|
|
896
924
|
return `if ((${targetCode} = ${exprCode})) ${ctrlCode}`;
|
|
@@ -934,6 +962,8 @@ export class CodeEmitter {
|
|
|
934
962
|
let unwrapped = Array.isArray(wrappedValue) && wrappedValue.length === 1 ? wrappedValue[0] : wrappedValue;
|
|
935
963
|
let fullValue = [binOp, left, unwrapped];
|
|
936
964
|
let t = this.emit(target, 'value'), c = this.emit(condition, 'value'), v = this.emit(fullValue, 'value');
|
|
965
|
+
let tgtName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
|
|
966
|
+
if (tgtName && this._inlineVarsPending?.delete(tgtName)) return `let ${t};\n${this.indent()}if (${c}) ${t} = ${v}`;
|
|
937
967
|
return `if (${c}) ${t} = ${v}`;
|
|
938
968
|
}
|
|
939
969
|
}
|
|
@@ -948,6 +978,8 @@ export class CodeEmitter {
|
|
|
948
978
|
let t = this.emit(target, 'value');
|
|
949
979
|
let condCode = this.unwrapLogical(this.emit(condition, 'value'));
|
|
950
980
|
let v = this.emit(unwrapped, 'value');
|
|
981
|
+
let tgtName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
|
|
982
|
+
if (tgtName && this._inlineVarsPending?.delete(tgtName)) return `let ${t};\n${this.indent()}if (${condCode}) ${t} = ${v}`;
|
|
951
983
|
return `if (${condCode}) ${t} = ${v}`;
|
|
952
984
|
}
|
|
953
985
|
}
|
|
@@ -963,12 +995,23 @@ export class CodeEmitter {
|
|
|
963
995
|
}
|
|
964
996
|
|
|
965
997
|
const prevComponentName = this._componentName;
|
|
966
|
-
|
|
998
|
+
const prevComponentTypeParams = this._componentTypeParams;
|
|
999
|
+
if (this.is(value, 'component') && (typeof target === 'string' || target instanceof String)) {
|
|
1000
|
+
this._componentName = str(target);
|
|
1001
|
+
this._componentTypeParams = target.typeParams || '';
|
|
1002
|
+
}
|
|
967
1003
|
let valueCode = this.emit(value, 'value');
|
|
968
1004
|
this._componentName = prevComponentName;
|
|
1005
|
+
this._componentTypeParams = prevComponentTypeParams;
|
|
969
1006
|
let isObjLit = this.is(value, 'object');
|
|
970
1007
|
if (!isObjLit) valueCode = this.unwrap(valueCode);
|
|
971
1008
|
|
|
1009
|
+
// Inline `let` at first assignment for variables classified as inlinable
|
|
1010
|
+
let targetName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
|
|
1011
|
+
if (head === '=' && targetName && context === 'statement' && this._inlineVarsPending?.delete(targetName)) {
|
|
1012
|
+
return `let ${targetCode} = ${valueCode}`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
972
1015
|
let needsParensVal = context === 'value';
|
|
973
1016
|
let needsParensObj = context === 'statement' && this.is(target, 'object');
|
|
974
1017
|
if (needsParensVal || needsParensObj) return `(${targetCode} ${op} ${valueCode})`;
|
|
@@ -1143,7 +1186,7 @@ export class CodeEmitter {
|
|
|
1143
1186
|
if (rest.length === 0) return 'return';
|
|
1144
1187
|
let [expr] = rest;
|
|
1145
1188
|
if (this.sideEffectOnly && !(this.is(expr, '->') || this.is(expr, '=>'))) {
|
|
1146
|
-
|
|
1189
|
+
this.error('Cannot return a value from a void function (declared with !)', sexpr);
|
|
1147
1190
|
}
|
|
1148
1191
|
|
|
1149
1192
|
if (this.is(expr, 'if')) {
|
|
@@ -1220,10 +1263,6 @@ export class CodeEmitter {
|
|
|
1220
1263
|
return `(${this.emit(rest[0], 'value')} != null)`;
|
|
1221
1264
|
}
|
|
1222
1265
|
|
|
1223
|
-
emitDefined(head, rest) {
|
|
1224
|
-
return `(${this.emit(rest[0], 'value')} !== undefined)`;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
1266
|
emitPresence(head, rest) {
|
|
1228
1267
|
return `(${this.emit(rest[0], 'value')} ? true : undefined)`;
|
|
1229
1268
|
}
|
|
@@ -1826,7 +1865,7 @@ export class CodeEmitter {
|
|
|
1826
1865
|
return switchBody;
|
|
1827
1866
|
}
|
|
1828
1867
|
|
|
1829
|
-
emitWhen() {
|
|
1868
|
+
emitWhen(head, rest, context, sexpr) { this.error('when clause should be handled by switch', sexpr); }
|
|
1830
1869
|
|
|
1831
1870
|
// ---------------------------------------------------------------------------
|
|
1832
1871
|
// Comprehensions
|
|
@@ -2153,9 +2192,14 @@ export class CodeEmitter {
|
|
|
2153
2192
|
if (this.options.skipExports) {
|
|
2154
2193
|
if (this.is(decl, '=')) {
|
|
2155
2194
|
const prev = this._componentName;
|
|
2156
|
-
|
|
2195
|
+
const prevTP = this._componentTypeParams;
|
|
2196
|
+
if (this.is(decl[2], 'component')) {
|
|
2197
|
+
this._componentName = str(decl[1]);
|
|
2198
|
+
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2199
|
+
}
|
|
2157
2200
|
const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2158
2201
|
this._componentName = prev;
|
|
2202
|
+
this._componentTypeParams = prevTP;
|
|
2159
2203
|
return result;
|
|
2160
2204
|
}
|
|
2161
2205
|
if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return '';
|
|
@@ -2163,9 +2207,14 @@ export class CodeEmitter {
|
|
|
2163
2207
|
}
|
|
2164
2208
|
if (this.is(decl, '=')) {
|
|
2165
2209
|
const prev = this._componentName;
|
|
2166
|
-
|
|
2210
|
+
const prevTP = this._componentTypeParams;
|
|
2211
|
+
if (this.is(decl[2], 'component')) {
|
|
2212
|
+
this._componentName = str(decl[1]);
|
|
2213
|
+
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2214
|
+
}
|
|
2167
2215
|
const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2168
2216
|
this._componentName = prev;
|
|
2217
|
+
this._componentTypeParams = prevTP;
|
|
2169
2218
|
return result;
|
|
2170
2219
|
}
|
|
2171
2220
|
if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return `export { ${decl.join(', ')} }`;
|
|
@@ -2393,9 +2442,19 @@ export class CodeEmitter {
|
|
|
2393
2442
|
this.restMiddleParam = null;
|
|
2394
2443
|
}
|
|
2395
2444
|
|
|
2445
|
+
// Classify variables: inline `let` at first assignment vs hoist to top
|
|
2446
|
+
let prevInlinePending = this._inlineVarsPending;
|
|
2447
|
+
let inlineVars = new Set();
|
|
2448
|
+
if (newVars.size > 0 && statements.length > 0) {
|
|
2449
|
+
let classified = this.classifyVarsForInlining(statements, newVars);
|
|
2450
|
+
inlineVars = classified.inlineVars;
|
|
2451
|
+
if (inlineVars.size > 0) this._inlineVarsPending = new Set(inlineVars);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2396
2454
|
this.indentLevel++;
|
|
2397
2455
|
let code = '{\n';
|
|
2398
|
-
|
|
2456
|
+
let hoistVars = new Set([...newVars].filter(v => !inlineVars.has(v)));
|
|
2457
|
+
if (hoistVars.size > 0) code += this.indent() + `let ${Array.from(hoistVars).sort().join(', ')};\n`;
|
|
2399
2458
|
|
|
2400
2459
|
let firstIsSuper = autoAssignments.length > 0 && statements.length > 0 &&
|
|
2401
2460
|
Array.isArray(statements[0]) && statements[0][0] === 'super';
|
|
@@ -2426,6 +2485,8 @@ export class CodeEmitter {
|
|
|
2426
2485
|
if (typeof target === 'string' && Array.isArray(value)) {
|
|
2427
2486
|
let vh = value[0];
|
|
2428
2487
|
if (vh === 'comprehension' || vh === 'for-in') {
|
|
2488
|
+
// Comprehension targets with inline vars: declare before the loop
|
|
2489
|
+
if (this._inlineVarsPending?.delete(target)) code += this.indent() + `let ${target};\n`;
|
|
2429
2490
|
this.comprehensionTarget = target;
|
|
2430
2491
|
code += this.emit(value, 'value');
|
|
2431
2492
|
this.comprehensionTarget = null;
|
|
@@ -2433,6 +2494,14 @@ export class CodeEmitter {
|
|
|
2433
2494
|
return;
|
|
2434
2495
|
}
|
|
2435
2496
|
}
|
|
2497
|
+
// Handle inline vars at last statement (auto-return): split into let + return
|
|
2498
|
+
if ((typeof target === 'string' || target instanceof String) && this._inlineVarsPending?.has(str(target))) {
|
|
2499
|
+
this._inlineVarsPending.delete(str(target));
|
|
2500
|
+
let assignCode = this.emit(stmt, 'statement');
|
|
2501
|
+
code += this.indent() + 'let ' + this.addSemicolon(stmt, assignCode) + '\n';
|
|
2502
|
+
code += this.indent() + `return ${str(target)};\n`;
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2436
2505
|
}
|
|
2437
2506
|
|
|
2438
2507
|
let needsReturn = !isConstructor && !sideEffectOnly && isLast &&
|
|
@@ -2461,6 +2530,7 @@ export class CodeEmitter {
|
|
|
2461
2530
|
if (!noRetStmts.includes(lastH)) code += this.indent() + 'return;\n';
|
|
2462
2531
|
}
|
|
2463
2532
|
|
|
2533
|
+
this._inlineVarsPending = prevInlinePending;
|
|
2464
2534
|
this.indentLevel--;
|
|
2465
2535
|
code += this.indent() + '}';
|
|
2466
2536
|
this.scopeStack.pop();
|
|
@@ -3093,6 +3163,197 @@ export class CodeEmitter {
|
|
|
3093
3163
|
return false;
|
|
3094
3164
|
}
|
|
3095
3165
|
|
|
3166
|
+
// ---------------------------------------------------------------------------
|
|
3167
|
+
// Variable inlining — emit `let` at first assignment instead of hoisting
|
|
3168
|
+
// ---------------------------------------------------------------------------
|
|
3169
|
+
|
|
3170
|
+
// Check if an s-expression references a variable name (stopping at function boundaries)
|
|
3171
|
+
referencesVar(sexpr, varName) {
|
|
3172
|
+
if (!sexpr) return false;
|
|
3173
|
+
if (sexpr instanceof String) return str(sexpr) === varName;
|
|
3174
|
+
if (typeof sexpr === 'string') return sexpr === varName;
|
|
3175
|
+
if (!Array.isArray(sexpr)) return false;
|
|
3176
|
+
let h = sexpr[0];
|
|
3177
|
+
let hs = (typeof h === 'string') ? h : (h instanceof String) ? str(h) : null;
|
|
3178
|
+
if (hs === 'def' || hs === '->' || hs === '=>' || hs === 'effect') return false;
|
|
3179
|
+
// Property access: only check the object, not the property name
|
|
3180
|
+
if (hs === '.' || hs === '?.') return this.referencesVar(sexpr[1], varName);
|
|
3181
|
+
// Object literal: check values but not simple string keys
|
|
3182
|
+
if (hs === 'object') {
|
|
3183
|
+
for (let i = 1; i < sexpr.length; i++) {
|
|
3184
|
+
let pair = sexpr[i];
|
|
3185
|
+
if (Array.isArray(pair)) {
|
|
3186
|
+
if (this.is(pair, '...')) { if (this.referencesVar(pair[1], varName)) return true; }
|
|
3187
|
+
else if (pair.length >= 2) { if (this.referencesVar(pair[pair.length - 1], varName)) return true; }
|
|
3188
|
+
} else { if (this.referencesVar(pair, varName)) return true; }
|
|
3189
|
+
}
|
|
3190
|
+
return false;
|
|
3191
|
+
}
|
|
3192
|
+
return sexpr.some(item => this.referencesVar(item, varName));
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// Check if the first reference to a variable in DFS order is a `=` assignment
|
|
3196
|
+
// at statement level (not inside a value expression like another assignment's RHS).
|
|
3197
|
+
// Returns true only when it's safe to emit `let` at the first assignment site.
|
|
3198
|
+
firstRefIsAssignment(sexpr, varName) {
|
|
3199
|
+
let result = null; // null = not found, 'write' = statement-level assignment, 'read' = read/value
|
|
3200
|
+
let isVar = (n) => (n instanceof String ? str(n) : n) === varName;
|
|
3201
|
+
let walk = (node, inValue) => {
|
|
3202
|
+
if (result !== null) return;
|
|
3203
|
+
if (!node) return;
|
|
3204
|
+
if (!Array.isArray(node)) { if (isVar(node)) result = 'read'; return; }
|
|
3205
|
+
let h = node[0];
|
|
3206
|
+
let hs = (typeof h === 'string') ? h : (h instanceof String) ? str(h) : null;
|
|
3207
|
+
if (hs === 'def' || hs === '->' || hs === '=>' || hs === 'effect') return;
|
|
3208
|
+
// Assignment to our variable
|
|
3209
|
+
if (hs === '=' && (typeof node[1] === 'string' || node[1] instanceof String) && str(node[1]) === varName) {
|
|
3210
|
+
if (inValue) { result = 'read'; return; } // In value context — can't emit `let` here
|
|
3211
|
+
result = this.referencesVar(node[2], varName) ? 'read' : 'write';
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
// Compound assignment reads the variable
|
|
3215
|
+
if (CodeEmitter.ASSIGNMENT_OPS.has(hs) && hs !== '=' &&
|
|
3216
|
+
(typeof node[1] === 'string' || node[1] instanceof String) && str(node[1]) === varName) {
|
|
3217
|
+
result = 'read'; return;
|
|
3218
|
+
}
|
|
3219
|
+
// Property access: only check object
|
|
3220
|
+
if (hs === '.' || hs === '?.') { walk(node[1], inValue); return; }
|
|
3221
|
+
// Object literal: skip simple string keys
|
|
3222
|
+
if (hs === 'object') {
|
|
3223
|
+
for (let i = 1; i < node.length; i++) {
|
|
3224
|
+
let pair = node[i];
|
|
3225
|
+
if (Array.isArray(pair)) {
|
|
3226
|
+
if (this.is(pair, '...')) walk(pair[1], true);
|
|
3227
|
+
else if (pair.length >= 2) walk(pair[pair.length - 1], true);
|
|
3228
|
+
} else walk(pair, true);
|
|
3229
|
+
}
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
// Block: children are at statement level
|
|
3233
|
+
if (hs === 'block') { for (let i = 1; i < node.length; i++) walk(node[i], false); return; }
|
|
3234
|
+
// If: condition is value, branches maintain parent context
|
|
3235
|
+
if (hs === 'if') { walk(node[1], true); for (let i = 2; i < node.length; i++) walk(node[i], inValue); return; }
|
|
3236
|
+
// Loops: iterable/condition is value, body is statement
|
|
3237
|
+
if (hs === 'for-in' || hs === 'for-of' || hs === 'for-as') { walk(node[2], true); if (node.length > 3) walk(node[node.length - 1], false); return; }
|
|
3238
|
+
if (hs === 'while') { walk(node[1], true); walk(node[2], false); return; }
|
|
3239
|
+
// Try: blocks are statement level
|
|
3240
|
+
if (hs === 'try') { for (let i = 1; i < node.length; i++) walk(node[i], false); return; }
|
|
3241
|
+
// Other assignments: RHS is value context
|
|
3242
|
+
if (CodeEmitter.ASSIGNMENT_OPS.has(hs)) { walk(node[2], true); return; }
|
|
3243
|
+
// Default: everything else is value context
|
|
3244
|
+
for (let i = 0; i < node.length; i++) walk(node[i], true);
|
|
3245
|
+
};
|
|
3246
|
+
walk(sexpr, false); // top of a statement is statement context
|
|
3247
|
+
return result === 'write';
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// Check if all references to a variable within a statement are contained in a
|
|
3251
|
+
// single block child. Returns false if the variable appears at multiple nesting
|
|
3252
|
+
// levels (e.g., inside an if-branch AND at the same level as the if).
|
|
3253
|
+
allRefsInSingleBlock(sexpr, varName) {
|
|
3254
|
+
if (!Array.isArray(sexpr)) return false;
|
|
3255
|
+
let h = sexpr[0];
|
|
3256
|
+
let hs = (typeof h === 'string') ? h : (h instanceof String) ? str(h) : null;
|
|
3257
|
+
// For 'if': check condition + each branch. Variable must appear in exactly one branch,
|
|
3258
|
+
// and NOT in the condition. Then recurse into that branch.
|
|
3259
|
+
if (hs === 'if') {
|
|
3260
|
+
if (this.referencesVar(sexpr[1], varName)) return false; // in condition
|
|
3261
|
+
let branchCount = 0;
|
|
3262
|
+
let refBranch = null;
|
|
3263
|
+
for (let i = 2; i < sexpr.length; i++) {
|
|
3264
|
+
if (this.referencesVar(sexpr[i], varName)) { branchCount++; refBranch = sexpr[i]; }
|
|
3265
|
+
}
|
|
3266
|
+
if (branchCount !== 1) return false;
|
|
3267
|
+
return this.allRefsInSingleBlock(refBranch, varName);
|
|
3268
|
+
}
|
|
3269
|
+
// For 'block': variable must appear in only one child statement.
|
|
3270
|
+
// Then recurse into that child.
|
|
3271
|
+
if (hs === 'block') {
|
|
3272
|
+
let childCount = 0;
|
|
3273
|
+
let refChild = null;
|
|
3274
|
+
for (let i = 1; i < sexpr.length; i++) {
|
|
3275
|
+
if (this.referencesVar(sexpr[i], varName)) { childCount++; refChild = sexpr[i]; }
|
|
3276
|
+
}
|
|
3277
|
+
if (childCount !== 1) return false;
|
|
3278
|
+
// If the single child IS a direct assignment to this variable, we've bottomed out — safe.
|
|
3279
|
+
if (Array.isArray(refChild) && refChild[0] === '=' &&
|
|
3280
|
+
(typeof refChild[1] === 'string' || refChild[1] instanceof String) &&
|
|
3281
|
+
str(refChild[1]) === varName) return true;
|
|
3282
|
+
return this.allRefsInSingleBlock(refChild, varName);
|
|
3283
|
+
}
|
|
3284
|
+
// For loops: iterable/condition must not reference var, body is a block — recurse.
|
|
3285
|
+
if (hs === 'for-in' || hs === 'for-of' || hs === 'for-as') {
|
|
3286
|
+
if (this.referencesVar(sexpr[2], varName)) return false;
|
|
3287
|
+
return true;
|
|
3288
|
+
}
|
|
3289
|
+
if (hs === 'while') {
|
|
3290
|
+
if (this.referencesVar(sexpr[1], varName)) return false;
|
|
3291
|
+
return true;
|
|
3292
|
+
}
|
|
3293
|
+
// For try: recurse
|
|
3294
|
+
if (hs === 'try') return true;
|
|
3295
|
+
return false;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// Classify variables into those that can have `let` inlined at their first assignment
|
|
3299
|
+
// vs those that must be hoisted to the function/program top.
|
|
3300
|
+
classifyVarsForInlining(statements, vars) {
|
|
3301
|
+
if (vars.size === 0) return { inlineVars: new Set(), hoistVars: new Set(vars) };
|
|
3302
|
+
|
|
3303
|
+
// Pre-scan: variables with type annotations (::) must be hoisted. The DTS header
|
|
3304
|
+
// declares them as `let x: Type;` — inlining `let x = value;` in the body would
|
|
3305
|
+
// create a duplicate declaration, causing TS2454 ("used before being assigned").
|
|
3306
|
+
let typedVars = new Set();
|
|
3307
|
+
let findTypedAssigns = (node) => {
|
|
3308
|
+
if (!Array.isArray(node)) return;
|
|
3309
|
+
if (node[0] === '=' && node[1] instanceof String && node[1].type && vars.has(str(node[1]))) {
|
|
3310
|
+
typedVars.add(str(node[1]));
|
|
3311
|
+
}
|
|
3312
|
+
for (let i = 1; i < node.length; i++) findTypedAssigns(node[i]);
|
|
3313
|
+
};
|
|
3314
|
+
for (let stmt of statements) findTypedAssigns(stmt);
|
|
3315
|
+
|
|
3316
|
+
// For each variable, find which statement indices reference it
|
|
3317
|
+
let varStmts = new Map();
|
|
3318
|
+
for (let v of vars) varStmts.set(v, []);
|
|
3319
|
+
for (let i = 0; i < statements.length; i++) {
|
|
3320
|
+
for (let v of vars) {
|
|
3321
|
+
if (this.referencesVar(statements[i], v)) varStmts.get(v).push(i);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
let inlineVars = new Set(), hoistVars = new Set();
|
|
3325
|
+
for (let [v, indices] of varStmts) {
|
|
3326
|
+
if (indices.length === 0) { hoistVars.add(v); continue; }
|
|
3327
|
+
// Typed variables must be hoisted — DTS header already declares them
|
|
3328
|
+
if (typedVars.has(v)) { hoistVars.add(v); continue; }
|
|
3329
|
+
let firstIdx = indices[0];
|
|
3330
|
+
let firstStmt = statements[firstIdx];
|
|
3331
|
+
// Check if the first statement containing this variable is a direct `=` assignment to it
|
|
3332
|
+
let isDirectAssign = Array.isArray(firstStmt) && firstStmt[0] === '=' &&
|
|
3333
|
+
(typeof firstStmt[1] === 'string' || firstStmt[1] instanceof String) &&
|
|
3334
|
+
str(firstStmt[1]) === v &&
|
|
3335
|
+
!this.referencesVar(firstStmt[2], v);
|
|
3336
|
+
if (isDirectAssign) {
|
|
3337
|
+
// First reference is a direct body-level assignment — safe to inline `let` here
|
|
3338
|
+
// (same scope as hoisted `let`, but TS can infer the type from the initializer)
|
|
3339
|
+
inlineVars.add(v);
|
|
3340
|
+
} else if (indices.length === 1 && !this.is(firstStmt, 'switch') &&
|
|
3341
|
+
this.allRefsInSingleBlock(firstStmt, v) &&
|
|
3342
|
+
this.firstRefIsAssignment(firstStmt, v)) {
|
|
3343
|
+
// All references are within a single block branch of a non-switch statement
|
|
3344
|
+
// (e.g., all inside one if-branch, one for body, etc.) and the first DFS
|
|
3345
|
+
// reference is a direct assignment. JS 'let' block scoping is safe here
|
|
3346
|
+
// because the variable never escapes that single block.
|
|
3347
|
+
inlineVars.add(v);
|
|
3348
|
+
} else {
|
|
3349
|
+
// Variable spans multiple statements, is in a switch, or first ref is a read.
|
|
3350
|
+
// Must hoist to ensure correct scoping.
|
|
3351
|
+
hoistVars.add(v);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
return { inlineVars, hoistVars };
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3096
3357
|
// Class helpers
|
|
3097
3358
|
extractMemberName(mk) {
|
|
3098
3359
|
if (this.is(mk, '.') && mk[1] === 'this') return mk[2];
|
|
@@ -3362,7 +3623,12 @@ export class Compiler {
|
|
|
3362
3623
|
|
|
3363
3624
|
// Step 1: Tokenize (includes rewriteTypes() via installTypeSupport)
|
|
3364
3625
|
let lexer = new Lexer();
|
|
3365
|
-
let tokens
|
|
3626
|
+
let tokens;
|
|
3627
|
+
try {
|
|
3628
|
+
tokens = lexer.tokenize(source);
|
|
3629
|
+
} catch (err) {
|
|
3630
|
+
throw toRipError(err, source, this.options.filename);
|
|
3631
|
+
}
|
|
3366
3632
|
if (this.options.showTokens) {
|
|
3367
3633
|
tokens.forEach(t => console.log(`${t[0].padEnd(12)} ${JSON.stringify(t[1])}`));
|
|
3368
3634
|
console.log();
|
|
@@ -3378,6 +3644,46 @@ export class Compiler {
|
|
|
3378
3644
|
// Remove TYPE_DECL markers — the parser doesn't know about them
|
|
3379
3645
|
tokens = tokens.filter(t => t[0] !== 'TYPE_DECL');
|
|
3380
3646
|
|
|
3647
|
+
// Elide type-only imports — after type stripping, imported names that were
|
|
3648
|
+
// only used in type annotations no longer appear in the token stream.
|
|
3649
|
+
// Only elide when at least one name was consumed by type annotation stripping.
|
|
3650
|
+
if (lexer.typeRefNames?.size > 0) {
|
|
3651
|
+
let typeRefNames = lexer.typeRefNames;
|
|
3652
|
+
let usedNames = new Set();
|
|
3653
|
+
let inImport = false;
|
|
3654
|
+
for (let t of tokens) {
|
|
3655
|
+
if (t[0] === 'IMPORT') { inImport = true; continue; }
|
|
3656
|
+
if (inImport && t[0] === 'TERMINATOR') { inImport = false; continue; }
|
|
3657
|
+
if (inImport) continue;
|
|
3658
|
+
if (t[0] === 'IDENTIFIER') usedNames.add(t[1]);
|
|
3659
|
+
}
|
|
3660
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
3661
|
+
if (tokens[i][0] !== 'IMPORT') continue;
|
|
3662
|
+
let j = i + 1;
|
|
3663
|
+
if (j >= tokens.length) continue;
|
|
3664
|
+
// Skip dynamic imports: import(expr)
|
|
3665
|
+
if (tokens[j][0] === 'CALL_START' || tokens[j][0] === '(') continue;
|
|
3666
|
+
// Skip side-effect imports: import 'module'
|
|
3667
|
+
if (tokens[j][0] === 'STRING') continue;
|
|
3668
|
+
// Collect imported names between IMPORT and FROM
|
|
3669
|
+
let names = [];
|
|
3670
|
+
while (j < tokens.length && tokens[j][0] !== 'FROM' && tokens[j][0] !== 'TERMINATOR') {
|
|
3671
|
+
if (tokens[j][0] === 'IDENTIFIER') names.push(tokens[j][1]);
|
|
3672
|
+
j++;
|
|
3673
|
+
}
|
|
3674
|
+
if (names.length === 0) continue;
|
|
3675
|
+
// Keep if any name is used at runtime
|
|
3676
|
+
if (names.some(n => usedNames.has(n))) continue;
|
|
3677
|
+
// Only elide if at least one name was used in a type annotation
|
|
3678
|
+
if (!names.some(n => typeRefNames.has(n))) continue;
|
|
3679
|
+
// All imported names are type-only — remove IMPORT through TERMINATOR
|
|
3680
|
+
let end = j;
|
|
3681
|
+
while (end < tokens.length && tokens[end][0] !== 'TERMINATOR') end++;
|
|
3682
|
+
if (end < tokens.length) end++; // include TERMINATOR
|
|
3683
|
+
tokens.splice(i, end - i);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3381
3687
|
// Strip leading terminators that may result from removed type declarations
|
|
3382
3688
|
while (tokens.length > 0 && tokens[0][0] === 'TERMINATOR') {
|
|
3383
3689
|
tokens.shift();
|
|
@@ -3390,6 +3696,7 @@ export class Compiler {
|
|
|
3390
3696
|
}
|
|
3391
3697
|
|
|
3392
3698
|
// Step 3: Parse — shim adapter wraps token values with metadata
|
|
3699
|
+
let lastLexedLoc = null;
|
|
3393
3700
|
parser.lexer = {
|
|
3394
3701
|
tokens, pos: 0,
|
|
3395
3702
|
setInput: function() {},
|
|
@@ -3404,6 +3711,8 @@ export class Compiler {
|
|
|
3404
3711
|
}
|
|
3405
3712
|
this.text = val;
|
|
3406
3713
|
this.loc = token.loc;
|
|
3714
|
+
this.line = token.loc?.r;
|
|
3715
|
+
lastLexedLoc = token.loc;
|
|
3407
3716
|
return token[0];
|
|
3408
3717
|
}
|
|
3409
3718
|
};
|
|
@@ -3411,11 +3720,21 @@ export class Compiler {
|
|
|
3411
3720
|
let sexpr;
|
|
3412
3721
|
try {
|
|
3413
3722
|
sexpr = parser.parse(source);
|
|
3414
|
-
} catch (
|
|
3723
|
+
} catch (err) {
|
|
3415
3724
|
if (/\?\s*\([^)]*\?[^)]*:[^)]*\)\s*:/.test(source) || /\?\s+\w+\s+\?\s+/.test(source)) {
|
|
3416
|
-
throw new
|
|
3725
|
+
throw new RipError('Nested ternary operators are not supported', {
|
|
3726
|
+
code: 'E_PARSE', source, file: this.options.filename,
|
|
3727
|
+
suggestion: 'Use if/else statements instead.',
|
|
3728
|
+
phase: 'parser',
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
let re = toRipError(err, source, this.options.filename);
|
|
3732
|
+
if (re.phase === 'parser' && lastLexedLoc) {
|
|
3733
|
+
re.line = lastLexedLoc.r ?? re.line;
|
|
3734
|
+
re.column = lastLexedLoc.c ?? re.column;
|
|
3735
|
+
re.length = lastLexedLoc.n || 1;
|
|
3417
3736
|
}
|
|
3418
|
-
throw
|
|
3737
|
+
throw re;
|
|
3419
3738
|
}
|
|
3420
3739
|
|
|
3421
3740
|
if (this.options.showSExpr) {
|
|
@@ -3434,6 +3753,7 @@ export class Compiler {
|
|
|
3434
3753
|
let generator = new CodeEmitter({
|
|
3435
3754
|
dataSection,
|
|
3436
3755
|
source,
|
|
3756
|
+
filename: this.options.filename,
|
|
3437
3757
|
skipPreamble: this.options.skipPreamble,
|
|
3438
3758
|
skipRuntimes: this.options.skipRuntimes,
|
|
3439
3759
|
skipExports: this.options.skipExports,
|
|
@@ -3521,3 +3841,4 @@ export function getComponentRuntime() {
|
|
|
3521
3841
|
}
|
|
3522
3842
|
|
|
3523
3843
|
export { formatSExpr };
|
|
3844
|
+
export { RipError, toRipError, formatError, formatErrorHTML } from './error.js';
|