@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/LICENSE +21 -0
- package/README.md +91 -0
- package/SPEC.md +159 -0
- package/dist/cli.js +741 -0
- package/dist/index.js +684 -0
- package/examples/fibonacci.snx +14 -0
- package/examples/fizzbuzz.snx +34 -0
- package/examples/greet.snx +5 -0
- package/examples/hello.snx +1 -0
- package/examples/if-else.snx +7 -0
- package/examples/loop.snx +3 -0
- package/examples/variables.snx +2 -0
- package/package.json +49 -0
- package/src/ast.ts +151 -0
- package/src/cli.ts +101 -0
- package/src/codegen.ts +236 -0
- package/src/diagnostics.ts +63 -0
- package/src/index.ts +11 -0
- package/src/lexer.ts +438 -0
- package/src/parser.ts +441 -0
- package/synax.d.ts +236 -0
- package/synax.html +281 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# FizzBuzz for 1..15.
|
|
2
|
+
#
|
|
3
|
+
# Synax has no `%` operator, so divisibility is tracked with two counters that
|
|
4
|
+
# wrap around at 3 and 5. After the wrap, a zero counter means "divisible by".
|
|
5
|
+
|
|
6
|
+
set fizz = 0
|
|
7
|
+
set buzz = 0
|
|
8
|
+
|
|
9
|
+
for i in 1..15
|
|
10
|
+
set fizz = fizz + 1
|
|
11
|
+
set buzz = buzz + 1
|
|
12
|
+
|
|
13
|
+
if fizz == 3
|
|
14
|
+
set fizz = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if buzz == 5
|
|
18
|
+
set buzz = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if fizz == 0
|
|
22
|
+
if buzz == 0
|
|
23
|
+
print "FizzBuzz"
|
|
24
|
+
else
|
|
25
|
+
print "Fizz"
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
if buzz == 0
|
|
29
|
+
print "Buzz"
|
|
30
|
+
else
|
|
31
|
+
print i
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
print "Hello, World!"
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@user-synax/synax",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A small scripting language that transpiles to JavaScript.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"language",
|
|
9
|
+
"compiler",
|
|
10
|
+
"transpiler",
|
|
11
|
+
"javascript",
|
|
12
|
+
"bun",
|
|
13
|
+
"synax"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"synax": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./synax.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./synax.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src",
|
|
31
|
+
"examples",
|
|
32
|
+
"synax.d.ts",
|
|
33
|
+
"synax.html",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"SPEC.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "bun build src/cli.ts src/index.ts --outdir dist --target bun",
|
|
40
|
+
"docs": "bun run scripts/build-docs.ts",
|
|
41
|
+
"dev": "bun run --watch src/cli.ts",
|
|
42
|
+
"prepublishOnly": "bun test && bun run typecheck && bun run build",
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"bun": ">=1.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/ast.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synax abstract syntax tree.
|
|
3
|
+
*
|
|
4
|
+
* The parser (`src/parser.ts`) turns a flat token stream into these nodes; the
|
|
5
|
+
* code generator (`src/codegen.ts`) walks them. Every node carries a 1-based
|
|
6
|
+
* `line` — the line of the node's first token — so later stages can report
|
|
7
|
+
* positioned errors without re-deriving them from the source.
|
|
8
|
+
*
|
|
9
|
+
* Note: `Token` / `TokenType` deliberately still live in `src/lexer.ts` (see the
|
|
10
|
+
* NOTE at the top of that file). This module defines only the tree itself, so it
|
|
11
|
+
* has no imports.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Expressions
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** The operator spellings shared by every binary expression. */
|
|
19
|
+
export type BinaryOperator =
|
|
20
|
+
| "=="
|
|
21
|
+
| "!="
|
|
22
|
+
| ">"
|
|
23
|
+
| "<"
|
|
24
|
+
| ">="
|
|
25
|
+
| "<="
|
|
26
|
+
| "+"
|
|
27
|
+
| "-"
|
|
28
|
+
| "*"
|
|
29
|
+
| "/";
|
|
30
|
+
|
|
31
|
+
export interface NumberLiteral {
|
|
32
|
+
readonly kind: "NumberLiteral";
|
|
33
|
+
readonly value: number;
|
|
34
|
+
readonly line: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StringLiteral {
|
|
38
|
+
readonly kind: "StringLiteral";
|
|
39
|
+
/** The decoded value (escapes already applied), without the quotes. */
|
|
40
|
+
readonly value: string;
|
|
41
|
+
readonly line: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Identifier {
|
|
45
|
+
readonly kind: "Identifier";
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly line: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `left <operator> right`. `line` is the line of the left-most operand, i.e. the
|
|
52
|
+
* first token of the whole expression.
|
|
53
|
+
*/
|
|
54
|
+
export interface BinaryExpr {
|
|
55
|
+
readonly kind: "BinaryExpr";
|
|
56
|
+
readonly operator: BinaryOperator;
|
|
57
|
+
readonly left: Expression;
|
|
58
|
+
readonly right: Expression;
|
|
59
|
+
readonly line: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `callee(arg, ...)`. Per the grammar the callee is always a bare identifier. */
|
|
63
|
+
export interface FunctionCall {
|
|
64
|
+
readonly kind: "FunctionCall";
|
|
65
|
+
readonly callee: string;
|
|
66
|
+
readonly args: Expression[];
|
|
67
|
+
readonly line: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type Expression =
|
|
71
|
+
| NumberLiteral
|
|
72
|
+
| StringLiteral
|
|
73
|
+
| Identifier
|
|
74
|
+
| BinaryExpr
|
|
75
|
+
| FunctionCall;
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Statements
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** `set name = expression` — also used for reassignment (see SPEC.md §6). */
|
|
82
|
+
export interface VarDecl {
|
|
83
|
+
readonly kind: "VarDecl";
|
|
84
|
+
readonly name: string;
|
|
85
|
+
readonly value: Expression;
|
|
86
|
+
readonly line: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `print expression`. */
|
|
90
|
+
export interface PrintStmt {
|
|
91
|
+
readonly kind: "PrintStmt";
|
|
92
|
+
readonly argument: Expression;
|
|
93
|
+
readonly line: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `if condition <thenBranch> (else <elseBranch>)? end`. */
|
|
97
|
+
export interface IfStmt {
|
|
98
|
+
readonly kind: "IfStmt";
|
|
99
|
+
readonly condition: Expression;
|
|
100
|
+
readonly thenBranch: Statement[];
|
|
101
|
+
/** `null` when the `if` has no `else` — distinct from an empty else block. */
|
|
102
|
+
readonly elseBranch: Statement[] | null;
|
|
103
|
+
readonly line: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `fn name(params) <body> end`. */
|
|
107
|
+
export interface FnDecl {
|
|
108
|
+
readonly kind: "FnDecl";
|
|
109
|
+
readonly name: string;
|
|
110
|
+
readonly params: string[];
|
|
111
|
+
readonly body: Statement[];
|
|
112
|
+
readonly line: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `for variable in start..end <body> end`. */
|
|
116
|
+
export interface ForStmt {
|
|
117
|
+
readonly kind: "ForStmt";
|
|
118
|
+
readonly variable: string;
|
|
119
|
+
readonly start: Expression;
|
|
120
|
+
readonly end: Expression;
|
|
121
|
+
readonly body: Statement[];
|
|
122
|
+
readonly line: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A bare expression used as a statement, e.g. a function call. */
|
|
126
|
+
export interface ExprStmt {
|
|
127
|
+
readonly kind: "ExprStmt";
|
|
128
|
+
readonly expression: Expression;
|
|
129
|
+
readonly line: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type Statement =
|
|
133
|
+
| VarDecl
|
|
134
|
+
| PrintStmt
|
|
135
|
+
| IfStmt
|
|
136
|
+
| FnDecl
|
|
137
|
+
| ForStmt
|
|
138
|
+
| ExprStmt;
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Root
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
export interface Program {
|
|
145
|
+
readonly kind: "Program";
|
|
146
|
+
readonly body: Statement[];
|
|
147
|
+
readonly line: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Any node in the tree. */
|
|
151
|
+
export type Node = Program | Statement | Expression;
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Synax CLI entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run src/cli.ts <file>
|
|
7
|
+
*
|
|
8
|
+
* Reads a Synax source file, runs it through the pipeline
|
|
9
|
+
* (`Lexer` -> `Parser` -> `generate`) and writes the resulting JavaScript to
|
|
10
|
+
* stdout.
|
|
11
|
+
*
|
|
12
|
+
* Diagnostics go to stderr as a positioned message plus a code frame, with a
|
|
13
|
+
* non-zero exit code — never a raw stack trace:
|
|
14
|
+
*
|
|
15
|
+
* examples/bad.snx:1:7: error: Unexpected character "@"
|
|
16
|
+
* 1 | print @
|
|
17
|
+
* | ^
|
|
18
|
+
*
|
|
19
|
+
* Set `SYNAX_DEBUG=1` to include the stack for an internal compiler error.
|
|
20
|
+
*/
|
|
21
|
+
import { generate } from "./codegen";
|
|
22
|
+
import { formatDiagnostic } from "./diagnostics";
|
|
23
|
+
import { Lexer, LexerError } from "./lexer";
|
|
24
|
+
import { Parser, ParserError } from "./parser";
|
|
25
|
+
|
|
26
|
+
const USAGE = "Usage: synax <file>";
|
|
27
|
+
|
|
28
|
+
export async function main(
|
|
29
|
+
argv: readonly string[] = Bun.argv.slice(2),
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const filePath = argv[0];
|
|
32
|
+
|
|
33
|
+
if (filePath === undefined) {
|
|
34
|
+
console.error(USAGE);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let source: string;
|
|
40
|
+
try {
|
|
41
|
+
source = await Bun.file(filePath).text();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(
|
|
44
|
+
`synax: cannot read '${filePath}': ${describeReadFailure(error)}`,
|
|
45
|
+
);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const tokens = new Lexer(source).tokenize();
|
|
52
|
+
const ast = new Parser(tokens).parse();
|
|
53
|
+
console.log(generate(ast));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
|
|
57
|
+
// Expected diagnostics: a positioned message and a code frame.
|
|
58
|
+
if (error instanceof LexerError || error instanceof ParserError) {
|
|
59
|
+
console.error(
|
|
60
|
+
formatDiagnostic(
|
|
61
|
+
{
|
|
62
|
+
file: filePath,
|
|
63
|
+
line: error.line,
|
|
64
|
+
column: error.column,
|
|
65
|
+
length: error.length,
|
|
66
|
+
reason: error.reason,
|
|
67
|
+
},
|
|
68
|
+
source,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A bug in the compiler rather than in the input. Keep it to one line by
|
|
75
|
+
// default so it reads as a diagnostic, not a crash.
|
|
76
|
+
console.error(`synax: internal error: ${describeError(error)}`);
|
|
77
|
+
if (process.env["SYNAX_DEBUG"]) console.error(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Turn a filesystem error into a short, human-readable reason. */
|
|
82
|
+
function describeReadFailure(error: unknown): string {
|
|
83
|
+
switch ((error as { code?: unknown }).code) {
|
|
84
|
+
case "ENOENT":
|
|
85
|
+
return "no such file";
|
|
86
|
+
case "EISDIR":
|
|
87
|
+
return "is a directory";
|
|
88
|
+
case "EACCES":
|
|
89
|
+
return "permission denied";
|
|
90
|
+
default:
|
|
91
|
+
return "could not be read";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function describeError(error: unknown): string {
|
|
96
|
+
return error instanceof Error ? error.message : String(error);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (import.meta.main) {
|
|
100
|
+
await main();
|
|
101
|
+
}
|
package/src/codegen.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synax code generator — walks the AST and prints equivalent JavaScript.
|
|
3
|
+
*
|
|
4
|
+
* Mapping (SPEC.md §8 examples drive most of this):
|
|
5
|
+
* set x = 5 -> let x = 5; (assignment on reassignment, see below)
|
|
6
|
+
* print e -> console.log(e);
|
|
7
|
+
* if/else/end -> if (cond) { ... } else { ... }
|
|
8
|
+
* fn/end -> function name(params) { ... }
|
|
9
|
+
* for i in a..b -> for (let i = a; i <= b; i++) { ... } (INCLUSIVE)
|
|
10
|
+
*
|
|
11
|
+
* Two semantics decided with the spec's author:
|
|
12
|
+
* - `a..b` is INCLUSIVE of `b`, so the emitted loop test is `i <= b`.
|
|
13
|
+
* - `+` keeps JavaScript's coercion, so `"n=" + 5` concatenates rather than
|
|
14
|
+
* erroring. That matches SPEC.md §3 ("+ used for concat").
|
|
15
|
+
*
|
|
16
|
+
* `set` both declares and reassigns (SPEC.md §6). JavaScript's `let` cannot be
|
|
17
|
+
* redeclared, so the generator tracks which names are already in scope and
|
|
18
|
+
* emits `name = value;` for a rebind instead of a second `let`.
|
|
19
|
+
*
|
|
20
|
+
* Known limitation: Synax scoping is function-scoped (§6), but a `let` emitted
|
|
21
|
+
* inside an `if`/`for` block is block-scoped in JS. A name first `set` inside a
|
|
22
|
+
* nested block therefore will not be visible after that block. Hoisting
|
|
23
|
+
* declarations (or emitting `var`) would fix this; neither example in §8 needs
|
|
24
|
+
* it. Flag it if you want it addressed.
|
|
25
|
+
*/
|
|
26
|
+
import type {
|
|
27
|
+
BinaryExpr,
|
|
28
|
+
BinaryOperator,
|
|
29
|
+
Expression,
|
|
30
|
+
FnDecl,
|
|
31
|
+
ForStmt,
|
|
32
|
+
IfStmt,
|
|
33
|
+
PrintStmt,
|
|
34
|
+
Program,
|
|
35
|
+
Statement,
|
|
36
|
+
VarDecl,
|
|
37
|
+
} from "./ast";
|
|
38
|
+
|
|
39
|
+
/** Indentation unit for emitted blocks. */
|
|
40
|
+
const INDENT = " ";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Binding strength by operator, loosest to tightest (SPEC.md §5). Used only to
|
|
44
|
+
* decide where parentheses are actually required — every emitted expression
|
|
45
|
+
* also has to match JS's own precedence, which is identical for these
|
|
46
|
+
* operators.
|
|
47
|
+
*/
|
|
48
|
+
const PRECEDENCE: Record<BinaryOperator, number> = {
|
|
49
|
+
"==": 1,
|
|
50
|
+
"!=": 1,
|
|
51
|
+
">": 1,
|
|
52
|
+
"<": 1,
|
|
53
|
+
">=": 1,
|
|
54
|
+
"<=": 1,
|
|
55
|
+
"+": 2,
|
|
56
|
+
"-": 2,
|
|
57
|
+
"*": 3,
|
|
58
|
+
"/": 3,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Translate a parsed `Program` into JavaScript source.
|
|
63
|
+
*
|
|
64
|
+
* @returns JS source with no trailing newline; `""` for an empty program.
|
|
65
|
+
*/
|
|
66
|
+
export function generate(program: Program): string {
|
|
67
|
+
return new Generator().program(program);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class Generator {
|
|
71
|
+
private indentLevel = 0;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Names already declared in the current function scope. `set` on a name that
|
|
75
|
+
* is already here becomes a plain assignment instead of a second `let`.
|
|
76
|
+
*/
|
|
77
|
+
private scope = new Set<string>();
|
|
78
|
+
|
|
79
|
+
program(node: Program): string {
|
|
80
|
+
return node.body.map((statement) => this.statement(statement)).join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
// Statements
|
|
85
|
+
// -------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
private statement(node: Statement): string {
|
|
88
|
+
switch (node.kind) {
|
|
89
|
+
case "VarDecl":
|
|
90
|
+
return this.varDecl(node);
|
|
91
|
+
case "PrintStmt":
|
|
92
|
+
return this.printStmt(node);
|
|
93
|
+
case "IfStmt":
|
|
94
|
+
return this.ifStmt(node);
|
|
95
|
+
case "FnDecl":
|
|
96
|
+
return this.fnDecl(node);
|
|
97
|
+
case "ForStmt":
|
|
98
|
+
return this.forStmt(node);
|
|
99
|
+
case "ExprStmt":
|
|
100
|
+
return `${this.indent()}${this.expression(node.expression)};`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `set name = value` — declares on first use, assigns thereafter. */
|
|
105
|
+
private varDecl(node: VarDecl): string {
|
|
106
|
+
const value = this.expression(node.value);
|
|
107
|
+
|
|
108
|
+
if (this.scope.has(node.name)) {
|
|
109
|
+
return `${this.indent()}${node.name} = ${value};`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.scope.add(node.name);
|
|
113
|
+
return `${this.indent()}let ${node.name} = ${value};`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private printStmt(node: PrintStmt): string {
|
|
117
|
+
return `${this.indent()}console.log(${this.expression(node.argument)});`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private ifStmt(node: IfStmt): string {
|
|
121
|
+
const header = `${this.indent()}if (${this.expression(node.condition)}) `;
|
|
122
|
+
|
|
123
|
+
let output = header + this.block(node.thenBranch);
|
|
124
|
+
if (node.elseBranch !== null) {
|
|
125
|
+
output += ` else ` + this.block(node.elseBranch);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return output;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fnDecl(node: FnDecl): string {
|
|
132
|
+
const header = `${this.indent()}function ${node.name}(${node.params.join(", ")}) `;
|
|
133
|
+
|
|
134
|
+
// A function introduces a fresh scope; its parameters are already declared.
|
|
135
|
+
const outer = this.scope;
|
|
136
|
+
this.scope = new Set(node.params);
|
|
137
|
+
const body = this.block(node.body);
|
|
138
|
+
this.scope = outer;
|
|
139
|
+
|
|
140
|
+
return header + body;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `for i in start..end` — INCLUSIVE of `end` (decided for this project), so
|
|
145
|
+
* the emitted test is `i <= end`.
|
|
146
|
+
*/
|
|
147
|
+
private forStmt(node: ForStmt): string {
|
|
148
|
+
const { variable } = node;
|
|
149
|
+
const start = this.expression(node.start);
|
|
150
|
+
const end = this.expression(node.end);
|
|
151
|
+
|
|
152
|
+
const header = `${this.indent()}for (let ${variable} = ${start}; ${variable} <= ${end}; ${variable}++) `;
|
|
153
|
+
|
|
154
|
+
// `variable` is intentionally not added to the enclosing scope: JS's
|
|
155
|
+
// `let` in the for-head already scopes it to the loop.
|
|
156
|
+
return header + this.block(node.body);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// -------------------------------------------------------------------------
|
|
160
|
+
// Expressions
|
|
161
|
+
// -------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
private expression(node: Expression): string {
|
|
164
|
+
switch (node.kind) {
|
|
165
|
+
case "NumberLiteral":
|
|
166
|
+
return String(node.value);
|
|
167
|
+
case "StringLiteral":
|
|
168
|
+
// Reuse JSON's string escaping: it matches JS string syntax exactly.
|
|
169
|
+
return JSON.stringify(node.value);
|
|
170
|
+
case "Identifier":
|
|
171
|
+
return node.name;
|
|
172
|
+
case "BinaryExpr":
|
|
173
|
+
return this.binary(node, 0, false);
|
|
174
|
+
case "FunctionCall":
|
|
175
|
+
return `${node.callee}(${node.args
|
|
176
|
+
.map((argument) => this.expression(argument))
|
|
177
|
+
.join(", ")})`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Emit a binary expression, parenthesizing only where required.
|
|
183
|
+
*
|
|
184
|
+
* All Synax binary operators are left-associative, so a right-hand child of
|
|
185
|
+
* equal precedence needs parentheses (`1 - (2 - 3)`) while a left-hand child
|
|
186
|
+
* does not (`1 - 2 - 3`).
|
|
187
|
+
*/
|
|
188
|
+
private binary(
|
|
189
|
+
node: BinaryExpr,
|
|
190
|
+
parentPrecedence: number,
|
|
191
|
+
isRightChild: boolean,
|
|
192
|
+
): string {
|
|
193
|
+
const precedence = PRECEDENCE[node.operator];
|
|
194
|
+
|
|
195
|
+
const left = this.operand(node.left, precedence, false);
|
|
196
|
+
const right = this.operand(node.right, precedence, true);
|
|
197
|
+
const text = `${left} ${node.operator} ${right}`;
|
|
198
|
+
|
|
199
|
+
const needsParens =
|
|
200
|
+
precedence < parentPrecedence ||
|
|
201
|
+
(precedence === parentPrecedence && isRightChild);
|
|
202
|
+
|
|
203
|
+
return needsParens ? `(${text})` : text;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Emit a binary operand, telling it where it sits in the tree. */
|
|
207
|
+
private operand(
|
|
208
|
+
node: Expression,
|
|
209
|
+
parentPrecedence: number,
|
|
210
|
+
isRightChild: boolean,
|
|
211
|
+
): string {
|
|
212
|
+
if (node.kind === "BinaryExpr") {
|
|
213
|
+
return this.binary(node, parentPrecedence, isRightChild);
|
|
214
|
+
}
|
|
215
|
+
return this.expression(node);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// -------------------------------------------------------------------------
|
|
219
|
+
// Helpers
|
|
220
|
+
// -------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/** Emit `{ ... }` with the body one level deeper. */
|
|
223
|
+
private block(statements: readonly Statement[]): string {
|
|
224
|
+
if (statements.length === 0) return "{}";
|
|
225
|
+
|
|
226
|
+
this.indentLevel++;
|
|
227
|
+
const body = statements.map((statement) => this.statement(statement)).join("\n");
|
|
228
|
+
this.indentLevel--;
|
|
229
|
+
|
|
230
|
+
return `{\n${body}\n${this.indent()}}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private indent(): string {
|
|
234
|
+
return INDENT.repeat(this.indentLevel);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic rendering.
|
|
3
|
+
*
|
|
4
|
+
* Turns a positioned compiler error into a readable message that points at the
|
|
5
|
+
* offending line of Synax source:
|
|
6
|
+
*
|
|
7
|
+
* examples/bad.snx:1:7: error: Unexpected character "@"
|
|
8
|
+
* 1 | print @
|
|
9
|
+
* | ^
|
|
10
|
+
*
|
|
11
|
+
* This module is deliberately dependency-free: it renders a plain `Diagnostic`,
|
|
12
|
+
* so the lexer/parser error classes do not need to be imported here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A positioned error, ready to render. All positions are 1-based. */
|
|
16
|
+
export interface Diagnostic {
|
|
17
|
+
readonly file: string;
|
|
18
|
+
/** 1-based line. */
|
|
19
|
+
readonly line: number;
|
|
20
|
+
/** 1-based column. */
|
|
21
|
+
readonly column: number;
|
|
22
|
+
/** Characters to underline; at least 1. */
|
|
23
|
+
readonly length: number;
|
|
24
|
+
/** The message, without any positional suffix. */
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Indentation before the line-number gutter. */
|
|
29
|
+
const LEAD = " ";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render `diagnostic` against `source`, including a code frame when the line
|
|
33
|
+
* can be located.
|
|
34
|
+
*
|
|
35
|
+
* @returns A multi-line string with no trailing newline.
|
|
36
|
+
*/
|
|
37
|
+
export function formatDiagnostic(diagnostic: Diagnostic, source: string): string {
|
|
38
|
+
const { file, line, column, length, reason } = diagnostic;
|
|
39
|
+
const header = `${file}:${line}:${column}: error: ${reason}`;
|
|
40
|
+
|
|
41
|
+
// Split on \r?\n so CRLF files do not leave a stray \r in the frame.
|
|
42
|
+
const sourceLine = source.split(/\r?\n/)[line - 1];
|
|
43
|
+
|
|
44
|
+
// Nothing useful to show for a missing line, or the empty line that EOF
|
|
45
|
+
// errors point at when the file ends with a newline.
|
|
46
|
+
if (sourceLine === undefined || sourceLine.trim() === "") return header;
|
|
47
|
+
|
|
48
|
+
// Clamp to the line: an EOF error points one past the last character.
|
|
49
|
+
const start = Math.max(0, Math.min(column - 1, sourceLine.length));
|
|
50
|
+
const span = Math.max(1, Math.min(length, sourceLine.length - start));
|
|
51
|
+
|
|
52
|
+
const number = String(line);
|
|
53
|
+
const gutter = " ".repeat(number.length);
|
|
54
|
+
|
|
55
|
+
// Replace every non-tab character with a space so the caret keeps whatever
|
|
56
|
+
// tab alignment the rendered line above it has.
|
|
57
|
+
const padding = sourceLine.slice(0, start).replace(/[^\t]/g, " ");
|
|
58
|
+
|
|
59
|
+
const rendered = `${LEAD}${number} | ${sourceLine}`;
|
|
60
|
+
const caretLine = `${LEAD}${gutter} | ${padding}${"^".repeat(span)}`;
|
|
61
|
+
|
|
62
|
+
return `${header}\n${rendered}\n${caretLine}`;
|
|
63
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synax public API surface.
|
|
3
|
+
*
|
|
4
|
+
* TODO: re-export the AST node types from `./ast` once they are defined (they
|
|
5
|
+
* will need `export type { ... }` under `verbatimModuleSyntax`).
|
|
6
|
+
*/
|
|
7
|
+
export { generate } from "./codegen";
|
|
8
|
+
export { formatDiagnostic } from "./diagnostics";
|
|
9
|
+
export type { Diagnostic } from "./diagnostics";
|
|
10
|
+
export { Lexer, LexerError } from "./lexer";
|
|
11
|
+
export { Parser, ParserError } from "./parser";
|