code-the-jewels 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/.claude/settings.local.json +7 -0
- package/PROMPTS/01-build-v0.1.md +10 -0
- package/README.md +186 -0
- package/dist/ast.d.ts +143 -0
- package/dist/ast.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +145 -0
- package/dist/diagnostics.d.ts +7 -0
- package/dist/diagnostics.js +16 -0
- package/dist/generator.d.ts +11 -0
- package/dist/generator.js +126 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +15 -0
- package/dist/lexer.d.ts +18 -0
- package/dist/lexer.js +210 -0
- package/dist/parser.d.ts +40 -0
- package/dist/parser.js +394 -0
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +132 -0
- package/dist/runtime/atl-data.d.ts +4 -0
- package/dist/runtime/atl-data.js +18 -0
- package/dist/runtime/atl-flow.d.ts +1 -0
- package/dist/runtime/atl-flow.js +5 -0
- package/dist/runtime/bk-parse.d.ts +3 -0
- package/dist/runtime/bk-parse.js +9 -0
- package/dist/runtime/bk-text.d.ts +5 -0
- package/dist/runtime/bk-text.js +13 -0
- package/dist/runtime/rtj-core.d.ts +1 -0
- package/dist/runtime/rtj-core.js +51 -0
- package/dist/semantic.d.ts +11 -0
- package/dist/semantic.js +153 -0
- package/dist/tests/basic.test.d.ts +1 -0
- package/dist/tests/basic.test.js +69 -0
- package/dist/token.d.ts +56 -0
- package/dist/token.js +77 -0
- package/examples/cities.rtj +11 -0
- package/examples/count-words.rtj +12 -0
- package/examples/duo.rtj +12 -0
- package/examples/hello.rtj +1 -0
- package/examples/pipes.rtj +6 -0
- package/package.json +22 -0
- package/public/_redirects +1 -0
- package/public/index.html +559 -0
- package/src/ast.ts +189 -0
- package/src/cli.ts +120 -0
- package/src/diagnostics.ts +15 -0
- package/src/generator.ts +129 -0
- package/src/index.ts +7 -0
- package/src/lexer.ts +208 -0
- package/src/parser.ts +461 -0
- package/src/repl.ts +105 -0
- package/src/runtime/atl-data.ts +11 -0
- package/src/runtime/atl-flow.ts +1 -0
- package/src/runtime/bk-parse.ts +3 -0
- package/src/runtime/bk-text.ts +5 -0
- package/src/runtime/rtj-core.ts +21 -0
- package/src/semantic.ts +144 -0
- package/src/tests/basic.test.ts +74 -0
- package/src/token.ts +85 -0
- package/tsconfig.json +15 -0
package/src/ast.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export interface Location {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type Statement =
|
|
7
|
+
| VarDecl
|
|
8
|
+
| FunctionDecl
|
|
9
|
+
| ReturnStmt
|
|
10
|
+
| TalkStmt
|
|
11
|
+
| IfStmt
|
|
12
|
+
| LoopStmt
|
|
13
|
+
| ImportStmt
|
|
14
|
+
| ThrowStmt
|
|
15
|
+
| ExpressionStmt
|
|
16
|
+
| BlockStmt;
|
|
17
|
+
|
|
18
|
+
export type Expression =
|
|
19
|
+
| Identifier
|
|
20
|
+
| StringLiteral
|
|
21
|
+
| NumberLiteral
|
|
22
|
+
| BooleanLiteral
|
|
23
|
+
| NullLiteral
|
|
24
|
+
| ArrayLiteral
|
|
25
|
+
| ObjectLiteral
|
|
26
|
+
| BinaryExpr
|
|
27
|
+
| UnaryExpr
|
|
28
|
+
| CallExpr
|
|
29
|
+
| MemberExpr
|
|
30
|
+
| PipeExpr
|
|
31
|
+
| DuoExpr;
|
|
32
|
+
|
|
33
|
+
export interface Program {
|
|
34
|
+
type: 'Program';
|
|
35
|
+
body: Statement[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface VarDecl {
|
|
39
|
+
type: 'VarDecl';
|
|
40
|
+
name: string;
|
|
41
|
+
init: Expression;
|
|
42
|
+
loc: Location;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FunctionDecl {
|
|
46
|
+
type: 'FunctionDecl';
|
|
47
|
+
name: string;
|
|
48
|
+
params: string[];
|
|
49
|
+
body: BlockStmt;
|
|
50
|
+
loc: Location;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ReturnStmt {
|
|
54
|
+
type: 'ReturnStmt';
|
|
55
|
+
value?: Expression;
|
|
56
|
+
loc: Location;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TalkStmt {
|
|
60
|
+
type: 'TalkStmt';
|
|
61
|
+
value: Expression;
|
|
62
|
+
loc: Location;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IfStmt {
|
|
66
|
+
type: 'IfStmt';
|
|
67
|
+
condition: Expression;
|
|
68
|
+
consequent: BlockStmt;
|
|
69
|
+
alternate?: BlockStmt;
|
|
70
|
+
loc: Location;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LoopStmt {
|
|
74
|
+
type: 'LoopStmt';
|
|
75
|
+
variable: string;
|
|
76
|
+
iterable: Expression;
|
|
77
|
+
body: BlockStmt;
|
|
78
|
+
loc: Location;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ImportStmt {
|
|
82
|
+
type: 'ImportStmt';
|
|
83
|
+
names: string[];
|
|
84
|
+
source: string;
|
|
85
|
+
loc: Location;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ThrowStmt {
|
|
89
|
+
type: 'ThrowStmt';
|
|
90
|
+
value: Expression;
|
|
91
|
+
loc: Location;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ExpressionStmt {
|
|
95
|
+
type: 'ExpressionStmt';
|
|
96
|
+
expr: Expression;
|
|
97
|
+
loc: Location;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BlockStmt {
|
|
101
|
+
type: 'BlockStmt';
|
|
102
|
+
body: Statement[];
|
|
103
|
+
loc: Location;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Identifier {
|
|
107
|
+
type: 'Identifier';
|
|
108
|
+
name: string;
|
|
109
|
+
loc: Location;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface StringLiteral {
|
|
113
|
+
type: 'StringLiteral';
|
|
114
|
+
value: string;
|
|
115
|
+
loc: Location;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface NumberLiteral {
|
|
119
|
+
type: 'NumberLiteral';
|
|
120
|
+
value: number;
|
|
121
|
+
loc: Location;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface BooleanLiteral {
|
|
125
|
+
type: 'BooleanLiteral';
|
|
126
|
+
value: boolean;
|
|
127
|
+
loc: Location;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface NullLiteral {
|
|
131
|
+
type: 'NullLiteral';
|
|
132
|
+
loc: Location;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ArrayLiteral {
|
|
136
|
+
type: 'ArrayLiteral';
|
|
137
|
+
elements: Expression[];
|
|
138
|
+
loc: Location;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ObjectLiteral {
|
|
142
|
+
type: 'ObjectLiteral';
|
|
143
|
+
pairs: { key: string; value: Expression }[];
|
|
144
|
+
loc: Location;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface BinaryExpr {
|
|
148
|
+
type: 'BinaryExpr';
|
|
149
|
+
op: string;
|
|
150
|
+
left: Expression;
|
|
151
|
+
right: Expression;
|
|
152
|
+
loc: Location;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface UnaryExpr {
|
|
156
|
+
type: 'UnaryExpr';
|
|
157
|
+
op: string;
|
|
158
|
+
operand: Expression;
|
|
159
|
+
loc: Location;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface CallExpr {
|
|
163
|
+
type: 'CallExpr';
|
|
164
|
+
callee: Expression;
|
|
165
|
+
args: Expression[];
|
|
166
|
+
loc: Location;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface MemberExpr {
|
|
170
|
+
type: 'MemberExpr';
|
|
171
|
+
object: Expression;
|
|
172
|
+
property: Expression;
|
|
173
|
+
computed: boolean;
|
|
174
|
+
loc: Location;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface PipeExpr {
|
|
178
|
+
type: 'PipeExpr';
|
|
179
|
+
steps: Expression[];
|
|
180
|
+
loc: Location;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface DuoExpr {
|
|
184
|
+
type: 'DuoExpr';
|
|
185
|
+
input: Expression;
|
|
186
|
+
mikePipeline: Expression[];
|
|
187
|
+
elPipeline: Expression[];
|
|
188
|
+
loc: Location;
|
|
189
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { Lexer } from './lexer';
|
|
6
|
+
import { Parser } from './parser';
|
|
7
|
+
import { Semantic } from './semantic';
|
|
8
|
+
import { Generator } from './generator';
|
|
9
|
+
import { RTJError } from './diagnostics';
|
|
10
|
+
import { startRepl } from './repl';
|
|
11
|
+
|
|
12
|
+
function compile(source: string): string {
|
|
13
|
+
const lexer = new Lexer(source);
|
|
14
|
+
const tokens = lexer.tokenize();
|
|
15
|
+
const parser = new Parser(tokens);
|
|
16
|
+
const ast = parser.parse();
|
|
17
|
+
const semantic = new Semantic();
|
|
18
|
+
semantic.analyze(ast);
|
|
19
|
+
const gen = new Generator();
|
|
20
|
+
return gen.generate(ast);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveRuntimePath(): string {
|
|
24
|
+
// When running from dist/, runtime is at dist/runtime/rtj-core.js
|
|
25
|
+
// When running via ts-node, runtime is at src/runtime/rtj-core.ts
|
|
26
|
+
const distRuntime = path.join(__dirname, 'runtime', 'rtj-core');
|
|
27
|
+
return distRuntime;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run(filePath: string): void {
|
|
31
|
+
const absPath = path.resolve(filePath);
|
|
32
|
+
if (!fs.existsSync(absPath)) {
|
|
33
|
+
console.error(`File not found: ${absPath}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const source = fs.readFileSync(absPath, 'utf-8');
|
|
37
|
+
let js = compile(source);
|
|
38
|
+
|
|
39
|
+
// Replace the generic runtime require with the actual path
|
|
40
|
+
const runtimePath = resolveRuntimePath().replace(/\\/g, '/');
|
|
41
|
+
js = js.replace(
|
|
42
|
+
'const __rtj = require("./runtime/rtj-core");',
|
|
43
|
+
`const __rtj = require("${runtimePath}");`
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Write to temp file and execute
|
|
47
|
+
const tmpFile = path.join(os.tmpdir(), `rtj_${Date.now()}_${Math.random().toString(36).slice(2)}.js`);
|
|
48
|
+
fs.writeFileSync(tmpFile, js, 'utf-8');
|
|
49
|
+
try {
|
|
50
|
+
require(tmpFile);
|
|
51
|
+
} finally {
|
|
52
|
+
fs.unlinkSync(tmpFile);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function compileToFile(filePath: string): void {
|
|
57
|
+
const absPath = path.resolve(filePath);
|
|
58
|
+
if (!fs.existsSync(absPath)) {
|
|
59
|
+
console.error(`File not found: ${absPath}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const source = fs.readFileSync(absPath, 'utf-8');
|
|
63
|
+
const js = compile(source);
|
|
64
|
+
const outPath = absPath.replace(/\.rtj$/, '.js');
|
|
65
|
+
fs.writeFileSync(outPath, js, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function check(filePath: string): void {
|
|
69
|
+
const absPath = path.resolve(filePath);
|
|
70
|
+
if (!fs.existsSync(absPath)) {
|
|
71
|
+
console.error(`File not found: ${absPath}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const source = fs.readFileSync(absPath, 'utf-8');
|
|
75
|
+
compile(source); // throws on error
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const [,, command, ...args] = process.argv;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
switch (command) {
|
|
82
|
+
case 'run': {
|
|
83
|
+
if (!args[0]) {
|
|
84
|
+
console.error('Usage: rtj run <file.rtj>');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
run(args[0]);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'compile': {
|
|
91
|
+
if (!args[0]) {
|
|
92
|
+
console.error('Usage: rtj compile <file.rtj>');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
compileToFile(args[0]);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'check': {
|
|
99
|
+
if (!args[0]) {
|
|
100
|
+
console.error('Usage: rtj check <file.rtj>');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
check(args[0]);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'repl': {
|
|
107
|
+
startRepl();
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
default: {
|
|
111
|
+
console.log('Usage: rtj <run|compile|check|repl> [file]');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err instanceof RTJError) {
|
|
116
|
+
console.error(err.format());
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class RTJError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public kind: 'SyntaxError' | 'NameError' | 'ImportError' | 'DuoError' | 'RuntimeError',
|
|
4
|
+
message: string,
|
|
5
|
+
public line?: number,
|
|
6
|
+
public column?: number
|
|
7
|
+
) {
|
|
8
|
+
super(message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
format(): string {
|
|
12
|
+
const loc = this.line ? ` near line ${this.line}` : '';
|
|
13
|
+
return `${this.kind}: ${this.message}${loc}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/generator.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Program, Statement, Expression } from './ast';
|
|
2
|
+
|
|
3
|
+
export class Generator {
|
|
4
|
+
private indent: number = 0;
|
|
5
|
+
|
|
6
|
+
generate(program: Program): string {
|
|
7
|
+
const lines: string[] = [
|
|
8
|
+
'"use strict";',
|
|
9
|
+
'const __rtj = require("./runtime/rtj-core");',
|
|
10
|
+
'const __modules = __rtj.modules;',
|
|
11
|
+
'',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (const stmt of program.body) {
|
|
15
|
+
lines.push(this.genStatement(stmt));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return lines.join('\n') + '\n';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private pad(): string {
|
|
22
|
+
return ' '.repeat(this.indent);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private genStatement(stmt: Statement): string {
|
|
26
|
+
switch (stmt.type) {
|
|
27
|
+
case 'VarDecl':
|
|
28
|
+
return `${this.pad()}const ${stmt.name} = ${this.genExpression(stmt.init)};`;
|
|
29
|
+
case 'FunctionDecl': {
|
|
30
|
+
const params = stmt.params.join(', ');
|
|
31
|
+
const body = this.genBlock(stmt.body);
|
|
32
|
+
return `${this.pad()}function ${stmt.name}(${params}) ${body}`;
|
|
33
|
+
}
|
|
34
|
+
case 'ReturnStmt':
|
|
35
|
+
return stmt.value
|
|
36
|
+
? `${this.pad()}return ${this.genExpression(stmt.value)};`
|
|
37
|
+
: `${this.pad()}return;`;
|
|
38
|
+
case 'TalkStmt':
|
|
39
|
+
return `${this.pad()}__rtj.talk(${this.genExpression(stmt.value)});`;
|
|
40
|
+
case 'IfStmt': {
|
|
41
|
+
let result = `${this.pad()}if (${this.genExpression(stmt.condition)}) ${this.genBlock(stmt.consequent)}`;
|
|
42
|
+
if (stmt.alternate) {
|
|
43
|
+
result += ` else ${this.genBlock(stmt.alternate)}`;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
case 'LoopStmt':
|
|
48
|
+
return `${this.pad()}for (const ${stmt.variable} of ${this.genExpression(stmt.iterable)}) ${this.genBlock(stmt.body)}`;
|
|
49
|
+
case 'ImportStmt': {
|
|
50
|
+
const names = stmt.names.join(', ');
|
|
51
|
+
return `${this.pad()}const { ${names} } = __modules["${stmt.source}"];`;
|
|
52
|
+
}
|
|
53
|
+
case 'ThrowStmt':
|
|
54
|
+
return `${this.pad()}throw new Error(${this.genExpression(stmt.value)});`;
|
|
55
|
+
case 'ExpressionStmt':
|
|
56
|
+
return `${this.pad()}${this.genExpression(stmt.expr)};`;
|
|
57
|
+
case 'BlockStmt':
|
|
58
|
+
return this.genBlock(stmt);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private genBlock(block: { body: Statement[] }): string {
|
|
63
|
+
const lines: string[] = ['{'];
|
|
64
|
+
this.indent++;
|
|
65
|
+
for (const stmt of block.body) {
|
|
66
|
+
lines.push(this.genStatement(stmt));
|
|
67
|
+
}
|
|
68
|
+
this.indent--;
|
|
69
|
+
lines.push(`${this.pad()}}`);
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private genExpression(expr: Expression): string {
|
|
74
|
+
switch (expr.type) {
|
|
75
|
+
case 'Identifier':
|
|
76
|
+
return expr.name;
|
|
77
|
+
case 'StringLiteral':
|
|
78
|
+
return JSON.stringify(expr.value);
|
|
79
|
+
case 'NumberLiteral':
|
|
80
|
+
return String(expr.value);
|
|
81
|
+
case 'BooleanLiteral':
|
|
82
|
+
return String(expr.value);
|
|
83
|
+
case 'NullLiteral':
|
|
84
|
+
return 'null';
|
|
85
|
+
case 'ArrayLiteral':
|
|
86
|
+
return `[${expr.elements.map(e => this.genExpression(e)).join(', ')}]`;
|
|
87
|
+
case 'ObjectLiteral': {
|
|
88
|
+
const pairs = expr.pairs.map(p => `${JSON.stringify(p.key)}: ${this.genExpression(p.value)}`);
|
|
89
|
+
return `{ ${pairs.join(', ')} }`;
|
|
90
|
+
}
|
|
91
|
+
case 'BinaryExpr':
|
|
92
|
+
return `(${this.genExpression(expr.left)} ${expr.op} ${this.genExpression(expr.right)})`;
|
|
93
|
+
case 'UnaryExpr':
|
|
94
|
+
return `(${expr.op}${this.genExpression(expr.operand)})`;
|
|
95
|
+
case 'CallExpr': {
|
|
96
|
+
const callee = this.genExpression(expr.callee);
|
|
97
|
+
const args = expr.args.map(a => this.genExpression(a)).join(', ');
|
|
98
|
+
return `${callee}(${args})`;
|
|
99
|
+
}
|
|
100
|
+
case 'MemberExpr':
|
|
101
|
+
if (expr.computed) {
|
|
102
|
+
return `${this.genExpression(expr.object)}[${this.genExpression(expr.property)}]`;
|
|
103
|
+
}
|
|
104
|
+
return `${this.genExpression(expr.object)}.${this.genExpression(expr.property)}`;
|
|
105
|
+
case 'PipeExpr':
|
|
106
|
+
return this.flattenPipe(expr.steps);
|
|
107
|
+
case 'DuoExpr': {
|
|
108
|
+
const allSteps = [...expr.mikePipeline, ...expr.elPipeline];
|
|
109
|
+
const initial = this.genExpression(expr.input);
|
|
110
|
+
return this.flattenPipeFromStrings(initial, allSteps);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private flattenPipe(steps: Expression[]): string {
|
|
116
|
+
if (steps.length === 0) return '';
|
|
117
|
+
if (steps.length === 1) return this.genExpression(steps[0]);
|
|
118
|
+
const initial = this.genExpression(steps[0]);
|
|
119
|
+
return this.flattenPipeFromStrings(initial, steps.slice(1));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private flattenPipeFromStrings(initial: string, fns: Expression[]): string {
|
|
123
|
+
let result = initial;
|
|
124
|
+
for (const fn of fns) {
|
|
125
|
+
result = `${this.genExpression(fn)}(${result})`;
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Lexer } from './lexer';
|
|
2
|
+
export { Parser } from './parser';
|
|
3
|
+
export { Semantic } from './semantic';
|
|
4
|
+
export { Generator } from './generator';
|
|
5
|
+
export { RTJError } from './diagnostics';
|
|
6
|
+
export { TokenType } from './token';
|
|
7
|
+
export type { Token } from './token';
|
package/src/lexer.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Token, TokenType, KEYWORDS } from './token';
|
|
2
|
+
import { RTJError } from './diagnostics';
|
|
3
|
+
|
|
4
|
+
export class Lexer {
|
|
5
|
+
private source: string;
|
|
6
|
+
private pos: number = 0;
|
|
7
|
+
private line: number = 1;
|
|
8
|
+
private col: number = 1;
|
|
9
|
+
|
|
10
|
+
constructor(source: string) {
|
|
11
|
+
this.source = source;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
tokenize(): Token[] {
|
|
15
|
+
const tokens: Token[] = [];
|
|
16
|
+
|
|
17
|
+
while (this.pos < this.source.length) {
|
|
18
|
+
const ch = this.source[this.pos];
|
|
19
|
+
|
|
20
|
+
// Skip whitespace (not newlines)
|
|
21
|
+
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
22
|
+
this.advance();
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Newlines
|
|
27
|
+
if (ch === '\n') {
|
|
28
|
+
this.advance();
|
|
29
|
+
this.line++;
|
|
30
|
+
this.col = 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Line comments
|
|
35
|
+
if (ch === '/' && this.peek(1) === '/') {
|
|
36
|
+
while (this.pos < this.source.length && this.source[this.pos] !== '\n') {
|
|
37
|
+
this.advance();
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Block comments
|
|
43
|
+
if (ch === '/' && this.peek(1) === '*') {
|
|
44
|
+
this.advance();
|
|
45
|
+
this.advance();
|
|
46
|
+
while (this.pos < this.source.length) {
|
|
47
|
+
if (this.source[this.pos] === '*' && this.peek(1) === '/') {
|
|
48
|
+
this.advance();
|
|
49
|
+
this.advance();
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (this.source[this.pos] === '\n') {
|
|
53
|
+
this.line++;
|
|
54
|
+
this.col = 0;
|
|
55
|
+
}
|
|
56
|
+
this.advance();
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Strings
|
|
62
|
+
if (ch === '"' || ch === "'") {
|
|
63
|
+
tokens.push(this.readString(ch));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Numbers
|
|
68
|
+
if (this.isDigit(ch)) {
|
|
69
|
+
tokens.push(this.readNumber());
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Identifiers / keywords
|
|
74
|
+
if (this.isAlpha(ch)) {
|
|
75
|
+
tokens.push(this.readIdentifier());
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Pipe operator |>
|
|
80
|
+
if (ch === '|' && this.peek(1) === '>') {
|
|
81
|
+
tokens.push(this.makeToken(TokenType.PIPE, '|>', 2));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Two-char operators
|
|
86
|
+
if (ch === '=' && this.peek(1) === '=') { tokens.push(this.makeToken(TokenType.EQ_EQ, '==', 2)); continue; }
|
|
87
|
+
if (ch === '!' && this.peek(1) === '=') { tokens.push(this.makeToken(TokenType.BANG_EQ, '!=', 2)); continue; }
|
|
88
|
+
if (ch === '>' && this.peek(1) === '=') { tokens.push(this.makeToken(TokenType.GT_EQ, '>=', 2)); continue; }
|
|
89
|
+
if (ch === '<' && this.peek(1) === '=') { tokens.push(this.makeToken(TokenType.LT_EQ, '<=', 2)); continue; }
|
|
90
|
+
if (ch === '&' && this.peek(1) === '&') { tokens.push(this.makeToken(TokenType.AND_AND, '&&', 2)); continue; }
|
|
91
|
+
if (ch === '|' && this.peek(1) === '|') { tokens.push(this.makeToken(TokenType.OR_OR, '||', 2)); continue; }
|
|
92
|
+
|
|
93
|
+
// Single-char tokens
|
|
94
|
+
const singles: Record<string, TokenType> = {
|
|
95
|
+
'+': TokenType.PLUS,
|
|
96
|
+
'-': TokenType.MINUS,
|
|
97
|
+
'*': TokenType.STAR,
|
|
98
|
+
'/': TokenType.SLASH,
|
|
99
|
+
'%': TokenType.PERCENT,
|
|
100
|
+
'>': TokenType.GT,
|
|
101
|
+
'<': TokenType.LT,
|
|
102
|
+
'!': TokenType.BANG,
|
|
103
|
+
'=': TokenType.ASSIGN,
|
|
104
|
+
'{': TokenType.LBRACE,
|
|
105
|
+
'}': TokenType.RBRACE,
|
|
106
|
+
'(': TokenType.LPAREN,
|
|
107
|
+
')': TokenType.RPAREN,
|
|
108
|
+
'[': TokenType.LBRACKET,
|
|
109
|
+
']': TokenType.RBRACKET,
|
|
110
|
+
',': TokenType.COMMA,
|
|
111
|
+
':': TokenType.COLON,
|
|
112
|
+
'.': TokenType.DOT,
|
|
113
|
+
';': TokenType.SEMICOLON,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (singles[ch]) {
|
|
117
|
+
tokens.push(this.makeToken(singles[ch], ch, 1));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new RTJError('SyntaxError', `unexpected character '${ch}'`, this.line, this.col);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
tokens.push({ type: TokenType.EOF, value: '', line: this.line, column: this.col });
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private advance(): void {
|
|
129
|
+
this.pos++;
|
|
130
|
+
this.col++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private peek(offset: number): string | undefined {
|
|
134
|
+
return this.source[this.pos + offset];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private makeToken(type: TokenType, value: string, length: number): Token {
|
|
138
|
+
const token: Token = { type, value, line: this.line, column: this.col };
|
|
139
|
+
for (let i = 0; i < length; i++) this.advance();
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private isDigit(ch: string): boolean {
|
|
144
|
+
return ch >= '0' && ch <= '9';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private isAlpha(ch: string): boolean {
|
|
148
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private isAlphaNumeric(ch: string): boolean {
|
|
152
|
+
return this.isAlpha(ch) || this.isDigit(ch);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private readString(quote: string): Token {
|
|
156
|
+
const startCol = this.col;
|
|
157
|
+
this.advance(); // skip opening quote
|
|
158
|
+
let value = '';
|
|
159
|
+
while (this.pos < this.source.length && this.source[this.pos] !== quote) {
|
|
160
|
+
if (this.source[this.pos] === '\\') {
|
|
161
|
+
this.advance();
|
|
162
|
+
const esc = this.source[this.pos];
|
|
163
|
+
if (esc === 'n') value += '\n';
|
|
164
|
+
else if (esc === 't') value += '\t';
|
|
165
|
+
else if (esc === '\\') value += '\\';
|
|
166
|
+
else if (esc === quote) value += quote;
|
|
167
|
+
else value += '\\' + esc;
|
|
168
|
+
} else {
|
|
169
|
+
value += this.source[this.pos];
|
|
170
|
+
}
|
|
171
|
+
this.advance();
|
|
172
|
+
}
|
|
173
|
+
if (this.pos >= this.source.length) {
|
|
174
|
+
throw new RTJError('SyntaxError', 'unterminated string', this.line, startCol);
|
|
175
|
+
}
|
|
176
|
+
this.advance(); // skip closing quote
|
|
177
|
+
return { type: TokenType.STRING, value, line: this.line, column: startCol };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private readNumber(): Token {
|
|
181
|
+
const startCol = this.col;
|
|
182
|
+
let value = '';
|
|
183
|
+
while (this.pos < this.source.length && this.isDigit(this.source[this.pos])) {
|
|
184
|
+
value += this.source[this.pos];
|
|
185
|
+
this.advance();
|
|
186
|
+
}
|
|
187
|
+
if (this.pos < this.source.length && this.source[this.pos] === '.' && this.peek(1) && this.isDigit(this.peek(1)!)) {
|
|
188
|
+
value += '.';
|
|
189
|
+
this.advance();
|
|
190
|
+
while (this.pos < this.source.length && this.isDigit(this.source[this.pos])) {
|
|
191
|
+
value += this.source[this.pos];
|
|
192
|
+
this.advance();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { type: TokenType.NUMBER, value, line: this.line, column: startCol };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private readIdentifier(): Token {
|
|
199
|
+
const startCol = this.col;
|
|
200
|
+
let value = '';
|
|
201
|
+
while (this.pos < this.source.length && this.isAlphaNumeric(this.source[this.pos])) {
|
|
202
|
+
value += this.source[this.pos];
|
|
203
|
+
this.advance();
|
|
204
|
+
}
|
|
205
|
+
const type = KEYWORDS[value] || TokenType.IDENTIFIER;
|
|
206
|
+
return { type, value, line: this.line, column: startCol };
|
|
207
|
+
}
|
|
208
|
+
}
|