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 +157 -0
- package/bin/structscript.js +260 -0
- package/examples/classes.ss +30 -0
- package/examples/fibonacci.ss +10 -0
- package/examples/hello.ss +8 -0
- package/lib/interpreter.js +720 -0
- package/package.json +30 -0
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,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
|
+
}
|