ga-lang 1.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/.github/workflows/test.yml +35 -0
- package/.pnpmrc +1 -0
- package/.prettierrc.json +8 -0
- package/bin/ga.js +3 -0
- package/dist/src/ast.js +38 -0
- package/dist/src/interpreter.js +35 -0
- package/dist/src/lexer.js +187 -0
- package/dist/src/main.js +27 -0
- package/dist/src/parser.js +77 -0
- package/dist/src/token.js +56 -0
- package/examples/init.ga +2 -0
- package/package.json +50 -0
- package/src/ast.ts +69 -0
- package/src/interpreter.ts +53 -0
- package/src/lexer.ts +202 -0
- package/src/main.ts +34 -0
- package/src/parser.ts +99 -0
- package/src/token.ts +62 -0
- package/tests/lexer.test.ts +88 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Node.js Test CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ['main']
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ['main']
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [22.x]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install pnpm
|
|
20
|
+
uses: pnpm/action-setup@v4
|
|
21
|
+
|
|
22
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
23
|
+
uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: ${{ matrix.node-version }}
|
|
26
|
+
cache: 'pnpm'
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: pnpm install --frozen-lockfile
|
|
30
|
+
|
|
31
|
+
- name: Build
|
|
32
|
+
run: pnpm build
|
|
33
|
+
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: pnpm test
|
package/.pnpmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
engine-strict=true
|
package/.prettierrc.json
ADDED
package/bin/ga.js
ADDED
package/dist/src/ast.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class VariableExpr {
|
|
2
|
+
name;
|
|
3
|
+
constructor(name) {
|
|
4
|
+
this.name = name;
|
|
5
|
+
}
|
|
6
|
+
accept(visitor) {
|
|
7
|
+
return visitor.visitVariableExpr(this);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class LiteralExpr {
|
|
11
|
+
value;
|
|
12
|
+
constructor(value) {
|
|
13
|
+
this.value = value;
|
|
14
|
+
}
|
|
15
|
+
accept(visitor) {
|
|
16
|
+
return visitor.visitLiteralExpr(this);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class PrintStmt {
|
|
20
|
+
expression;
|
|
21
|
+
constructor(expression) {
|
|
22
|
+
this.expression = expression;
|
|
23
|
+
}
|
|
24
|
+
accept(visitor) {
|
|
25
|
+
return visitor.visitPrintStmt(this);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class VarStmt {
|
|
29
|
+
name;
|
|
30
|
+
initializer;
|
|
31
|
+
constructor(name, initializer) {
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.initializer = initializer;
|
|
34
|
+
}
|
|
35
|
+
accept(visitor) {
|
|
36
|
+
return visitor.visitVarStmt(this);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class Interpreter {
|
|
2
|
+
interpret(statements) {
|
|
3
|
+
for (const statement of statements) {
|
|
4
|
+
this.execute(statement);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
execute(stmt) {
|
|
8
|
+
stmt.accept(this);
|
|
9
|
+
}
|
|
10
|
+
/*
|
|
11
|
+
* Process print statement
|
|
12
|
+
*/
|
|
13
|
+
visitPrintStmt(stmt) {
|
|
14
|
+
const value = this.evaluate(stmt.expression);
|
|
15
|
+
console.log(value);
|
|
16
|
+
}
|
|
17
|
+
visitVarStmt(stmt) {
|
|
18
|
+
// TODO: yet to implement
|
|
19
|
+
}
|
|
20
|
+
visitLiteralExpr(expr) {
|
|
21
|
+
if (typeof expr.value === 'string' && expr.value.startsWith('"')) {
|
|
22
|
+
return expr.value.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
return expr.value;
|
|
25
|
+
}
|
|
26
|
+
visitVariableExpr(expr) {
|
|
27
|
+
// TODO: yet to implement
|
|
28
|
+
}
|
|
29
|
+
/*
|
|
30
|
+
* Evaluate an expression
|
|
31
|
+
*/
|
|
32
|
+
evaluate(expr) {
|
|
33
|
+
return expr.accept(this);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { keywords, Token, TokenKind } from './token.js';
|
|
2
|
+
export class Lexer {
|
|
3
|
+
source;
|
|
4
|
+
tokens;
|
|
5
|
+
startPos;
|
|
6
|
+
currentPos;
|
|
7
|
+
line;
|
|
8
|
+
constructor(source) {
|
|
9
|
+
this.source = source;
|
|
10
|
+
this.tokens = [];
|
|
11
|
+
this.startPos = 0;
|
|
12
|
+
this.currentPos = 0;
|
|
13
|
+
this.line = 1;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read tokens from source
|
|
17
|
+
*/
|
|
18
|
+
readTokens() {
|
|
19
|
+
while (!this.isAtEnd()) {
|
|
20
|
+
this.startPos = this.currentPos;
|
|
21
|
+
this.readToken();
|
|
22
|
+
}
|
|
23
|
+
this.tokens.push(new Token(TokenKind.Eof, '', this.line));
|
|
24
|
+
return this.tokens;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read individual lexeme
|
|
28
|
+
*/
|
|
29
|
+
readToken() {
|
|
30
|
+
const char = this.readChar();
|
|
31
|
+
switch (char) {
|
|
32
|
+
case ',':
|
|
33
|
+
this.createToken(TokenKind.Comma);
|
|
34
|
+
break;
|
|
35
|
+
case ':':
|
|
36
|
+
this.createToken(TokenKind.Colon);
|
|
37
|
+
break;
|
|
38
|
+
case '.':
|
|
39
|
+
this.createToken(TokenKind.Period);
|
|
40
|
+
break;
|
|
41
|
+
case ';':
|
|
42
|
+
this.createToken(TokenKind.Semicolon);
|
|
43
|
+
break;
|
|
44
|
+
case '(':
|
|
45
|
+
this.createToken(TokenKind.OpenParen);
|
|
46
|
+
break;
|
|
47
|
+
case ')':
|
|
48
|
+
this.createToken(TokenKind.CloseParen);
|
|
49
|
+
break;
|
|
50
|
+
case '{':
|
|
51
|
+
this.createToken(TokenKind.OpenCurly);
|
|
52
|
+
break;
|
|
53
|
+
case '}':
|
|
54
|
+
this.createToken(TokenKind.CloseCurly);
|
|
55
|
+
break;
|
|
56
|
+
case '+':
|
|
57
|
+
this.createToken(TokenKind.Plus);
|
|
58
|
+
break;
|
|
59
|
+
case '-':
|
|
60
|
+
this.createToken(TokenKind.Minus);
|
|
61
|
+
break;
|
|
62
|
+
case '*':
|
|
63
|
+
this.createToken(TokenKind.Star);
|
|
64
|
+
break;
|
|
65
|
+
case '/':
|
|
66
|
+
this.createToken(TokenKind.Slash);
|
|
67
|
+
break;
|
|
68
|
+
case '%':
|
|
69
|
+
this.createToken(TokenKind.Mod);
|
|
70
|
+
break;
|
|
71
|
+
case '!':
|
|
72
|
+
this.createToken(TokenKind.Bang);
|
|
73
|
+
break;
|
|
74
|
+
case '=':
|
|
75
|
+
this.createToken(TokenKind.Equal);
|
|
76
|
+
break;
|
|
77
|
+
/* Handle whitespaces */
|
|
78
|
+
case ' ':
|
|
79
|
+
case '\r':
|
|
80
|
+
case '\t':
|
|
81
|
+
break;
|
|
82
|
+
case '\n':
|
|
83
|
+
this.line++;
|
|
84
|
+
break;
|
|
85
|
+
case '"':
|
|
86
|
+
this.readDevanagariString();
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
if (this.isDevnagariChar(char)) {
|
|
90
|
+
if (this.isDevanagariDigit(char))
|
|
91
|
+
this.readDevanagariDigit();
|
|
92
|
+
else
|
|
93
|
+
this.readDevanagariIdentifier();
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read character
|
|
100
|
+
*/
|
|
101
|
+
readChar() {
|
|
102
|
+
if (this.isAtEnd())
|
|
103
|
+
return '\0';
|
|
104
|
+
return this.source.charAt(this.currentPos++);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Create token out of individual lexeme
|
|
108
|
+
* @param {TokenKind} kind
|
|
109
|
+
* @param {any} literal
|
|
110
|
+
*/
|
|
111
|
+
createToken(kind, literal) {
|
|
112
|
+
if (literal === undefined)
|
|
113
|
+
literal = null;
|
|
114
|
+
const text = this.source.substring(this.startPos, this.currentPos);
|
|
115
|
+
this.tokens.push(new Token(kind, text, this.line));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if we've reached the end of file
|
|
119
|
+
*/
|
|
120
|
+
isAtEnd() {
|
|
121
|
+
return this.currentPos >= this.source.length;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Peek next character
|
|
125
|
+
*/
|
|
126
|
+
peekNextChar() {
|
|
127
|
+
if (this.isAtEnd())
|
|
128
|
+
return '\0';
|
|
129
|
+
return this.source.charAt(this.currentPos);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get next character
|
|
133
|
+
*/
|
|
134
|
+
getNextChar() {
|
|
135
|
+
return this.source.charAt(this.currentPos++);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check if the character is devanagari character
|
|
139
|
+
* @param {string} char
|
|
140
|
+
*/
|
|
141
|
+
isDevnagariChar(char) {
|
|
142
|
+
return (('\u{0900}' <= char && char <= '\u{097F}') ||
|
|
143
|
+
('\u{A8E0}' <= char && char <= '\u{A8FF}'));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if the character is devanagari digit
|
|
147
|
+
* @param {string} char
|
|
148
|
+
*/
|
|
149
|
+
isDevanagariDigit(char) {
|
|
150
|
+
return '\u{0966}' <= char && char <= '\u{096F}';
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Read devanagari character sequence as number
|
|
154
|
+
*/
|
|
155
|
+
readDevanagariDigit() {
|
|
156
|
+
while (this.isDevanagariDigit(this.peekNextChar()))
|
|
157
|
+
this.getNextChar();
|
|
158
|
+
const literal = this.source.substring(this.startPos, this.currentPos);
|
|
159
|
+
this.createToken(TokenKind.Number, literal);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Read devanagari character sequence as identifier
|
|
163
|
+
*/
|
|
164
|
+
readDevanagariIdentifier() {
|
|
165
|
+
while (this.isDevnagariChar(this.peekNextChar()))
|
|
166
|
+
this.getNextChar();
|
|
167
|
+
const literal = this.source.substring(this.startPos, this.currentPos);
|
|
168
|
+
const keywordKind = keywords[literal];
|
|
169
|
+
if (keywordKind)
|
|
170
|
+
this.createToken(keywordKind, literal);
|
|
171
|
+
else
|
|
172
|
+
this.createToken(TokenKind.Identifier, literal);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read string literals
|
|
176
|
+
*/
|
|
177
|
+
readDevanagariString() {
|
|
178
|
+
while (this.peekNextChar() != '"' && !this.isAtEnd()) {
|
|
179
|
+
if (this.peekNextChar() === '\n')
|
|
180
|
+
this.line++;
|
|
181
|
+
this.getNextChar();
|
|
182
|
+
}
|
|
183
|
+
this.getNextChar();
|
|
184
|
+
const content = this.source.substring(this.startPos + 1, this.currentPos - 1);
|
|
185
|
+
this.createToken(TokenKind.String, content);
|
|
186
|
+
}
|
|
187
|
+
}
|
package/dist/src/main.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { Lexer } from './lexer.js';
|
|
3
|
+
import { Parser } from './parser.js';
|
|
4
|
+
import { Interpreter } from './interpreter.js';
|
|
5
|
+
function getFileNameFromArgs(args) {
|
|
6
|
+
if (args.length > 1) {
|
|
7
|
+
console.log('> Too many arguments.');
|
|
8
|
+
}
|
|
9
|
+
const fileName = args[0] || null;
|
|
10
|
+
return fileName;
|
|
11
|
+
}
|
|
12
|
+
function execFile(path) {
|
|
13
|
+
const source = readFileSync(path, 'utf-8');
|
|
14
|
+
const lexer = new Lexer(source);
|
|
15
|
+
const tokens = lexer.readTokens();
|
|
16
|
+
const parser = new Parser(tokens);
|
|
17
|
+
const stmts = parser.parse();
|
|
18
|
+
const interpreter = new Interpreter();
|
|
19
|
+
interpreter.interpret(stmts);
|
|
20
|
+
}
|
|
21
|
+
function main() {
|
|
22
|
+
const fileName = getFileNameFromArgs(process.argv.slice(2));
|
|
23
|
+
if (fileName !== null) {
|
|
24
|
+
execFile(fileName);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
main();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { LiteralExpr, PrintStmt, VariableExpr } from './ast.js';
|
|
2
|
+
import { TokenKind } from './token.js';
|
|
3
|
+
export class Parser {
|
|
4
|
+
tokens;
|
|
5
|
+
currentPos;
|
|
6
|
+
constructor(tokens) {
|
|
7
|
+
this.tokens = tokens;
|
|
8
|
+
this.currentPos = 0;
|
|
9
|
+
}
|
|
10
|
+
parse() {
|
|
11
|
+
const stmts = [];
|
|
12
|
+
while (!this.isAtEnd()) {
|
|
13
|
+
stmts.push(this.parseStmt());
|
|
14
|
+
}
|
|
15
|
+
return stmts;
|
|
16
|
+
}
|
|
17
|
+
parseStmt() {
|
|
18
|
+
if (this.match(TokenKind.Print)) {
|
|
19
|
+
return this.parsePrintStmt();
|
|
20
|
+
}
|
|
21
|
+
throw this.error(this.peek(), 'Expression expected.');
|
|
22
|
+
}
|
|
23
|
+
parsePrintStmt() {
|
|
24
|
+
this.consume(TokenKind.OpenParen, "Expect '(' after 'छाप'");
|
|
25
|
+
const expr = this.parseExpression();
|
|
26
|
+
this.consume(TokenKind.CloseParen, "Expect ')' after expression");
|
|
27
|
+
return new PrintStmt(expr);
|
|
28
|
+
}
|
|
29
|
+
parseExpression() {
|
|
30
|
+
return this.parsePrimary();
|
|
31
|
+
}
|
|
32
|
+
parsePrimary() {
|
|
33
|
+
if (this.match(TokenKind.String, TokenKind.Number)) {
|
|
34
|
+
return new LiteralExpr(this.previous().lexeme);
|
|
35
|
+
}
|
|
36
|
+
if (this.match(TokenKind.Identifier)) {
|
|
37
|
+
return new VariableExpr(this.previous());
|
|
38
|
+
}
|
|
39
|
+
throw this.error(this.peek(), 'Expression expected.');
|
|
40
|
+
}
|
|
41
|
+
match(...kinds) {
|
|
42
|
+
for (const kind of kinds) {
|
|
43
|
+
if (this.check(kind)) {
|
|
44
|
+
this.advance();
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
consume(kind, message) {
|
|
51
|
+
if (this.check(kind))
|
|
52
|
+
return this.advance();
|
|
53
|
+
throw this.error(this.peek(), message);
|
|
54
|
+
}
|
|
55
|
+
check(kind) {
|
|
56
|
+
if (this.isAtEnd())
|
|
57
|
+
return false;
|
|
58
|
+
return this.peek().kind === kind;
|
|
59
|
+
}
|
|
60
|
+
advance() {
|
|
61
|
+
if (!this.isAtEnd())
|
|
62
|
+
this.currentPos++;
|
|
63
|
+
return this.previous();
|
|
64
|
+
}
|
|
65
|
+
peek() {
|
|
66
|
+
return this.tokens[this.currentPos];
|
|
67
|
+
}
|
|
68
|
+
previous() {
|
|
69
|
+
return this.tokens[this.currentPos - 1];
|
|
70
|
+
}
|
|
71
|
+
isAtEnd() {
|
|
72
|
+
return this.peek().kind === TokenKind.Eof;
|
|
73
|
+
}
|
|
74
|
+
error(token, message) {
|
|
75
|
+
return new Error(`> [line ${token.line}] Error: ${message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export var TokenKind;
|
|
2
|
+
(function (TokenKind) {
|
|
3
|
+
TokenKind["Eof"] = "Eof";
|
|
4
|
+
TokenKind["Illegal"] = "Illegal";
|
|
5
|
+
/* -- Punctuation/Delimiters -- */
|
|
6
|
+
TokenKind["Comma"] = "Comma";
|
|
7
|
+
TokenKind["Colon"] = "Colon";
|
|
8
|
+
TokenKind["Period"] = "Period";
|
|
9
|
+
TokenKind["Semicolon"] = "Semicolon";
|
|
10
|
+
TokenKind["OpenParen"] = "OpenParen";
|
|
11
|
+
TokenKind["CloseParen"] = "CloseParen";
|
|
12
|
+
TokenKind["OpenCurly"] = "OpenCurly";
|
|
13
|
+
TokenKind["CloseCurly"] = "CloseCurly";
|
|
14
|
+
/* -- Operators -- */
|
|
15
|
+
TokenKind["Plus"] = "Plus";
|
|
16
|
+
TokenKind["Minus"] = "Minus";
|
|
17
|
+
TokenKind["Star"] = "Star";
|
|
18
|
+
TokenKind["Slash"] = "Slash";
|
|
19
|
+
TokenKind["Mod"] = "Mod";
|
|
20
|
+
TokenKind["Bang"] = "Bang";
|
|
21
|
+
TokenKind["Equal"] = "Equal";
|
|
22
|
+
/* -- Literals -- */
|
|
23
|
+
TokenKind["Number"] = "Number";
|
|
24
|
+
TokenKind["Character"] = "Character";
|
|
25
|
+
TokenKind["String"] = "String";
|
|
26
|
+
TokenKind["Identifier"] = "Identifier";
|
|
27
|
+
/* -- Keywords -- */
|
|
28
|
+
TokenKind["Let"] = "Let";
|
|
29
|
+
TokenKind["Function"] = "Function";
|
|
30
|
+
TokenKind["Print"] = "Print";
|
|
31
|
+
TokenKind["Return"] = "Return";
|
|
32
|
+
TokenKind["If"] = "If";
|
|
33
|
+
TokenKind["Else"] = "Else";
|
|
34
|
+
TokenKind["True"] = "True";
|
|
35
|
+
TokenKind["False"] = "False";
|
|
36
|
+
})(TokenKind || (TokenKind = {}));
|
|
37
|
+
export const keywords = {
|
|
38
|
+
मानौ: TokenKind.Let,
|
|
39
|
+
कार्य: TokenKind.Function,
|
|
40
|
+
छाप: TokenKind.Print,
|
|
41
|
+
यदि: TokenKind.If,
|
|
42
|
+
नभए: TokenKind.Else,
|
|
43
|
+
फिर्ता: TokenKind.Return,
|
|
44
|
+
सत्य: TokenKind.True,
|
|
45
|
+
असत्य: TokenKind.False
|
|
46
|
+
};
|
|
47
|
+
export class Token {
|
|
48
|
+
kind;
|
|
49
|
+
lexeme;
|
|
50
|
+
line;
|
|
51
|
+
constructor(kind, lexeme, line) {
|
|
52
|
+
this.kind = kind;
|
|
53
|
+
this.lexeme = lexeme;
|
|
54
|
+
this.line = line;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/examples/init.ga
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ga-lang",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "An interpreted toy programming language",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/main.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ga": "./bin/ga.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prettier:check": "prettier --check \"./**/*.{js,ts,json}\"",
|
|
12
|
+
"prettier:fix": "prettier --write \"./**/*.{js,ts,json}\"",
|
|
13
|
+
"clean": "rm -rf dist",
|
|
14
|
+
"build": "pnpm run clean && tsc",
|
|
15
|
+
"test": "pnpm run build && node --test ./dist/tests/**/*.js",
|
|
16
|
+
"ga": "pnpm run build && node dist/src/main.js"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/x-incubator/ga.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"devanagari",
|
|
24
|
+
"nepali",
|
|
25
|
+
"nepali-programming-language",
|
|
26
|
+
"programming-language"
|
|
27
|
+
],
|
|
28
|
+
"author": "Basanta Rai",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/x-incubator/ga/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/x-incubator/ga#readme",
|
|
34
|
+
"packageManager": "pnpm@10.25.0",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.x.x",
|
|
37
|
+
"pnpm": ">=10.x.x"
|
|
38
|
+
},
|
|
39
|
+
"devEngines": {
|
|
40
|
+
"runtime": {
|
|
41
|
+
"name": "node",
|
|
42
|
+
"onFail": "error"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^25.0.3",
|
|
47
|
+
"prettier": "^3.7.4",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/ast.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Token } from './token.js';
|
|
2
|
+
|
|
3
|
+
export interface Expr {
|
|
4
|
+
accept<T>(visitor: ExprVisitor<T>): T;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ExprVisitor<T> {
|
|
8
|
+
visitVariableExpr(expr: VariableExpr): T;
|
|
9
|
+
visitLiteralExpr(expr: LiteralExpr): T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Stmt {
|
|
13
|
+
accept<T>(visitor: StmtVisitor<T>): T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StmtVisitor<T> {
|
|
17
|
+
visitPrintStmt(stmt: PrintStmt): T;
|
|
18
|
+
visitVarStmt(stmt: VarStmt): T;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class VariableExpr implements Expr {
|
|
22
|
+
name: Token;
|
|
23
|
+
|
|
24
|
+
constructor(name: Token) {
|
|
25
|
+
this.name = name;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
accept<T>(visitor: ExprVisitor<T>): T {
|
|
29
|
+
return visitor.visitVariableExpr(this);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class LiteralExpr implements Expr {
|
|
34
|
+
value: any;
|
|
35
|
+
|
|
36
|
+
constructor(value: any) {
|
|
37
|
+
this.value = value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
accept<T>(visitor: ExprVisitor<T>): T {
|
|
41
|
+
return visitor.visitLiteralExpr(this);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class PrintStmt implements Stmt {
|
|
46
|
+
expression: Expr;
|
|
47
|
+
|
|
48
|
+
constructor(expression: Expr) {
|
|
49
|
+
this.expression = expression;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
accept<T>(visitor: StmtVisitor<T>): T {
|
|
53
|
+
return visitor.visitPrintStmt(this);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class VarStmt implements Stmt {
|
|
58
|
+
name: Token;
|
|
59
|
+
initializer: Expr | null;
|
|
60
|
+
|
|
61
|
+
constructor(name: Token, initializer: Expr | null) {
|
|
62
|
+
this.name = name;
|
|
63
|
+
this.initializer = initializer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
accept<T>(visitor: StmtVisitor<T>): T {
|
|
67
|
+
return visitor.visitVarStmt(this);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Expr,
|
|
3
|
+
ExprVisitor,
|
|
4
|
+
LiteralExpr,
|
|
5
|
+
PrintStmt,
|
|
6
|
+
Stmt,
|
|
7
|
+
StmtVisitor,
|
|
8
|
+
VariableExpr,
|
|
9
|
+
VarStmt
|
|
10
|
+
} from './ast.js';
|
|
11
|
+
|
|
12
|
+
export class Interpreter implements ExprVisitor<any>, StmtVisitor<void> {
|
|
13
|
+
interpret(statements: Stmt[]): void {
|
|
14
|
+
for (const statement of statements) {
|
|
15
|
+
this.execute(statement);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private execute(stmt: Stmt): void {
|
|
20
|
+
stmt.accept(this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* Process print statement
|
|
25
|
+
*/
|
|
26
|
+
visitPrintStmt(stmt: PrintStmt): void {
|
|
27
|
+
const value = this.evaluate(stmt.expression);
|
|
28
|
+
console.log(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
visitVarStmt(stmt: VarStmt): void {
|
|
32
|
+
// TODO: yet to implement
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
visitLiteralExpr(expr: LiteralExpr): any {
|
|
36
|
+
if (typeof expr.value === 'string' && expr.value.startsWith('"')) {
|
|
37
|
+
return expr.value.slice(1, -1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return expr.value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
visitVariableExpr(expr: VariableExpr): any {
|
|
44
|
+
// TODO: yet to implement
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* Evaluate an expression
|
|
49
|
+
*/
|
|
50
|
+
private evaluate(expr: Expr): any {
|
|
51
|
+
return expr.accept(this);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/lexer.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { keywords, Token, TokenKind } from './token.js';
|
|
2
|
+
|
|
3
|
+
export class Lexer {
|
|
4
|
+
private source: string;
|
|
5
|
+
private tokens: Token[];
|
|
6
|
+
private startPos: number;
|
|
7
|
+
private currentPos: number;
|
|
8
|
+
private line: number;
|
|
9
|
+
|
|
10
|
+
constructor(source: string) {
|
|
11
|
+
this.source = source;
|
|
12
|
+
this.tokens = [];
|
|
13
|
+
this.startPos = 0;
|
|
14
|
+
this.currentPos = 0;
|
|
15
|
+
this.line = 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read tokens from source
|
|
20
|
+
*/
|
|
21
|
+
readTokens(): Token[] {
|
|
22
|
+
while (!this.isAtEnd()) {
|
|
23
|
+
this.startPos = this.currentPos;
|
|
24
|
+
this.readToken();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.tokens.push(new Token(TokenKind.Eof, '', this.line));
|
|
28
|
+
return this.tokens;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read individual lexeme
|
|
33
|
+
*/
|
|
34
|
+
readToken(): void {
|
|
35
|
+
const char = this.readChar();
|
|
36
|
+
|
|
37
|
+
switch (char) {
|
|
38
|
+
case ',':
|
|
39
|
+
this.createToken(TokenKind.Comma);
|
|
40
|
+
break;
|
|
41
|
+
case ':':
|
|
42
|
+
this.createToken(TokenKind.Colon);
|
|
43
|
+
break;
|
|
44
|
+
case '.':
|
|
45
|
+
this.createToken(TokenKind.Period);
|
|
46
|
+
break;
|
|
47
|
+
case ';':
|
|
48
|
+
this.createToken(TokenKind.Semicolon);
|
|
49
|
+
break;
|
|
50
|
+
case '(':
|
|
51
|
+
this.createToken(TokenKind.OpenParen);
|
|
52
|
+
break;
|
|
53
|
+
case ')':
|
|
54
|
+
this.createToken(TokenKind.CloseParen);
|
|
55
|
+
break;
|
|
56
|
+
case '{':
|
|
57
|
+
this.createToken(TokenKind.OpenCurly);
|
|
58
|
+
break;
|
|
59
|
+
case '}':
|
|
60
|
+
this.createToken(TokenKind.CloseCurly);
|
|
61
|
+
break;
|
|
62
|
+
case '+':
|
|
63
|
+
this.createToken(TokenKind.Plus);
|
|
64
|
+
break;
|
|
65
|
+
case '-':
|
|
66
|
+
this.createToken(TokenKind.Minus);
|
|
67
|
+
break;
|
|
68
|
+
case '*':
|
|
69
|
+
this.createToken(TokenKind.Star);
|
|
70
|
+
break;
|
|
71
|
+
case '/':
|
|
72
|
+
this.createToken(TokenKind.Slash);
|
|
73
|
+
break;
|
|
74
|
+
case '%':
|
|
75
|
+
this.createToken(TokenKind.Mod);
|
|
76
|
+
break;
|
|
77
|
+
case '!':
|
|
78
|
+
this.createToken(TokenKind.Bang);
|
|
79
|
+
break;
|
|
80
|
+
case '=':
|
|
81
|
+
this.createToken(TokenKind.Equal);
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
/* Handle whitespaces */
|
|
85
|
+
case ' ':
|
|
86
|
+
case '\r':
|
|
87
|
+
case '\t':
|
|
88
|
+
break;
|
|
89
|
+
case '\n':
|
|
90
|
+
this.line++;
|
|
91
|
+
break;
|
|
92
|
+
case '"':
|
|
93
|
+
this.readDevanagariString();
|
|
94
|
+
break;
|
|
95
|
+
default:
|
|
96
|
+
if (this.isDevnagariChar(char)) {
|
|
97
|
+
if (this.isDevanagariDigit(char)) this.readDevanagariDigit();
|
|
98
|
+
else this.readDevanagariIdentifier();
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read character
|
|
106
|
+
*/
|
|
107
|
+
readChar(): string {
|
|
108
|
+
if (this.isAtEnd()) return '\0';
|
|
109
|
+
return this.source.charAt(this.currentPos++);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create token out of individual lexeme
|
|
114
|
+
* @param {TokenKind} kind
|
|
115
|
+
* @param {any} literal
|
|
116
|
+
*/
|
|
117
|
+
createToken(kind: TokenKind, literal?: any): void {
|
|
118
|
+
if (literal === undefined) literal = null;
|
|
119
|
+
const text = this.source.substring(this.startPos, this.currentPos);
|
|
120
|
+
this.tokens.push(new Token(kind, text, this.line));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if we've reached the end of file
|
|
125
|
+
*/
|
|
126
|
+
private isAtEnd(): boolean {
|
|
127
|
+
return this.currentPos >= this.source.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Peek next character
|
|
132
|
+
*/
|
|
133
|
+
private peekNextChar(): string {
|
|
134
|
+
if (this.isAtEnd()) return '\0';
|
|
135
|
+
return this.source.charAt(this.currentPos);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get next character
|
|
140
|
+
*/
|
|
141
|
+
private getNextChar(): string {
|
|
142
|
+
return this.source.charAt(this.currentPos++);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if the character is devanagari character
|
|
147
|
+
* @param {string} char
|
|
148
|
+
*/
|
|
149
|
+
private isDevnagariChar(char: string): boolean {
|
|
150
|
+
return (
|
|
151
|
+
('\u{0900}' <= char && char <= '\u{097F}') ||
|
|
152
|
+
('\u{A8E0}' <= char && char <= '\u{A8FF}')
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if the character is devanagari digit
|
|
158
|
+
* @param {string} char
|
|
159
|
+
*/
|
|
160
|
+
private isDevanagariDigit(char: string): boolean {
|
|
161
|
+
return '\u{0966}' <= char && char <= '\u{096F}';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Read devanagari character sequence as number
|
|
166
|
+
*/
|
|
167
|
+
private readDevanagariDigit(): void {
|
|
168
|
+
while (this.isDevanagariDigit(this.peekNextChar())) this.getNextChar();
|
|
169
|
+
const literal = this.source.substring(this.startPos, this.currentPos);
|
|
170
|
+
|
|
171
|
+
this.createToken(TokenKind.Number, literal);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Read devanagari character sequence as identifier
|
|
176
|
+
*/
|
|
177
|
+
private readDevanagariIdentifier(): void {
|
|
178
|
+
while (this.isDevnagariChar(this.peekNextChar())) this.getNextChar();
|
|
179
|
+
const literal = this.source.substring(this.startPos, this.currentPos);
|
|
180
|
+
|
|
181
|
+
const keywordKind = keywords[literal];
|
|
182
|
+
if (keywordKind) this.createToken(keywordKind, literal);
|
|
183
|
+
else this.createToken(TokenKind.Identifier, literal);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read string literals
|
|
188
|
+
*/
|
|
189
|
+
private readDevanagariString(): void {
|
|
190
|
+
while (this.peekNextChar() != '"' && !this.isAtEnd()) {
|
|
191
|
+
if (this.peekNextChar() === '\n') this.line++;
|
|
192
|
+
this.getNextChar();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.getNextChar();
|
|
196
|
+
const content = this.source.substring(
|
|
197
|
+
this.startPos + 1,
|
|
198
|
+
this.currentPos - 1
|
|
199
|
+
);
|
|
200
|
+
this.createToken(TokenKind.String, content);
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { Lexer } from './lexer.js';
|
|
3
|
+
import { Parser } from './parser.js';
|
|
4
|
+
import { Interpreter } from './interpreter.js';
|
|
5
|
+
|
|
6
|
+
function getFileNameFromArgs(args: string[]): string | null {
|
|
7
|
+
if (args.length > 1) {
|
|
8
|
+
console.log('> Too many arguments.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fileName = args[0] || null;
|
|
12
|
+
return fileName;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function execFile(path: string): void {
|
|
16
|
+
const source = readFileSync(path, 'utf-8');
|
|
17
|
+
const lexer = new Lexer(source);
|
|
18
|
+
const tokens = lexer.readTokens();
|
|
19
|
+
|
|
20
|
+
const parser = new Parser(tokens);
|
|
21
|
+
const stmts = parser.parse();
|
|
22
|
+
|
|
23
|
+
const interpreter = new Interpreter();
|
|
24
|
+
interpreter.interpret(stmts);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main(): void {
|
|
28
|
+
const fileName = getFileNameFromArgs(process.argv.slice(2));
|
|
29
|
+
if (fileName !== null) {
|
|
30
|
+
execFile(fileName);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main();
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LiteralExpr,
|
|
3
|
+
PrintStmt,
|
|
4
|
+
VariableExpr,
|
|
5
|
+
type Expr,
|
|
6
|
+
type Stmt
|
|
7
|
+
} from './ast.js';
|
|
8
|
+
import { TokenKind, type Token } from './token.js';
|
|
9
|
+
|
|
10
|
+
export class Parser {
|
|
11
|
+
private tokens: Token[];
|
|
12
|
+
private currentPos: number;
|
|
13
|
+
|
|
14
|
+
constructor(tokens: Token[]) {
|
|
15
|
+
this.tokens = tokens;
|
|
16
|
+
this.currentPos = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
parse(): Stmt[] {
|
|
20
|
+
const stmts: Stmt[] = [];
|
|
21
|
+
while (!this.isAtEnd()) {
|
|
22
|
+
stmts.push(this.parseStmt());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return stmts;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private parseStmt(): Stmt {
|
|
29
|
+
if (this.match(TokenKind.Print)) {
|
|
30
|
+
return this.parsePrintStmt();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw this.error(this.peek(), 'Expression expected.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private parsePrintStmt(): Stmt {
|
|
37
|
+
this.consume(TokenKind.OpenParen, "Expect '(' after 'छाप'");
|
|
38
|
+
const expr = this.parseExpression();
|
|
39
|
+
this.consume(TokenKind.CloseParen, "Expect ')' after expression");
|
|
40
|
+
return new PrintStmt(expr);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private parseExpression(): Expr {
|
|
44
|
+
return this.parsePrimary();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private parsePrimary(): Expr {
|
|
48
|
+
if (this.match(TokenKind.String, TokenKind.Number)) {
|
|
49
|
+
return new LiteralExpr(this.previous().lexeme);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.match(TokenKind.Identifier)) {
|
|
53
|
+
return new VariableExpr(this.previous());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw this.error(this.peek(), 'Expression expected.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private match(...kinds: TokenKind[]): boolean {
|
|
60
|
+
for (const kind of kinds) {
|
|
61
|
+
if (this.check(kind)) {
|
|
62
|
+
this.advance();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private consume(kind: TokenKind, message: string): Token {
|
|
70
|
+
if (this.check(kind)) return this.advance();
|
|
71
|
+
throw this.error(this.peek(), message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private check(kind: TokenKind): boolean {
|
|
75
|
+
if (this.isAtEnd()) return false;
|
|
76
|
+
return this.peek().kind === kind;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private advance(): Token {
|
|
80
|
+
if (!this.isAtEnd()) this.currentPos++;
|
|
81
|
+
return this.previous();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private peek(): Token {
|
|
85
|
+
return this.tokens[this.currentPos]!;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private previous(): Token {
|
|
89
|
+
return this.tokens[this.currentPos - 1]!;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private isAtEnd(): boolean {
|
|
93
|
+
return this.peek().kind === TokenKind.Eof;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private error(token: Token, message: string): Error {
|
|
97
|
+
return new Error(`> [line ${token.line}] Error: ${message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/token.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export enum TokenKind {
|
|
2
|
+
Eof = 'Eof',
|
|
3
|
+
Illegal = 'Illegal',
|
|
4
|
+
|
|
5
|
+
/* -- Punctuation/Delimiters -- */
|
|
6
|
+
Comma = 'Comma',
|
|
7
|
+
Colon = 'Colon',
|
|
8
|
+
Period = 'Period',
|
|
9
|
+
Semicolon = 'Semicolon',
|
|
10
|
+
OpenParen = 'OpenParen',
|
|
11
|
+
CloseParen = 'CloseParen',
|
|
12
|
+
OpenCurly = 'OpenCurly',
|
|
13
|
+
CloseCurly = 'CloseCurly',
|
|
14
|
+
|
|
15
|
+
/* -- Operators -- */
|
|
16
|
+
Plus = 'Plus',
|
|
17
|
+
Minus = 'Minus',
|
|
18
|
+
Star = 'Star',
|
|
19
|
+
Slash = 'Slash',
|
|
20
|
+
Mod = 'Mod',
|
|
21
|
+
Bang = 'Bang',
|
|
22
|
+
Equal = 'Equal',
|
|
23
|
+
|
|
24
|
+
/* -- Literals -- */
|
|
25
|
+
Number = 'Number',
|
|
26
|
+
Character = 'Character',
|
|
27
|
+
String = 'String',
|
|
28
|
+
Identifier = 'Identifier',
|
|
29
|
+
|
|
30
|
+
/* -- Keywords -- */
|
|
31
|
+
Let = 'Let',
|
|
32
|
+
Function = 'Function',
|
|
33
|
+
Print = 'Print',
|
|
34
|
+
Return = 'Return',
|
|
35
|
+
If = 'If',
|
|
36
|
+
Else = 'Else',
|
|
37
|
+
True = 'True',
|
|
38
|
+
False = 'False'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const keywords: Record<string, TokenKind> = {
|
|
42
|
+
मानौ: TokenKind.Let,
|
|
43
|
+
कार्य: TokenKind.Function,
|
|
44
|
+
छाप: TokenKind.Print,
|
|
45
|
+
यदि: TokenKind.If,
|
|
46
|
+
नभए: TokenKind.Else,
|
|
47
|
+
फिर्ता: TokenKind.Return,
|
|
48
|
+
सत्य: TokenKind.True,
|
|
49
|
+
असत्य: TokenKind.False
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export class Token {
|
|
53
|
+
kind: TokenKind;
|
|
54
|
+
lexeme: string;
|
|
55
|
+
line: number;
|
|
56
|
+
|
|
57
|
+
constructor(kind: TokenKind, lexeme: string, line: number) {
|
|
58
|
+
this.kind = kind;
|
|
59
|
+
this.lexeme = lexeme;
|
|
60
|
+
this.line = line;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import test, { describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Lexer } from '../src/lexer.js';
|
|
4
|
+
import { Token, TokenKind } from '../src/token.js';
|
|
5
|
+
|
|
6
|
+
describe('Lexer', () => {
|
|
7
|
+
test('scan punctuations & delimeters', () => {
|
|
8
|
+
const source: string = `,:.;(){}`;
|
|
9
|
+
|
|
10
|
+
const lexer: Lexer = new Lexer(source.trim());
|
|
11
|
+
const actual = lexer.readTokens();
|
|
12
|
+
|
|
13
|
+
const expected: Token[] = [
|
|
14
|
+
new Token(TokenKind.Comma, ',', 1),
|
|
15
|
+
new Token(TokenKind.Colon, ':', 1),
|
|
16
|
+
new Token(TokenKind.Period, '.', 1),
|
|
17
|
+
new Token(TokenKind.Semicolon, ';', 1),
|
|
18
|
+
new Token(TokenKind.OpenParen, '(', 1),
|
|
19
|
+
new Token(TokenKind.CloseParen, ')', 1),
|
|
20
|
+
new Token(TokenKind.OpenCurly, '{', 1),
|
|
21
|
+
new Token(TokenKind.CloseCurly, '}', 1),
|
|
22
|
+
new Token(TokenKind.Eof, '', 1)
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
assert.equal(actual.length, expected.length);
|
|
26
|
+
assert.deepEqual(actual, expected);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('scan operators', () => {
|
|
30
|
+
const source: string = `+-*/%!=`;
|
|
31
|
+
|
|
32
|
+
const lexer: Lexer = new Lexer(source.trim());
|
|
33
|
+
const actual = lexer.readTokens();
|
|
34
|
+
|
|
35
|
+
const expected: Token[] = [
|
|
36
|
+
new Token(TokenKind.Plus, '+', 1),
|
|
37
|
+
new Token(TokenKind.Minus, '-', 1),
|
|
38
|
+
new Token(TokenKind.Star, '*', 1),
|
|
39
|
+
new Token(TokenKind.Slash, '/', 1),
|
|
40
|
+
new Token(TokenKind.Mod, '%', 1),
|
|
41
|
+
new Token(TokenKind.Bang, '!', 1),
|
|
42
|
+
new Token(TokenKind.Equal, '=', 1),
|
|
43
|
+
new Token(TokenKind.Eof, '', 1)
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
assert.equal(actual.length, expected.length);
|
|
47
|
+
assert.deepEqual(actual, expected);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('scan literals & keywords', () => {
|
|
51
|
+
const source: string = `कार्य काम() {
|
|
52
|
+
सन्देश = "सोच्छौ के मेरो बारे?"
|
|
53
|
+
छाप(सन्देश)
|
|
54
|
+
छाप(१२३)
|
|
55
|
+
}
|
|
56
|
+
काम() `;
|
|
57
|
+
|
|
58
|
+
const lexer: Lexer = new Lexer(source.trim());
|
|
59
|
+
const actual = lexer.readTokens();
|
|
60
|
+
|
|
61
|
+
const expected: Token[] = [
|
|
62
|
+
new Token(TokenKind.Function, 'कार्य', 1),
|
|
63
|
+
new Token(TokenKind.Identifier, 'काम', 1),
|
|
64
|
+
new Token(TokenKind.OpenParen, '(', 1),
|
|
65
|
+
new Token(TokenKind.CloseParen, ')', 1),
|
|
66
|
+
new Token(TokenKind.OpenCurly, '{', 1),
|
|
67
|
+
new Token(TokenKind.Identifier, 'सन्देश', 2),
|
|
68
|
+
new Token(TokenKind.Equal, '=', 2),
|
|
69
|
+
new Token(TokenKind.String, '"सोच्छौ के मेरो बारे?"', 2),
|
|
70
|
+
new Token(TokenKind.Print, 'छाप', 3),
|
|
71
|
+
new Token(TokenKind.OpenParen, '(', 3),
|
|
72
|
+
new Token(TokenKind.Identifier, 'सन्देश', 3),
|
|
73
|
+
new Token(TokenKind.CloseParen, ')', 3),
|
|
74
|
+
new Token(TokenKind.Print, 'छाप', 4),
|
|
75
|
+
new Token(TokenKind.OpenParen, '(', 4),
|
|
76
|
+
new Token(TokenKind.Number, '१२३', 4),
|
|
77
|
+
new Token(TokenKind.CloseParen, ')', 4),
|
|
78
|
+
new Token(TokenKind.CloseCurly, '}', 5),
|
|
79
|
+
new Token(TokenKind.Identifier, 'काम', 6),
|
|
80
|
+
new Token(TokenKind.OpenParen, '(', 6),
|
|
81
|
+
new Token(TokenKind.CloseParen, ')', 6),
|
|
82
|
+
new Token(TokenKind.Eof, '', 6)
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
assert.equal(actual.length, expected.length);
|
|
86
|
+
assert.deepEqual(actual, expected);
|
|
87
|
+
});
|
|
88
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* Base Options: */
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"target": "es2022",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"moduleDetection": "force",
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
|
|
13
|
+
/* Strictness */
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUncheckedIndexedAccess": true,
|
|
16
|
+
"noImplicitOverride": true,
|
|
17
|
+
|
|
18
|
+
/* Transpiling */
|
|
19
|
+
"module": "NodeNext",
|
|
20
|
+
"rootDir": ".",
|
|
21
|
+
"outDir": "./dist"
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/**/*.ts", "tests/**/*.test.ts"],
|
|
24
|
+
"exclude": ["node_modules", "dist"]
|
|
25
|
+
}
|