structscript 1.2.0

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 ADDED
@@ -0,0 +1,157 @@
1
+ # StructScript
2
+
3
+ > A clean, readable scripting language that reads like English.
4
+
5
+ ```
6
+ ╔══════════════════════════════╗
7
+ ║ StructScript v1.2.0 ║
8
+ ║ The structured scripting ║
9
+ ║ language for everyone ║
10
+ ╚══════════════════════════════╝
11
+ ```
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g structscript
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ # Run a .ss file
23
+ structscript main.ss
24
+
25
+ # Interactive REPL
26
+ structscript repl
27
+
28
+ # Short alias
29
+ ss main.ss
30
+
31
+ # Check syntax
32
+ structscript check main.ss
33
+
34
+ # Show execution time
35
+ structscript main.ss --time
36
+ ```
37
+
38
+ ## The Language
39
+
40
+ StructScript uses Python-style indentation and reads like English:
41
+
42
+ ```
43
+ // Variables
44
+ let name = "Alex"
45
+ let age = 24
46
+ const PI = 3.14159
47
+
48
+ // String interpolation
49
+ say "Hello, {name}! You are {age} years old."
50
+
51
+ // Conditionals
52
+ if age >= 18:
53
+ say "Adult"
54
+ else if age >= 13:
55
+ say "Teenager"
56
+ else:
57
+ say "Child"
58
+
59
+ // Loops
60
+ count i from 1 to 10:
61
+ say i
62
+
63
+ repeat 3 times:
64
+ say "Hello!"
65
+
66
+ for each item in ["a", "b", "c"]:
67
+ say item
68
+
69
+ // Functions
70
+ define greet(name, msg = "Hello"):
71
+ say "{msg}, {name}!"
72
+
73
+ greet("World")
74
+ greet("Alex", "Welcome")
75
+
76
+ // Classes
77
+ class Animal:
78
+ name = "Unknown"
79
+ sound = "..."
80
+
81
+ define init(n, s):
82
+ set self.name = n
83
+ set self.sound = s
84
+
85
+ define speak():
86
+ say "{self.name} says {self.sound}!"
87
+
88
+ let dog = Animal()
89
+ dog.init("Rex", "Woof")
90
+ dog.speak()
91
+
92
+ // Error handling
93
+ try:
94
+ error("Something went wrong")
95
+ catch e:
96
+ say "Caught: {e}"
97
+ finally:
98
+ say "Done."
99
+
100
+ // Lists
101
+ let nums = [5, 3, 8, 1, 9]
102
+ say sort(nums)
103
+ say sum(nums)
104
+ say "Max: {max(1, 5, 9)}"
105
+ ```
106
+
107
+ ## Features
108
+
109
+ | Feature | Syntax |
110
+ |---------------------|---------------------------------|
111
+ | Variables | `let x = 10` |
112
+ | Constants | `const PI = 3.14` |
113
+ | Reassign | `set x = 20` / `set x += 5` |
114
+ | String interpolation| `"Hello, {name}!"` |
115
+ | Functions | `define add(a, b): return a+b` |
116
+ | Classes | `class Foo:` with `self` |
117
+ | Structs | `struct Point: x=0 y=0` |
118
+ | Conditionals | `if / else if / else` |
119
+ | Count loop | `count i from 1 to 10` |
120
+ | While loop | `while x < 100:` |
121
+ | For each | `for each item in list:` |
122
+ | Repeat | `repeat 5 times:` |
123
+ | Error handling | `try / catch / finally` |
124
+ | Break / continue | `break` / `continue` |
125
+
126
+ ## Built-in Functions
127
+
128
+ **Math:** `abs` `sqrt` `floor` `ceil` `round` `max` `min` `pow` `sin` `cos` `random` `randint`
129
+
130
+ **Strings:** `upper` `lower` `trim` `length` `contains` `replace` `split` `startsWith` `endsWith` `slice` `format`
131
+
132
+ **Lists:** `push` `pop` `sort` `reverse` `join` `range` `unique` `sum` `avg` `map` `filter` `reduce` `flatten` `zip`
133
+
134
+ **Types:** `str` `num` `bool` `type` `isNothing`
135
+
136
+ ## Use as a Library
137
+
138
+ ```js
139
+ const { Interpreter } = require('structscript');
140
+
141
+ const interp = new Interpreter({
142
+ output: msg => console.log(msg),
143
+ });
144
+
145
+ interp.run(`
146
+ let name = "World"
147
+ say "Hello, {name}!"
148
+ `);
149
+ ```
150
+
151
+ ## Playground
152
+
153
+ Try StructScript in your browser at [structscript.dev](https://structscript.dev)
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { Interpreter, SSError } = require('../lib/interpreter');
7
+
8
+ // ── Colours ────────────────────────────────────────────────
9
+ const C = {
10
+ reset: '\x1b[0m',
11
+ bold: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+ teal: '\x1b[36m',
14
+ lime: '\x1b[92m',
15
+ red: '\x1b[91m',
16
+ yellow: '\x1b[93m',
17
+ white: '\x1b[97m',
18
+ grey: '\x1b[90m',
19
+ };
20
+ const t = (color, str) => color + str + C.reset;
21
+
22
+ // ── Banner ─────────────────────────────────────────────────
23
+ const BANNER = `
24
+ ${t(C.teal, ' ╔══════════════════════════════╗')}
25
+ ${t(C.teal, ' ║')} ${t(C.bold+C.white, 'Struct')}${t(C.bold+C.lime, 'Script')} ${t(C.grey, 'v1.2.0')} ${t(C.teal, '║')}
26
+ ${t(C.teal, ' ║')} ${t(C.grey, 'The structured scripting')} ${t(C.teal, '║')}
27
+ ${t(C.teal, ' ║')} ${t(C.grey, 'language for everyone')} ${t(C.teal, '║')}
28
+ ${t(C.teal, ' ╚══════════════════════════════╝')}
29
+ `;
30
+
31
+ // ── Help ───────────────────────────────────────────────────
32
+ const HELP = `
33
+ ${BANNER}
34
+ ${t(C.bold, 'USAGE')}
35
+ ${t(C.lime, 'structscript')} ${t(C.teal, '<file.ss>')} Run a .ss file
36
+ ${t(C.lime, 'structscript')} ${t(C.teal, 'repl')} Start interactive REPL
37
+ ${t(C.lime, 'structscript')} ${t(C.teal, 'run <file.ss>')} Run a .ss file (explicit)
38
+ ${t(C.lime, 'structscript')} ${t(C.teal, 'check <file.ss>')} Check syntax without running
39
+ ${t(C.lime, 'structscript')} ${t(C.teal, 'version')} Show version info
40
+ ${t(C.lime, 'structscript')} ${t(C.teal, 'help')} Show this help
41
+
42
+ ${t(C.bold, 'OPTIONS')}
43
+ ${t(C.teal, '--time')} Show execution time
44
+ ${t(C.teal, '--no-color')} Disable colour output
45
+
46
+ ${t(C.bold, 'EXAMPLES')}
47
+ ${t(C.grey, '$ structscript main.ss')}
48
+ ${t(C.grey, '$ structscript repl')}
49
+ ${t(C.grey, '$ ss main.ss --time')}
50
+
51
+ ${t(C.bold, 'FILE EXTENSION')}
52
+ StructScript files use the ${t(C.lime, '.ss')} extension
53
+ `;
54
+
55
+ // ── Args ───────────────────────────────────────────────────
56
+ const args = process.argv.slice(2);
57
+ const flags = new Set(args.filter(a => a.startsWith('--')));
58
+ const params = args.filter(a => !a.startsWith('--'));
59
+ const noColor = flags.has('--no-color');
60
+ const showTime = flags.has('--time');
61
+
62
+ if (noColor) Object.keys(C).forEach(k => C[k] = '');
63
+
64
+ const cmd = params[0];
65
+
66
+ // ── Dispatch ───────────────────────────────────────────────
67
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
68
+ console.log(HELP);
69
+ process.exit(0);
70
+ }
71
+
72
+ if (cmd === 'version' || cmd === '--version' || cmd === '-v') {
73
+ console.log(t(C.bold+C.white, 'Struct') + t(C.bold+C.lime, 'Script') + t(C.grey, ' v1.2.0'));
74
+ process.exit(0);
75
+ }
76
+
77
+ if (cmd === 'repl') {
78
+ startREPL();
79
+ } else if (cmd === 'run') {
80
+ const file = params[1];
81
+ if (!file) { console.error(t(C.red, 'Error: specify a file to run')); process.exit(1); }
82
+ runFile(file);
83
+ } else if (cmd === 'check') {
84
+ const file = params[1];
85
+ if (!file) { console.error(t(C.red, 'Error: specify a file to check')); process.exit(1); }
86
+ checkFile(file);
87
+ } else if (cmd.endsWith('.ss') || cmd.endsWith('.txt')) {
88
+ runFile(cmd);
89
+ } else {
90
+ console.error(t(C.red, `Unknown command: "${cmd}"`));
91
+ console.error(t(C.grey, 'Run `structscript help` for usage.'));
92
+ process.exit(1);
93
+ }
94
+
95
+ // ── Run File ───────────────────────────────────────────────
96
+ function runFile(filePath) {
97
+ const resolved = path.resolve(filePath);
98
+ if (!fs.existsSync(resolved)) {
99
+ console.error(t(C.red, `Error: File not found: ${filePath}`));
100
+ process.exit(1);
101
+ }
102
+
103
+ let source;
104
+ try {
105
+ source = fs.readFileSync(resolved, 'utf8');
106
+ } catch (e) {
107
+ console.error(t(C.red, `Error reading file: ${e.message}`));
108
+ process.exit(1);
109
+ }
110
+
111
+ const t0 = Date.now();
112
+ const interp = new Interpreter({
113
+ output: msg => console.log(msg),
114
+ warn: msg => console.warn(t(C.yellow, '⚠ ' + msg)),
115
+ });
116
+
117
+ try {
118
+ interp.run(source);
119
+ if (showTime) {
120
+ const elapsed = Date.now() - t0;
121
+ console.error(t(C.grey, `\n⏱ ${elapsed}ms`));
122
+ }
123
+ } catch (e) {
124
+ printError(e, source, filePath);
125
+ process.exit(1);
126
+ }
127
+ }
128
+
129
+ // ── Check Syntax ───────────────────────────────────────────
130
+ function checkFile(filePath) {
131
+ const resolved = path.resolve(filePath);
132
+ if (!fs.existsSync(resolved)) {
133
+ console.error(t(C.red, `Error: File not found: ${filePath}`));
134
+ process.exit(1);
135
+ }
136
+ const source = fs.readFileSync(resolved, 'utf8');
137
+ // Basic syntax checks
138
+ const lines = source.split('\n');
139
+ let ok = true;
140
+ lines.forEach((line, i) => {
141
+ const trimmed = line.trim();
142
+ if (!trimmed || trimmed.startsWith('//')) return;
143
+ // Check indentation is spaces
144
+ const indent = line.match(/^(\t+)/);
145
+ if (indent) {
146
+ console.warn(t(C.yellow, ` Line ${i+1}: Tab indentation found — use spaces`));
147
+ ok = false;
148
+ }
149
+ });
150
+ if (ok) {
151
+ console.log(t(C.lime, `✓ ${path.basename(filePath)} looks good`));
152
+ } else {
153
+ console.log(t(C.yellow, `⚠ Issues found in ${path.basename(filePath)}`));
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ // ── Error Printer ──────────────────────────────────────────
159
+ function printError(e, source, filePath) {
160
+ const lines = source.split('\n');
161
+ console.error('');
162
+ console.error(t(C.red+C.bold, ' ● StructScript Error'));
163
+ console.error(t(C.red, ` ${e.message}`));
164
+
165
+ if (e.ssLine !== undefined) {
166
+ const lineNum = e.ssLine + 1;
167
+ const snippet = lines[e.ssLine] || '';
168
+ console.error('');
169
+ console.error(t(C.grey, ` ${path.basename(filePath)}:${lineNum}`));
170
+ console.error('');
171
+ // Context lines
172
+ for (let i = Math.max(0, e.ssLine - 2); i <= Math.min(lines.length - 1, e.ssLine + 1); i++) {
173
+ const isErr = i === e.ssLine;
174
+ const lineLabel = t(isErr ? C.red : C.grey, ` ${String(i+1).padStart(4)} │ `);
175
+ const lineText = isErr ? t(C.white, lines[i]) : t(C.grey, lines[i]);
176
+ console.error(lineLabel + lineText);
177
+ if (isErr) {
178
+ const indentCount = lines[i].match(/^\s*/)[0].length;
179
+ console.error(t(C.red, ' │ ' + ' '.repeat(indentCount) + '^ here'));
180
+ }
181
+ }
182
+ }
183
+ console.error('');
184
+ }
185
+
186
+ // ── REPL ───────────────────────────────────────────────────
187
+ function startREPL() {
188
+ const readline = require('readline');
189
+
190
+ console.log(BANNER);
191
+ console.log(t(C.grey, ' Type StructScript code and press Enter to run.'));
192
+ console.log(t(C.grey, ' Type .exit to quit, .clear to reset, .help for commands.\n'));
193
+
194
+ const rl = readline.createInterface({
195
+ input: process.stdin,
196
+ output: process.stdout,
197
+ prompt: t(C.teal, 'ss') + t(C.grey, ' › '),
198
+ });
199
+
200
+ let interp = new Interpreter({
201
+ output: msg => console.log(t(C.lime, ' ') + msg),
202
+ warn: msg => console.warn(t(C.yellow, ' ⚠ ' + msg)),
203
+ });
204
+
205
+ let multilineBuffer = [];
206
+ let inBlock = false;
207
+
208
+ rl.prompt();
209
+
210
+ rl.on('line', input => {
211
+ const line = input;
212
+ const trimmed = line.trim();
213
+
214
+ // REPL commands
215
+ if (!inBlock) {
216
+ if (trimmed === '.exit' || trimmed === '.quit') { console.log(t(C.grey, '\n Goodbye!\n')); process.exit(0); }
217
+ if (trimmed === '.clear' || trimmed === '.reset') {
218
+ interp = new Interpreter({ output: msg => console.log(t(C.lime, ' ') + msg), warn: msg => console.warn(t(C.yellow, ' ⚠ ' + msg)) });
219
+ console.log(t(C.grey, ' Environment cleared.')); rl.prompt(); return;
220
+ }
221
+ if (trimmed === '.help') {
222
+ console.log(t(C.grey, ' Commands: .exit .clear .help'));
223
+ console.log(t(C.grey, ' End a block with an empty line.'));
224
+ rl.prompt(); return;
225
+ }
226
+ }
227
+
228
+ // Collect multi-line blocks
229
+ if (trimmed.endsWith(':') && !trimmed.startsWith('//')) inBlock = true;
230
+ if (inBlock) {
231
+ multilineBuffer.push(line);
232
+ if (trimmed === '' && multilineBuffer.length > 1) {
233
+ // Execute block
234
+ const code = multilineBuffer.join('\n');
235
+ multilineBuffer = [];
236
+ inBlock = false;
237
+ executeREPL(interp, code);
238
+ } else {
239
+ process.stdout.write(t(C.grey, ' ... ')); return;
240
+ }
241
+ } else if (trimmed) {
242
+ executeREPL(interp, line);
243
+ }
244
+
245
+ rl.prompt();
246
+ });
247
+
248
+ rl.on('close', () => { console.log(t(C.grey, '\n Goodbye!\n')); process.exit(0); });
249
+ }
250
+
251
+ function executeREPL(interp, code) {
252
+ try {
253
+ interp.run(code);
254
+ } catch (e) {
255
+ console.error(t(C.red, ` ● ${e.message}`));
256
+ if (e.ssLine !== undefined && e.snippet) {
257
+ console.error(t(C.grey, ` → ${e.snippet.trim()}`));
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,30 @@
1
+ // classes.ss — Classes with methods
2
+
3
+ class Animal:
4
+ name = "Unknown"
5
+ sound = "..."
6
+
7
+ define init(n, s):
8
+ set self.name = n
9
+ set self.sound = s
10
+
11
+ define speak():
12
+ say "{self.name} says {self.sound}!"
13
+
14
+ class Counter:
15
+ value = 0
16
+
17
+ define increment(by = 1):
18
+ set self.value = self.value + by
19
+
20
+ define get():
21
+ return self.value
22
+
23
+ let dog = Animal()
24
+ dog.init("Rex", "Woof")
25
+ dog.speak()
26
+
27
+ let c = Counter()
28
+ c.increment()
29
+ c.increment(5)
30
+ say "Counter: {c.get()}"
@@ -0,0 +1,10 @@
1
+ // fibonacci.ss — Fibonacci sequence
2
+
3
+ define fib(n):
4
+ if n <= 1:
5
+ return n
6
+ return fib(n - 1) + fib(n - 2)
7
+
8
+ say "First 12 Fibonacci numbers:"
9
+ count i from 0 to 11:
10
+ say " fib({i}) = {fib(i)}"
@@ -0,0 +1,8 @@
1
+ // hello.ss — Your first StructScript program
2
+
3
+ let name = "World"
4
+ say "Hello, {name}!"
5
+ say "Welcome to StructScript v1.2"
6
+
7
+ const VERSION = 1.2
8
+ say "Running StructScript v{VERSION}"
@@ -0,0 +1,720 @@
1
+ 'use strict';
2
+
3
+ // ============================================================
4
+ // STRUCTSCRIPT INTERPRETER v1.2
5
+ // ============================================================
6
+
7
+ class SSError extends Error {
8
+ constructor(msg, line, snippet) {
9
+ super(msg);
10
+ this.name = 'StructScriptError';
11
+ this.ssLine = line;
12
+ this.snippet = snippet;
13
+ }
14
+ }
15
+
16
+ class ReturnSignal { constructor(v) { this.value = v; } }
17
+ class BreakSignal {}
18
+ class ContinueSignal {}
19
+
20
+ class Environment {
21
+ constructor(parent = null) {
22
+ this.vars = {};
23
+ this.consts = new Set();
24
+ this.parent = parent;
25
+ }
26
+ get(name) {
27
+ if (name in this.vars) return this.vars[name];
28
+ if (this.parent) return this.parent.get(name);
29
+ throw new SSError(`Undefined variable: "${name}"`);
30
+ }
31
+ set(name, value) {
32
+ if (name in this.vars) {
33
+ if (this.consts.has(name)) throw new SSError(`Cannot reassign constant "${name}"`);
34
+ this.vars[name] = value; return;
35
+ }
36
+ if (this.parent && this.parent.has(name)) { this.parent.set(name, value); return; }
37
+ this.vars[name] = value;
38
+ }
39
+ has(name) { return name in this.vars || (this.parent ? this.parent.has(name) : false); }
40
+ define(name, value, isConst = false) {
41
+ this.vars[name] = value;
42
+ if (isConst) this.consts.add(name);
43
+ }
44
+ }
45
+
46
+ class Interpreter {
47
+ constructor(options = {}) {
48
+ this.outputFn = options.output || (msg => process.stdout.write(String(msg) + '\n'));
49
+ this.warnFn = options.warn || (msg => process.stderr.write('Warning: ' + String(msg) + '\n'));
50
+ this.globals = new Environment();
51
+ this.structs = {};
52
+ this.callDepth = 0;
53
+ this.sourceLines = [];
54
+ this._registerBuiltins();
55
+ }
56
+
57
+ _registerBuiltins() {
58
+ const G = this.globals;
59
+
60
+ // I/O
61
+ G.define('say', a => { this.outputFn(this._str(a[0])); return null; });
62
+ G.define('print', a => { this.outputFn(a.map(x => this._str(x)).join(' ')); return null; });
63
+ G.define('warn', a => { this.warnFn(this._str(a[0])); return null; });
64
+
65
+ // Math
66
+ G.define('abs', a => Math.abs(a[0]));
67
+ G.define('sqrt', a => Math.sqrt(a[0]));
68
+ G.define('floor', a => Math.floor(a[0]));
69
+ G.define('ceil', a => Math.ceil(a[0]));
70
+ G.define('round', a => Math.round(a[0]));
71
+ G.define('max', a => Math.max(...a));
72
+ G.define('min', a => Math.min(...a));
73
+ G.define('pow', a => Math.pow(a[0], a[1]));
74
+ G.define('log', a => Math.log(a[0]));
75
+ G.define('log10', a => Math.log10(a[0]));
76
+ G.define('log2', a => Math.log2(a[0]));
77
+ G.define('sin', a => Math.sin(a[0]));
78
+ G.define('cos', a => Math.cos(a[0]));
79
+ G.define('tan', a => Math.tan(a[0]));
80
+ G.define('random', a => Math.random());
81
+ G.define('randint', a => Math.floor(Math.random() * (a[1] - a[0] + 1)) + a[0]);
82
+ G.define('pi', 0); // constant, overwritten below
83
+ this.globals.define('PI', Math.PI, true);
84
+ this.globals.define('E', Math.E, true);
85
+
86
+ // Type conversion
87
+ G.define('str', a => String(a[0] ?? ''));
88
+ G.define('num', a => { const n = Number(a[0]); if (isNaN(n)) throw new SSError(`Cannot convert "${a[0]}" to number`); return n; });
89
+ G.define('bool', a => Boolean(a[0]));
90
+ G.define('type', a => { if (Array.isArray(a[0])) return 'list'; if (a[0] === null || a[0] === undefined) return 'nothing'; return typeof a[0]; });
91
+ G.define('isNothing', a => a[0] === null || a[0] === undefined);
92
+ G.define('isNumber', a => typeof a[0] === 'number');
93
+ G.define('isString', a => typeof a[0] === 'string');
94
+ G.define('isList', a => Array.isArray(a[0]));
95
+
96
+ // String
97
+ G.define('upper', a => String(a[0]).toUpperCase());
98
+ G.define('lower', a => String(a[0]).toLowerCase());
99
+ G.define('trim', a => String(a[0]).trim());
100
+ G.define('trimStart', a => String(a[0]).trimStart());
101
+ G.define('trimEnd', a => String(a[0]).trimEnd());
102
+ G.define('length', a => Array.isArray(a[0]) ? a[0].length : String(a[0]).length);
103
+ G.define('contains', a => String(a[0]).includes(String(a[1])));
104
+ G.define('startsWith', a => String(a[0]).startsWith(String(a[1])));
105
+ G.define('endsWith', a => String(a[0]).endsWith(String(a[1])));
106
+ G.define('replace', a => String(a[0]).split(String(a[1])).join(String(a[2])));
107
+ G.define('replaceAll', a => String(a[0]).split(String(a[1])).join(String(a[2])));
108
+ G.define('split', a => String(a[0]).split(String(a[1])));
109
+ G.define('indexOf', a => Array.isArray(a[0]) ? a[0].indexOf(a[1]) : String(a[0]).indexOf(String(a[1])));
110
+ G.define('lastIndexOf',a => Array.isArray(a[0]) ? a[0].lastIndexOf(a[1]) : String(a[0]).lastIndexOf(String(a[1])));
111
+ G.define('slice', a => { const s = a[0]; const from = a[1] ?? 0; const to = a[2]; return Array.isArray(s) ? s.slice(from, to) : String(s).slice(from, to); });
112
+ G.define('char', a => String.fromCharCode(a[0]));
113
+ G.define('charCode', a => String(a[0]).charCodeAt(0));
114
+ G.define('repeat', a => String(a[0]).repeat(a[1]));
115
+ G.define('padStart', a => String(a[0]).padStart(a[1], a[2] ?? ' '));
116
+ G.define('padEnd', a => String(a[0]).padEnd(a[1], a[2] ?? ' '));
117
+ G.define('format', a => { let s = String(a[0]); for (let i = 1; i < a.length; i++) s = s.replace('{}', this._str(a[i])); return s; });
118
+
119
+ // List
120
+ G.define('push', a => { if (!Array.isArray(a[0])) throw new SSError('push() requires a list'); a[0].push(a[1]); return null; });
121
+ G.define('pop', a => { if (!Array.isArray(a[0])) throw new SSError('pop() requires a list'); return a[0].pop() ?? null; });
122
+ G.define('shift', a => { if (!Array.isArray(a[0])) return null; return a[0].shift() ?? null; });
123
+ G.define('unshift', a => { if (Array.isArray(a[0])) a[0].unshift(a[1]); return null; });
124
+ G.define('join', a => { if (!Array.isArray(a[0])) throw new SSError('join() requires a list'); return a[0].map(x => this._str(x)).join(a[1] ?? ''); });
125
+ G.define('sort', a => { if (!Array.isArray(a[0])) throw new SSError('sort() requires a list'); return [...a[0]].sort((x, y) => x < y ? -1 : x > y ? 1 : 0); });
126
+ G.define('sortBy', a => { if (!Array.isArray(a[0])) throw new SSError('sortBy() requires a list'); const fn = a[1]; return [...a[0]].sort((x, y) => { const ax = fn([x]); const ay = fn([y]); return ax < ay ? -1 : ax > ay ? 1 : 0; }); });
127
+ G.define('reverse', a => { if (!Array.isArray(a[0])) throw new SSError('reverse() requires a list'); return [...a[0]].reverse(); });
128
+ G.define('range', a => { const from = a[1] !== undefined ? a[0] : 0; const to = a[1] !== undefined ? a[1] : a[0]; const step = a[2] ?? 1; const r = []; for (let i = from; i < to; i += step) r.push(i); return r; });
129
+ G.define('flatten', a => Array.isArray(a[0]) ? a[0].flat() : [a[0]]);
130
+ G.define('unique', a => [...new Set(a[0])]);
131
+ G.define('sum', a => Array.isArray(a[0]) ? a[0].reduce((acc, v) => acc + v, 0) : 0);
132
+ G.define('avg', a => Array.isArray(a[0]) && a[0].length ? a[0].reduce((acc, v) => acc + v, 0) / a[0].length : 0);
133
+ G.define('map', a => { if (!Array.isArray(a[0])) throw new SSError('map() requires a list'); const fn = a[1]; return a[0].map(item => { const r = fn([item], null); return r instanceof ReturnSignal ? r.value : r; }); });
134
+ G.define('filter', a => { if (!Array.isArray(a[0])) throw new SSError('filter() requires a list'); const fn = a[1]; return a[0].filter(item => { const r = fn([item], null); return r instanceof ReturnSignal ? r.value : r; }); });
135
+ G.define('reduce', a => { if (!Array.isArray(a[0])) throw new SSError('reduce() requires a list'); const fn = a[1]; let acc = a[2]; return a[0].reduce((ac, item) => { const r = fn([ac, item], null); return r instanceof ReturnSignal ? r.value : r; }, acc); });
136
+ G.define('includes',a => Array.isArray(a[0]) ? a[0].includes(a[1]) : String(a[0]).includes(String(a[1])));
137
+ G.define('count', a => Array.isArray(a[0]) ? a[0].filter(x => x === a[1]).length : 0);
138
+ G.define('zip', a => { const la = a[0], lb = a[1]; const len = Math.min(la.length, lb.length); return Array.from({length: len}, (_, i) => [la[i], lb[i]]); });
139
+
140
+ // Utility
141
+ G.define('assert', a => { if (!a[0]) throw new SSError('Assertion failed' + (a[1] ? ': ' + a[1] : '')); return null; });
142
+ G.define('error', a => { throw new SSError(String(a[0] ?? 'Runtime error')); });
143
+ G.define('exit', a => { process.exit(a[0] ?? 0); });
144
+ }
145
+
146
+ run(source) {
147
+ this.sourceLines = source.split('\n');
148
+ this._execBlock(this.sourceLines, 0, this.sourceLines.length, this.globals);
149
+ }
150
+
151
+ _indent(line) {
152
+ let i = 0;
153
+ while (i < line.length && line[i] === ' ') i++;
154
+ return i;
155
+ }
156
+
157
+ _findBlock(lines, startLine, baseIndent) {
158
+ const block = []; let i = startLine;
159
+ while (i < lines.length) {
160
+ const line = lines[i], t = line.trim();
161
+ if (!t || t.startsWith('//')) { i++; continue; }
162
+ if (this._indent(line) <= baseIndent) break;
163
+ block.push(i); i++;
164
+ }
165
+ return { block, next: i };
166
+ }
167
+
168
+ _execBlock(lines, start, end, env) {
169
+ let i = start;
170
+ while (i < end) {
171
+ const raw = lines[i], t = raw.trim();
172
+ if (!t || t.startsWith('//')) { i++; continue; }
173
+ const indent = this._indent(raw);
174
+ const result = this._execLine(t, lines, i, indent, env);
175
+ if (result instanceof ReturnSignal || result instanceof BreakSignal || result instanceof ContinueSignal) return result;
176
+ if (result && result._skip) { i = result._skip; continue; }
177
+ i++;
178
+ }
179
+ }
180
+
181
+ _execLine(line, allLines, lineIdx, indent, env) {
182
+ try { return this._exec(line, allLines, lineIdx, indent, env); }
183
+ catch (e) {
184
+ if (e instanceof SSError) { if (e.ssLine === undefined) e.ssLine = lineIdx; if (!e.snippet) e.snippet = allLines[lineIdx]; throw e; }
185
+ throw new SSError(e.message, lineIdx, allLines[lineIdx]);
186
+ }
187
+ }
188
+
189
+ _exec(line, allLines, lineIdx, indent, env) {
190
+ // say shorthand
191
+ if (line.startsWith('say ')) { const v = this._eval(line.slice(4).trim(), env); this.outputFn(this._str(v)); return null; }
192
+
193
+ // let / const
194
+ if (line.startsWith('let ') || line.startsWith('const ')) {
195
+ const isConst = line.startsWith('const ');
196
+ const rest = line.slice(isConst ? 6 : 4);
197
+ const eq = rest.indexOf('='); if (eq === -1) throw new SSError(`Missing = in declaration`);
198
+ const name = rest.slice(0, eq).trim();
199
+ const val = this._eval(rest.slice(eq + 1).trim(), env);
200
+ env.define(name, val, isConst); return null;
201
+ }
202
+
203
+ // set (with compound assignment)
204
+ if (line.startsWith('set ')) {
205
+ const rest = line.slice(4);
206
+ const compM = rest.match(/^([a-zA-Z_][\w.]*|\w+\[.+?\])\s*(\+|-|\*|\/|%|\*\*|\/\/)=\s*(.+)$/);
207
+ if (compM) {
208
+ const [, target, op, valExpr] = compM;
209
+ const cur = this._getTarget(target, env);
210
+ const val = this._eval(valExpr.trim(), env);
211
+ this._setTarget(target, this._applyOp(cur, op, val), env); return null;
212
+ }
213
+ const eq = rest.indexOf('='); if (eq === -1) throw new SSError(`Missing = in set`);
214
+ const target = rest.slice(0, eq).trim(), val = this._eval(rest.slice(eq + 1).trim(), env);
215
+ this._setTarget(target, val, env); return null;
216
+ }
217
+
218
+ // define function
219
+ if (line.startsWith('define ')) {
220
+ const sig = line.slice(7).trim().replace(/:$/, '');
221
+ const po = sig.indexOf('('), pc = sig.lastIndexOf(')');
222
+ const fname = sig.slice(0, po).trim();
223
+ const rawParams = sig.slice(po + 1, pc).trim();
224
+ const params = rawParams ? rawParams.split(',').map(p => p.trim()).filter(Boolean) : [];
225
+ const defaults = {};
226
+ params.forEach((p, i) => { const eq = p.indexOf('='); if (eq !== -1) { defaults[p.slice(0, eq).trim()] = this._eval(p.slice(eq + 1).trim(), env); params[i] = p.slice(0, eq).trim(); } });
227
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
228
+ env.define(fname, (args, callEnv) => {
229
+ if (this.callDepth > 500) throw new SSError('Stack overflow: too many nested calls');
230
+ this.callDepth++;
231
+ const fnEnv = new Environment(env);
232
+ params.forEach((p, i) => fnEnv.define(p, args[i] !== undefined ? args[i] : (defaults[p] !== undefined ? defaults[p] : null)));
233
+ const blockLines = block.map(b => allLines[b]);
234
+ const res = this._execBlock(blockLines, 0, blockLines.length, fnEnv);
235
+ this.callDepth--;
236
+ if (res instanceof ReturnSignal) return res.value;
237
+ return null;
238
+ });
239
+ return { _skip: next };
240
+ }
241
+
242
+ // struct
243
+ if (line.startsWith('struct ')) {
244
+ const name = line.slice(7).trim().replace(/:$/, '');
245
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
246
+ const defs = {};
247
+ block.forEach(bi => { const t = allLines[bi].trim(); if (!t || t.startsWith('//')) return; const eq = t.indexOf('='); if (eq !== -1) { defs[t.slice(0, eq).trim()] = this._eval(t.slice(eq + 1).trim(), this.globals); } });
248
+ this.structs[name] = defs;
249
+ env.define(name, args => {
250
+ const inst = { __struct: name };
251
+ Object.entries(defs).forEach(([k, v]) => { inst[k] = Array.isArray(v) ? [...v] : v; });
252
+ return inst;
253
+ });
254
+ return { _skip: next };
255
+ }
256
+
257
+ // class
258
+ if (line.startsWith('class ')) {
259
+ const name = line.slice(6).trim().replace(/:$/, '');
260
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
261
+ const fields = {}, methods = {};
262
+ let i2 = 0;
263
+ const bLines = block.map(b => allLines[b]);
264
+ while (i2 < bLines.length) {
265
+ const t = bLines[i2].trim();
266
+ if (!t || t.startsWith('//')) { i2++; continue; }
267
+ const bIndent = this._indent(bLines[i2]);
268
+ if (t.startsWith('define ')) {
269
+ const sig = t.slice(7).replace(/:$/, '');
270
+ const po = sig.indexOf('('), pc = sig.lastIndexOf(')');
271
+ const mname = sig.slice(0, po).trim();
272
+ const rawP = sig.slice(po + 1, pc).trim();
273
+ const params = rawP ? rawP.split(',').map(p => p.trim()).filter(Boolean) : [];
274
+ const defs2 = {};
275
+ params.forEach((p, pi) => { const eq = p.indexOf('='); if (eq !== -1) { defs2[p.slice(0, eq).trim()] = this._eval(p.slice(eq + 1).trim(), env); params[pi] = p.slice(0, eq).trim(); } });
276
+ const { block: mb, next: mn } = this._findBlock(bLines, i2 + 1, bIndent);
277
+ methods[mname] = { params, defaults: defs2, body: mb.map(b => bLines[b]) };
278
+ i2 = mn;
279
+ } else {
280
+ const eq = t.indexOf('=');
281
+ if (eq !== -1) fields[t.slice(0, eq).trim()] = this._eval(t.slice(eq + 1).trim(), this.globals);
282
+ i2++;
283
+ }
284
+ }
285
+ const classEnv = env;
286
+ const interpRef = this;
287
+ env.define(name, args => {
288
+ const inst = { __struct: name, __class: name };
289
+ Object.entries(fields).forEach(([k, v]) => { inst[k] = Array.isArray(v) ? [...v] : (typeof v === 'object' && v ? Object.assign({}, v) : v); });
290
+ Object.entries(methods).forEach(([mname, m]) => {
291
+ inst[mname] = callArgs => {
292
+ if (interpRef.callDepth > 500) throw new SSError('Stack overflow');
293
+ interpRef.callDepth++;
294
+ const fnEnv = new Environment(classEnv);
295
+ fnEnv.define('self', inst);
296
+ fnEnv.define('this', inst);
297
+ m.params.forEach((p, i) => fnEnv.define(p, callArgs[i] !== undefined ? callArgs[i] : (m.defaults[p] !== undefined ? m.defaults[p] : null)));
298
+ const res = interpRef._execBlock(m.body, 0, m.body.length, fnEnv);
299
+ interpRef.callDepth--;
300
+ if (res instanceof ReturnSignal) return res.value;
301
+ return null;
302
+ };
303
+ });
304
+ if (inst.init) inst.init(args);
305
+ return inst;
306
+ });
307
+ return { _skip: next };
308
+ }
309
+
310
+ // return / break / continue
311
+ if (line === 'return') return new ReturnSignal(null);
312
+ if (line.startsWith('return ')) return new ReturnSignal(this._eval(line.slice(7).trim(), env));
313
+ if (line === 'break') return new BreakSignal();
314
+ if (line === 'continue') return new ContinueSignal();
315
+
316
+ // try/catch/finally
317
+ if (line === 'try:') {
318
+ const { block: tryBlock, next: tryNext } = this._findBlock(allLines, lineIdx + 1, indent);
319
+ let catchVar = null, catchBlock = [], finallyBlock = [], cur = tryNext;
320
+ if (cur < allLines.length) {
321
+ const ct = allLines[cur].trim();
322
+ if (ct.startsWith('catch')) {
323
+ const m = ct.match(/^catch\s+(\w+):?$/); catchVar = m ? m[1] : '_err';
324
+ const { block: cb, next: cn } = this._findBlock(allLines, cur + 1, this._indent(allLines[cur]));
325
+ catchBlock = cb; cur = cn;
326
+ }
327
+ }
328
+ if (cur < allLines.length && allLines[cur].trim() === 'finally:') {
329
+ const { block: fb, next: fn } = this._findBlock(allLines, cur + 1, this._indent(allLines[cur]));
330
+ finallyBlock = fb; cur = fn;
331
+ }
332
+ let result = null;
333
+ try {
334
+ const bLines = tryBlock.map(b => allLines[b]);
335
+ result = this._execBlock(bLines, 0, bLines.length, new Environment(env));
336
+ } catch (e) {
337
+ if (catchBlock.length) {
338
+ const cEnv = new Environment(env);
339
+ if (catchVar) cEnv.define(catchVar, e.message || String(e));
340
+ const cLines = catchBlock.map(b => allLines[b]);
341
+ result = this._execBlock(cLines, 0, cLines.length, cEnv);
342
+ }
343
+ } finally {
344
+ if (finallyBlock.length) {
345
+ const fLines = finallyBlock.map(b => allLines[b]);
346
+ this._execBlock(fLines, 0, fLines.length, new Environment(env));
347
+ }
348
+ }
349
+ return { _skip: cur };
350
+ }
351
+
352
+ // if / else if / else
353
+ if (line.startsWith('if ') && line.endsWith(':')) return this._handleIf(line, allLines, lineIdx, indent, env);
354
+ if (line.startsWith('else if ') || line === 'else:') return null;
355
+
356
+ // while
357
+ if (line.startsWith('while ') && line.endsWith(':')) {
358
+ const cond = line.slice(6, -1).trim();
359
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
360
+ let iters = 0;
361
+ while (this._truthy(this._eval(cond, env))) {
362
+ if (++iters > 100000) throw new SSError('Infinite loop detected (>100000 iterations)');
363
+ const bLines = block.map(b => allLines[b]);
364
+ const res = this._execBlock(bLines, 0, bLines.length, new Environment(env));
365
+ if (res instanceof BreakSignal) break;
366
+ if (res instanceof ReturnSignal) return res;
367
+ }
368
+ return { _skip: next };
369
+ }
370
+
371
+ // repeat N times
372
+ if (line.startsWith('repeat ')) {
373
+ const m = line.match(/^repeat (.+?) times:?$/);
374
+ if (m) {
375
+ const count = Math.floor(Number(this._eval(m[1].trim(), env)));
376
+ if (isNaN(count)) throw new SSError(`repeat count must be a number`);
377
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
378
+ for (let i = 0; i < count; i++) {
379
+ const bLines = block.map(b => allLines[b]);
380
+ const res = this._execBlock(bLines, 0, bLines.length, new Environment(env));
381
+ if (res instanceof BreakSignal) break;
382
+ if (res instanceof ReturnSignal) return res;
383
+ }
384
+ return { _skip: next };
385
+ }
386
+ }
387
+
388
+ // count i from X to Y (step Z)
389
+ if (line.startsWith('count ')) {
390
+ const m = line.match(/^count (\w+) from (.+?) to (.+?)(?:\s+step\s+(.+?))?:?$/);
391
+ if (m) {
392
+ const varName = m[1];
393
+ const from = Number(this._eval(m[2].trim(), env));
394
+ const to = Number(this._eval(m[3].trim(), env));
395
+ const step = m[4] ? Number(this._eval(m[4].trim(), env)) : (from <= to ? 1 : -1);
396
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
397
+ for (let i = from; step > 0 ? i <= to : i >= to; i += step) {
398
+ const loopEnv = new Environment(env); loopEnv.define(varName, i);
399
+ const bLines = block.map(b => allLines[b]);
400
+ const res = this._execBlock(bLines, 0, bLines.length, loopEnv);
401
+ if (res instanceof BreakSignal) break;
402
+ if (res instanceof ReturnSignal) return res;
403
+ }
404
+ return { _skip: next };
405
+ }
406
+ }
407
+
408
+ // for each item in list
409
+ if (line.startsWith('for each ')) {
410
+ const m = line.match(/^for each (\w+) in (.+?):?$/);
411
+ if (m) {
412
+ const varName = m[1], listVal = this._eval(m[2].trim(), env);
413
+ if (!Array.isArray(listVal) && typeof listVal !== 'string') throw new SSError(`"${m[2].trim()}" is not iterable`);
414
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
415
+ for (const item of listVal) {
416
+ const loopEnv = new Environment(env); loopEnv.define(varName, item);
417
+ const bLines = block.map(b => allLines[b]);
418
+ const res = this._execBlock(bLines, 0, bLines.length, loopEnv);
419
+ if (res instanceof BreakSignal) break;
420
+ if (res instanceof ReturnSignal) return res;
421
+ }
422
+ return { _skip: next };
423
+ }
424
+ }
425
+
426
+ // bare expression
427
+ this._eval(line, env);
428
+ return null;
429
+ }
430
+
431
+ _handleIf(line, allLines, lineIdx, indent, env) {
432
+ const cond = line.slice(3, -1).trim();
433
+ const { block, next } = this._findBlock(allLines, lineIdx + 1, indent);
434
+ if (this._truthy(this._eval(cond, env))) {
435
+ const bLines = block.map(b => allLines[b]);
436
+ const res = this._execBlock(bLines, 0, bLines.length, new Environment(env));
437
+ if (res instanceof ReturnSignal || res instanceof BreakSignal) return res;
438
+ let skip = next;
439
+ while (skip < allLines.length) {
440
+ const t = allLines[skip].trim();
441
+ if (t.startsWith('else')) { const { next: n2 } = this._findBlock(allLines, skip + 1, this._indent(allLines[skip])); skip = n2; } else break;
442
+ }
443
+ return { _skip: skip };
444
+ } else {
445
+ let cur = next;
446
+ while (cur < allLines.length) {
447
+ const t = allLines[cur].trim();
448
+ const ci = this._indent(allLines[cur]);
449
+ if (t.startsWith('else if ') && t.endsWith(':')) {
450
+ const cond2 = t.slice(8, -1).trim();
451
+ const { block: b2, next: n2 } = this._findBlock(allLines, cur + 1, ci);
452
+ if (this._truthy(this._eval(cond2, env))) {
453
+ const bLines = b2.map(b => allLines[b]);
454
+ const res = this._execBlock(bLines, 0, bLines.length, new Environment(env));
455
+ if (res instanceof ReturnSignal || res instanceof BreakSignal) return res;
456
+ let skip2 = n2;
457
+ while (skip2 < allLines.length) { const tt = allLines[skip2].trim(); if (tt.startsWith('else')) { const { next: n3 } = this._findBlock(allLines, skip2 + 1, this._indent(allLines[skip2])); skip2 = n3; } else break; }
458
+ return { _skip: skip2 };
459
+ }
460
+ cur = n2;
461
+ } else if (t === 'else:') {
462
+ const { block: b3, next: n3 } = this._findBlock(allLines, cur + 1, ci);
463
+ const bLines = b3.map(b => allLines[b]);
464
+ const res = this._execBlock(bLines, 0, bLines.length, new Environment(env));
465
+ if (res instanceof ReturnSignal || res instanceof BreakSignal) return res;
466
+ return { _skip: n3 };
467
+ } else break;
468
+ }
469
+ return { _skip: cur };
470
+ }
471
+ }
472
+
473
+ _getTarget(target, env) {
474
+ if (target.includes('.')) { const parts = target.split('.'); let obj = env.get(parts[0]); for (let i = 1; i < parts.length - 1; i++) obj = obj[parts[i]]; return obj[parts[parts.length - 1]]; }
475
+ if (target.includes('[')) { const m = target.match(/^(\w+)\[(.+)\]$/); if (m) { const arr = env.get(m[1]); return arr[this._eval(m[2], env)]; } }
476
+ return env.get(target);
477
+ }
478
+
479
+ _setTarget(target, value, env) {
480
+ if (target.includes('.')) { const parts = target.split('.'); let obj = env.get(parts[0]); for (let i = 1; i < parts.length - 1; i++) obj = obj[parts[i]]; obj[parts[parts.length - 1]] = value; return; }
481
+ if (target.includes('[')) { const m = target.match(/^(\w+)\[(.+)\]$/); if (m) { const arr = env.get(m[1]); arr[this._eval(m[2], env)] = value; return; } }
482
+ env.set(target, value);
483
+ }
484
+
485
+ _applyOp(a, op, b) {
486
+ switch (op) {
487
+ case '+': return a + b; case '-': return a - b;
488
+ case '*': return a * b; case '/': return a / b;
489
+ case '%': return a % b; case '**': return Math.pow(a, b);
490
+ case '//': return Math.floor(a / b);
491
+ }
492
+ }
493
+
494
+ _truthy(v) { return v !== null && v !== undefined && v !== false && v !== 0 && v !== ''; }
495
+
496
+ _eval(expr, env) {
497
+ expr = expr.trim();
498
+ if (!expr) return null;
499
+ if (expr === 'true') return true;
500
+ if (expr === 'false') return false;
501
+ if (expr === 'null' || expr === 'nothing') return null;
502
+
503
+ // String literal
504
+ if ((expr[0] === '"' && expr[expr.length - 1] === '"') || (expr[0] === "'" && expr[expr.length - 1] === "'"))
505
+ return this._parseString(expr.slice(1, -1), env);
506
+
507
+ // Number
508
+ if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr);
509
+
510
+ // List
511
+ if (expr[0] === '[' && expr[expr.length - 1] === ']') {
512
+ const inner = expr.slice(1, -1).trim();
513
+ if (!inner) return [];
514
+ return this._splitArgs(inner).map(a => this._eval(a, env));
515
+ }
516
+
517
+ // Parens
518
+ if (expr[0] === '(' && expr[expr.length - 1] === ')') return this._eval(expr.slice(1, -1), env);
519
+
520
+ // Ternary
521
+ const ternIdx = this._findOp(expr, ' if ');
522
+ if (ternIdx !== -1) {
523
+ const elseIdx = this._findOp(expr, ' else ');
524
+ if (elseIdx > ternIdx) {
525
+ const val = expr.slice(0, ternIdx).trim();
526
+ const cond = expr.slice(ternIdx + 4, elseIdx).trim();
527
+ const alt = expr.slice(elseIdx + 6).trim();
528
+ return this._truthy(this._eval(cond, env)) ? this._eval(val, env) : this._eval(alt, env);
529
+ }
530
+ }
531
+
532
+ // Logical
533
+ for (const op of [' or ', ' and ']) {
534
+ const i = this._findOp(expr, op);
535
+ if (i !== -1) {
536
+ if (op === ' or ') return this._truthy(this._eval(expr.slice(0, i), env)) || this._truthy(this._eval(expr.slice(i + 4), env));
537
+ if (op === ' and ') return this._truthy(this._eval(expr.slice(0, i), env)) && this._truthy(this._eval(expr.slice(i + 5), env));
538
+ }
539
+ }
540
+ if (expr.startsWith('not ')) return !this._truthy(this._eval(expr.slice(4), env));
541
+
542
+ // Comparison
543
+ for (const op of ['>=', '<=', '!=', '==', '>', '<']) {
544
+ const i = this._findOp(expr, op);
545
+ if (i !== -1) {
546
+ const l = this._eval(expr.slice(0, i).trim(), env), r = this._eval(expr.slice(i + op.length).trim(), env);
547
+ switch (op) { case '==': return l == r; case '!=': return l != r; case '>': return l > r; case '<': return l < r; case '>=': return l >= r; case '<=': return l <= r; }
548
+ }
549
+ }
550
+
551
+ // Arithmetic
552
+ const addI = this._findOp(expr, '+');
553
+ if (addI !== -1) return this._eval(expr.slice(0, addI).trim(), env) + this._eval(expr.slice(addI + 1).trim(), env);
554
+ const subI = this._findBinaryMinus(expr);
555
+ if (subI !== -1) return this._eval(expr.slice(0, subI).trim(), env) - this._eval(expr.slice(subI + 1).trim(), env);
556
+ const modI = this._findOp(expr, '%');
557
+ if (modI !== -1) return this._eval(expr.slice(0, modI).trim(), env) % this._eval(expr.slice(modI + 1).trim(), env);
558
+ const powI = this._findOp(expr, '**');
559
+ if (powI !== -1) return Math.pow(this._eval(expr.slice(0, powI).trim(), env), this._eval(expr.slice(powI + 2).trim(), env));
560
+ const fdivI = this._findOp(expr, '//');
561
+ if (fdivI !== -1) return Math.floor(this._eval(expr.slice(0, fdivI).trim(), env) / this._eval(expr.slice(fdivI + 2).trim(), env));
562
+ const mulI = this._findOp(expr, '*');
563
+ if (mulI !== -1) return this._eval(expr.slice(0, mulI).trim(), env) * this._eval(expr.slice(mulI + 1).trim(), env);
564
+ const divI = this._findOp(expr, '/');
565
+ if (divI !== -1) return this._eval(expr.slice(0, divI).trim(), env) / this._eval(expr.slice(divI + 1).trim(), env);
566
+ if (expr[0] === '-') return -this._eval(expr.slice(1), env);
567
+
568
+ // Method call: obj.method(args) — obj must be identifier or index expression
569
+ const methodM = expr.match(/^([a-zA-Z_]\w*(?:\[.+?\])?(?:\.[a-zA-Z_]\w*(?:\[.+?\])?)*)\.([a-zA-Z_]\w*)\((.*)\)$/s);
570
+ if (methodM) {
571
+ const [, objExpr, mname, argsRaw] = methodM;
572
+ const obj = this._eval(objExpr, env);
573
+ const args = argsRaw.trim() ? this._splitArgs(argsRaw).map(a => this._eval(a.trim(), env)) : [];
574
+ if (typeof obj === 'object' && obj !== null && typeof obj[mname] === 'function') return obj[mname](args);
575
+ if (typeof obj === 'string') {
576
+ if (mname === 'upper') return obj.toUpperCase();
577
+ if (mname === 'lower') return obj.toLowerCase();
578
+ if (mname === 'trim') return obj.trim();
579
+ if (mname === 'length') return obj.length;
580
+ if (mname === 'split') return obj.split(args[0] ?? '');
581
+ if (mname === 'contains') return obj.includes(String(args[0]));
582
+ if (mname === 'replace') return obj.split(String(args[0])).join(String(args[1]));
583
+ if (mname === 'startsWith') return obj.startsWith(String(args[0]));
584
+ if (mname === 'endsWith') return obj.endsWith(String(args[0]));
585
+ }
586
+ if (Array.isArray(obj)) {
587
+ if (mname === 'push') { obj.push(args[0]); return null; }
588
+ if (mname === 'pop') return obj.pop() ?? null;
589
+ if (mname === 'length') return obj.length;
590
+ if (mname === 'sort') return [...obj].sort();
591
+ if (mname === 'reverse') return [...obj].reverse();
592
+ if (mname === 'join') return obj.map(x => this._str(x)).join(args[0] ?? '');
593
+ if (mname === 'contains') return obj.includes(args[0]);
594
+ }
595
+ throw new SSError(`"${mname}" is not a method of ${this._str(obj)}`);
596
+ }
597
+
598
+ // Function call
599
+ const callM = expr.match(/^([a-zA-Z_][\w]*)\((.*)\)$/s);
600
+ if (callM) {
601
+ const fname = callM[1], argsRaw = callM[2].trim();
602
+ const args = argsRaw ? this._splitArgs(argsRaw).map(a => this._eval(a.trim(), env)) : [];
603
+ const fn = env.get(fname);
604
+ if (typeof fn !== 'function') throw new SSError(`"${fname}" is not a function`);
605
+ this.callDepth++;
606
+ if (this.callDepth > 500) { this.callDepth--; throw new SSError('Stack overflow'); }
607
+ const res = fn(args, env);
608
+ this.callDepth--;
609
+ return res;
610
+ }
611
+
612
+ // Index access
613
+ const bracketM = expr.match(/^(.+?)\[(.+)\]$/);
614
+ if (bracketM) {
615
+ const obj = this._eval(bracketM[1].trim(), env);
616
+ const idx = this._eval(bracketM[2].trim(), env);
617
+ if (Array.isArray(obj) || typeof obj === 'string') return obj[idx] ?? null;
618
+ if (typeof obj === 'object' && obj !== null) return obj[idx] ?? null;
619
+ return null;
620
+ }
621
+
622
+ // Dot access
623
+ if (expr.includes('.') && !/^\d/.test(expr)) {
624
+ const dot = expr.lastIndexOf('.');
625
+ const obj = this._eval(expr.slice(0, dot), env);
626
+ const key = expr.slice(dot + 1);
627
+ if (typeof obj === 'object' && obj !== null) return obj[key] ?? null;
628
+ return null;
629
+ }
630
+
631
+ // Variable
632
+ return env.get(expr);
633
+ }
634
+
635
+ _parseString(s, env) {
636
+ s = s.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\\\/g, '\\').replace(/\\"/g, '"').replace(/\\'/g, "'");
637
+ if (!env) return s;
638
+ // Find all {expr} blocks, handling nested parens/brackets
639
+ let result = '';
640
+ let i = 0;
641
+ while (i < s.length) {
642
+ if (s[i] === '{') {
643
+ let depth = 1, j = i + 1;
644
+ while (j < s.length && depth > 0) {
645
+ if (s[j] === '{') depth++;
646
+ else if (s[j] === '}') depth--;
647
+ j++;
648
+ }
649
+ const expr = s.slice(i + 1, j - 1);
650
+ try { result += this._str(this._eval(expr.trim(), env)); }
651
+ catch (e) { result += '{' + expr + '}'; }
652
+ i = j;
653
+ } else {
654
+ result += s[i++];
655
+ }
656
+ }
657
+ return result;
658
+ }
659
+
660
+ _findOp(expr, op) {
661
+ let depth = 0, inStr = false, sc = '';
662
+ for (let i = 0; i < expr.length; i++) {
663
+ const c = expr[i];
664
+ if (inStr) { if (c === sc) inStr = false; continue; }
665
+ if (c === '"' || c === "'") { inStr = true; sc = c; continue; }
666
+ if (c === '(' || c === '[') depth++;
667
+ else if (c === ')' || c === ']') depth--;
668
+ else if (depth === 0 && expr.slice(i, i + op.length) === op) return i;
669
+ }
670
+ return -1;
671
+ }
672
+
673
+ _findBinaryMinus(expr) {
674
+ let depth = 0, inStr = false, sc = '';
675
+ for (let i = expr.length - 1; i > 0; i--) {
676
+ const c = expr[i];
677
+ if (c === '"' || c === "'") inStr = !inStr;
678
+ if (inStr) continue;
679
+ if (c === ')' || c === ']') depth++;
680
+ else if (c === '(' || c === '[') depth--;
681
+ else if (depth === 0 && c === '-') {
682
+ // Look backwards past spaces
683
+ let prev = i - 1;
684
+ while (prev >= 0 && expr[prev] === ' ') prev--;
685
+ if (prev >= 0 && /[\w\d\)"'\]]/.test(expr[prev])) return i;
686
+ }
687
+ }
688
+ return -1;
689
+ }
690
+
691
+ _splitArgs(str) {
692
+ const args = []; let depth = 0, inStr = false, sc = '', cur = '';
693
+ for (let i = 0; i < str.length; i++) {
694
+ const c = str[i];
695
+ if (inStr) { cur += c; if (c === sc) inStr = false; continue; }
696
+ if (c === '"' || c === "'") { inStr = true; sc = c; cur += c; continue; }
697
+ if (c === '(' || c === '[') { depth++; cur += c; continue; }
698
+ if (c === ')' || c === ']') { depth--; cur += c; continue; }
699
+ if (c === ',' && depth === 0) { args.push(cur.trim()); cur = ''; continue; }
700
+ cur += c;
701
+ }
702
+ if (cur.trim()) args.push(cur.trim());
703
+ return args;
704
+ }
705
+
706
+ _str(val) {
707
+ if (val === null || val === undefined) return 'nothing';
708
+ if (val === true) return 'true';
709
+ if (val === false) return 'false';
710
+ if (Array.isArray(val)) return '[' + val.map(v => this._str(v)).join(', ') + ']';
711
+ if (typeof val === 'object') {
712
+ const n = val.__struct || 'object';
713
+ const fields = Object.entries(val).filter(([k]) => !k.startsWith('__') && typeof val[k] !== 'function').map(([k, v]) => `${k}: ${this._str(v)}`).join(', ');
714
+ return `${n}{ ${fields} }`;
715
+ }
716
+ return String(val);
717
+ }
718
+ }
719
+
720
+ module.exports = { Interpreter, SSError, Environment };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "structscript",
3
+ "version": "1.2.0",
4
+ "description": "The StructScript programming language — clean, readable scripting for everyone",
5
+ "keywords": ["structscript", "language", "interpreter", "scripting", "programming-language"],
6
+ "author": "StructScript",
7
+ "license": "MIT",
8
+ "main": "lib/interpreter.js",
9
+ "bin": {
10
+ "structscript": "./bin/structscript.js",
11
+ "ss": "./bin/structscript.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=14.0.0"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/structscript/structscript"
19
+ },
20
+ "homepage": "https://structscript.dev",
21
+ "scripts": {
22
+ "test": "node test/run.js"
23
+ },
24
+ "files": [
25
+ "bin/",
26
+ "lib/",
27
+ "examples/",
28
+ "README.md"
29
+ ]
30
+ }