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.
@@ -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
@@ -0,0 +1,8 @@
1
+ {
2
+ "printWidth": 80,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "semi": true,
6
+ "bracketSpacing": true,
7
+ "requirePragma": false
8
+ }
package/bin/ga.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import '../dist/src/main.js';
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ छाप("सोच्छौ के मेरो बारे?")
2
+ छाप(१२३)
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
+ }