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.
Files changed (60) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/PROMPTS/01-build-v0.1.md +10 -0
  3. package/README.md +186 -0
  4. package/dist/ast.d.ts +143 -0
  5. package/dist/ast.js +2 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +145 -0
  8. package/dist/diagnostics.d.ts +7 -0
  9. package/dist/diagnostics.js +16 -0
  10. package/dist/generator.d.ts +11 -0
  11. package/dist/generator.js +126 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.js +15 -0
  14. package/dist/lexer.d.ts +18 -0
  15. package/dist/lexer.js +210 -0
  16. package/dist/parser.d.ts +40 -0
  17. package/dist/parser.js +394 -0
  18. package/dist/repl.d.ts +1 -0
  19. package/dist/repl.js +132 -0
  20. package/dist/runtime/atl-data.d.ts +4 -0
  21. package/dist/runtime/atl-data.js +18 -0
  22. package/dist/runtime/atl-flow.d.ts +1 -0
  23. package/dist/runtime/atl-flow.js +5 -0
  24. package/dist/runtime/bk-parse.d.ts +3 -0
  25. package/dist/runtime/bk-parse.js +9 -0
  26. package/dist/runtime/bk-text.d.ts +5 -0
  27. package/dist/runtime/bk-text.js +13 -0
  28. package/dist/runtime/rtj-core.d.ts +1 -0
  29. package/dist/runtime/rtj-core.js +51 -0
  30. package/dist/semantic.d.ts +11 -0
  31. package/dist/semantic.js +153 -0
  32. package/dist/tests/basic.test.d.ts +1 -0
  33. package/dist/tests/basic.test.js +69 -0
  34. package/dist/token.d.ts +56 -0
  35. package/dist/token.js +77 -0
  36. package/examples/cities.rtj +11 -0
  37. package/examples/count-words.rtj +12 -0
  38. package/examples/duo.rtj +12 -0
  39. package/examples/hello.rtj +1 -0
  40. package/examples/pipes.rtj +6 -0
  41. package/package.json +22 -0
  42. package/public/_redirects +1 -0
  43. package/public/index.html +559 -0
  44. package/src/ast.ts +189 -0
  45. package/src/cli.ts +120 -0
  46. package/src/diagnostics.ts +15 -0
  47. package/src/generator.ts +129 -0
  48. package/src/index.ts +7 -0
  49. package/src/lexer.ts +208 -0
  50. package/src/parser.ts +461 -0
  51. package/src/repl.ts +105 -0
  52. package/src/runtime/atl-data.ts +11 -0
  53. package/src/runtime/atl-flow.ts +1 -0
  54. package/src/runtime/bk-parse.ts +3 -0
  55. package/src/runtime/bk-text.ts +5 -0
  56. package/src/runtime/rtj-core.ts +21 -0
  57. package/src/semantic.ts +144 -0
  58. package/src/tests/basic.test.ts +74 -0
  59. package/src/token.ts +85 -0
  60. 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
+ }
@@ -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
+ }