rip-lang 2.8.6 → 2.8.7

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 CHANGED
@@ -9,9 +9,9 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-2.8.6-blue.svg" alt="Version"></a>
12
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-2.8.7-blue.svg" alt="Version"></a>
13
13
  <a href="#zero-dependencies"><img src="https://img.shields.io/badge/dependencies-ZERO-brightgreen.svg" alt="Dependencies"></a>
14
- <a href="#"><img src="https://img.shields.io/badge/tests-1021%2F1021-brightgreen.svg" alt="Tests"></a>
14
+ <a href="#"><img src="https://img.shields.io/badge/tests-1098%2F1098-brightgreen.svg" alt="Tests"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
16
16
  </p>
17
17
 
@@ -122,6 +122,8 @@ State, computed values, and effects as language operators:
122
122
  | `=~` | `str =~ /Hello, (\w+)/` | Match (captures in `_`) |
123
123
  | `[//, n]` | `str[/Hello, (\w+)/, 1]` | Extract capture n |
124
124
  | `.new()` | `Dog.new()` | Ruby-style constructor |
125
+ | `or return` | `x = get() or return err` | Guard clause (Ruby-style) |
126
+ | `?? throw` | `x = get() ?? throw err` | Nullish guard (null/undefined only) |
125
127
 
126
128
  **Optional chaining** — Both CoffeeScript and ES6 styles are supported:
127
129
 
@@ -261,7 +263,7 @@ rip file.rip # Run
261
263
  rip -c file.rip # Compile
262
264
  rip -t file.rip # Tokens
263
265
  rip -s file.rip # S-expressions
264
- bun run test # 1021 tests
266
+ bun run test # 1098 tests
265
267
  bun run parser # Rebuild parser
266
268
  bun run browser # Build browser bundle
