rip-lang 3.10.6 → 3.10.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.10.6",
3
+ "version": "3.10.8",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
package/src/compiler.js CHANGED
@@ -2980,6 +2980,16 @@ function __state(initialValue) {
2980
2980
  },
2981
2981
 
2982
2982
  read() { return value; },
2983
+ touch() {
2984
+ if (dead || notifying) return;
2985
+ notifying = true;
2986
+ for (const sub of subscribers) {
2987
+ if (sub.markDirty) sub.markDirty();
2988
+ else __pendingEffects.add(sub);
2989
+ }
2990
+ if (!__batching) __flushEffects();
2991
+ notifying = false;
2992
+ },
2983
2993
  lock() { locked = true; return state; },
2984
2994
  free() { subscribers.clear(); return state; },
2985
2995
  kill() { dead = true; subscribers.clear(); return value; },
package/src/components.js CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Naming: All render-tree generators use generate* (consistent with compiler).
9
9
 
10
- import { TEMPLATE_TAGS } from './tags.js';
10
+ import { TEMPLATE_TAGS, SVG_TAGS } from './tags.js';
11
11
 
12
12
  // ============================================================================
13
13
  // Constants
@@ -24,6 +24,8 @@ const BOOLEAN_ATTRS = new Set([
24
24
  'allowfullscreen', 'inert',
25
25
  ]);
26
26
 
27
+ const SVG_NS = 'http://www.w3.org/2000/svg';
28
+
27
29
  // ============================================================================
28
30
  // Standalone Utilities
29
31
  // ============================================================================
@@ -490,7 +492,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
490
492
  }
491
493
  current = current[1];
492
494
  }
493
- let raw = typeof current === 'string' ? current : (current instanceof String ? current.valueOf() : 'div');
495
+ let raw = typeof current === 'string' ? current : (current instanceof String ? current.valueOf() : null);
496
+ if (raw === null) return { tag: null, classes, id: undefined };
494
497
  // Split tag#id — e.g. "div#content" → tag: "div", id: "content"
495
498
  let [tag, id] = raw.split('#');
496
499
  if (!tag) tag = 'div'; // bare #id → div
@@ -792,6 +795,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
792
795
  this._createLines = [];
793
796
  this._setupLines = [];
794
797
  this._blockFactories = [];
798
+ this._loopVarStack = [];
795
799
 
796
800
  const statements = this.is(body, 'block') ? body.slice(1) : [body];
797
801
 
@@ -855,7 +859,12 @@ export function installComponentSupport(CodeGenerator, Lexer) {
855
859
  // Static tag without content (possibly with #id)
856
860
  const [tagStr, idStr] = str.split('#');
857
861
  const elVar = this.newElementVar();
858
- this._createLines.push(`${elVar} = document.createElement('${tagStr || 'div'}');`);
862
+ const actualTag = tagStr || 'div';
863
+ if (SVG_TAGS.has(actualTag) || this._svgDepth > 0) {
864
+ this._createLines.push(`${elVar} = document.createElementNS('${SVG_NS}', '${actualTag}');`);
865
+ } else {
866
+ this._createLines.push(`${elVar} = document.createElement('${actualTag}');`);
867
+ }
859
868
  if (idStr) this._createLines.push(`${elVar}.id = '${idStr}';`);
860
869
  return elVar;
861
870
  }
@@ -1011,16 +1020,49 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1011
1020
 
