rip-lang 3.14.1 → 3.14.3
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 +42 -32
- package/docs/RIP-LANG.md +89 -4
- package/docs/RIP-SCHEMA.md +98 -8
- package/docs/dist/rip.js +693 -446
- package/docs/dist/rip.min.js +210 -176
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/extensions/duckdb/index.html +97 -0
- package/docs/extensions/duckdb/manifest.json +11 -0
- 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/index.html +82 -0
- package/docs/extensions/vscode/print/index.html +40 -0
- package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +40 -0
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/package.json +1 -1
- package/src/AGENTS.md +32 -0
- package/src/compiler.js +140 -22
- package/src/grammar/grammar.rip +41 -0
- package/src/lexer.js +146 -17
- package/src/parser.js +213 -205
- package/src/schema.js +66 -4
package/src/compiler.js
CHANGED
|
@@ -198,6 +198,12 @@ export class CodeEmitter {
|
|
|
198
198
|
'optcall': 'emitOptCall',
|
|
199
199
|
'regex-index': 'emitRegexIndex',
|
|
200
200
|
|
|
201
|
+
// Pick operator — obj.{a, b: c, d = default}
|
|
202
|
+
// Heads are non-identifier shapes so they can't collide with a user
|
|
203
|
+
// function named `pick` (e.g. `pick = (x) -> ...; pick(false)`).
|
|
204
|
+
'.{}': 'emitPick',
|
|
205
|
+
'?.{}': 'emitOptPick',
|
|
206
|
+
|
|
201
207
|
// Functions
|
|
202
208
|
'def': 'emitDef',
|
|
203
209
|
'->': 'emitThinArrow',
|
|
@@ -463,6 +469,12 @@ export class CodeEmitter {
|
|
|
463
469
|
return;
|
|
464
470
|
}
|
|
465
471
|
|
|
472
|
+
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
473
|
+
this.collectVarsFromLoopHead(rest[0], this.programVars);
|
|
474
|
+
rest.slice(1).forEach(item => this.collectProgramVariables(item));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
466
478
|
if (head === 'def' || head === '->' || head === '=>' || head === 'effect') return;
|
|
467
479
|
|
|
468
480
|
if (head === 'if') {
|
|
@@ -515,6 +527,11 @@ export class CodeEmitter {
|
|
|
515
527
|
collect(value);
|
|
516
528
|
return;
|
|
517
529
|
}
|
|
530
|
+
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
531
|
+
this.collectVarsFromLoopHead(rest[0], vars);
|
|
532
|
+
rest.slice(1).forEach(collect);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
518
535
|
if (head === 'def' || head === '->' || head === '=>' || head === 'effect') return;
|
|
519
536
|
if (head === 'try') {
|
|
520
537
|
collect(rest[0]);
|
|
@@ -1023,7 +1040,7 @@ export class CodeEmitter {
|
|
|
1023
1040
|
this._componentName = prevComponentName;
|
|
1024
1041
|
this._componentTypeParams = prevComponentTypeParams;
|
|
1025
1042
|
this._schemaName = prevSchemaName;
|
|
1026
|
-
let isObjLit = this.is(value, 'object');
|
|
1043
|
+
let isObjLit = this.is(value, 'object') || this.is(value, '.{}') || this.is(value, '?.{}');
|
|
1027
1044
|
if (!isObjLit) valueCode = this.unwrap(valueCode);
|
|
1028
1045
|
|
|
1029
1046
|
let needsParensVal = context === 'value';
|
|
@@ -1058,6 +1075,81 @@ export class CodeEmitter {
|
|
|
1058
1075
|
return `${this.emit(obj, 'value')}?.${prop}`;
|
|
1059
1076
|
}
|
|
1060
1077
|
|
|
1078
|
+
// Pick operator: obj.{a, b: c, d = default}
|
|
1079
|
+
//
|
|
1080
|
+
// Semantics:
|
|
1081
|
+
// - missing key → `undefined` (source.key just reads as undefined)
|
|
1082
|
+
// - default fires on nullish (`??`), deliberately broader than JS
|
|
1083
|
+
// destructure's undefined-only defaults, to match DB NULL reality.
|
|
1084
|
+
// - rename `a: b` emits `b: source.a` (inverse of destructure pattern)
|
|
1085
|
+
//
|
|
1086
|
+
// Codegen strategy:
|
|
1087
|
+
// - Simple source (bare identifier, `this`) → inline, no function alloc
|
|
1088
|
+
// - Complex source (call, member, indexed, etc.) → arrow IIFE binds
|
|
1089
|
+
// a single-letter temp to ensure single evaluation and avoid
|
|
1090
|
+
// repeating getter reads.
|
|
1091
|
+
//
|
|
1092
|
+
// AST: [".{}", source, [srcKey, dstKey, defaultOrNull], ...]
|
|
1093
|
+
emitPick(head, rest, context, sexpr) {
|
|
1094
|
+
let [source, ...items] = rest;
|
|
1095
|
+
let sourceCode = this.emit(source, 'value');
|
|
1096
|
+
let simple = this._isSimplePickSource(source);
|
|
1097
|
+
let ref = simple ? sourceCode : '_';
|
|
1098
|
+
|
|
1099
|
+
let body = items.map(([srcKey, dstKey, def]) => {
|
|
1100
|
+
let access = `${ref}.${str(srcKey)}`;
|
|
1101
|
+
if (def !== null && def !== undefined) {
|
|
1102
|
+
access = `(${access} ?? ${this.emit(def, 'value')})`;
|
|
1103
|
+
}
|
|
1104
|
+
return `${str(dstKey)}: ${access}`;
|
|
1105
|
+
}).join(', ');
|
|
1106
|
+
|
|
1107
|
+
// Always parenthesize the object literal so at statement position it
|
|
1108
|
+
// parses as an expression, not a block. `x = ({a:...})` is harmless;
|
|
1109
|
+
// bare `{a:...}` at statement-top would parse as a block in JS.
|
|
1110
|
+
if (simple) return `({${body}})`;
|
|
1111
|
+
return `((_) => ({${body}}))(${sourceCode})`;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Optional-chain pick: obj?.{a, b}
|
|
1115
|
+
// - If source is null/undefined, result is `undefined` (not `{}`)
|
|
1116
|
+
// - Otherwise identical to pick semantics above
|
|
1117
|
+
emitOptPick(head, rest, context, sexpr) {
|
|
1118
|
+
let [source, ...items] = rest;
|
|
1119
|
+
let sourceCode = this.emit(source, 'value');
|
|
1120
|
+
let simple = this._isSimplePickSource(source);
|
|
1121
|
+
let ref = simple ? sourceCode : '_';
|
|
1122
|
+
|
|
1123
|
+
let body = items.map(([srcKey, dstKey, def]) => {
|
|
1124
|
+
let access = `${ref}.${str(srcKey)}`;
|
|
1125
|
+
if (def !== null && def !== undefined) {
|
|
1126
|
+
access = `(${access} ?? ${this.emit(def, 'value')})`;
|
|
1127
|
+
}
|
|
1128
|
+
return `${str(dstKey)}: ${access}`;
|
|
1129
|
+
}).join(', ');
|
|
1130
|
+
|
|
1131
|
+
if (simple) return `(${sourceCode} == null ? undefined : {${body}})`;
|
|
1132
|
+
return `((_) => _ == null ? undefined : ({${body}}))(${sourceCode})`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// A pick source is "simple" only when it's safe to reference multiple
|
|
1136
|
+
// times with no observable difference from a single-evaluation form.
|
|
1137
|
+
// Restricted to AST shapes that are atomically identifier-like:
|
|
1138
|
+
// - bare identifier (AST is a plain string like "whom")
|
|
1139
|
+
// - `this` (AST is the literal string "this")
|
|
1140
|
+
// - `@` (AST is the literal string "@")
|
|
1141
|
+
// Member access like `this.x` or `obj.y` is NOT simple: getters and
|
|
1142
|
+
// reactive tracking can observe each read, so we force an IIFE to
|
|
1143
|
+
// evaluate the source exactly once.
|
|
1144
|
+
_isSimplePickSource(node) {
|
|
1145
|
+
if (typeof node !== 'string') return false;
|
|
1146
|
+
// Identifier-shape or `@`/`this` only. Rejects string-typed AST nodes
|
|
1147
|
+
// that happen to carry non-identifier content (defensive; current AST
|
|
1148
|
+
// doesn't produce such strings but future shape changes are bounded).
|
|
1149
|
+
return node === 'this' || node === '@' ||
|
|
1150
|
+
/^[A-Za-z_$][\w$]*$/.test(node);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1061
1153
|
emitRegexIndex(head, rest) {
|
|
1062
1154
|
let [value, regex, captureIndex] = rest;
|
|
1063
1155
|
this.helpers.add('toMatchable');
|
|
@@ -1380,7 +1472,7 @@ export class CodeEmitter {
|
|
|
1380
1472
|
let stmts = body.slice(1);
|
|
1381
1473
|
this.indentLevel++;
|
|
1382
1474
|
let lines = [];
|
|
1383
|
-
if (!noVar) lines.push(
|
|
1475
|
+
if (!noVar) lines.push(`${itemVarPattern} = ${iterCode}[${idxName}];`);
|
|
1384
1476
|
if (guard) {
|
|
1385
1477
|
lines.push(`if (${this.emit(guard, 'value')}) {`);
|
|
1386
1478
|
this.indentLevel++;
|
|
@@ -1400,8 +1492,8 @@ export class CodeEmitter {
|
|
|
1400
1492
|
: loopHeader + `{ ${this.emit(body, 'statement')}; }`;
|
|
1401
1493
|
}
|
|
1402
1494
|
return guard
|
|
1403
|
-
? loopHeader + `{
|
|
1404
|
-
: loopHeader + `{
|
|
1495
|
+
? loopHeader + `{ ${itemVarPattern} = ${iterCode}[${idxName}]; if (${this.emit(guard, 'value')}) ${this.emit(body, 'statement')}; }`
|
|
1496
|
+
: loopHeader + `{ ${itemVarPattern} = ${iterCode}[${idxName}]; ${this.emit(body, 'statement')}; }`;
|
|
1405
1497
|
}
|
|
1406
1498
|
|
|
1407
1499
|
// Index variable → traditional for loop
|
|
@@ -1411,7 +1503,7 @@ export class CodeEmitter {
|
|
|
1411
1503
|
if (this.is(body, 'block')) {
|
|
1412
1504
|
code += '{\n';
|
|
1413
1505
|
this.indentLevel++;
|
|
1414
|
-
code += this.indent() +
|
|
1506
|
+
code += this.indent() + `${itemVarPattern} = ${iterCode}[${indexVar}];\n`;
|
|
1415
1507
|
if (guard) {
|
|
1416
1508
|
code += this.indent() + `if (${this.unwrap(this.emit(guard, 'value'))}) {\n`;
|
|
1417
1509
|
this.indentLevel++;
|
|
@@ -1425,8 +1517,8 @@ export class CodeEmitter {
|
|
|
1425
1517
|
code += this.indent() + '}';
|
|
1426
1518
|
} else {
|
|
1427
1519
|
code += guard
|
|
1428
|
-
? `{
|
|
1429
|
-
: `{
|
|
1520
|
+
? `{ ${itemVarPattern} = ${iterCode}[${indexVar}]; if (${this.unwrap(this.emit(guard, 'value'))}) ${this.emit(body, 'statement')}; }`
|
|
1521
|
+
: `{ ${itemVarPattern} = ${iterCode}[${indexVar}]; ${this.emit(body, 'statement')}; }`;
|
|
1430
1522
|
}
|
|
1431
1523
|
return code;
|
|
1432
1524
|
}
|
|
@@ -1450,7 +1542,8 @@ export class CodeEmitter {
|
|
|
1450
1542
|
}
|
|
1451
1543
|
|
|
1452
1544
|
// Default: for-of
|
|
1453
|
-
let
|
|
1545
|
+
let bind = noVar ? 'let ' : '';
|
|
1546
|
+
let code = `for (${bind}${itemVarPattern} of ${this.emit(iterable, 'value')}) `;
|
|
1454
1547
|
code += guard ? this.emitLoopBodyWithGuard(body, guard) : this.emitLoopBody(body);
|
|
1455
1548
|
return code;
|
|
1456
1549
|
}
|
|
@@ -1465,7 +1558,7 @@ export class CodeEmitter {
|
|
|
1465
1558
|
|
|
1466
1559
|
let [keyVar, valueVar] = Array.isArray(vars) ? vars : [vars];
|
|
1467
1560
|
let objCode = this.emit(obj, 'value');
|
|
1468
|
-
let code = `for (
|
|
1561
|
+
let code = `for (${keyVar} in ${objCode}) `;
|
|
1469
1562
|
|
|
1470
1563
|
if (own && !valueVar && !guard) {
|
|
1471
1564
|
if (this.is(body, 'block')) {
|
|
@@ -1483,7 +1576,7 @@ export class CodeEmitter {
|
|
|
1483
1576
|
this.indentLevel++;
|
|
1484
1577
|
let lines = [];
|
|
1485
1578
|
if (own) lines.push(`if (!Object.hasOwn(${objCode}, ${keyVar})) continue;`);
|
|
1486
|
-
lines.push(
|
|
1579
|
+
lines.push(`${valueVar} = ${objCode}[${keyVar}];`);
|
|
1487
1580
|
if (guard) {
|
|
1488
1581
|
lines.push(`if (${this.emit(guard, 'value')}) {`);
|
|
1489
1582
|
this.indentLevel++;
|
|
@@ -1498,7 +1591,7 @@ export class CodeEmitter {
|
|
|
1498
1591
|
}
|
|
1499
1592
|
let inline = '';
|
|
1500
1593
|
if (own) inline += `if (!Object.hasOwn(${objCode}, ${keyVar})) continue; `;
|
|
1501
|
-
inline +=
|
|
1594
|
+
inline += `${valueVar} = ${objCode}[${keyVar}]; `;
|
|
1502
1595
|
if (guard) inline += `if (${this.emit(guard, 'value')}) `;
|
|
1503
1596
|
inline += `${this.emit(body, 'statement')};`;
|
|
1504
1597
|
return code + `{ ${inline} }`;
|
|
@@ -1545,7 +1638,7 @@ export class CodeEmitter {
|
|
|
1545
1638
|
itemVarPattern = this.emitDestructuringPattern(firstVar);
|
|
1546
1639
|
else itemVarPattern = firstVar;
|
|
1547
1640
|
|
|
1548
|
-
let code = `for ${awaitKw}(
|
|
1641
|
+
let code = `for ${awaitKw}(${itemVarPattern} of ${iterCode}) `;
|
|
1549
1642
|
|
|
1550
1643
|
if (needsTempVar && destructStmts.length > 0) {
|
|
1551
1644
|
let stmts = this.unwrapBlock(body);
|
|
@@ -1919,16 +2012,17 @@ export class CodeEmitter {
|
|
|
1919
2012
|
let header = isNeg
|
|
1920
2013
|
? `for (let ${idxN} = ${ic}.length - 1; ${idxN} >= 0; ${update})`
|
|
1921
2014
|
: `for (let ${idxN} = 0; ${idxN} < ${ic}.length; ${update})`;
|
|
1922
|
-
return { header, setup: noVar ? null :
|
|
2015
|
+
return { header, setup: noVar ? null : `${ivp} = ${ic}[${idxN}];` };
|
|
1923
2016
|
}
|
|
1924
2017
|
if (indexVar) {
|
|
1925
2018
|
let ic = this.emit(iterable, 'value');
|
|
1926
2019
|
return {
|
|
1927
2020
|
header: `for (let ${indexVar} = 0; ${indexVar} < ${ic}.length; ${indexVar}++)`,
|
|
1928
|
-
setup:
|
|
2021
|
+
setup: `${ivp} = ${ic}[${indexVar}];`,
|
|
1929
2022
|
};
|
|
1930
2023
|
}
|
|
1931
|
-
|
|
2024
|
+
let bind = noVar ? 'let ' : '';
|
|
2025
|
+
return { header: `for (${bind}${ivp} of ${this.emit(iterable, 'value')})`, setup: null };
|
|
1932
2026
|
}
|
|
1933
2027
|
|
|
1934
2028
|
// Shared: parse a for-of (object) iterator and return { header, own, vv, oc, kvp }.
|
|
@@ -1938,7 +2032,7 @@ export class CodeEmitter {
|
|
|
1938
2032
|
let kvp = (this.is(kv, 'array') || this.is(kv, 'object'))
|
|
1939
2033
|
? this.emitDestructuringPattern(kv) : kv;
|
|
1940
2034
|
let oc = this.emit(iterable, 'value');
|
|
1941
|
-
return { header: `for (
|
|
2035
|
+
return { header: `for (${kvp} in ${oc})`, own, vv, oc, kvp };
|
|
1942
2036
|
}
|
|
1943
2037
|
|
|
1944
2038
|
// Shared: parse a for-as (iterator) spec and return { header }.
|
|
@@ -1947,7 +2041,7 @@ export class CodeEmitter {
|
|
|
1947
2041
|
let [fv] = va;
|
|
1948
2042
|
let ivp = (this.is(fv, 'array') || this.is(fv, 'object'))
|
|
1949
2043
|
? this.emitDestructuringPattern(fv) : fv;
|
|
1950
|
-
return { header: `for ${isAwait ? 'await ' : ''}(
|
|
2044
|
+
return { header: `for ${isAwait ? 'await ' : ''}(${ivp} of ${this.emit(iterable, 'value')})` };
|
|
1951
2045
|
}
|
|
1952
2046
|
|
|
1953
2047
|
emitComprehension(head, rest, context) {
|
|
@@ -1974,7 +2068,7 @@ export class CodeEmitter {
|
|
|
1974
2068
|
code += this.indent() + header + ' {\n';
|
|
1975
2069
|
this.indentLevel++;
|
|
1976
2070
|
if (own) code += this.indent() + `if (!Object.hasOwn(${oc}, ${kvp})) continue;\n`;
|
|
1977
|
-
if (vv) code += this.indent() +
|
|
2071
|
+
if (vv) code += this.indent() + `${vv} = ${oc}[${kvp}];\n`;
|
|
1978
2072
|
} else if (iterType === 'for-as') {
|
|
1979
2073
|
let { header } = this._forAsHeader(vars, iterable, iter[3]);
|
|
1980
2074
|
code += this.indent() + header + ' {\n';
|
|
@@ -2038,10 +2132,10 @@ export class CodeEmitter {
|
|
|
2038
2132
|
if (iterType === 'for-of') {
|
|
2039
2133
|
let [kv, vv] = vars;
|
|
2040
2134
|
let oc = this.emit(iterable, 'value');
|
|
2041
|
-
code += this.indent() + `for (
|
|
2135
|
+
code += this.indent() + `for (${kv} in ${oc}) {\n`;
|
|
2042
2136
|
this.indentLevel++;
|
|
2043
2137
|
if (own) code += this.indent() + `if (!Object.hasOwn(${oc}, ${kv})) continue;\n`;
|
|
2044
|
-
if (vv) code += this.indent() +
|
|
2138
|
+
if (vv) code += this.indent() + `${vv} = ${oc}[${kv}];\n`;
|
|
2045
2139
|
}
|
|
2046
2140
|
}
|
|
2047
2141
|
for (let guard of guards) { code += this.indent() + `if (${this.emit(guard, 'value')}) {\n`; this.indentLevel++; }
|
|
@@ -2385,7 +2479,14 @@ export class CodeEmitter {
|
|
|
2385
2479
|
if (typeof param === 'string') return param;
|
|
2386
2480
|
if (param instanceof String) return param.valueOf();
|
|
2387
2481
|
if (this.is(param, 'rest')) return `...${param[1]}`;
|
|
2388
|
-
if (this.is(param, 'default'))
|
|
2482
|
+
if (this.is(param, 'default')) {
|
|
2483
|
+
// `param[1]` is either a plain identifier string (e.g. `x = 5`) or a
|
|
2484
|
+
// destructuring pattern AST node (e.g. `{a, b} = {}`). Recurse via
|
|
2485
|
+
// `formatParam` so patterns emit as `{a, b}` / `[x, y]` instead of
|
|
2486
|
+
// being coerced to a string via `Array.prototype.toString`, which
|
|
2487
|
+
// produced the famous `(object,,a,a,,b,b = {})` mis-rendering.
|
|
2488
|
+
return `${this.formatParam(param[1])} = ${this.emit(param[2], 'value')}`;
|
|
2489
|
+
}
|
|
2389
2490
|
if (this.is(param, '.') && param[1] === 'this') return param[2];
|
|
2390
2491
|
if (this.is(param, 'array')) {
|
|
2391
2492
|
let els = param.slice(1).map(el => {
|
|
@@ -2695,7 +2796,7 @@ export class CodeEmitter {
|
|
|
2695
2796
|
code += header + ' {\n';
|
|
2696
2797
|
this.indentLevel++;
|
|
2697
2798
|
if (own) code += this.indent() + `if (!Object.hasOwn(${oc}, ${kvp})) continue;\n`;
|
|
2698
|
-
if (vv) code += this.indent() +
|
|
2799
|
+
if (vv) code += this.indent() + `${vv} = ${oc}[${kvp}];\n`;
|
|
2699
2800
|
emitBody();
|
|
2700
2801
|
this.indentLevel--;
|
|
2701
2802
|
code += this.indent() + '}';
|
|
@@ -3059,12 +3160,29 @@ export class CodeEmitter {
|
|
|
3059
3160
|
if (typeof item === 'string') { varSet.add(item); return; }
|
|
3060
3161
|
if (Array.isArray(item)) {
|
|
3061
3162
|
if (item[0] === '...' && typeof item[1] === 'string') varSet.add(item[1]);
|
|
3163
|
+
else if (item[0] === '=' && typeof item[1] === 'string') varSet.add(item[1]);
|
|
3062
3164
|
else if (item[0] === 'array') this.collectVarsFromArray(item, varSet);
|
|
3063
3165
|
else if (item[0] === 'object') this.collectVarsFromObject(item, varSet);
|
|
3064
3166
|
}
|
|
3065
3167
|
});
|
|
3066
3168
|
}
|
|
3067
3169
|
|
|
3170
|
+
// Collect names bound by a for-in / for-of / for-as head. `vars` is the
|
|
3171
|
+
// second slot of the loop s-expression and always arrives as an array of
|
|
3172
|
+
// entries — e.g. ['x'], ['x', 'i'], ['k', 'v'], [['array', 'a', 'b']],
|
|
3173
|
+
// or [undefined] for no-var range loops.
|
|
3174
|
+
collectVarsFromLoopHead(vars, varSet) {
|
|
3175
|
+
if (!Array.isArray(vars)) return;
|
|
3176
|
+
vars.forEach(v => {
|
|
3177
|
+
if (v == null) return;
|
|
3178
|
+
if (typeof v === 'string') { varSet.add(v); return; }
|
|
3179
|
+
if (Array.isArray(v)) {
|
|
3180
|
+
if (v[0] === 'array') this.collectVarsFromArray(v, varSet);
|
|
3181
|
+
else if (v[0] === 'object') this.collectVarsFromObject(v, varSet);
|
|
3182
|
+
}
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3068
3186
|
collectVarsFromObject(obj, varSet) {
|
|
3069
3187
|
obj.slice(1).forEach(pair => {
|
|
3070
3188
|
if (!Array.isArray(pair)) return;
|
package/src/grammar/grammar.rip
CHANGED
|
@@ -243,6 +243,13 @@ grammar =
|
|
|
243
243
|
o 'Value INDEX_START INDENT Expression OUTDENT INDEX_END' , '["[]", 1, 4]'
|
|
244
244
|
o 'Value INDEX_START Slice INDEX_END' , '["[]", 1, 3]'
|
|
245
245
|
o 'Value INDEX_START INDENT Slice OUTDENT INDEX_END' , '["[]", 1, 4]'
|
|
246
|
+
# Pick operator: obj.{a, b: c, d = default} → { a: obj.a, c: obj.b, d: obj.d ?? default }
|
|
247
|
+
# Heads `.{}` and `?.{}` are syntax-shape strings that can't collide with
|
|
248
|
+
# a user function named `pick` (Rip encodes calls as [name, ...args]).
|
|
249
|
+
o 'Value PICK_START PickList OptComma PICK_END' , '[".{}", 1, ...3]'
|
|
250
|
+
o 'Value OPTPICK_START PickList OptComma PICK_END' , '["?.{}", 1, ...3]'
|
|
251
|
+
o 'Value PICK_START INDENT PickList OptComma OUTDENT PICK_END', '[".{}", 1, ...4]'
|
|
252
|
+
o 'Value OPTPICK_START INDENT PickList OptComma OUTDENT PICK_END', '["?.{}", 1, ...4]'
|
|
246
253
|
# Regex indexing with capture group
|
|
247
254
|
o 'Value INDEX_START RegexWithIndex INDEX_END' , '[$3[0], $1, ...$3.slice(1)]'
|
|
248
255
|
# ES6 optional indexing (?.[ )
|
|
@@ -323,6 +330,11 @@ grammar =
|
|
|
323
330
|
# Indexing
|
|
324
331
|
o 'ObjSpreadExpr INDEX_START Expression INDEX_END' , '["[]", 1, 3]'
|
|
325
332
|
o 'ObjSpreadExpr INDEX_START INDENT Expression OUTDENT INDEX_END', '["[]", 1, 4]'
|
|
333
|
+
# Pick — {...obj.{a, b}} inside object literal spread
|
|
334
|
+
o 'ObjSpreadExpr PICK_START PickList OptComma PICK_END' , '[".{}", 1, ...3]'
|
|
335
|
+
o 'ObjSpreadExpr OPTPICK_START PickList OptComma PICK_END' , '["?.{}", 1, ...3]'
|
|
336
|
+
o 'ObjSpreadExpr PICK_START INDENT PickList OptComma OUTDENT PICK_END', '[".{}", 1, ...4]'
|
|
337
|
+
o 'ObjSpreadExpr OPTPICK_START INDENT PickList OptComma OUTDENT PICK_END', '["?.{}", 1, ...4]'
|
|
326
338
|
]
|
|
327
339
|
|
|
328
340
|
# ============================================================================
|
|
@@ -348,6 +360,35 @@ grammar =
|
|
|
348
360
|
o '... Expression' , '["...", 2]'
|
|
349
361
|
]
|
|
350
362
|
|
|
363
|
+
# ============================================================================
|
|
364
|
+
# Pick Expressions — obj.{a, b: newB, c = 'default', d: newD = 'x'}
|
|
365
|
+
#
|
|
366
|
+
# Each item compiles to a 3-tuple [srcKey, dstKey, defaultOrNull] that
|
|
367
|
+
# the codegen emits as `dstKey: (source.srcKey ?? default)` — nullish,
|
|
368
|
+
# intentionally different from JS destructure defaults (undefined-only).
|
|
369
|
+
#
|
|
370
|
+
# PickKey is restricted to Identifier/Property so we don't admit strings,
|
|
371
|
+
# numbers, or computed keys in v1.
|
|
372
|
+
# ============================================================================
|
|
373
|
+
|
|
374
|
+
PickList: [
|
|
375
|
+
o 'PickItem' , '[1]'
|
|
376
|
+
o 'PickList , PickItem' , '[...1, 3]'
|
|
377
|
+
o 'PickList OptComma TERMINATOR PickItem', '[...1, 4]'
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
PickItem: [
|
|
381
|
+
o 'PickKey' , '[1, 1, null]' # shorthand {a} → ["a", "a", null]
|
|
382
|
+
o 'PickKey : PickKey' , '[1, 3, null]' # rename {a: b} → ["a", "b", null]
|
|
383
|
+
o 'PickKey = Expression' , '[1, 1, 3]' # default {a = 5} → ["a", "a", 5]
|
|
384
|
+
o 'PickKey : PickKey = Expression' , '[1, 3, 5]' # both {a: b = 5} → ["a", "b", 5]
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
PickKey: [
|
|
388
|
+
o 'Identifier'
|
|
389
|
+
o 'Property'
|
|
390
|
+
]
|
|
391
|
+
|
|
351
392
|
MapAssignable: [
|
|
352
393
|
o 'Identifier'
|
|
353
394
|
o 'Property'
|
package/src/lexer.js
CHANGED
|
@@ -140,8 +140,8 @@ let IMPLICIT_COMMA_BEFORE_ARROW = new Set([
|
|
|
140
140
|
]);
|
|
141
141
|
|
|
142
142
|
// Tokens that start/end balanced pairs
|
|
143
|
-
let EXPRESSION_START = new Set(['(', '[', '{', 'MAP_START', 'INDENT', 'CALL_START', 'PARAM_START', 'INDEX_START', 'STRING_START', 'INTERPOLATION_START', 'REGEX_START']);
|
|
144
|
-
let EXPRESSION_END = new Set([')', ']', '}', 'MAP_END', 'OUTDENT', 'CALL_END', 'PARAM_END', 'INDEX_END', 'STRING_END', 'INTERPOLATION_END', 'REGEX_END']);
|
|
143
|
+
let EXPRESSION_START = new Set(['(', '[', '{', 'MAP_START', 'PICK_START', 'OPTPICK_START', 'INDENT', 'CALL_START', 'PARAM_START', 'INDEX_START', 'STRING_START', 'INTERPOLATION_START', 'REGEX_START']);
|
|
144
|
+
let EXPRESSION_END = new Set([')', ']', '}', 'MAP_END', 'PICK_END', 'OUTDENT', 'CALL_END', 'PARAM_END', 'INDEX_END', 'STRING_END', 'INTERPOLATION_END', 'REGEX_END']);
|
|
145
145
|
|
|
146
146
|
// Balanced pair inverses
|
|
147
147
|
let INVERSES = {
|
|
@@ -156,6 +156,8 @@ let INVERSES = {
|
|
|
156
156
|
'INTERPOLATION_START': 'INTERPOLATION_END', 'INTERPOLATION_END': 'INTERPOLATION_START',
|
|
157
157
|
'REGEX_START': 'REGEX_END', 'REGEX_END': 'REGEX_START',
|
|
158
158
|
'MAP_START': 'MAP_END', 'MAP_END': 'MAP_START',
|
|
159
|
+
'PICK_START': 'PICK_END', 'PICK_END': 'PICK_START',
|
|
160
|
+
'OPTPICK_START': 'PICK_END',
|
|
159
161
|
};
|
|
160
162
|
|
|
161
163
|
// Tokens that close a clause (for normalizeLines)
|
|
@@ -181,6 +183,25 @@ let SINGLE_CLOSERS = new Set(['TERMINATOR', 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT
|
|
|
181
183
|
// Tokens that indicate end-of-line
|
|
182
184
|
let LINE_BREAK = new Set(['INDENT', 'OUTDENT', 'TERMINATOR']);
|
|
183
185
|
|
|
186
|
+
// Compound-key recognition shared by three lexer sites:
|
|
187
|
+
// - the implicit-call guard (blocks `IDENT -IDENT :` from wrapping as `IDENT(-IDENT)`)
|
|
188
|
+
// - the `:` handler (collapses the chain into a single STRING token)
|
|
189
|
+
// - `looksObjectish` (tells the implicit-object tracker an indented chain is still a key)
|
|
190
|
+
//
|
|
191
|
+
// A compound key is an IDENTIFIER followed by `[ (. | -) (IDENTIFIER|PROPERTY) ]+` and then `:`.
|
|
192
|
+
// `.` accepts any spacing (pre-existing behavior). `-` requires no whitespace *or*
|
|
193
|
+
// newline on either side, so legitimate subtraction (`a - b: 1`) and line-broken
|
|
194
|
+
// expressions (`foo-\n bar:`) remain unaffected.
|
|
195
|
+
let isIdentOrProp = t => t?.[0] === 'IDENTIFIER' || t?.[0] === 'PROPERTY';
|
|
196
|
+
let isCompoundSep = (sep, rhs) => {
|
|
197
|
+
if (sep?.[0] === '.') return true;
|
|
198
|
+
if (sep?.[0] === '-') {
|
|
199
|
+
return sep.spaced === false && !sep.newLine &&
|
|
200
|
+
rhs?.spaced === false && !rhs?.newLine;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
};
|
|
204
|
+
|
|
184
205
|
// Tokens that close implicit calls when following a newline
|
|
185
206
|
let CALL_CLOSERS = new Set(['.', '?.']);
|
|
186
207
|
|
|
@@ -426,6 +447,47 @@ export class Lexer {
|
|
|
426
447
|
return p ? p[1] : undefined;
|
|
427
448
|
}
|
|
428
449
|
|
|
450
|
+
// True when the next identifier/keyword-shaped token would sit in a
|
|
451
|
+
// pick-key position of a `.{` or `?.{` body. Used by identifierToken()
|
|
452
|
+
// to tag reserved-word keys (`default`, `class`, `delete`, …) as
|
|
453
|
+
// PROPERTY so they don't later become keyword/UNARY tokens (which
|
|
454
|
+
// would suppress a following TERMINATOR in multi-line bodies).
|
|
455
|
+
//
|
|
456
|
+
// Pick-key position means prev is one of: `{` (first key), `,`
|
|
457
|
+
// (subsequent key), `:` (rename dest), TERMINATOR/INDENT/OUTDENT
|
|
458
|
+
// (newline-separated key), AND we're inside a `{` that was
|
|
459
|
+
// preceded by a tight `.` or `?.`.
|
|
460
|
+
inPickKeyPos() {
|
|
461
|
+
let prev = this.prev();
|
|
462
|
+
if (!prev) return false;
|
|
463
|
+
let p = prev[0];
|
|
464
|
+
if (p !== '{' && p !== ',' && p !== ':' &&
|
|
465
|
+
p !== 'TERMINATOR' && p !== 'INDENT' && p !== 'OUTDENT') return false;
|
|
466
|
+
// Walk back to find the enclosing `{`. Bail out on a few structural
|
|
467
|
+
// tokens that mean we've left any pick body.
|
|
468
|
+
let depth = 0;
|
|
469
|
+
let toks = this.tokens;
|
|
470
|
+
for (let k = toks.length - 1; k >= 0; k--) {
|
|
471
|
+
let t = toks[k];
|
|
472
|
+
let tt = t[0];
|
|
473
|
+
if (tt === '}' || tt === 'PICK_END') { depth++; continue; }
|
|
474
|
+
if (tt === '{' || tt === 'PICK_START' || tt === 'OPTPICK_START') {
|
|
475
|
+
if (depth === 0) {
|
|
476
|
+
let before = toks[k - 1];
|
|
477
|
+
return !!(before && (before[0] === '.' || before[0] === '?.') &&
|
|
478
|
+
!before.spaced && !t.spaced);
|
|
479
|
+
}
|
|
480
|
+
depth--;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
// Cheap early-outs — if we escape the local block context, we
|
|
484
|
+
// clearly aren't inside any pick body.
|
|
485
|
+
if (depth === 0 && (tt === '(' || tt === 'CALL_START' ||
|
|
486
|
+
tt === '[' || tt === 'INDEX_START')) return false;
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
|
|
429
491
|
// --------------------------------------------------------------------------
|
|
430
492
|
// 1. Identifier Token
|
|
431
493
|
// --------------------------------------------------------------------------
|
|
@@ -514,7 +576,7 @@ export class Lexer {
|
|
|
514
576
|
if (colon && colon.length > 1 && /[a-zA-Z_$]/.test(this.chunk[idLen + colon.length])) colon = null;
|
|
515
577
|
|
|
516
578
|
// Property vs identifier
|
|
517
|
-
if (colon || (prev && (prev[0] === '.' || prev[0] === '?.' || (!prev.spaced && prev[0] === '@')))) {
|
|
579
|
+
if (colon || (prev && (prev[0] === '.' || prev[0] === '?.' || (!prev.spaced && prev[0] === '@'))) || this.inPickKeyPos()) {
|
|
518
580
|
tag = 'PROPERTY';
|
|
519
581
|
|
|
520
582
|
// In render blocks, consume hyphenated CSS class names: .counter-display → PROPERTY "counter-display"
|
|
@@ -1414,6 +1476,8 @@ export class Lexer {
|
|
|
1414
1476
|
rewrite(tokens) {
|
|
1415
1477
|
this.tokens = tokens;
|
|
1416
1478
|
this.removeLeadingNewlines();
|
|
1479
|
+
this.rewriteDottedPicks(); // .{ and ?.{ — must run before map literals so
|
|
1480
|
+
// pick bodies see raw { } for depth counting
|
|
1417
1481
|
this.rewriteMapLiterals();
|
|
1418
1482
|
this.closeMergeAssignments();
|
|
1419
1483
|
this.closeOpenCalls();
|
|
@@ -1457,6 +1521,52 @@ export class Lexer {
|
|
|
1457
1521
|
}
|
|
1458
1522
|
}
|
|
1459
1523
|
|
|
1524
|
+
// Pick operator: `obj.{a, b}` → { a: obj.a, b: obj.b }
|
|
1525
|
+
// `obj?.{a, b}` → obj == null ? undefined : { a: obj.a, b: obj.b }
|
|
1526
|
+
//
|
|
1527
|
+
// Recognize `.` or `?.` followed immediately by `{` at postfix position
|
|
1528
|
+
// (preceded by an INDEXABLE value). Retag the `.` away and mark the braces
|
|
1529
|
+
// as PICK_START/PICK_END (or OPTPICK_START/PICK_END for optional chain),
|
|
1530
|
+
// leaving the body tokens to parse via the Pick grammar.
|
|
1531
|
+
//
|
|
1532
|
+
// Runs before rewriteMapLiterals() so pick bodies see raw `{`/`}` for
|
|
1533
|
+
// depth tracking; map literals inside defaults are handled on the next
|
|
1534
|
+
// pass after pick braces have been distinguished.
|
|
1535
|
+
rewriteDottedPicks() {
|
|
1536
|
+
let tokens = this.tokens;
|
|
1537
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1538
|
+
let tag = tokens[i][0];
|
|
1539
|
+
if (tag !== '.' && tag !== '?.') continue;
|
|
1540
|
+
let next = tokens[i + 1];
|
|
1541
|
+
if (!next || next[0] !== '{') continue;
|
|
1542
|
+
// `.` must sit tight to the next `{`: no whitespace between them.
|
|
1543
|
+
// In Rip, token.spaced means "followed by whitespace", so checking
|
|
1544
|
+
// tokens[i].spaced (the `.`) catches `. {` cases. We DON'T check
|
|
1545
|
+
// next.spaced — that would mean "whitespace after `{`" which is
|
|
1546
|
+
// perfectly fine (`obj.{ a, b }` should work like `obj.{a, b}`).
|
|
1547
|
+
// Similarly, next.newLine is OK — it allows multiline bodies like
|
|
1548
|
+
// `obj.{\n a\n b\n}`.
|
|
1549
|
+
if (tokens[i].spaced || tokens[i].newLine) continue;
|
|
1550
|
+
let prev = tokens[i - 1];
|
|
1551
|
+
if (!prev || !INDEXABLE.has(prev[0])) continue;
|
|
1552
|
+
let optional = (tag === '?.');
|
|
1553
|
+
tokens.splice(i, 1);
|
|
1554
|
+
tokens[i][0] = optional ? 'OPTPICK_START' : 'PICK_START';
|
|
1555
|
+
// Find the matching close brace, retagging as PICK_END. Reserved-word
|
|
1556
|
+
// keys inside the body are already handled upstream by inPickKeyPos()
|
|
1557
|
+
// during initial tokenization — no second pass needed here.
|
|
1558
|
+
let depth = 1;
|
|
1559
|
+
for (let j = i + 1; j < tokens.length && depth > 0; j++) {
|
|
1560
|
+
let tt = tokens[j][0];
|
|
1561
|
+
if (tt === '{' || tt === 'PICK_START' || tt === 'OPTPICK_START') depth++;
|
|
1562
|
+
else if (tt === '}') {
|
|
1563
|
+
depth--;
|
|
1564
|
+
if (depth === 0) { tokens[j][0] = 'PICK_END'; break; }
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1460
1570
|
closeOpenCalls() {
|
|
1461
1571
|
this.scanTokens((token, i) => {
|
|
1462
1572
|
if (token[0] === 'CALL_START') {
|
|
@@ -1708,7 +1818,20 @@ export class Lexer {
|
|
|
1708
1818
|
}
|
|
1709
1819
|
|
|
1710
1820
|
// Detect implicit function calls
|
|
1711
|
-
|
|
1821
|
+
//
|
|
1822
|
+
// Before firing on `IDENT -IDENT`, check if the real pattern is a
|
|
1823
|
+
// compound-key chain (`data-src:`). If so, let the `:` handler
|
|
1824
|
+
// collapse it to a STRING — don't wrap it as `data(-src)`.
|
|
1825
|
+
let looksLikeCompoundKey = false;
|
|
1826
|
+
if (nextTag === '-' && !nextToken.spaced && !nextToken.newLine && isIdentOrProp(token)) {
|
|
1827
|
+
for (let k = i; k < tokens.length - 2; k += 2) {
|
|
1828
|
+
if (!isCompoundSep(tokens[k + 1], tokens[k + 2])) break;
|
|
1829
|
+
if (!isIdentOrProp(tokens[k + 2])) break;
|
|
1830
|
+
if (tokens[k + 3]?.[0] === ':') { looksLikeCompoundKey = true; break; }
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
if (!looksLikeCompoundKey && IMPLICIT_FUNC.has(tag) && token.spaced &&
|
|
1712
1835
|
(IMPLICIT_CALL.has(nextTag) || (nextTag === '...' && IMPLICIT_CALL.has(tokens[i + 2]?.[0])) ||
|
|
1713
1836
|
(IMPLICIT_UNSPACED_CALL.has(nextTag) && !nextToken.spaced && !nextToken.newLine)) &&
|
|
1714
1837
|
!((tag === ']' || tag === '}') && (nextTag === '->' || nextTag === '=>'))) {
|
|
@@ -1735,17 +1858,21 @@ export class Lexer {
|
|
|
1735
1858
|
return forward(1);
|
|
1736
1859
|
}
|
|
1737
1860
|
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1861
|
+
// Compound keys: collapse `IDENTIFIER [ (. | -) (IDENTIFIER|PROPERTY) ]+` into a STRING.
|
|
1862
|
+
// Walks backwards from `:` through a chain, then rebuilds the string by concatenating
|
|
1863
|
+
// each token's raw value (identifiers carry safe characters; separators contribute
|
|
1864
|
+
// a literal `.` or `-`). See `isCompoundSep` for spacing rules.
|
|
1865
|
+
// Examples: `www.amazon.com:`, `data-src:`, `beta-site.amazon.com:`.
|
|
1866
|
+
if (isIdentOrProp(tokens[i - 1]) && isCompoundSep(tokens[i - 2], tokens[i - 1])) {
|
|
1740
1867
|
let j = i - 2;
|
|
1741
|
-
while (j >= 2 && tokens[j]
|
|
1868
|
+
while (j >= 2 && isCompoundSep(tokens[j], tokens[j + 1]) && isIdentOrProp(tokens[j - 1])) {
|
|
1742
1869
|
j -= 2;
|
|
1743
1870
|
}
|
|
1744
1871
|
j += 1;
|
|
1745
|
-
if (tokens[j]
|
|
1746
|
-
let
|
|
1747
|
-
for (let k = j; k < i; k +=
|
|
1748
|
-
let str = gen('STRING', `"${
|
|
1872
|
+
if (isIdentOrProp(tokens[j])) {
|
|
1873
|
+
let buf = '';
|
|
1874
|
+
for (let k = j; k < i; k++) buf += tokens[k][1];
|
|
1875
|
+
let str = gen('STRING', `"${buf}"`, tokens[j]);
|
|
1749
1876
|
str.pre = tokens[j].pre;
|
|
1750
1877
|
str.spaced = tokens[j].spaced;
|
|
1751
1878
|
str.newLine = tokens[j].newLine;
|
|
@@ -1765,7 +1892,7 @@ export class Lexer {
|
|
|
1765
1892
|
if (stackTop()) {
|
|
1766
1893
|
let [stackTag, stackIdx] = stackTop();
|
|
1767
1894
|
let stackNext = stack[stack.length - 2];
|
|
1768
|
-
let isBrace = (t) => t === '{' || t === 'MAP_START';
|
|
1895
|
+
let isBrace = (t) => t === '{' || t === 'MAP_START' || t === 'PICK_START' || t === 'OPTPICK_START';
|
|
1769
1896
|
if ((isBrace(stackTag) || (stackTag === 'INDENT' && isBrace(stackNext?.[0]) && !isImplicit(stackNext))) &&
|
|
1770
1897
|
(startsLine || this.tokens[s - 1]?.[0] === ',' || isBrace(this.tokens[s - 1]?.[0]) || isBrace(this.tokens[s]?.[0]))) {
|
|
1771
1898
|
return forward(1);
|
|
@@ -1943,11 +2070,13 @@ export class Lexer {
|
|
|
1943
2070
|
if (!this.tokens[j]) return false;
|
|
1944
2071
|
if (this.tokens[j]?.[0] === '@' && this.tokens[j + 2]?.[0] === ':') return true;
|
|
1945
2072
|
if (this.tokens[j + 1]?.[0] === ':') return true;
|
|
1946
|
-
//
|
|
1947
|
-
if ((this.tokens[j]
|
|
1948
|
-
let k = j + 2
|
|
1949
|
-
|
|
1950
|
-
|
|
2073
|
+
// Compound keys: IDENTIFIER [ (. | -) (IDENTIFIER|PROPERTY) ]+ :
|
|
2074
|
+
if (isIdentOrProp(this.tokens[j])) {
|
|
2075
|
+
for (let k = j + 1; k < this.tokens.length; k += 2) {
|
|
2076
|
+
if (!isCompoundSep(this.tokens[k], this.tokens[k + 1])) break;
|
|
2077
|
+
if (!isIdentOrProp(this.tokens[k + 1])) break;
|
|
2078
|
+
if (this.tokens[k + 2]?.[0] === ':') return true;
|
|
2079
|
+
}
|
|
1951
2080
|
}
|
|
1952
2081
|
if (EXPRESSION_START.has(this.tokens[j]?.[0])) {
|
|
1953
2082
|
let end = null;
|