@user-synax/synax 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,684 @@
1
+ // @bun
2
+ // src/codegen.ts
3
+ var INDENT = " ";
4
+ var PRECEDENCE = {
5
+ "==": 1,
6
+ "!=": 1,
7
+ ">": 1,
8
+ "<": 1,
9
+ ">=": 1,
10
+ "<=": 1,
11
+ "+": 2,
12
+ "-": 2,
13
+ "*": 3,
14
+ "/": 3
15
+ };
16
+ function generate(program) {
17
+ return new Generator().program(program);
18
+ }
19
+
20
+ class Generator {
21
+ indentLevel = 0;
22
+ scope = new Set;
23
+ program(node) {
24
+ return node.body.map((statement) => this.statement(statement)).join(`
25
+ `);
26
+ }
27
+ statement(node) {
28
+ switch (node.kind) {
29
+ case "VarDecl":
30
+ return this.varDecl(node);
31
+ case "PrintStmt":
32
+ return this.printStmt(node);
33
+ case "IfStmt":
34
+ return this.ifStmt(node);
35
+ case "FnDecl":
36
+ return this.fnDecl(node);
37
+ case "ForStmt":
38
+ return this.forStmt(node);
39
+ case "ExprStmt":
40
+ return `${this.indent()}${this.expression(node.expression)};`;
41
+ }
42
+ }
43
+ varDecl(node) {
44
+ const value = this.expression(node.value);
45
+ if (this.scope.has(node.name)) {
46
+ return `${this.indent()}${node.name} = ${value};`;
47
+ }
48
+ this.scope.add(node.name);
49
+ return `${this.indent()}let ${node.name} = ${value};`;
50
+ }
51
+ printStmt(node) {
52
+ return `${this.indent()}console.log(${this.expression(node.argument)});`;
53
+ }
54
+ ifStmt(node) {
55
+ const header = `${this.indent()}if (${this.expression(node.condition)}) `;
56
+ let output = header + this.block(node.thenBranch);
57
+ if (node.elseBranch !== null) {
58
+ output += ` else ` + this.block(node.elseBranch);
59
+ }
60
+ return output;
61
+ }
62
+ fnDecl(node) {
63
+ const header = `${this.indent()}function ${node.name}(${node.params.join(", ")}) `;
64
+ const outer = this.scope;
65
+ this.scope = new Set(node.params);
66
+ const body = this.block(node.body);
67
+ this.scope = outer;
68
+ return header + body;
69
+ }
70
+ forStmt(node) {
71
+ const { variable } = node;
72
+ const start = this.expression(node.start);
73
+ const end = this.expression(node.end);
74
+ const header = `${this.indent()}for (let ${variable} = ${start}; ${variable} <= ${end}; ${variable}++) `;
75
+ return header + this.block(node.body);
76
+ }
77
+ expression(node) {
78
+ switch (node.kind) {
79
+ case "NumberLiteral":
80
+ return String(node.value);
81
+ case "StringLiteral":
82
+ return JSON.stringify(node.value);
83
+ case "Identifier":
84
+ return node.name;
85
+ case "BinaryExpr":
86
+ return this.binary(node, 0, false);
87
+ case "FunctionCall":
88
+ return `${node.callee}(${node.args.map((argument) => this.expression(argument)).join(", ")})`;
89
+ }
90
+ }
91
+ binary(node, parentPrecedence, isRightChild) {
92
+ const precedence = PRECEDENCE[node.operator];
93
+ const left = this.operand(node.left, precedence, false);
94
+ const right = this.operand(node.right, precedence, true);
95
+ const text = `${left} ${node.operator} ${right}`;
96
+ const needsParens = precedence < parentPrecedence || precedence === parentPrecedence && isRightChild;
97
+ return needsParens ? `(${text})` : text;
98
+ }
99
+ operand(node, parentPrecedence, isRightChild) {
100
+ if (node.kind === "BinaryExpr") {
101
+ return this.binary(node, parentPrecedence, isRightChild);
102
+ }
103
+ return this.expression(node);
104
+ }
105
+ block(statements) {
106
+ if (statements.length === 0)
107
+ return "{}";
108
+ this.indentLevel++;
109
+ const body = statements.map((statement) => this.statement(statement)).join(`
110
+ `);
111
+ this.indentLevel--;
112
+ return `{
113
+ ${body}
114
+ ${this.indent()}}`;
115
+ }
116
+ indent() {
117
+ return INDENT.repeat(this.indentLevel);
118
+ }
119
+ }
120
+ // src/diagnostics.ts
121
+ var LEAD = " ";
122
+ function formatDiagnostic(diagnostic, source) {
123
+ const { file, line, column, length, reason } = diagnostic;
124
+ const header = `${file}:${line}:${column}: error: ${reason}`;
125
+ const sourceLine = source.split(/\r?\n/)[line - 1];
126
+ if (sourceLine === undefined || sourceLine.trim() === "")
127
+ return header;
128
+ const start = Math.max(0, Math.min(column - 1, sourceLine.length));
129
+ const span = Math.max(1, Math.min(length, sourceLine.length - start));
130
+ const number = String(line);
131
+ const gutter = " ".repeat(number.length);
132
+ const padding = sourceLine.slice(0, start).replace(/[^\t]/g, " ");
133
+ const rendered = `${LEAD}${number} | ${sourceLine}`;
134
+ const caretLine = `${LEAD}${gutter} | ${padding}${"^".repeat(span)}`;
135
+ return `${header}
136
+ ${rendered}
137
+ ${caretLine}`;
138
+ }
139
+ // src/lexer.ts
140
+ var TokenType = {
141
+ SET: "SET",
142
+ PRINT: "PRINT",
143
+ IF: "IF",
144
+ ELSE: "ELSE",
145
+ END: "END",
146
+ FN: "FN",
147
+ FOR: "FOR",
148
+ IN: "IN",
149
+ NUMBER: "NUMBER",
150
+ STRING: "STRING",
151
+ IDENT: "IDENT",
152
+ EQUALS: "EQUALS",
153
+ PLUS: "PLUS",
154
+ MINUS: "MINUS",
155
+ STAR: "STAR",
156
+ SLASH: "SLASH",
157
+ EQEQ: "EQEQ",
158
+ BANGEQ: "BANGEQ",
159
+ GT: "GT",
160
+ LT: "LT",
161
+ GTEQ: "GTEQ",
162
+ LTEQ: "LTEQ",
163
+ DOTDOT: "DOTDOT",
164
+ LPAREN: "LPAREN",
165
+ RPAREN: "RPAREN",
166
+ COMMA: "COMMA",
167
+ NEWLINE: "NEWLINE",
168
+ EOF: "EOF"
169
+ };
170
+
171
+ class LexerError extends Error {
172
+ line;
173
+ column;
174
+ length;
175
+ reason;
176
+ constructor(reason, line, column, length = 1) {
177
+ super(`${reason} (line ${line})`);
178
+ this.name = "LexerError";
179
+ this.reason = reason;
180
+ this.line = line;
181
+ this.column = column;
182
+ this.length = length;
183
+ }
184
+ }
185
+ var KEYWORDS = new Map([
186
+ ["set", TokenType.SET],
187
+ ["print", TokenType.PRINT],
188
+ ["if", TokenType.IF],
189
+ ["else", TokenType.ELSE],
190
+ ["end", TokenType.END],
191
+ ["fn", TokenType.FN],
192
+ ["for", TokenType.FOR],
193
+ ["in", TokenType.IN]
194
+ ]);
195
+ var TWO_CHAR_OPERATORS = new Map([
196
+ ["==", TokenType.EQEQ],
197
+ ["!=", TokenType.BANGEQ],
198
+ [">=", TokenType.GTEQ],
199
+ ["<=", TokenType.LTEQ],
200
+ ["..", TokenType.DOTDOT]
201
+ ]);
202
+ var ONE_CHAR_TOKENS = new Map([
203
+ ["=", TokenType.EQUALS],
204
+ ["+", TokenType.PLUS],
205
+ ["-", TokenType.MINUS],
206
+ ["*", TokenType.STAR],
207
+ ["/", TokenType.SLASH],
208
+ [">", TokenType.GT],
209
+ ["<", TokenType.LT],
210
+ ["(", TokenType.LPAREN],
211
+ [")", TokenType.RPAREN],
212
+ [",", TokenType.COMMA]
213
+ ]);
214
+ var ESCAPES = new Map([
215
+ ["n", `
216
+ `],
217
+ ["t", "\t"],
218
+ ['"', '"'],
219
+ ["\\", "\\"]
220
+ ]);
221
+ function isDigit(char) {
222
+ return char !== undefined && char >= "0" && char <= "9";
223
+ }
224
+ function isLetter(char) {
225
+ return char !== undefined && (char >= "a" && char <= "z" || char >= "A" && char <= "Z");
226
+ }
227
+ function isIdentifierStart(char) {
228
+ return isLetter(char) || char === "_";
229
+ }
230
+ function isIdentifierPart(char) {
231
+ return isIdentifierStart(char) || isDigit(char);
232
+ }
233
+
234
+ class Lexer {
235
+ source;
236
+ index = 0;
237
+ line = 1;
238
+ lineStart = 0;
239
+ constructor(source) {
240
+ this.source = source;
241
+ }
242
+ get column() {
243
+ return this.index - this.lineStart + 1;
244
+ }
245
+ tokenize() {
246
+ this.index = 0;
247
+ this.line = 1;
248
+ this.lineStart = 0;
249
+ const tokens = [];
250
+ while (true) {
251
+ const char = this.source[this.index];
252
+ if (char === undefined)
253
+ break;
254
+ if (char === " " || char === "\t" || char === "\r") {
255
+ this.index++;
256
+ continue;
257
+ }
258
+ if (char === `
259
+ `) {
260
+ tokens.push({
261
+ type: TokenType.NEWLINE,
262
+ lexeme: `
263
+ `,
264
+ line: this.line,
265
+ column: this.column
266
+ });
267
+ this.index++;
268
+ this.line++;
269
+ this.lineStart = this.index;
270
+ continue;
271
+ }
272
+ if (char === "#") {
273
+ this.skipLineComment();
274
+ continue;
275
+ }
276
+ if (char === "/" && this.source[this.index + 1] === "#") {
277
+ this.skipBlockComment();
278
+ continue;
279
+ }
280
+ if (char === '"') {
281
+ tokens.push(this.readString());
282
+ continue;
283
+ }
284
+ if (isDigit(char)) {
285
+ tokens.push(this.readNumber());
286
+ continue;
287
+ }
288
+ if (isIdentifierStart(char)) {
289
+ tokens.push(this.readIdentifier());
290
+ continue;
291
+ }
292
+ const operator = this.readOperator();
293
+ if (operator !== undefined) {
294
+ tokens.push(operator);
295
+ continue;
296
+ }
297
+ throw new LexerError(`Unexpected character ${JSON.stringify(char)}`, this.line, this.column);
298
+ }
299
+ tokens.push({
300
+ type: TokenType.EOF,
301
+ lexeme: "",
302
+ line: this.line,
303
+ column: this.column
304
+ });
305
+ return tokens;
306
+ }
307
+ skipLineComment() {
308
+ this.index++;
309
+ while (true) {
310
+ const char = this.source[this.index];
311
+ if (char === undefined || char === `
312
+ `)
313
+ return;
314
+ this.index++;
315
+ }
316
+ }
317
+ skipBlockComment() {
318
+ const startLine = this.line;
319
+ const startColumn = this.column;
320
+ this.index += 2;
321
+ while (true) {
322
+ const char = this.source[this.index];
323
+ if (char === undefined) {
324
+ throw new LexerError("Unterminated multi-line comment", startLine, startColumn, 2);
325
+ }
326
+ if (char === "#" && this.source[this.index + 1] === "/") {
327
+ this.index += 2;
328
+ return;
329
+ }
330
+ if (char === `
331
+ `) {
332
+ this.line++;
333
+ this.index++;
334
+ this.lineStart = this.index;
335
+ continue;
336
+ }
337
+ this.index++;
338
+ }
339
+ }
340
+ readString() {
341
+ const line = this.line;
342
+ const column = this.column;
343
+ const start = this.index;
344
+ this.index++;
345
+ let value = "";
346
+ while (true) {
347
+ const char = this.source[this.index];
348
+ if (char === undefined || char === `
349
+ `) {
350
+ throw new LexerError("Unterminated string", line, column);
351
+ }
352
+ if (char === '"') {
353
+ this.index++;
354
+ const lexeme = this.source.slice(start, this.index);
355
+ return { type: TokenType.STRING, lexeme, value, line, column };
356
+ }
357
+ if (char === "\\") {
358
+ const escaped = this.source[this.index + 1];
359
+ const decoded = escaped === undefined ? undefined : ESCAPES.get(escaped);
360
+ if (decoded === undefined) {
361
+ throw new LexerError(`Invalid escape sequence \\${escaped ?? ""}`, this.line, this.column, 2);
362
+ }
363
+ value += decoded;
364
+ this.index += 2;
365
+ continue;
366
+ }
367
+ value += char;
368
+ this.index++;
369
+ }
370
+ }
371
+ readNumber() {
372
+ const line = this.line;
373
+ const column = this.column;
374
+ const start = this.index;
375
+ while (isDigit(this.source[this.index]))
376
+ this.index++;
377
+ if (this.source[this.index] === "." && isDigit(this.source[this.index + 1])) {
378
+ this.index++;
379
+ while (isDigit(this.source[this.index]))
380
+ this.index++;
381
+ }
382
+ const lexeme = this.source.slice(start, this.index);
383
+ return {
384
+ type: TokenType.NUMBER,
385
+ lexeme,
386
+ value: Number(lexeme),
387
+ line,
388
+ column
389
+ };
390
+ }
391
+ readIdentifier() {
392
+ const line = this.line;
393
+ const column = this.column;
394
+ const start = this.index;
395
+ while (isIdentifierPart(this.source[this.index]))
396
+ this.index++;
397
+ const lexeme = this.source.slice(start, this.index);
398
+ return {
399
+ type: KEYWORDS.get(lexeme) ?? TokenType.IDENT,
400
+ lexeme,
401
+ line,
402
+ column
403
+ };
404
+ }
405
+ readOperator() {
406
+ const line = this.line;
407
+ const column = this.column;
408
+ const two = this.source.slice(this.index, this.index + 2);
409
+ const twoType = TWO_CHAR_OPERATORS.get(two);
410
+ if (twoType !== undefined) {
411
+ this.index += 2;
412
+ return { type: twoType, lexeme: two, line, column };
413
+ }
414
+ const one = this.source[this.index];
415
+ if (one === undefined)
416
+ return;
417
+ const oneType = ONE_CHAR_TOKENS.get(one);
418
+ if (oneType === undefined)
419
+ return;
420
+ this.index += 1;
421
+ return { type: oneType, lexeme: one, line, column };
422
+ }
423
+ }
424
+ // src/parser.ts
425
+ class ParserError extends Error {
426
+ line;
427
+ column;
428
+ length;
429
+ reason;
430
+ constructor(reason, line, column, length = 1) {
431
+ super(`${reason} (line ${line})`);
432
+ this.name = "ParserError";
433
+ this.reason = reason;
434
+ this.line = line;
435
+ this.column = column;
436
+ this.length = length;
437
+ }
438
+ }
439
+ var COMPARISON_OPERATORS = new Map([
440
+ [TokenType.GTEQ, ">="],
441
+ [TokenType.LTEQ, "<="],
442
+ [TokenType.GT, ">"],
443
+ [TokenType.LT, "<"],
444
+ [TokenType.EQEQ, "=="],
445
+ [TokenType.BANGEQ, "!="]
446
+ ]);
447
+ var ADDITIVE_OPERATORS = new Map([
448
+ [TokenType.PLUS, "+"],
449
+ [TokenType.MINUS, "-"]
450
+ ]);
451
+ var MULTIPLICATIVE_OPERATORS = new Map([
452
+ [TokenType.STAR, "*"],
453
+ [TokenType.SLASH, "/"]
454
+ ]);
455
+ var END = [TokenType.END];
456
+ var ELSE_OR_END = [TokenType.ELSE, TokenType.END];
457
+ function describe(token) {
458
+ switch (token.type) {
459
+ case TokenType.EOF:
460
+ return "end of input";
461
+ case TokenType.NEWLINE:
462
+ return "end of line";
463
+ default:
464
+ return `'${token.lexeme}'`;
465
+ }
466
+ }
467
+
468
+ class Parser {
469
+ tokens;
470
+ index = 0;
471
+ constructor(tokens = []) {
472
+ this.tokens = tokens;
473
+ }
474
+ parse() {
475
+ this.index = 0;
476
+ const first = this.peek();
477
+ const body = this.parseStatements([]);
478
+ const token = this.peek();
479
+ if (token.type !== TokenType.EOF) {
480
+ throw this.error(token, `Unexpected ${describe(token)} after program`);
481
+ }
482
+ return { kind: "Program", body, line: first.line };
483
+ }
484
+ parseStatements(terminators) {
485
+ const statements = [];
486
+ while (true) {
487
+ this.skipNewlines();
488
+ const token = this.peek();
489
+ if (token.type === TokenType.EOF || terminators.includes(token.type)) {
490
+ return statements;
491
+ }
492
+ statements.push(this.parseStatement());
493
+ const next = this.peek();
494
+ if (next.type !== TokenType.NEWLINE && next.type !== TokenType.EOF && !terminators.includes(next.type)) {
495
+ throw this.error(next, `Expected end of statement but found ${describe(next)}`);
496
+ }
497
+ }
498
+ }
499
+ parseStatement() {
500
+ switch (this.peek().type) {
501
+ case TokenType.SET:
502
+ return this.parseVarDecl();
503
+ case TokenType.PRINT:
504
+ return this.parsePrintStmt();
505
+ case TokenType.IF:
506
+ return this.parseIfStmt();
507
+ case TokenType.FN:
508
+ return this.parseFnDecl();
509
+ case TokenType.FOR:
510
+ return this.parseForStmt();
511
+ default:
512
+ return this.parseExprStmt();
513
+ }
514
+ }
515
+ parseVarDecl() {
516
+ const keyword = this.expect(TokenType.SET, "'set'");
517
+ const name = this.expect(TokenType.IDENT, "a variable name after 'set'");
518
+ this.expect(TokenType.EQUALS, "'=' after the variable name");
519
+ const value = this.parseExpression();
520
+ return { kind: "VarDecl", name: name.lexeme, value, line: keyword.line };
521
+ }
522
+ parsePrintStmt() {
523
+ const keyword = this.expect(TokenType.PRINT, "'print'");
524
+ const argument = this.parseExpression();
525
+ return { kind: "PrintStmt", argument, line: keyword.line };
526
+ }
527
+ parseIfStmt() {
528
+ const keyword = this.expect(TokenType.IF, "'if'");
529
+ const condition = this.parseExpression();
530
+ const thenBranch = this.parseStatements(ELSE_OR_END);
531
+ let elseBranch = null;
532
+ if (this.peek().type === TokenType.ELSE) {
533
+ this.advance();
534
+ elseBranch = this.parseStatements(END);
535
+ }
536
+ this.expect(TokenType.END, "'end' to close the 'if'");
537
+ return { kind: "IfStmt", condition, thenBranch, elseBranch, line: keyword.line };
538
+ }
539
+ parseFnDecl() {
540
+ const keyword = this.expect(TokenType.FN, "'fn'");
541
+ const name = this.expect(TokenType.IDENT, "a function name after 'fn'");
542
+ this.expect(TokenType.LPAREN, "'(' after the function name");
543
+ const params = [];
544
+ if (this.peek().type !== TokenType.RPAREN) {
545
+ params.push(this.expect(TokenType.IDENT, "a parameter name").lexeme);
546
+ while (this.peek().type === TokenType.COMMA) {
547
+ this.advance();
548
+ params.push(this.expect(TokenType.IDENT, "a parameter name").lexeme);
549
+ }
550
+ }
551
+ this.expect(TokenType.RPAREN, "')' after the parameter list");
552
+ const body = this.parseStatements(END);
553
+ this.expect(TokenType.END, "'end' to close the function body");
554
+ return { kind: "FnDecl", name: name.lexeme, params, body, line: keyword.line };
555
+ }
556
+ parseForStmt() {
557
+ const keyword = this.expect(TokenType.FOR, "'for'");
558
+ const variable = this.expect(TokenType.IDENT, "a loop variable after 'for'");
559
+ this.expect(TokenType.IN, "'in' after the loop variable");
560
+ const start = this.parseExpression();
561
+ this.expect(TokenType.DOTDOT, "'..' between the range bounds");
562
+ const end = this.parseExpression();
563
+ const body = this.parseStatements(END);
564
+ this.expect(TokenType.END, "'end' to close the 'for' loop");
565
+ return {
566
+ kind: "ForStmt",
567
+ variable: variable.lexeme,
568
+ start,
569
+ end,
570
+ body,
571
+ line: keyword.line
572
+ };
573
+ }
574
+ parseExprStmt() {
575
+ const expression = this.parseExpression();
576
+ return { kind: "ExprStmt", expression, line: expression.line };
577
+ }
578
+ parseExpression() {
579
+ return this.parseComparison();
580
+ }
581
+ parseComparison() {
582
+ return this.parseBinary(() => this.parseTerm(), COMPARISON_OPERATORS);
583
+ }
584
+ parseTerm() {
585
+ return this.parseBinary(() => this.parseFactor(), ADDITIVE_OPERATORS);
586
+ }
587
+ parseFactor() {
588
+ return this.parseBinary(() => this.parsePrimary(), MULTIPLICATIVE_OPERATORS);
589
+ }
590
+ parseBinary(operand, operators) {
591
+ let left = operand();
592
+ while (true) {
593
+ const operator = operators.get(this.peek().type);
594
+ if (operator === undefined)
595
+ return left;
596
+ this.advance();
597
+ const right = operand();
598
+ left = { kind: "BinaryExpr", operator, left, right, line: left.line };
599
+ }
600
+ }
601
+ parsePrimary() {
602
+ const token = this.peek();
603
+ switch (token.type) {
604
+ case TokenType.NUMBER: {
605
+ this.advance();
606
+ const value = typeof token.value === "number" ? token.value : Number(token.lexeme);
607
+ return { kind: "NumberLiteral", value, line: token.line };
608
+ }
609
+ case TokenType.STRING: {
610
+ this.advance();
611
+ const value = typeof token.value === "string" ? token.value : token.lexeme.slice(1, -1);
612
+ return { kind: "StringLiteral", value, line: token.line };
613
+ }
614
+ case TokenType.IDENT: {
615
+ this.advance();
616
+ if (this.peek().type === TokenType.LPAREN) {
617
+ return this.finishFunctionCall(token);
618
+ }
619
+ return { kind: "Identifier", name: token.lexeme, line: token.line };
620
+ }
621
+ case TokenType.LPAREN: {
622
+ this.advance();
623
+ const expression = this.parseExpression();
624
+ this.expect(TokenType.RPAREN, "')' to close the group");
625
+ return expression;
626
+ }
627
+ default:
628
+ throw this.error(token, `Expected an expression but found ${describe(token)}`);
629
+ }
630
+ }
631
+ finishFunctionCall(callee) {
632
+ this.expect(TokenType.LPAREN, "'('");
633
+ const args = [];
634
+ if (this.peek().type !== TokenType.RPAREN) {
635
+ args.push(this.parseExpression());
636
+ while (this.peek().type === TokenType.COMMA) {
637
+ this.advance();
638
+ args.push(this.parseExpression());
639
+ }
640
+ }
641
+ this.expect(TokenType.RPAREN, "')' to close the argument list");
642
+ return {
643
+ kind: "FunctionCall",
644
+ callee: callee.lexeme,
645
+ args,
646
+ line: callee.line
647
+ };
648
+ }
649
+ peek(offset = 0) {
650
+ const token = this.tokens[this.index + offset];
651
+ if (token === undefined) {
652
+ throw new Error("Parser ran past the end of the token stream");
653
+ }
654
+ return token;
655
+ }
656
+ advance() {
657
+ const token = this.peek();
658
+ this.index++;
659
+ return token;
660
+ }
661
+ expect(type, description) {
662
+ const token = this.peek();
663
+ if (token.type !== type) {
664
+ throw this.error(token, `Expected ${description} but found ${describe(token)}`);
665
+ }
666
+ this.index++;
667
+ return token;
668
+ }
669
+ skipNewlines() {
670
+ while (this.peek().type === TokenType.NEWLINE)
671
+ this.advance();
672
+ }
673
+ error(token, message) {
674
+ return new ParserError(message, token.line, token.column, Math.max(token.lexeme.length, 1));
675
+ }
676
+ }
677
+ export {
678
+ generate,
679
+ formatDiagnostic,
680
+ ParserError,
681
+ Parser,
682
+ LexerError,
683
+ Lexer
684
+ };
@@ -0,0 +1,14 @@
1
+ # The first 10 Fibonacci numbers.
2
+ #
3
+ # Synax has no `return`, so a function cannot hand back a value. This version
4
+ # instead carries the running pair (a, b) through recursive parameters and
5
+ # prints each term as it goes, using `n` as the countdown.
6
+
7
+ fn fib(a, b, n)
8
+ if n > 0
9
+ print a
10
+ fib(b, a + b, n - 1)
11
+ end
12
+ end
13
+
14
+ fib(0, 1, 10)