267
269
  ```
@@ -7627,8 +7627,8 @@ function compileToJS(source, options = {}) {
7627
7627
  return new Compiler(options).compileToJS(source);
7628
7628
  }
7629
7629
  // src/browser.js
7630
- var VERSION = "2.8.6";
7631
- var BUILD_DATE = "2026-02-04@11:11:08GMT";
7630
+ var VERSION = "2.8.7";
7631
+ var BUILD_DATE = "2026-02-04@11:45:06GMT";
7632
7632
  var dedent = (s) => {
7633
7633
  const m = s.match(/^[ \t]*(?=\S)/gm);
7634
7634
  const i = Math.min(...(m || []).map((x) => x.length));
@@ -530,4 +530,4 @@ function __catchErrors(fn) {
530
530
  `),X=E.findIndex((G)=>G==="__DATA__");if(X!==-1){let G=E.slice(X+1);_=G.length>0?G.join(`
531
531
  `)+`
532
532
  `:"",$=E.slice(0,X).join(`
533
- `)}let A=new m1().tokenize($);if(this.options.showTokens)A.forEach((G)=>console.log(`${G[0].padEnd(12)} ${JSON.stringify(G[1])}`)),console.log();$1.lexer={tokens:A,pos:0,setInput:function(){},lex:function(){if(this.pos>=this.tokens.length)return 1;let G=this.tokens[this.pos++];return this.yytext=G[1],this.yylloc=G[2],G[0]}};let U;try{U=$1.parse($)}catch(G){if(/\?\s*\([^)]*\?[^)]*:[^)]*\)\s*:/.test($)||/\?\s+\w+\s+\?\s+/.test($))throw Error("Nested ternary operators are not supported. Use if/else statements instead.");throw G}if(this.options.showSExpr)console.log(o(U,0,!0)),console.log();let Y=new c({dataSection:_,skipReactiveRuntime:this.options.skipReactiveRuntime,reactiveVars:this.options.reactiveVars}),F=Y.compile(U);return{tokens:A,sexpr:U,code:F,data:_,reactiveVars:Y.reactiveVars}}compileToJS($){return this.compile($).code}compileToSExpr($){return this.compile($).sexpr}}function U3($,_={}){return new O1(_).compile($)}function S1($,_={}){return new O1(_).compileToJS($)}var Q3="2.8.6",W3="2026-02-04@11:11:08GMT",$3=($)=>{let _=$.match(/^[ \t]*(?=\S)/gm),E=Math.min(...(_||[]).map((X)=>X.length));return $.replace(RegExp(`^[ ]{${E}}`,"gm"),"").trim()};async function k2(){let $=document.querySelectorAll('script[type="text/rip"]');for(let _ of $){if(_.hasAttribute("data-rip-processed"))continue;try{let E=$3(_.textContent),X=S1(E);(0,eval)(X),_.setAttribute("data-rip-processed","true")}catch(E){console.error("Error compiling Rip script:",E),console.error("Script content:",_.textContent)}}}if(typeof document<"u")if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",k2);else k2();function X3($){try{let E=S1($).replace(/^let\s+[^;]+;\s*\n\s*/m,"");E=E.replace(/^const\s+/gm,"var ");let X=(0,eval)(E);if(X!==void 0)globalThis._=X;return X}catch(_){console.error("Rip compilation error:",_.message);return}}if(typeof globalThis<"u")globalThis.rip=X3;export{X3 as rip,k2 as processRipScripts,$1 as parser,o as formatSExpr,S1 as compileToJS,U3 as compile,Q3 as VERSION,m1 as Lexer,O1 as Compiler,c as CodeGenerator,W3 as BUILD_DATE};
533
+ `)}let A=new m1().tokenize($);if(this.options.showTokens)A.forEach((G)=>console.log(`${G[0].padEnd(12)} ${JSON.stringify(G[1])}`)),console.log();$1.lexer={tokens:A,pos:0,setInput:function(){},lex:function(){if(this.pos>=this.tokens.length)return 1;let G=this.tokens[this.pos++];return this.yytext=G[1],this.yylloc=G[2],G[0]}};let U;try{U=$1.parse($)}catch(G){if(/\?\s*\([^)]*\?[^)]*:[^)]*\)\s*:/.test($)||/\?\s+\w+\s+\?\s+/.test($))throw Error("Nested ternary operators are not supported. Use if/else statements instead.");throw G}if(this.options.showSExpr)console.log(o(U,0,!0)),console.log();let Y=new c({dataSection:_,skipReactiveRuntime:this.options.skipReactiveRuntime,reactiveVars:this.options.reactiveVars}),F=Y.compile(U);return{tokens:A,sexpr:U,code:F,data:_,reactiveVars:Y.reactiveVars}}compileToJS($){return this.compile($).code}compileToSExpr($){return this.compile($).sexpr}}function U3($,_={}){return new O1(_).compile($)}function S1($,_={}){return new O1(_).compileToJS($)}var Q3="2.8.7",W3="2026-02-04@11:45:06GMT",$3=($)=>{let _=$.match(/^[ \t]*(?=\S)/gm),E=Math.min(...(_||[]).map((X)=>X.length));return $.replace(RegExp(`^[ ]{${E}}`,"gm"),"").trim()};async function k2(){let $=document.querySelectorAll('script[type="text/rip"]');for(let _ of $){if(_.hasAttribute("data-rip-processed"))continue;try{let E=$3(_.textContent),X=S1(E);(0,eval)(X),_.setAttribute("data-rip-processed","true")}catch(E){console.error("Error compiling Rip script:",E),console.error("Script content:",_.textContent)}}}if(typeof document<"u")if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",k2);else k2();function X3($){try{let E=S1($).replace(/^let\s+[^;]+;\s*\n\s*/m,"");E=E.replace(/^const\s+/gm,"var ");let X=(0,eval)(E);if(X!==void 0)globalThis._=X;return X}catch(_){console.error("Rip compilation error:",_.message);return}}if(typeof globalThis<"u")globalThis.rip=X3;export{X3 as rip,k2 as processRipScripts,$1 as parser,o as formatSExpr,S1 as compileToJS,U3 as compile,Q3 as VERSION,m1 as Lexer,O1 as Compiler,c as CodeGenerator,W3 as BUILD_DATE};
Binary file
package/docs/repl.html CHANGED
@@ -659,6 +659,7 @@ console.log "Domain:", domain</textarea>
659
659
  let replHistory = [];
660
660
  let historyIndex = -1;
661
661
  let replBuffer = '';
662
+ let reactiveVars = new Set(); // Track reactive variables across evaluations
662
663
 
663
664
  // Create isolated iframe context for REPL (like vm.createContext in Node)
664
665
  const iframe = document.createElement('iframe');
@@ -670,6 +671,90 @@ console.log "Domain:", domain</textarea>
670
671
  replContext.console = console;
671
672
  replContext.showSexp = false;
672
673
  replContext.showTokens = false;
674
+ replContext.__reactiveVars = {}; // Store reactive objects persistently
675
+
676
+ // Inject reactive runtime into iframe context
677
+ (function injectReactiveRuntime() {
678
+ const ctx = replContext;
679
+ ctx.__currentEffect = null;
680
+ ctx.__pendingEffects = new Set();
681
+
682
+ ctx.__state = function(v) {
683
+ const subs = new Set();
684
+ let notifying = false, locked = false, dead = false;
685
+ const s = {
686
+ get value() { if (dead) return v; if (ctx.__currentEffect) { subs.add(ctx.__currentEffect); ctx.__currentEffect.dependencies.add(subs); } return v; },
687
+ set value(n) {
688
+ if (dead || locked || n === v || notifying) return;
689
+ v = n;
690
+ notifying = true;
691
+ for (const sub of subs) if (sub.markDirty) sub.markDirty();
692
+ for (const sub of subs) if (!sub.markDirty) ctx.__pendingEffects.add(sub);
693
+ const fx = [...ctx.__pendingEffects]; ctx.__pendingEffects.clear();
694
+ for (const e of fx) e.run();
695
+ notifying = false;
696
+ },
697
+ read() { return v; },
698
+ lock() { locked = true; return s; },
699
+ free() { subs.clear(); return s; },
700
+ kill() { dead = true; subs.clear(); return v; },
701
+ valueOf() { return this.value; },
702
+ toString() { return String(this.value); },
703
+ [Symbol.toPrimitive](hint) { return hint === 'string' ? this.toString() : this.valueOf(); }
704
+ };
705
+ return s;
706
+ };
707
+
708
+ ctx.__computed = function(fn) {
709
+ let v, dirty = true, locked = false, dead = false;
710
+ const subs = new Set();
711
+ const c = {
712
+ dependencies: new Set(),
713
+ markDirty() {
714
+ if (dead || locked || dirty) return;
715
+ dirty = true;
716
+ for (const s of subs) if (s.markDirty) s.markDirty();
717
+ for (const s of subs) if (!s.markDirty) ctx.__pendingEffects.add(s);
718
+ },
719
+ get value() {
720
+ if (dead) return v;
721
+ if (ctx.__currentEffect) { subs.add(ctx.__currentEffect); ctx.__currentEffect.dependencies.add(subs); }
722
+ if (dirty && !locked) {
723
+ for (const d of c.dependencies) d.delete(c); c.dependencies.clear();
724
+ const prev = ctx.__currentEffect; ctx.__currentEffect = c;
725
+ try { v = fn(); } finally { ctx.__currentEffect = prev; }
726
+ dirty = false;
727
+ }
728
+ return v;
729
+ },
730
+ read() { return dead ? v : c.value; },
731
+ lock() { locked = true; c.value; return c; },
732
+ free() { for (const d of c.dependencies) d.delete(c); c.dependencies.clear(); subs.clear(); return c; },
733
+ kill() { dead = true; const result = v; c.free(); return result; },
734
+ valueOf() { return this.value; },
735
+ toString() { return String(this.value); },
736
+ [Symbol.toPrimitive](hint) { return hint === 'string' ? this.toString() : this.valueOf(); }
737
+ };
738
+ return c;
739
+ };
740
+
741
+ ctx.__effect = function(fn) {
742
+ const e = {
743
+ dependencies: new Set(),
744
+ run() {
745
+ for (const d of e.dependencies) d.delete(e); e.dependencies.clear();
746
+ const prev = ctx.__currentEffect; ctx.__currentEffect = e;
747
+ try { fn(); } finally { ctx.__currentEffect = prev; }
748
+ },
749
+ free() { for (const d of e.dependencies) d.delete(e); e.dependencies.clear(); }
750
+ };
751
+ e.run();
752
+ return () => e.free();
753
+ };
754
+
755
+ ctx.__batch = function(fn) { fn(); };
756
+ ctx.__readonly = function(v) { return Object.freeze({ value: v }); };
757
+ })();
673
758
 
674
759
  function addOutput(content, className = '') {
675
760
  const line = document.createElement('div');
@@ -681,9 +766,17 @@ console.log "Domain:", domain</textarea>
681
766
 
682
767
  function evaluateRip(code) {
683
768
  try {
684
- const result = compile(code);
769
+ // Pass reactiveVars to compiler so it knows which vars need .value access
770
+ const result = compile(code, { reactiveVars, skipReactiveRuntime: true });
685
771
  let js = result.code;
686
772
 
773
+ // Track new reactive variables
774
+ if (result.reactiveVars) {
775
+ for (const v of result.reactiveVars) {
776
+ reactiveVars.add(v);
777
+ }
778
+ }
779
+
687
780
  // REPL strategy: Strip let/const declarations entirely
688
781
  // Assignments will create properties on iframe's window (global scope)
689
782
  // This allows variables to persist between eval() calls
@@ -691,13 +784,22 @@ console.log "Domain:", domain</textarea>
691
784
  // Remove: let x, y, z;\n
692
785
  js = js.replace(/^let\s+[^;]+;\s*\n+/m, '');
693
786
 
694
- // Remove: const x = ... (but keep the assignment)
787
+ // Transform reactive declarations to persist in __reactiveVars
788
+ // const x = __state(...) → x = __reactiveVars.x ?? (__reactiveVars.x = __state(...))
789
+ js = js.replace(
790
+ /^const\s+(\w+)\s*=\s*((?:__state|__computed|__effect)\(.+\));?$/gm,
791
+ (match, varName, rhs) => {
792
+ return `${varName} = __reactiveVars['${varName}'] ?? (__reactiveVars['${varName}'] = ${rhs});`;
793
+ }
794
+ );
795
+
796
+ // Remove remaining const (but keep the assignment) for non-reactive
695
797
  js = js.replace(/^const\s+(\w+)\s*=/gm, '$1 =');
696
798
 
697
799
  // Evaluate in iframe context - assignments create globals
698
800
  const evalResult = replContext.eval(js);
699
801
 
700
- // Store in _
802
+ // Store in _ (unwrap reactive values)
701
803
  if (evalResult !== undefined) {
702
804
  replContext._ = evalResult;
703
805
  }
@@ -730,13 +832,27 @@ console.log "Domain:", domain</textarea>
730
832
 
731
833
  case '.clear':
732
834
  replOutput.innerHTML = '';
733
- addOutput('<div class="welcome">Output cleared.</div>');
835
+ // Reset reactive tracking
836
+ reactiveVars.clear();
837
+ replContext.__reactiveVars = {};
838
+ // Clear all user-defined globals in iframe
839
+ const builtinsToKeep = ['console', 'showSexp', 'showTokens', 'eval', 'window', 'document',
840
+ 'location', 'navigator', 'self', 'top', 'parent', 'frames', '__state', '__computed',
841
+ '__effect', '__batch', '__readonly', '__currentEffect', '__pendingEffects', '__reactiveVars'];
842
+ for (const key of Object.keys(replContext)) {
843
+ if (!builtinsToKeep.includes(key) && !key.startsWith('__')) {
844
+ try { delete replContext[key]; } catch {}
845
+ }
846
+ }
847
+ addOutput('<div class="welcome">Output and context cleared.</div>');
734
848
  break;
735
849
 
736
850
  case '.vars':
737
- const builtins = ['console', 'showSexp', 'showTokens', 'eval', 'window', 'document', 'location', 'navigator', 'self', 'top', 'parent', 'frames'];
851
+ const builtins = ['console', 'showSexp', 'showTokens', 'eval', 'window', 'document',
852
+ 'location', 'navigator', 'self', 'top', 'parent', 'frames', '__state', '__computed',
853
+ '__effect', '__batch', '__readonly', '__currentEffect', '__pendingEffects', '__reactiveVars'];
738
854
  const vars = Object.keys(replContext).filter(k =>
739
- !builtins.includes(k) && !k.startsWith('_') || k === '_'
855
+ !builtins.includes(k) && !k.startsWith('__') || k === '_'
740
856
  );
741
857
  if (vars.length === 0 || (vars.length === 1 && vars[0] === '_' && replContext._ === undefined)) {
742
858
  addOutput('<span class="help-text">No variables defined</span>', 'command-output');
@@ -744,8 +860,21 @@ console.log "Domain:", domain</textarea>
744
860
  let output = '<span class="help-text">Variables:</span>\\n';
745
861
  vars.forEach(v => {
746
862
  try {
747
- const val = JSON.stringify(replContext[v]);
748
- output += ` <span class="var-name">${v}</span> = <span class="var-value">${val}</span>\\n`;
863
+ let val = replContext[v];
864
+ let typeIndicator = '=';
865
+ // Check if it's a reactive value
866
+ if (val && typeof val === 'object' && 'value' in val) {
867
+ if (typeof val.markDirty === 'function') {
868
+ typeIndicator = '<span style="color:#c586c0">~=</span>'; // computed
869
+ } else if (typeof val === 'function') {
870
+ typeIndicator = '<span style="color:#c586c0">~&gt;</span>'; // effect
871
+ } else {
872
+ typeIndicator = '<span style="color:#c586c0">:=</span>'; // state
873
+ }
874
+ val = val.value;
875
+ }
876
+ const formatted = formatValue(val);
877
+ output += ` <span class="var-name">${v}</span> ${typeIndicator} <span class="var-value">${escapeHtml(formatted)}</span>\\n`;
749
878
  } catch {
750
879
  output += ` <span class="var-name">${v}</span> = <span class="var-value">[object]</span>\\n`;
751
880
  }
@@ -809,11 +938,14 @@ console.log "Domain:", domain</textarea>
809
938
  addOutput(`${formatted}`, 'command-output');
810
939
  }
811
940
 
812
- // Show result
941
+ // Show result (unwrap reactive values)
813
942
  if (evalResult.value !== undefined) {
814
- const formatted = typeof evalResult.value === 'object'
815
- ? JSON.stringify(evalResult.value, null, 2)
816
- : String(evalResult.value);
943
+ let displayValue = evalResult.value;
944
+ // Unwrap reactive objects to show their .value
945
+ if (displayValue && typeof displayValue === 'object' && 'value' in displayValue) {
946
+ displayValue = displayValue.value;
947
+ }
948
+ const formatted = formatValue(displayValue);
817
949
  addOutput(`<span class="result">→ ${escapeHtml(formatted)}</span>`);
818
950
  }
819
951
 
@@ -838,6 +970,23 @@ console.log "Domain:", domain</textarea>
838
970
  .replace(/"/g, '&quot;');
839
971
  }
840
972
 
973
+ // Format values for display (like Node's util.inspect)
974
+ function formatValue(val) {
975
+ if (val === null) return 'null';
976
+ if (val === undefined) return 'undefined';
977
+ if (val instanceof RegExp) return val.toString();
978
+ if (typeof val === 'function') return val.toString();
979
+ if (typeof val === 'string') return JSON.stringify(val);
980
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val);
981
+ if (Array.isArray(val)) {
982
+ try { return JSON.stringify(val); } catch { return '[Array]'; }
983
+ }
984
+ if (typeof val === 'object') {
985
+ try { return JSON.stringify(val, null, 2); } catch { return '[Object]'; }
986
+ }
987
+ return String(val);
988
+ }
989
+
841
990
  // REPL input handling
842
991
  replInput.addEventListener('keydown', (e) => {
843
992
  if (e.key === 'Enter') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "2.8.6",
3
+ "version": "2.8.7",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
package/src/repl.js CHANGED
@@ -43,13 +43,13 @@ export class RipREPL {
43
43
  this.historyFile = path.join(os.homedir(), '.rip_history');
44
44
  this.reactiveVars = new Set(); // Track reactive variables across lines
45
45
  this.cwd = process.cwd();
46
-
46
+
47
47
  // Persisted variables across evaluations
48
48
  this.vars = {};
49
-
49
+
50
50
  // Module cache for linked modules
51
51
  this.moduleCache = new Map();
52
-
52
+
53
53
  // VM context with necessary globals
54
54
  this.vmContext = vm.createContext({
55
55
  console,
@@ -116,7 +116,7 @@ export class RipREPL {
116
116
  injectReactiveRuntime() {
117
117
  // Define reactive primitives in the VM context
118
118
  const ctx = this.vmContext;
119
-
119
+
120
120
  ctx.__currentEffect = null;
121
121
  ctx.__pendingEffects = new Set();
122
122
 
@@ -367,11 +367,26 @@ export class RipREPL {
367
367
 
368
368
  // Evaluate using vm.SourceTextModule (no temp files!)
369
369
  async moduleEval(js) {
370
- // Extract declared variables
370
+ // Extract declared variables (both let and const)
371
371
  const declaredVars = new Set();
372
372
  for (const match of js.matchAll(/^let\s+(\w+)/gm)) {
373
373
  declaredVars.add(match[1]);
374
374
  }
375
+ for (const match of js.matchAll(/^const\s+(\w+)\s*=/gm)) {
376
+ declaredVars.add(match[1]);
377
+ }
378
+
379
+ // Transform reactive declarations to persist in __vars
380
+ // const x = __state(...) → const x = __vars.x ?? (__vars.x = __state(...)); x;
381
+ // const x = __computed(...) → const x = __vars.x ?? (__vars.x = __computed(...)); x;
382
+ // const x = __effect(...) → const x = __vars.x ?? (__vars.x = __effect(...)); x;
383
+ // The trailing `x;` ensures the value is captured and displayed
384
+ js = js.replace(
385
+ /^const\s+(\w+)\s*=\s*((?:__state|__computed|__effect)\(.+\));?$/gm,
386
+ (match, varName, rhs) => {
387
+ return `const ${varName} = __vars['${varName}'] ?? (__vars['${varName}'] = ${rhs});\n${varName};`;
388
+ }
389
+ );
375
390
 
376
391
  // Transform await import() to static imports (workaround for Bun bug #24217)
377
392
  const dynamicImports = [];
@@ -388,20 +403,33 @@ export class RipREPL {
388
403
 
389
404
  // Restore existing variables and remove duplicate declarations
390
405
  const existingVars = Object.keys(this.vars);
391
- for (const v of existingVars) {
406
+ const existingNonReactive = existingVars.filter(v => !this.reactiveVars.has(v));
407
+
408
+ for (const v of existingNonReactive) {
392
409
  js = js.replace(new RegExp(`^let ${v};\\n?`, 'm'), '');
393
410
  js = js.replace(new RegExp(`^let ${v}(\\s*=)`, 'm'), `${v}$1`);
394
411
  }
395
412
 
396
- // Build restore code (get vars from __vars)
397
- const restoreCode = existingVars
413
+ // Build restore code
414
+ // Non-reactive vars: let x = __vars['x'];
415
+ // Reactive vars: const x = __vars['x']; (they're already stored)
416
+ const nonReactiveRestore = existingNonReactive
398
417
  .filter(k => k !== '_')
399
418
  .map(v => `let ${v} = __vars['${v}'];`)
400
419
  .join('\n');
401
420
 
402
- // Build save code (save vars back to __vars)
403
- const allVars = [...new Set([...existingVars, ...declaredVars])].filter(k => k !== '_');
404
- const saveCode = allVars
421
+ const reactiveRestore = [...this.reactiveVars]
422
+ .filter(v => existingVars.includes(v))
423
+ .map(v => `const ${v} = __vars['${v}'];`)
424
+ .join('\n');
425
+
426
+ const restoreCode = [nonReactiveRestore, reactiveRestore].filter(Boolean).join('\n');
427
+
428
+ // Build save code (save non-reactive vars back to __vars)
429
+ // Reactive vars are already saved via the const transformation
430
+ const nonReactiveVars = [...new Set([...existingNonReactive, ...declaredVars])]
431
+ .filter(k => k !== '_' && !this.reactiveVars.has(k));
432
+ const saveCode = nonReactiveVars
405
433
  .map(v => `if (typeof ${v} !== 'undefined') __vars['${v}'] = ${v};`)
406
434
  .join('\n');
407
435
 
@@ -437,7 +465,14 @@ export { __result };
437
465
  }
438
466
 
439
467
  printResult(value) {
440
- const formatted = inspect(value, {
468
+ // For reactive values, show the actual value
469
+ let displayValue = value;
470
+ if (value && typeof value === 'object' && 'value' in value &&
471
+ (typeof value.valueOf === 'function' || typeof value.markDirty === 'function')) {
472
+ displayValue = value.value;
473
+ }
474
+
475
+ const formatted = inspect(displayValue, {
441
476
  colors: true,
442
477
  depth: 3,
443
478
  maxArrayLength: 100
@@ -539,8 +574,27 @@ ${colors.cyan}Tips:${colors.reset}
539
574
  console.log(`${colors.bright}Defined variables:${colors.reset}`);
540
575
  userVars.forEach(key => {
541
576
  const value = this.vars[key];
542
- const preview = inspect(value, { colors: true, depth: 0, maxArrayLength: 3 });
543
- console.log(` ${colors.cyan}${key}${colors.reset} = ${preview}`);
577
+ const isReactive = this.reactiveVars.has(key);
578
+
579
+ // For reactive vars, display the .value and indicate the type
580
+ let displayValue, typeIndicator;
581
+ if (isReactive && value && typeof value === 'object' && 'value' in value) {
582
+ displayValue = value.value;
583
+ // Determine reactive type
584
+ if (typeof value.markDirty === 'function') {
585
+ typeIndicator = `${colors.magenta}~=${colors.reset}`; // computed
586
+ } else if (typeof value.run === 'function' || typeof value === 'function') {
587
+ typeIndicator = `${colors.magenta}~>${colors.reset}`; // effect
588
+ } else {
589
+ typeIndicator = `${colors.magenta}:=${colors.reset}`; // state
590
+ }
591
+ } else {
592
+ displayValue = value;
593
+ typeIndicator = `${colors.gray}=${colors.reset}`;
594
+ }
595
+
596
+ const preview = inspect(displayValue, { colors: true, depth: 0, maxArrayLength: 3 });
597
+ console.log(` ${colors.cyan}${key}${colors.reset} ${typeIndicator} ${preview}`);
544
598
  });
545
599
 
546
600
  if (this.vars._ !== undefined) {