1012
1021
  proto.generateTag = function(tag, classes, args, id) {
1013
1022
  const elVar = this.newElementVar();
1014
- this._createLines.push(`${elVar} = document.createElement('${tag}');`);
1023
+ const isSvg = SVG_TAGS.has(tag) || this._svgDepth > 0;
1024
+ if (isSvg) {
1025
+ this._createLines.push(`${elVar} = document.createElementNS('${SVG_NS}', '${tag}');`);
1026
+ } else {
1027
+ this._createLines.push(`${elVar} = document.createElement('${tag}');`);
1028
+ }
1015
1029
 
1016
1030
  if (id) {
1017
1031
  this._createLines.push(`${elVar}.id = '${id}';`);
1018
1032
  }
1033
+
1034
+ // Defer class emission when selector classes exist so class: attributes merge
1035
+ const prevClassArgs = this._pendingClassArgs;
1036
+ const prevClassEl = this._pendingClassEl;
1019
1037
  if (classes.length > 0) {
1020
- this._createLines.push(`${elVar}.className = '${classes.join(' ')}';`);
1038
+ this._pendingClassArgs = [`'${classes.join(' ')}'`];
1039
+ this._pendingClassEl = elVar;
1021
1040
  }
1022
1041
 
1042
+ if (tag === 'svg') this._svgDepth = (this._svgDepth || 0) + 1;
1023
1043
  this.appendChildren(elVar, args);
1044
+ if (tag === 'svg') this._svgDepth--;
1045
+
1046
+ // Emit final class: if only selector classes (no dynamic additions), set statically
1047
+ if (classes.length > 0) {
1048
+ if (this._pendingClassArgs.length === 1) {
1049
+ if (isSvg) {
1050
+ this._createLines.push(`${elVar}.setAttribute('class', '${classes.join(' ')}');`);
1051
+ } else {
1052
+ this._createLines.push(`${elVar}.className = '${classes.join(' ')}';`);
1053
+ }
1054
+ } else {
1055
+ const combined = this._pendingClassArgs.join(', ');
1056
+ if (isSvg) {
1057
+ this._setupLines.push(`__effect(() => { ${elVar}.setAttribute('class', __clsx(${combined})); });`);
1058
+ } else {
1059
+ this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${combined}); });`);
1060
+ }
1061
+ }
1062
+ this._pendingClassArgs = prevClassArgs;
1063
+ this._pendingClassEl = prevClassEl;
1064
+ }
1065
+
1024
1066
  return elVar;
1025
1067
  };
1026
1068
 
@@ -1030,7 +1072,11 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1030
1072
 
1031
1073
  proto.generateDynamicTag = function(tag, classExprs, children) {
1032
1074
  const elVar = this.newElementVar();
1033
- this._createLines.push(`${elVar} = document.createElement('${tag}');`);
1075
+ if (SVG_TAGS.has(tag) || this._svgDepth > 0) {
1076
+ this._createLines.push(`${elVar} = document.createElementNS('${SVG_NS}', '${tag}');`);
1077
+ } else {
1078
+ this._createLines.push(`${elVar} = document.createElement('${tag}');`);
1079
+ }
1034
1080
 
1035
1081
  // Defer className emission so class: attributes can merge with .() classes
1036
1082
  const classArgs = classExprs.map(e => this.generateInComponent(e, 'value'));
@@ -1039,11 +1085,18 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1039
1085
  this._pendingClassArgs = classArgs;
1040
1086
  this._pendingClassEl = elVar;
1041
1087
 
1088
+ if (tag === 'svg') this._svgDepth = (this._svgDepth || 0) + 1;
1042
1089
  this.appendChildren(elVar, children);
1090
+ if (tag === 'svg') this._svgDepth--;
1043
1091
 
1044
1092
  if (this._pendingClassArgs.length > 0) {
1045
1093
  const combined = this._pendingClassArgs.join(', ');
1046
- this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${combined}); });`);
1094
+ const isSvg = SVG_TAGS.has(tag) || this._svgDepth > 0;
1095
+ if (isSvg) {
1096
+ this._setupLines.push(`__effect(() => { ${elVar}.setAttribute('class', __clsx(${combined})); });`);
1097
+ } else {
1098
+ this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${combined}); });`);
1099
+ }
1047
1100
  }
1048
1101
  this._pendingClassArgs = prevClassArgs;
1049
1102
  this._pendingClassEl = prevClassEl;
@@ -1087,9 +1140,17 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1087
1140
  if (this._pendingClassArgs && this._pendingClassEl === elVar) {
1088
1141
  this._pendingClassArgs.push(valueCode);
1089
1142
  } else if (this.hasReactiveDeps(value)) {
1090
- this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${valueCode}); });`);
1143
+ if (this._svgDepth > 0) {
1144
+ this._setupLines.push(`__effect(() => { ${elVar}.setAttribute('class', __clsx(${valueCode})); });`);
1145
+ } else {
1146
+ this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${valueCode}); });`);
1147
+ }
1091
1148
  } else {
1092
- this._createLines.push(`${elVar}.className = ${valueCode};`);
1149
+ if (this._svgDepth > 0) {
1150
+ this._createLines.push(`${elVar}.setAttribute('class', ${valueCode});`);
1151
+ } else {
1152
+ this._createLines.push(`${elVar}.className = ${valueCode};`);
1153
+ }
1093
1154
  }
1094
1155
  continue;
1095
1156
  }
@@ -1117,7 +1178,12 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1117
1178
  }
1118
1179
 
1119
1180
  this._setupLines.push(`__effect(() => { ${elVar}.${prop} = ${valueCode}; });`);
1120
- this._createLines.push(`${elVar}.addEventListener('${event}', (e) => ${valueCode} = ${valueAccessor});`);
1181
+ let assignCode = `${valueCode} = ${valueAccessor}`;
1182
+ const rootMember = !this.isSimpleAssignable(value) && this.findRootReactiveMember(value);
1183
+ if (rootMember) {
1184
+ assignCode += `; this.${rootMember}.touch?.()`;
1185
+ }
1186
+ this._createLines.push(`${elVar}.addEventListener('${event}', (e) => { ${assignCode}; });`);
1121
1187
  continue;
1122
1188
  }
1123
1189
 
@@ -1126,20 +1192,30 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1126
1192
  // Smart two-way binding for value/checked when bound to reactive state
1127
1193
  if ((key === 'value' || key === 'checked') && this.hasReactiveDeps(value)) {
1128
1194
  this._setupLines.push(`__effect(() => { ${elVar}.${key} = ${valueCode}; });`);
1129
- // Only generate reverse binding when the value is a simple assignable
1130
- // target (plain reactive member or @prop), not a complex expression
1131
- // like selected.includes(opt) which can't be assigned to.
1132
- if (this.isSimpleAssignable(value)) {
1195
+ // Generate reverse binding for simple assignable targets or nested
1196
+ // reactive paths (with touch() for Svelte-style invalidation)
1197
+ const rootMemberImplicit = !this.isSimpleAssignable(value) && this.findRootReactiveMember(value);
1198
+ if (this.isSimpleAssignable(value) || rootMemberImplicit) {
1133
1199
  const event = key === 'checked' ? 'change' : 'input';
1134
1200
  const accessor = key === 'checked' ? 'e.target.checked'
1135
1201
  : (inputType === 'number' || inputType === 'range') ? 'e.target.valueAsNumber'
1136
1202
  : 'e.target.value';
1137
- this._createLines.push(`${elVar}.addEventListener('${event}', (e) => { ${valueCode} = ${accessor}; });`);
1203
+ let assignCode = `${valueCode} = ${accessor}`;
1204
+ if (rootMemberImplicit) {
1205
+ assignCode += `; this.${rootMemberImplicit}.touch?.()`;
1206
+ }
1207
+ this._createLines.push(`${elVar}.addEventListener('${event}', (e) => { ${assignCode}; });`);
1138
1208
  }
1139
1209
  continue;
1140
1210
  }
1141
1211
 
1142
- if (BOOLEAN_ATTRS.has(key)) {
1212
+ if (key === 'innerHTML' || key === 'textContent' || key === 'innerText') {
1213
+ if (this.hasReactiveDeps(value)) {
1214
+ this._setupLines.push(`__effect(() => { ${elVar}.${key} = ${valueCode}; });`);
1215
+ } else {
1216
+ this._createLines.push(`${elVar}.${key} = ${valueCode};`);
1217
+ }
1218
+ } else if (BOOLEAN_ATTRS.has(key)) {
1143
1219
  if (this.hasReactiveDeps(value)) {
1144
1220
  this._setupLines.push(`__effect(() => { ${elVar}.toggleAttribute('${key}', !!${valueCode}); });`);
1145
1221
  } else {
@@ -1194,6 +1270,10 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1194
1270
 
1195
1271
  const condCode = this.generateInComponent(condition, 'value');
1196
1272
 
1273
+ // Collect loop variables from enclosing for-loops
1274
+ const loopParams = this._loopVarStack.map(v => `${v.itemVar}, ${v.indexVar}`).join(', ');
1275
+ const extraArgs = loopParams ? `, ${loopParams}` : '';
1276
+
1197
1277
  const thenBlockName = this.newBlockVar();
1198
1278
  this.generateConditionBranch(thenBlockName, thenBlock);
1199
1279
 
@@ -1221,17 +1301,17 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1221
1301
  setupLines.push(` showing = want;`);
1222
1302
  setupLines.push(``);
1223
1303
  setupLines.push(` if (want === 'then') {`);
1224
- setupLines.push(` currentBlock = ${thenBlockName}(this);`);
1304
+ setupLines.push(` currentBlock = ${thenBlockName}(this${extraArgs});`);
1225
1305
  setupLines.push(` currentBlock.c();`);
1226
1306
  setupLines.push(` currentBlock.m(anchor.parentNode, anchor.nextSibling);`);
1227
- setupLines.push(` currentBlock.p(this);`);
1307
+ setupLines.push(` currentBlock.p(this${extraArgs});`);
1228
1308
  setupLines.push(` }`);
1229
1309
  if (elseBlock) {
1230
1310
  setupLines.push(` if (want === 'else') {`);
1231
- setupLines.push(` currentBlock = ${elseBlockName}(this);`);
1311
+ setupLines.push(` currentBlock = ${elseBlockName}(this${extraArgs});`);
1232
1312
  setupLines.push(` currentBlock.c();`);
1233
1313
  setupLines.push(` currentBlock.m(anchor.parentNode, anchor.nextSibling);`);
1234
- setupLines.push(` currentBlock.p(this);`);
1314
+ setupLines.push(` currentBlock.p(this${extraArgs});`);
1235
1315
  setupLines.push(` }`);
1236
1316
  }
1237
1317
  setupLines.push(` });`);
@@ -1262,8 +1342,12 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1262
1342
 
1263
1343
  const localizeVar = (line) => this.localizeVar(line);
1264
1344
 
1345
+ // Include enclosing loop variables in the factory signature
1346
+ const loopParams = this._loopVarStack.map(v => `${v.itemVar}, ${v.indexVar}`).join(', ');
1347
+ const extraParams = loopParams ? `, ${loopParams}` : '';
1348
+
1265
1349
  const factoryLines = [];
1266
- factoryLines.push(`function ${blockName}(ctx) {`);
1350
+ factoryLines.push(`function ${blockName}(ctx${extraParams}) {`);
1267
1351
 
1268
1352
  // Declare local variables
1269
1353
  const localVars = new Set();
@@ -1295,7 +1379,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1295
1379
  factoryLines.push(` },`);
1296
1380
 
1297
1381
  // p() - update/patch
1298
- factoryLines.push(` p(ctx) {`);
1382
+ factoryLines.push(` p(ctx${extraParams}) {`);
1299
1383
  if (hasEffects) {
1300
1384
  factoryLines.push(` disposers.forEach(d => d());`);
1301
1385
  factoryLines.push(` disposers = [];`);
@@ -1379,7 +1463,9 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1379
1463
  this._createLines = [];
1380
1464
  this._setupLines = [];
1381
1465
 
1466
+ this._loopVarStack.push({ itemVar, indexVar });
1382
1467
  const itemNode = this.generateTemplateBlock(body);
1468
+ this._loopVarStack.pop();
1383
1469
  const itemCreateLines = this._createLines;
1384
1470
  const itemSetupLines = this._setupLines;
1385
1471
 
@@ -1469,32 +1555,32 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1469
1555
  const setupLines = [];
1470
1556
  setupLines.push(`// Loop: ${blockName}`);
1471
1557
  setupLines.push(`{`);
1472
- setupLines.push(` const anchor = ${anchorVar};`);
1473
- setupLines.push(` const map = new Map();`);
1558
+ setupLines.push(` const __anchor = ${anchorVar};`);
1559
+ setupLines.push(` const __map = new Map();`);
1474
1560
  setupLines.push(` __effect(() => {`);
1475
- setupLines.push(` const items = ${collectionCode};`);
1476
- setupLines.push(` const parent = anchor.parentNode;`);
1477
- setupLines.push(` const newMap = new Map();`);
1561
+ setupLines.push(` const __items = ${collectionCode};`);
1562
+ setupLines.push(` const __parent = __anchor.parentNode;`);
1563
+ setupLines.push(` const __newMap = new Map();`);
1478
1564
  setupLines.push(``);
1479
- setupLines.push(` for (let ${indexVar} = 0; ${indexVar} < items.length; ${indexVar}++) {`);
1480
- setupLines.push(` const ${itemVar} = items[${indexVar}];`);
1481
- setupLines.push(` const key = ${keyExpr};`);
1482
- setupLines.push(` let block = map.get(key);`);
1483
- setupLines.push(` if (!block) {`);
1484
- setupLines.push(` block = ${blockName}(this, ${itemVar}, ${indexVar});`);
1485
- setupLines.push(` block.c();`);
1565
+ setupLines.push(` for (let ${indexVar} = 0; ${indexVar} < __items.length; ${indexVar}++) {`);
1566
+ setupLines.push(` const ${itemVar} = __items[${indexVar}];`);
1567
+ setupLines.push(` const __key = ${keyExpr};`);
1568
+ setupLines.push(` let __block = __map.get(__key);`);
1569
+ setupLines.push(` if (!__block) {`);
1570
+ setupLines.push(` __block = ${blockName}(this, ${itemVar}, ${indexVar});`);
1571
+ setupLines.push(` __block.c();`);
1486
1572
  setupLines.push(` }`);
1487
- setupLines.push(` block.m(parent, anchor);`);
1488
- setupLines.push(` block.p(this, ${itemVar}, ${indexVar});`);
1489
- setupLines.push(` newMap.set(key, block);`);
1573
+ setupLines.push(` __block.m(__parent, __anchor);`);
1574
+ setupLines.push(` __block.p(this, ${itemVar}, ${indexVar});`);
1575
+ setupLines.push(` __newMap.set(__key, __block);`);
1490
1576
  setupLines.push(` }`);
1491
1577
  setupLines.push(``);
1492
- setupLines.push(` for (const [key, block] of map) {`);
1493
- setupLines.push(` if (!newMap.has(key)) block.d(true);`);
1578
+ setupLines.push(` for (const [__k, __b] of __map) {`);
1579
+ setupLines.push(` if (!__newMap.has(__k)) __b.d(true);`);
1494
1580
  setupLines.push(` }`);
1495
1581
  setupLines.push(``);
1496
- setupLines.push(` map.clear();`);
1497
- setupLines.push(` for (const [k, v] of newMap) map.set(k, v);`);
1582
+ setupLines.push(` __map.clear();`);
1583
+ setupLines.push(` for (const [__k, __v] of __newMap) __map.set(__k, __v);`);
1498
1584
  setupLines.push(` });`);
1499
1585
  setupLines.push(`}`);
1500
1586
 
@@ -1510,7 +1596,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1510
1596
  proto.generateChildComponent = function(componentName, args) {
1511
1597
  const instVar = this.newElementVar('inst');
1512
1598
  const elVar = this.newElementVar('el');
1513
- const { propsCode, childrenSetupLines } = this.buildComponentProps(args);
1599
+ const { propsCode, reactiveProps, childrenSetupLines } = this.buildComponentProps(args);
1514
1600
 
1515
1601
  this._createLines.push(`${instVar} = new ${componentName}(${propsCode});`);
1516
1602
  this._createLines.push(`${elVar} = ${instVar}._create();`);
@@ -1518,6 +1604,10 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1518
1604
 
1519
1605
  this._setupLines.push(`if (${instVar}._setup) ${instVar}._setup();`);
1520
1606
 
1607
+ for (const { key, valueCode } of reactiveProps) {
1608
+ this._setupLines.push(`__effect(() => { if (${instVar}.${key}) ${instVar}.${key}.value = ${valueCode}; });`);
1609
+ }
1610
+
1521
1611
  for (const line of childrenSetupLines) {
1522
1612
  this._setupLines.push(line);
1523
1613
  }
@@ -1531,6 +1621,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1531
1621
 
1532
1622
  proto.buildComponentProps = function(args) {
1533
1623
  const props = [];
1624
+ const reactiveProps = [];
1534
1625
  let childrenVar = null;
1535
1626
  const childrenSetupLines = [];
1536
1627
 
@@ -1539,13 +1630,22 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1539
1630
  for (let i = 1; i < arg.length; i++) {
1540
1631
  const [key, value] = arg[i];
1541
1632
  if (typeof key === 'string') {
1542
- // Pass reactive members as signals (not values) for reactive prop binding.
1543
- // Child's __state passthrough returns the signal as-is shared reactivity.
1544
- const prevReactive = this.reactiveMembers;
1545
- this.reactiveMembers = new Set();
1546
- const valueCode = this.generateInComponent(value, 'value');
1547
- this.reactiveMembers = prevReactive;
1548
- props.push(`${key}: ${valueCode}`);
1633
+ // Simple reactive identifier pass signal directly for shared reactivity.
1634
+ // Complex expressions use normal .value unwrapping to compute the value.
1635
+ const isSimpleReactive = this.reactiveMembers && (
1636
+ (typeof value === 'string' && this.reactiveMembers.has(value)) ||
1637
+ (Array.isArray(value) && value[0] === '.' && value[1] === 'this' && typeof value[2] === 'string' && this.reactiveMembers.has(value[2]))
1638
+ );
1639
+ if (isSimpleReactive) {
1640
+ const member = typeof value === 'string' ? value : value[2];
1641
+ props.push(`${key}: this.${member}`);
1642
+ } else {
1643
+ const valueCode = this.generateInComponent(value, 'value');
1644
+ props.push(`${key}: ${valueCode}`);
1645
+ if (this.hasReactiveDeps(value)) {
1646
+ reactiveProps.push({ key, valueCode });
1647
+ }
1648
+ }
1549
1649
  }
1550
1650
  }
1551
1651
  } else if (Array.isArray(arg) && (arg[0] === '->' || arg[0] === '=>')) {
@@ -1559,11 +1659,20 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1559
1659
  for (let i = 1; i < child.length; i++) {
1560
1660
  const [key, value] = child[i];
1561
1661
  if (typeof key === 'string') {
1562
- const prevReactive = this.reactiveMembers;
1563
- this.reactiveMembers = new Set();
1564
- const valueCode = this.generateInComponent(value, 'value');
1565
- this.reactiveMembers = prevReactive;
1566
- props.push(`${key}: ${valueCode}`);
1662
+ const isSimpleReactive = this.reactiveMembers && (
1663
+ (typeof value === 'string' && this.reactiveMembers.has(value)) ||
1664
+ (Array.isArray(value) && value[0] === '.' && value[1] === 'this' && typeof value[2] === 'string' && this.reactiveMembers.has(value[2]))
1665
+ );
1666
+ if (isSimpleReactive) {
1667
+ const member = typeof value === 'string' ? value : value[2];
1668
+ props.push(`${key}: this.${member}`);
1669
+ } else {
1670
+ const valueCode = this.generateInComponent(value, 'value');
1671
+ props.push(`${key}: ${valueCode}`);
1672
+ if (this.hasReactiveDeps(value)) {
1673
+ reactiveProps.push({ key, valueCode });
1674
+ }
1675
+ }
1567
1676
  }
1568
1677
  }
1569
1678
  } else {
@@ -1598,7 +1707,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1598
1707
  }
1599
1708
 
1600
1709
  const propsCode = props.length > 0 ? `{ ${props.join(', ')} }` : '{}';
1601
- return { propsCode, childrenSetupLines };
1710
+ return { propsCode, reactiveProps, childrenSetupLines };
1602
1711
  };
1603
1712
 
1604
1713
  // --------------------------------------------------------------------------
@@ -1646,6 +1755,24 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1646
1755
  return false;
1647
1756
  };
1648
1757
 
1758
+ // findRootReactiveMember — walk a nested access chain to find the root reactive member
1759
+ // e.g. (. ([] history 0) triglycerides) → 'history'
1760
+ // --------------------------------------------------------------------------
1761
+
1762
+ proto.findRootReactiveMember = function(sexpr) {
1763
+ if (typeof sexpr === 'string') {
1764
+ return this.reactiveMembers?.has(sexpr) ? sexpr : null;
1765
+ }
1766
+ if (!Array.isArray(sexpr)) return null;
1767
+ if (sexpr[0] === '.' && sexpr[1] === 'this' && typeof sexpr[2] === 'string') {
1768
+ return this.reactiveMembers?.has(sexpr[2]) ? sexpr[2] : null;
1769
+ }
1770
+ if (sexpr[0] === '.' || sexpr[0] === '[]') {
1771
+ return this.findRootReactiveMember(sexpr[1]);
1772
+ }
1773
+ return null;
1774
+ };
1775
+
1649
1776
  // _rootsAtThis — check if a property-access chain is rooted at 'this'
1650
1777
  // --------------------------------------------------------------------------
1651
1778
 
@@ -73,7 +73,7 @@ grammar =
73
73
  o 'ReactiveAssign'
74
74
  o 'ComputedAssign'
75
75
  o 'ReadonlyAssign'
76
- o 'ReactAssign'
76
+ o 'Effect'
77
77
  o 'If'
78
78
  o 'Try'
79
79
  o 'While'
@@ -208,13 +208,13 @@ grammar =
208
208
  ]
209
209
 
210
210
  # Reactive effect (~>) — side effects that run when dependencies change
211
- ReactAssign: [
212
- o 'Assignable REACT_ASSIGN Expression' , '["effect", 1, 3]'
213
- o 'Assignable REACT_ASSIGN TERMINATOR Expression' , '["effect", 1, 4]'
214
- o 'Assignable REACT_ASSIGN INDENT Expression OUTDENT', '["effect", 1, 4]'
215
- o 'REACT_ASSIGN Expression' , '["effect", null, 2]'
216
- o 'REACT_ASSIGN TERMINATOR Expression' , '["effect", null, 3]'
217
- o 'REACT_ASSIGN INDENT Expression OUTDENT' , '["effect", null, 3]'
211
+ Effect: [
212
+ o 'Assignable EFFECT Expression' , '["effect", 1, 3]'
213
+ o 'Assignable EFFECT TERMINATOR Expression' , '["effect", 1, 4]'
214
+ o 'Assignable EFFECT Block' , '["effect", 1, 3]'
215
+ o 'EFFECT Expression' , '["effect", null, 2]'
216
+ o 'EFFECT TERMINATOR Expression' , '["effect", null, 3]'
217
+ o 'EFFECT Block' , '["effect", null, 3]'
218
218
  ]
219
219
 
220
220
  # ============================================================================
@@ -802,7 +802,7 @@ grammar =
802
802
  o 'EXPORT ReactiveAssign' , '["export", 2]'
803
803
  o 'EXPORT ComputedAssign' , '["export", 2]'
804
804
  o 'EXPORT ReadonlyAssign' , '["export", 2]'
805
- o 'EXPORT ReactAssign' , '["export", 2]'
805
+ o 'EXPORT Effect' , '["export", 2]'
806
806
  o 'EXPORT DEFAULT Expression' , '["export-default", 3]'
807
807
  o 'EXPORT DEFAULT INDENT Object OUTDENT' , '["export-default", 4]'
808
808
  o 'EXPORT EXPORT_ALL FROM String' , '["export-all", 4]'
@@ -437,7 +437,7 @@ export install = (Generator) ->
437
437
  # Statement tokens handled by parseUnary (for 'break if done', 'return x unless err')
438
438
  @_exprHandledTokens.add 'STATEMENT'
439
439
  @_exprHandledTokens.add 'RETURN'
440
- @_exprHandledTokens.add 'REACT_ASSIGN'
440
+ @_exprHandledTokens.add 'EFFECT'
441
441
 
442
442
  Generator::_findKeywordTokens = (type, dispatchName, handledTokens, visited) ->
443
443
  return if visited.has type.name
@@ -2271,7 +2271,7 @@ export install = (Generator) ->
2271
2271
  lines.push " if (token === 'STATEMENT') { const v = tokenText; advance(); return v; }"
2272
2272
  lines.push " if (token === 'RETURN') return parseReturn();"
2273
2273
  # Fire-and-forget effect (~> expr) — prefix form without left-hand side
2274
- lines.push " if (token === 'REACT_ASSIGN') return parseReactAssign();"
2274
+ lines.push " if (token === 'EFFECT') return parseEffect();"
2275
2275
  lines.push " throw new Error('Parse error: unexpected token ' + token + ' at line ' + ((tokenLoc && tokenLoc.r || 0) + 1));"
2276
2276
  lines.push "}"
2277
2277
  lines.join '\n'
package/src/lexer.js CHANGED
@@ -1156,7 +1156,7 @@ export class Lexer {
1156
1156
  else if (val === '~=') tag = 'COMPUTED_ASSIGN';
1157
1157
  else if (val === ':=') tag = 'REACTIVE_ASSIGN';
1158
1158
  else if (val === '<=>') tag = 'BIND';
1159
- else if (val === '~>') tag = 'REACT_ASSIGN';
1159
+ else if (val === '~>') tag = 'EFFECT';
1160
1160
  else if (val === '=!') tag = 'READONLY_ASSIGN';
1161
1161
  // Merge assignment: *config = {a: 1} → Object.assign(config, {a: 1})
1162
1162
  // Also supports *@ = props → Object.assign(this, props)