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.
- package/README.md +5 -3
- package/docs/dist/rip.browser.js +83 -15
- package/docs/dist/rip.browser.min.js +99 -99
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/docs/repl.html +161 -12
- package/package.json +1 -1
- package/src/compiler.js +102 -0
- package/src/grammar/grammar.rip +9 -0
- package/src/parser.js +14 -13
- package/src/repl.js +68 -14
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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',
|
|
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('
|
|
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
|
-
|
|
748
|
-
|
|
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">~></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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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, '"');
|
|
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
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]
|
package/src/grammar/grammar.rip
CHANGED
|
@@ -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
|