stone-lang 0.1.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 +52 -0
- package/StoneEngine.js +879 -0
- package/StoneEngineService.js +1727 -0
- package/adapters/FileSystemAdapter.js +230 -0
- package/adapters/OutputAdapter.js +208 -0
- package/adapters/index.js +6 -0
- package/cli/CLIOutputAdapter.js +196 -0
- package/cli/DaemonClient.js +349 -0
- package/cli/JSONOutputAdapter.js +135 -0
- package/cli/ReplSession.js +567 -0
- package/cli/ViewerServer.js +590 -0
- package/cli/commands/check.js +84 -0
- package/cli/commands/daemon.js +189 -0
- package/cli/commands/kill.js +66 -0
- package/cli/commands/package.js +713 -0
- package/cli/commands/ps.js +65 -0
- package/cli/commands/run.js +537 -0
- package/cli/entry.js +169 -0
- package/cli/index.js +14 -0
- package/cli/stonec.js +358 -0
- package/cli/test-compiler.js +181 -0
- package/cli/viewer/index.html +495 -0
- package/daemon/IPCServer.js +455 -0
- package/daemon/ProcessManager.js +327 -0
- package/daemon/ProcessRunner.js +307 -0
- package/daemon/daemon.js +398 -0
- package/daemon/index.js +16 -0
- package/frontend/analysis/index.js +5 -0
- package/frontend/analysis/livenessAnalyzer.js +568 -0
- package/frontend/analysis/treeShaker.js +265 -0
- package/frontend/index.js +20 -0
- package/frontend/parsing/astBuilder.js +2196 -0
- package/frontend/parsing/index.js +7 -0
- package/frontend/parsing/sonParser.js +592 -0
- package/frontend/parsing/stoneAstTypes.js +703 -0
- package/frontend/parsing/terminal-registry.js +435 -0
- package/frontend/parsing/tokenizer.js +692 -0
- package/frontend/type-checker/OverloadedFunctionType.js +43 -0
- package/frontend/type-checker/TypeEnvironment.js +165 -0
- package/frontend/type-checker/bidirectionalInference.js +149 -0
- package/frontend/type-checker/index.js +10 -0
- package/frontend/type-checker/moduleAnalysis.js +248 -0
- package/frontend/type-checker/operatorMappings.js +35 -0
- package/frontend/type-checker/overloadResolution.js +605 -0
- package/frontend/type-checker/typeChecker.js +452 -0
- package/frontend/type-checker/typeCompatibility.js +389 -0
- package/frontend/type-checker/visitors/controlFlow.js +483 -0
- package/frontend/type-checker/visitors/functions.js +604 -0
- package/frontend/type-checker/visitors/index.js +38 -0
- package/frontend/type-checker/visitors/literals.js +341 -0
- package/frontend/type-checker/visitors/modules.js +159 -0
- package/frontend/type-checker/visitors/operators.js +109 -0
- package/frontend/type-checker/visitors/statements.js +768 -0
- package/frontend/types/index.js +5 -0
- package/frontend/types/operatorMap.js +134 -0
- package/frontend/types/types.js +2046 -0
- package/frontend/utils/errorCollector.js +244 -0
- package/frontend/utils/index.js +5 -0
- package/frontend/utils/moduleResolver.js +479 -0
- package/package.json +50 -0
- package/packages/browserCache.js +359 -0
- package/packages/fetcher.js +236 -0
- package/packages/index.js +130 -0
- package/packages/lockfile.js +271 -0
- package/packages/manifest.js +291 -0
- package/packages/packageResolver.js +356 -0
- package/packages/resolver.js +310 -0
- package/packages/semver.js +635 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend Parsing - Barrel exports
|
|
3
|
+
*/
|
|
4
|
+
export { Lexer, TokenType, tokenizeSource, tokenize } from './tokenizer.js';
|
|
5
|
+
export { Parser, buildASTFromSource, buildASTFromTokens, parse } from './astBuilder.js';
|
|
6
|
+
export * from './stoneAstTypes.js';
|
|
7
|
+
export { createTerminalConstructors } from './terminal-registry.js';
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stone Object Notation (.son) Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses .son files - a subset of Stone syntax for data/config.
|
|
5
|
+
* Supports: strings, numbers, booleans, arrays, objects, comments.
|
|
6
|
+
* No functions, imports, or control flow.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Token types for SON (simplified subset)
|
|
10
|
+
const SONTokenType = {
|
|
11
|
+
// Literals
|
|
12
|
+
NUMBER: 'NUMBER',
|
|
13
|
+
STRING: 'STRING',
|
|
14
|
+
TRUE: 'TRUE',
|
|
15
|
+
FALSE: 'FALSE',
|
|
16
|
+
NONE: 'NONE',
|
|
17
|
+
|
|
18
|
+
// Identifiers (for keys)
|
|
19
|
+
IDENTIFIER: 'IDENTIFIER',
|
|
20
|
+
|
|
21
|
+
// Delimiters
|
|
22
|
+
LBRACE: 'LBRACE', // {
|
|
23
|
+
RBRACE: 'RBRACE', // }
|
|
24
|
+
LBRACKET: 'LBRACKET', // [
|
|
25
|
+
RBRACKET: 'RBRACKET', // ]
|
|
26
|
+
|
|
27
|
+
// Punctuation
|
|
28
|
+
COMMA: 'COMMA', // ,
|
|
29
|
+
ASSIGN: 'ASSIGN', // =
|
|
30
|
+
COLON: 'COLON', // :
|
|
31
|
+
|
|
32
|
+
// Special
|
|
33
|
+
EOF: 'EOF',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* SON Token
|
|
38
|
+
*/
|
|
39
|
+
class SONToken {
|
|
40
|
+
constructor(type, value, line, column) {
|
|
41
|
+
this.type = type;
|
|
42
|
+
this.value = value;
|
|
43
|
+
this.line = line;
|
|
44
|
+
this.column = column;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SON Lexer - tokenizes .son files
|
|
50
|
+
*/
|
|
51
|
+
class SONLexer {
|
|
52
|
+
constructor(source, filename = '<son>') {
|
|
53
|
+
this.source = source;
|
|
54
|
+
this.filename = filename;
|
|
55
|
+
this.pos = 0;
|
|
56
|
+
this.line = 1;
|
|
57
|
+
this.column = 1;
|
|
58
|
+
this.tokens = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
tokenize() {
|
|
62
|
+
while (this.pos < this.source.length) {
|
|
63
|
+
this.skipWhitespaceAndComments();
|
|
64
|
+
if (this.pos >= this.source.length) break;
|
|
65
|
+
|
|
66
|
+
const token = this.nextToken();
|
|
67
|
+
if (token) {
|
|
68
|
+
this.tokens.push(token);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.tokens.push(new SONToken(SONTokenType.EOF, null, this.line, this.column));
|
|
73
|
+
return this.tokens;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
nextToken() {
|
|
77
|
+
const char = this.peek();
|
|
78
|
+
const startLine = this.line;
|
|
79
|
+
const startColumn = this.column;
|
|
80
|
+
|
|
81
|
+
// Numbers (including negative)
|
|
82
|
+
if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek(1)))) {
|
|
83
|
+
return this.readNumber(startLine, startColumn);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Strings
|
|
87
|
+
if (char === '"' || char === "'") {
|
|
88
|
+
return this.readString(char, startLine, startColumn);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Identifiers and keywords
|
|
92
|
+
if (this.isAlpha(char) || char === '_') {
|
|
93
|
+
return this.readIdentifier(startLine, startColumn);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Single-character tokens
|
|
97
|
+
this.advance();
|
|
98
|
+
switch (char) {
|
|
99
|
+
case '{':
|
|
100
|
+
return new SONToken(SONTokenType.LBRACE, '{', startLine, startColumn);
|
|
101
|
+
case '}':
|
|
102
|
+
return new SONToken(SONTokenType.RBRACE, '}', startLine, startColumn);
|
|
103
|
+
case '[':
|
|
104
|
+
return new SONToken(SONTokenType.LBRACKET, '[', startLine, startColumn);
|
|
105
|
+
case ']':
|
|
106
|
+
return new SONToken(SONTokenType.RBRACKET, ']', startLine, startColumn);
|
|
107
|
+
case ',':
|
|
108
|
+
return new SONToken(SONTokenType.COMMA, ',', startLine, startColumn);
|
|
109
|
+
case '=':
|
|
110
|
+
return new SONToken(SONTokenType.ASSIGN, '=', startLine, startColumn);
|
|
111
|
+
case ':':
|
|
112
|
+
return new SONToken(SONTokenType.COLON, ':', startLine, startColumn);
|
|
113
|
+
default:
|
|
114
|
+
throw new SONParseError(
|
|
115
|
+
`Unexpected character '${char}'`,
|
|
116
|
+
this.filename,
|
|
117
|
+
startLine,
|
|
118
|
+
startColumn
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
readNumber(line, column) {
|
|
124
|
+
let num = '';
|
|
125
|
+
let isFloat = false;
|
|
126
|
+
|
|
127
|
+
// Handle negative sign
|
|
128
|
+
if (this.peek() === '-') {
|
|
129
|
+
num += '-';
|
|
130
|
+
this.advance();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
while (this.isDigit(this.peek()) || this.peek() === '.') {
|
|
134
|
+
if (this.peek() === '.') {
|
|
135
|
+
// Check if it's really a decimal point
|
|
136
|
+
if (!this.isDigit(this.peek(1))) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (isFloat) {
|
|
140
|
+
throw new SONParseError(
|
|
141
|
+
'Invalid number: multiple decimal points',
|
|
142
|
+
this.filename,
|
|
143
|
+
line,
|
|
144
|
+
column
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
isFloat = true;
|
|
148
|
+
}
|
|
149
|
+
num += this.peek();
|
|
150
|
+
this.advance();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Scientific notation
|
|
154
|
+
if (this.peek() === 'e' || this.peek() === 'E') {
|
|
155
|
+
num += this.peek();
|
|
156
|
+
this.advance();
|
|
157
|
+
isFloat = true;
|
|
158
|
+
|
|
159
|
+
if (this.peek() === '+' || this.peek() === '-') {
|
|
160
|
+
num += this.peek();
|
|
161
|
+
this.advance();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!this.isDigit(this.peek())) {
|
|
165
|
+
throw new SONParseError(
|
|
166
|
+
'Invalid number: expected digit after exponent',
|
|
167
|
+
this.filename,
|
|
168
|
+
line,
|
|
169
|
+
column
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
while (this.isDigit(this.peek())) {
|
|
174
|
+
num += this.peek();
|
|
175
|
+
this.advance();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const value = isFloat ? parseFloat(num) : parseInt(num, 10);
|
|
180
|
+
return new SONToken(SONTokenType.NUMBER, value, line, column);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
readString(quote, line, column) {
|
|
184
|
+
this.advance(); // Skip opening quote
|
|
185
|
+
let str = '';
|
|
186
|
+
|
|
187
|
+
while (this.peek() !== quote && this.pos < this.source.length) {
|
|
188
|
+
if (this.peek() === '\\') {
|
|
189
|
+
this.advance();
|
|
190
|
+
const escaped = this.peek();
|
|
191
|
+
switch (escaped) {
|
|
192
|
+
case 'n': str += '\n'; break;
|
|
193
|
+
case 't': str += '\t'; break;
|
|
194
|
+
case 'r': str += '\r'; break;
|
|
195
|
+
case '\\': str += '\\'; break;
|
|
196
|
+
case quote: str += quote; break;
|
|
197
|
+
default: str += escaped;
|
|
198
|
+
}
|
|
199
|
+
this.advance();
|
|
200
|
+
} else {
|
|
201
|
+
if (this.peek() === '\n') {
|
|
202
|
+
this.line++;
|
|
203
|
+
this.column = 0;
|
|
204
|
+
}
|
|
205
|
+
str += this.peek();
|
|
206
|
+
this.advance();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.peek() !== quote) {
|
|
211
|
+
throw new SONParseError(
|
|
212
|
+
'Unterminated string',
|
|
213
|
+
this.filename,
|
|
214
|
+
line,
|
|
215
|
+
column
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.advance(); // Skip closing quote
|
|
220
|
+
return new SONToken(SONTokenType.STRING, str, line, column);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
readIdentifier(line, column) {
|
|
224
|
+
let id = '';
|
|
225
|
+
|
|
226
|
+
while (this.isAlphaNumeric(this.peek()) || this.peek() === '_' || this.peek() === '-') {
|
|
227
|
+
id += this.peek();
|
|
228
|
+
this.advance();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Keywords
|
|
232
|
+
if (id === 'true') {
|
|
233
|
+
return new SONToken(SONTokenType.TRUE, true, line, column);
|
|
234
|
+
}
|
|
235
|
+
if (id === 'false') {
|
|
236
|
+
return new SONToken(SONTokenType.FALSE, false, line, column);
|
|
237
|
+
}
|
|
238
|
+
if (id === 'none' || id === 'null') {
|
|
239
|
+
return new SONToken(SONTokenType.NONE, null, line, column);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new SONToken(SONTokenType.IDENTIFIER, id, line, column);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
skipWhitespaceAndComments() {
|
|
246
|
+
while (this.pos < this.source.length) {
|
|
247
|
+
const char = this.peek();
|
|
248
|
+
|
|
249
|
+
// Whitespace
|
|
250
|
+
if (char === ' ' || char === '\t' || char === '\r' || char === '\n') {
|
|
251
|
+
if (char === '\n') {
|
|
252
|
+
this.line++;
|
|
253
|
+
this.column = 1;
|
|
254
|
+
} else {
|
|
255
|
+
this.column++;
|
|
256
|
+
}
|
|
257
|
+
this.pos++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Comments: // to end of line
|
|
262
|
+
if (char === '/' && this.peek(1) === '/') {
|
|
263
|
+
while (this.peek() !== '\n' && this.pos < this.source.length) {
|
|
264
|
+
this.advance();
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
peek(offset = 0) {
|
|
274
|
+
const pos = this.pos + offset;
|
|
275
|
+
return pos < this.source.length ? this.source[pos] : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
advance(n = 1) {
|
|
279
|
+
for (let i = 0; i < n; i++) {
|
|
280
|
+
if (this.pos < this.source.length) {
|
|
281
|
+
if (this.source[this.pos] === '\n') {
|
|
282
|
+
this.line++;
|
|
283
|
+
this.column = 1;
|
|
284
|
+
} else {
|
|
285
|
+
this.column++;
|
|
286
|
+
}
|
|
287
|
+
this.pos++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
isDigit(char) {
|
|
293
|
+
return char !== null && char >= '0' && char <= '9';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
isAlpha(char) {
|
|
297
|
+
return char !== null && ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z'));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
isAlphaNumeric(char) {
|
|
301
|
+
return this.isAlpha(char) || this.isDigit(char);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* SON Parse Error
|
|
307
|
+
*/
|
|
308
|
+
export class SONParseError extends Error {
|
|
309
|
+
constructor(message, filename, line, column) {
|
|
310
|
+
super(`${message} at ${filename}:${line}:${column}`);
|
|
311
|
+
this.name = 'SONParseError';
|
|
312
|
+
this.filename = filename;
|
|
313
|
+
this.line = line;
|
|
314
|
+
this.column = column;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* SON Parser - parses tokens into a JavaScript object
|
|
320
|
+
*/
|
|
321
|
+
class SONParser {
|
|
322
|
+
constructor(tokens, filename = '<son>') {
|
|
323
|
+
this.tokens = tokens;
|
|
324
|
+
this.filename = filename;
|
|
325
|
+
this.pos = 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
parse() {
|
|
329
|
+
const result = {};
|
|
330
|
+
|
|
331
|
+
// Parse top-level key = value pairs
|
|
332
|
+
while (!this.isAtEnd()) {
|
|
333
|
+
if (this.check(SONTokenType.EOF)) break;
|
|
334
|
+
|
|
335
|
+
const { key, value } = this.parseAssignment();
|
|
336
|
+
result[key] = value;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
parseAssignment() {
|
|
343
|
+
const keyToken = this.expect(SONTokenType.IDENTIFIER, 'Expected identifier');
|
|
344
|
+
const key = keyToken.value;
|
|
345
|
+
|
|
346
|
+
this.expect(SONTokenType.ASSIGN, "Expected '=' after key");
|
|
347
|
+
|
|
348
|
+
const value = this.parseValue();
|
|
349
|
+
|
|
350
|
+
// Optional comma (allow trailing commas and no commas between top-level pairs)
|
|
351
|
+
this.match(SONTokenType.COMMA);
|
|
352
|
+
|
|
353
|
+
return { key, value };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
parseValue() {
|
|
357
|
+
// Object
|
|
358
|
+
if (this.match(SONTokenType.LBRACE)) {
|
|
359
|
+
return this.parseObject();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Array
|
|
363
|
+
if (this.match(SONTokenType.LBRACKET)) {
|
|
364
|
+
return this.parseArray();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// String
|
|
368
|
+
if (this.check(SONTokenType.STRING)) {
|
|
369
|
+
return this.advance().value;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Number
|
|
373
|
+
if (this.check(SONTokenType.NUMBER)) {
|
|
374
|
+
return this.advance().value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Boolean
|
|
378
|
+
if (this.check(SONTokenType.TRUE) || this.check(SONTokenType.FALSE)) {
|
|
379
|
+
return this.advance().value;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// None/null
|
|
383
|
+
if (this.check(SONTokenType.NONE)) {
|
|
384
|
+
this.advance();
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Identifier as string (for unquoted values)
|
|
389
|
+
if (this.check(SONTokenType.IDENTIFIER)) {
|
|
390
|
+
return this.advance().value;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const token = this.peek();
|
|
394
|
+
throw new SONParseError(
|
|
395
|
+
`Unexpected token: ${token.type}`,
|
|
396
|
+
this.filename,
|
|
397
|
+
token.line,
|
|
398
|
+
token.column
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
parseObject() {
|
|
403
|
+
const obj = {};
|
|
404
|
+
|
|
405
|
+
while (!this.check(SONTokenType.RBRACE) && !this.isAtEnd()) {
|
|
406
|
+
// Key can be identifier or string
|
|
407
|
+
let key;
|
|
408
|
+
if (this.check(SONTokenType.IDENTIFIER)) {
|
|
409
|
+
key = this.advance().value;
|
|
410
|
+
} else if (this.check(SONTokenType.STRING)) {
|
|
411
|
+
key = this.advance().value;
|
|
412
|
+
} else {
|
|
413
|
+
const token = this.peek();
|
|
414
|
+
throw new SONParseError(
|
|
415
|
+
'Expected object key (identifier or string)',
|
|
416
|
+
this.filename,
|
|
417
|
+
token.line,
|
|
418
|
+
token.column
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// = or : for assignment
|
|
423
|
+
if (!this.match(SONTokenType.ASSIGN) && !this.match(SONTokenType.COLON)) {
|
|
424
|
+
const token = this.peek();
|
|
425
|
+
throw new SONParseError(
|
|
426
|
+
"Expected '=' or ':' after object key",
|
|
427
|
+
this.filename,
|
|
428
|
+
token.line,
|
|
429
|
+
token.column
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
obj[key] = this.parseValue();
|
|
434
|
+
|
|
435
|
+
// Optional comma
|
|
436
|
+
this.match(SONTokenType.COMMA);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.expect(SONTokenType.RBRACE, "Expected '}' to close object");
|
|
440
|
+
return obj;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
parseArray() {
|
|
444
|
+
const arr = [];
|
|
445
|
+
|
|
446
|
+
while (!this.check(SONTokenType.RBRACKET) && !this.isAtEnd()) {
|
|
447
|
+
arr.push(this.parseValue());
|
|
448
|
+
|
|
449
|
+
// Optional comma
|
|
450
|
+
this.match(SONTokenType.COMMA);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this.expect(SONTokenType.RBRACKET, "Expected ']' to close array");
|
|
454
|
+
return arr;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Helper methods
|
|
458
|
+
peek() {
|
|
459
|
+
return this.tokens[this.pos];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
advance() {
|
|
463
|
+
if (!this.isAtEnd()) {
|
|
464
|
+
this.pos++;
|
|
465
|
+
}
|
|
466
|
+
return this.tokens[this.pos - 1];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
check(type) {
|
|
470
|
+
return !this.isAtEnd() && this.peek().type === type;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
match(type) {
|
|
474
|
+
if (this.check(type)) {
|
|
475
|
+
this.advance();
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
expect(type, message) {
|
|
482
|
+
if (this.check(type)) {
|
|
483
|
+
return this.advance();
|
|
484
|
+
}
|
|
485
|
+
const token = this.peek();
|
|
486
|
+
throw new SONParseError(
|
|
487
|
+
message,
|
|
488
|
+
this.filename,
|
|
489
|
+
token.line,
|
|
490
|
+
token.column
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
isAtEnd() {
|
|
495
|
+
return this.pos >= this.tokens.length || this.peek().type === SONTokenType.EOF;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Parse a .son file content into a JavaScript object
|
|
501
|
+
* @param {string} source - The .son file content
|
|
502
|
+
* @param {string} filename - Optional filename for error messages
|
|
503
|
+
* @returns {object} Parsed JavaScript object
|
|
504
|
+
*/
|
|
505
|
+
export function parseSON(source, filename = '<son>') {
|
|
506
|
+
const lexer = new SONLexer(source, filename);
|
|
507
|
+
const tokens = lexer.tokenize();
|
|
508
|
+
const parser = new SONParser(tokens, filename);
|
|
509
|
+
return parser.parse();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Stringify a JavaScript object to .son format
|
|
514
|
+
* @param {object} obj - The object to stringify
|
|
515
|
+
* @param {number} indent - Indentation level (default 2)
|
|
516
|
+
* @returns {string} .son formatted string
|
|
517
|
+
*/
|
|
518
|
+
export function stringifySON(obj, indent = 2) {
|
|
519
|
+
// Top-level objects are written as bare key = value pairs (no wrapping braces)
|
|
520
|
+
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
|
|
521
|
+
const keys = Object.keys(obj);
|
|
522
|
+
if (keys.length === 0) return '';
|
|
523
|
+
|
|
524
|
+
const pairs = keys.map(key => {
|
|
525
|
+
const keyStr = /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key) ? key : JSON.stringify(key);
|
|
526
|
+
return keyStr + ' = ' + stringifyValue(obj[key], 0, indent);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return pairs.join('\n') + '\n';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return stringifyValue(obj, 0, indent);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function stringifyValue(value, depth, indent) {
|
|
536
|
+
if (value === null || value === undefined) {
|
|
537
|
+
return 'none';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (typeof value === 'boolean') {
|
|
541
|
+
return value ? 'true' : 'false';
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (typeof value === 'number') {
|
|
545
|
+
return String(value);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (typeof value === 'string') {
|
|
549
|
+
// Use quotes if string contains special chars, otherwise can be unquoted for simple identifiers
|
|
550
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value) && !isKeyword(value)) {
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
return JSON.stringify(value);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (Array.isArray(value)) {
|
|
557
|
+
if (value.length === 0) return '[]';
|
|
558
|
+
|
|
559
|
+
const spaces = ' '.repeat((depth + 1) * indent);
|
|
560
|
+
const closingSpaces = ' '.repeat(depth * indent);
|
|
561
|
+
|
|
562
|
+
const items = value.map(v => spaces + stringifyValue(v, depth + 1, indent));
|
|
563
|
+
return '[\n' + items.join(',\n') + '\n' + closingSpaces + ']';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (typeof value === 'object') {
|
|
567
|
+
const keys = Object.keys(value);
|
|
568
|
+
if (keys.length === 0) return '{}';
|
|
569
|
+
|
|
570
|
+
const spaces = ' '.repeat((depth + 1) * indent);
|
|
571
|
+
const closingSpaces = ' '.repeat(depth * indent);
|
|
572
|
+
|
|
573
|
+
const pairs = keys.map(key => {
|
|
574
|
+
const keyStr = /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(key) ? key : JSON.stringify(key);
|
|
575
|
+
return spaces + keyStr + ' = ' + stringifyValue(value[key], depth + 1, indent);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return '{\n' + pairs.join(',\n') + '\n' + closingSpaces + '}';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return String(value);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function isKeyword(str) {
|
|
585
|
+
return str === 'true' || str === 'false' || str === 'none' || str === 'null';
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export default {
|
|
589
|
+
parse: parseSON,
|
|
590
|
+
stringify: stringifySON,
|
|
591
|
+
SONParseError,
|
|
592
|
+
};
|