rip-lang 2.8.5 → 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.
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.5",
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/compiler.js CHANGED
@@ -227,6 +227,7 @@ export class CodeGenerator {
227
227
  'until': 'generateUntil',
228
228
  'try': 'generateTry',
229
229
  'throw': 'generateThrow',
230
+ 'control': 'generateControl',
230
231
  'switch': 'generateSwitch',
231
232
  'when': 'generateWhen',
232
233
 
@@ -1081,6 +1082,53 @@ export class CodeGenerator {
1081
1082
  return valueCode;
1082
1083
  }
1083
1084
 
1085
+ // Handle control flow short-circuits: x = expr or return/throw
1086
+ // Pattern: ["=", target, ["control", op, expr, ["return", value]]]
1087
+ // Operators: || (falsy), ?? (nullish), && (truthy)
1088
+ if (Array.isArray(value) && op === '=' && value[0] === 'control') {
1089
+ const [, rawCtrlOp, expr, ctrlSexpr] = value;
1090
+ const ctrlOp = rawCtrlOp instanceof String ? rawCtrlOp.valueOf() : rawCtrlOp;
1091
+ const isReturn = ctrlSexpr[0] === 'return';
1092
+
1093
+ // Declare the target variable if it's a simple identifier
1094
+ const targetCode = this.generate(target, 'value');
1095
+ if (typeof target === 'string') {
1096
+ this.programVars.add(target);
1097
+ }
1098
+
1099
+ // Generate the expression that will be assigned
1100
+ const exprCode = this.generate(expr, 'value');
1101
+
1102
+ // Generate the control flow statement
1103
+ const ctrlValue = ctrlSexpr.length > 1 ? ctrlSexpr[1] : null;
1104
+ const ctrlCode = isReturn
1105
+ ? (ctrlValue ? `return ${this.generate(ctrlValue, 'value')}` : 'return')
1106
+ : (ctrlValue ? `throw ${this.generate(ctrlValue, 'value')}` : 'throw new Error()');
1107
+
1108
+ // In value context, wrap in IIFE that returns the assigned value
1109
+ if (context === 'value') {
1110
+ if (ctrlOp === '??') {
1111
+ return `(() => { const __v = ${exprCode}; if (__v == null) ${ctrlCode}; return (${targetCode} = __v); })()`;
1112
+ } else if (ctrlOp === '||') {
1113
+ return `(() => { const __v = ${exprCode}; if (!__v) ${ctrlCode}; return (${targetCode} = __v); })()`;
1114
+ } else {
1115
+ return `(() => { const __v = ${exprCode}; if (__v) ${ctrlCode}; return (${targetCode} = __v); })()`;
1116
+ }
1117
+ }
1118
+
1119
+ // Statement context: if (condition) return/throw value;
1120
+ // || → if (!(target = expr))
1121
+ // ?? → if ((target = expr) == null)
1122
+ // && → if ((target = expr))
1123
+ if (ctrlOp === '??') {
1124
+ return `if ((${targetCode} = ${exprCode}) == null) ${ctrlCode}`;
1125
+ } else if (ctrlOp === '||') {
1126
+ return `if (!(${targetCode} = ${exprCode})) ${ctrlCode}`;
1127
+ } else {
1128
+ return `if ((${targetCode} = ${exprCode})) ${ctrlCode}`;
1129
+ }
1130
+ }
1131
+
1084
1132
  // Check for middle/leading rest in array destructuring
1085
1133
  if (Array.isArray(target) && target[0] === 'array') {
1086
1134
  const restIndex = target.slice(1).findIndex(el =>
@@ -2739,6 +2787,60 @@ export class CodeGenerator {
2739
2787
  return throwStmt;
2740
2788
  }
2741
2789
 
2790
+ /**
2791
+ * Generate control flow short-circuits: or/and return/throw
2792
+ * Pattern: ["control", op, expr, ["return", value]] or ["control", op, expr, ["throw", error]]
2793
+ * Operators: || (falsy), ?? (nullish), && (truthy)
2794
+ */
2795
+ generateControl(head, rest, context, sexpr) {
2796
+ const [rawOp, expr, ctrlSexpr] = rest;
2797
+ const op = rawOp instanceof String ? rawOp.valueOf() : rawOp;
2798
+ const isReturn = ctrlSexpr[0] === 'return';
2799
+
2800
+ // Generate expression and control flow statement
2801
+ const exprCode = this.generate(expr, 'value');
2802
+ const ctrlValue = ctrlSexpr.length > 1 ? ctrlSexpr[1] : null;
2803
+ const ctrlCode = isReturn
2804
+ ? (ctrlValue ? `return ${this.generate(ctrlValue, 'value')}` : 'return')
2805
+ : (ctrlValue ? `throw ${this.generate(ctrlValue, 'value')}` : 'throw new Error()');
2806
+
2807
+ // Build condition based on operator:
2808
+ // || → trigger on falsy (!expr)
2809
+ // ?? → trigger on nullish (expr == null)
2810
+ // && → trigger on truthy (expr)
2811
+ const wrappedExpr = this.wrapForCondition(exprCode);
2812
+
2813
+ // Value context: wrap in IIFE
2814
+ if (context === 'value') {
2815
+ if (op === '??') {
2816
+ return `(() => { const __v = ${exprCode}; if (__v == null) ${ctrlCode}; return __v; })()`;
2817
+ } else if (op === '||') {
2818
+ return `(() => { const __v = ${exprCode}; if (!__v) ${ctrlCode}; return __v; })()`;
2819
+ } else {
2820
+ return `(() => { const __v = ${exprCode}; if (__v) ${ctrlCode}; return __v; })()`;
2821
+ }
2822
+ }
2823
+
2824
+ // Statement context
2825
+ if (op === '??') {
2826
+ return `if (${wrappedExpr} == null) ${ctrlCode}`;
2827
+ } else if (op === '||') {
2828
+ return `if (!${wrappedExpr}) ${ctrlCode}`;
2829
+ } else {
2830
+ return `if (${wrappedExpr}) ${ctrlCode}`;
2831
+ }
2832
+ }
2833
+
2834
+ /**
2835
+ * Wrap code for use in a condition (add parens if needed)
2836
+ */
2837
+ wrapForCondition(code) {
2838
+ // If it's a simple identifier or already wrapped, don't add extra parens
2839
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(code)) return code;
2840
+ if (code.startsWith('(') && code.endsWith(')')) return code;
2841
+ return `(${code})`;
2842
+ }
2843
+
2742
2844
  /**
2743
2845
  * Generate switch statement
2744
2846
  * Pattern: ["switch", discriminant, whens, defaultCase]
@@ -778,6 +778,15 @@ grammar =
778
778
  o 'Expression & Expression' , '["&", 1, 3]'
779
779
  o 'Expression ^ Expression' , '["^", 1, 3]'
780
780
  o 'Expression | Expression' , '["|", 1, 3]'
781
+
782
+ # Control flow short-circuits (must be before generic && || ?? to take precedence)
783
+ o 'Expression || Return' , '["control", 2, 1, 3]'
784
+ o 'Expression || Throw' , '["control", 2, 1, 3]'
785
+ o 'Expression ?? Return' , '["control", 2, 1, 3]'
786
+ o 'Expression ?? Throw' , '["control", 2, 1, 3]'
787
+ o 'Expression && Return' , '["control", 2, 1, 3]'
788
+ o 'Expression && Throw' , '["control", 2, 1, 3]'
789
+
781
790
  o 'Expression && Expression' , '["&&", 1, 3]'
782
791
  o 'Expression || Expression' , '["||", 1, 3]'
783
792
  o 'Expression ?? Expression' , '["??", 1, 3]' # Nullish coalescing