@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.
@@ -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,5 @@
1
+ fn greet(name)
2
+ print "Hello, " + name
3
+ end
4
+
5
+ greet("Ayush")
@@ -0,0 +1 @@
1
+ print "Hello, World!"
@@ -0,0 +1,7 @@
1
+ set age = 19
2
+
3
+ if age >= 18
4
+ print "adult"
5
+ else
6
+ print "minor"
7
+ end
@@ -0,0 +1,3 @@
1
+ for i in 1..5
2
+ print i
3
+ end
@@ -0,0 +1,2 @@
1
+ set x = 5
2
+ print x
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";