ga-lang 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ast.js +42 -0
- package/dist/src/interpreter.js +39 -0
- package/dist/src/lexer.js +12 -0
- package/dist/src/parser.js +55 -3
- package/examples/init.ga +14 -4
- package/package.json +10 -11
- package/src/ast.ts +58 -0
- package/src/interpreter.ts +63 -9
- package/src/lexer.ts +14 -0
- package/src/parser.ts +75 -2
- package/tests/lexer.test.ts +28 -0
- package/tests/parser.test.ts +44 -1
package/dist/src/ast.js
CHANGED
|
@@ -16,6 +16,17 @@ export class LiteralExpr {
|
|
|
16
16
|
return visitor.visitLiteralExpr(this);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
export class CallExpr {
|
|
20
|
+
callee;
|
|
21
|
+
args;
|
|
22
|
+
constructor(callee, args) {
|
|
23
|
+
this.callee = callee;
|
|
24
|
+
this.args = args;
|
|
25
|
+
}
|
|
26
|
+
accept(visitor) {
|
|
27
|
+
return visitor.visitCallExpr(this);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
19
30
|
export class PrintStmt {
|
|
20
31
|
expression;
|
|
21
32
|
constructor(expression) {
|
|
@@ -36,3 +47,34 @@ export class VarStmt {
|
|
|
36
47
|
return visitor.visitVarStmt(this);
|
|
37
48
|
}
|
|
38
49
|
}
|
|
50
|
+
export class FunctionStmt {
|
|
51
|
+
name;
|
|
52
|
+
params;
|
|
53
|
+
body;
|
|
54
|
+
constructor(name, params, body) {
|
|
55
|
+
this.name = name;
|
|
56
|
+
this.params = params;
|
|
57
|
+
this.body = body;
|
|
58
|
+
}
|
|
59
|
+
accept(visitor) {
|
|
60
|
+
return visitor.visitFunctionStmt(this);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class BlockStmt {
|
|
64
|
+
statements;
|
|
65
|
+
constructor(statements) {
|
|
66
|
+
this.statements = statements;
|
|
67
|
+
}
|
|
68
|
+
accept(visitor) {
|
|
69
|
+
return visitor.visitBlockStmt(this);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class ExpressionStmt {
|
|
73
|
+
expression;
|
|
74
|
+
constructor(expression) {
|
|
75
|
+
this.expression = expression;
|
|
76
|
+
}
|
|
77
|
+
accept(visitor) {
|
|
78
|
+
return visitor.visitExpressionStmt(this);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/src/interpreter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FunctionStmt } from './ast.js';
|
|
1
2
|
export class Interpreter {
|
|
2
3
|
environment = new Map();
|
|
3
4
|
interpret(statements) {
|
|
@@ -19,6 +20,17 @@ export class Interpreter {
|
|
|
19
20
|
const value = stmt.initializer !== null ? this.evaluate(stmt.initializer) : null;
|
|
20
21
|
this.environment.set(stmt.name.lexeme, value);
|
|
21
22
|
}
|
|
23
|
+
visitFunctionStmt(stmt) {
|
|
24
|
+
this.environment.set(stmt.name.lexeme, stmt);
|
|
25
|
+
}
|
|
26
|
+
visitBlockStmt(stmt) {
|
|
27
|
+
for (const statement of stmt.statements) {
|
|
28
|
+
this.execute(statement);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
visitExpressionStmt(stmt) {
|
|
32
|
+
this.evaluate(stmt.expression);
|
|
33
|
+
}
|
|
22
34
|
visitLiteralExpr(expr) {
|
|
23
35
|
if (typeof expr.value === 'string' && expr.value.startsWith('"')) {
|
|
24
36
|
return expr.value.slice(1, -1);
|
|
@@ -32,6 +44,33 @@ export class Interpreter {
|
|
|
32
44
|
}
|
|
33
45
|
return value;
|
|
34
46
|
}
|
|
47
|
+
visitCallExpr(expr) {
|
|
48
|
+
const func = this.environment.get(expr.callee.name.lexeme);
|
|
49
|
+
if (func === undefined) {
|
|
50
|
+
throw new Error(`Undefined function '${expr.callee.name.lexeme}'`);
|
|
51
|
+
}
|
|
52
|
+
if (!(func instanceof FunctionStmt)) {
|
|
53
|
+
throw new Error(`'${expr.callee.name.lexeme}' is not a function`);
|
|
54
|
+
}
|
|
55
|
+
// Create new environment for function scope
|
|
56
|
+
const prevEnvironment = this.environment;
|
|
57
|
+
this.environment = new Map(this.environment);
|
|
58
|
+
// Bind arguments to parameters
|
|
59
|
+
if (expr.args.length !== func.params.length) {
|
|
60
|
+
throw new Error(`Expected ${func.params.length} arguments but got ${expr.args.length}`);
|
|
61
|
+
}
|
|
62
|
+
for (let i = 0; i < func.params.length; i++) {
|
|
63
|
+
const value = this.evaluate(expr.args[i]);
|
|
64
|
+
this.environment.set(func.params[i].lexeme, value);
|
|
65
|
+
}
|
|
66
|
+
// Execute function body
|
|
67
|
+
for (const stmt of func.body) {
|
|
68
|
+
this.execute(stmt);
|
|
69
|
+
}
|
|
70
|
+
// Restore previous environment
|
|
71
|
+
this.environment = prevEnvironment;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
35
74
|
/*
|
|
36
75
|
* Evaluate an expression
|
|
37
76
|
*/
|
package/dist/src/lexer.js
CHANGED
|
@@ -92,6 +92,11 @@ export class Lexer {
|
|
|
92
92
|
else
|
|
93
93
|
this.readDevanagariIdentifier();
|
|
94
94
|
}
|
|
95
|
+
else if (!this.isWhitespace(char)) {
|
|
96
|
+
// Report invalid character
|
|
97
|
+
throw new Error(`Invalid character '${char}' at line ${this.line}. ` +
|
|
98
|
+
`Only Devanagari characters, numbers, special characters, and whitespace are allowed.`);
|
|
99
|
+
}
|
|
95
100
|
break;
|
|
96
101
|
}
|
|
97
102
|
}
|
|
@@ -149,6 +154,13 @@ export class Lexer {
|
|
|
149
154
|
isDevanagariDigit(char) {
|
|
150
155
|
return '\u{0966}' <= char && char <= '\u{096F}';
|
|
151
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if the character is whitespace
|
|
159
|
+
* @param {string} char
|
|
160
|
+
*/
|
|
161
|
+
isWhitespace(char) {
|
|
162
|
+
return char === ' ' || char === '\r' || char === '\t' || char === '\n';
|
|
163
|
+
}
|
|
152
164
|
/**
|
|
153
165
|
* Read devanagari character sequence as number
|
|
154
166
|
*/
|
package/dist/src/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LiteralExpr, PrintStmt, VarStmt, VariableExpr } from './ast.js';
|
|
1
|
+
import { CallExpr, ExpressionStmt, FunctionStmt, LiteralExpr, PrintStmt, VarStmt, VariableExpr } from './ast.js';
|
|
2
2
|
import { TokenKind } from './token.js';
|
|
3
3
|
export class Parser {
|
|
4
4
|
tokens;
|
|
@@ -10,6 +10,10 @@ export class Parser {
|
|
|
10
10
|
parse() {
|
|
11
11
|
const stmts = [];
|
|
12
12
|
while (!this.isAtEnd()) {
|
|
13
|
+
if (this.check(TokenKind.Illegal)) {
|
|
14
|
+
const token = this.peek();
|
|
15
|
+
throw this.error(token, `Invalid character '${token.lexeme}'`);
|
|
16
|
+
}
|
|
13
17
|
stmts.push(this.parseStmt());
|
|
14
18
|
}
|
|
15
19
|
return stmts;
|
|
@@ -21,7 +25,14 @@ export class Parser {
|
|
|
21
25
|
if (this.match(TokenKind.Let)) {
|
|
22
26
|
return this.parseVarStmt();
|
|
23
27
|
}
|
|
24
|
-
|
|
28
|
+
if (this.match(TokenKind.Function)) {
|
|
29
|
+
return this.parseFunctionStmt();
|
|
30
|
+
}
|
|
31
|
+
return this.parseExpressionStmt();
|
|
32
|
+
}
|
|
33
|
+
parseExpressionStmt() {
|
|
34
|
+
const expr = this.parseExpression();
|
|
35
|
+
return new ExpressionStmt(expr);
|
|
25
36
|
}
|
|
26
37
|
parsePrintStmt() {
|
|
27
38
|
this.consume(TokenKind.OpenParen, "Expect '(' after 'छाप'");
|
|
@@ -37,8 +48,49 @@ export class Parser {
|
|
|
37
48
|
}
|
|
38
49
|
return new VarStmt(name, initializer);
|
|
39
50
|
}
|
|
51
|
+
parseFunctionStmt() {
|
|
52
|
+
const name = this.consume(TokenKind.Identifier, "Expect function name after 'कार्य'");
|
|
53
|
+
this.consume(TokenKind.OpenParen, "Expect '(' after function name");
|
|
54
|
+
const params = [];
|
|
55
|
+
if (!this.check(TokenKind.CloseParen)) {
|
|
56
|
+
do {
|
|
57
|
+
params.push(this.consume(TokenKind.Identifier, 'Expect parameter name'));
|
|
58
|
+
} while (this.match(TokenKind.Comma));
|
|
59
|
+
}
|
|
60
|
+
this.consume(TokenKind.CloseParen, "Expect ')' after parameters");
|
|
61
|
+
this.consume(TokenKind.OpenCurly, "Expect '{' before function body");
|
|
62
|
+
const body = this.parseBlock();
|
|
63
|
+
return new FunctionStmt(name, params, body);
|
|
64
|
+
}
|
|
65
|
+
parseBlock() {
|
|
66
|
+
const statements = [];
|
|
67
|
+
while (!this.check(TokenKind.CloseCurly) && !this.isAtEnd()) {
|
|
68
|
+
statements.push(this.parseStmt());
|
|
69
|
+
}
|
|
70
|
+
this.consume(TokenKind.CloseCurly, "Expect '}' after block");
|
|
71
|
+
return statements;
|
|
72
|
+
}
|
|
40
73
|
parseExpression() {
|
|
41
|
-
return this.
|
|
74
|
+
return this.parseCall();
|
|
75
|
+
}
|
|
76
|
+
parseCall() {
|
|
77
|
+
let expr = this.parsePrimary();
|
|
78
|
+
while (this.match(TokenKind.OpenParen)) {
|
|
79
|
+
const args = [];
|
|
80
|
+
if (!this.check(TokenKind.CloseParen)) {
|
|
81
|
+
do {
|
|
82
|
+
args.push(this.parseExpression());
|
|
83
|
+
} while (this.match(TokenKind.Comma));
|
|
84
|
+
}
|
|
85
|
+
this.consume(TokenKind.CloseParen, "Expect ')' after arguments");
|
|
86
|
+
if (expr instanceof VariableExpr) {
|
|
87
|
+
expr = new CallExpr(expr, args);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw this.error(this.previous(), 'Can only call functions');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return expr;
|
|
42
94
|
}
|
|
43
95
|
parsePrimary() {
|
|
44
96
|
if (this.match(TokenKind.String, TokenKind.Number)) {
|
package/examples/init.ga
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
मानौ
|
|
3
|
-
|
|
4
|
-
छाप(
|
|
1
|
+
कार्य सुरु () {
|
|
2
|
+
मानौ सन्देश = "सोच्छौ के मेरो बारे?"
|
|
3
|
+
मानौ एकदुइतिन = १२३
|
|
4
|
+
छाप(सन्देश)
|
|
5
|
+
छाप(एकदुइतिन)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
कार्य जोड( अ, आ) {
|
|
9
|
+
छाप(अ)
|
|
10
|
+
छाप(आ)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
सुरु()
|
|
14
|
+
जोड(१,२)
|
package/package.json
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ga-lang",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "An interpreted toy programming language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ga": "./bin/ga.js"
|
|
9
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
10
|
"repository": {
|
|
19
11
|
"type": "git",
|
|
20
12
|
"url": "git+https://github.com/bvsvntv/ga.git"
|
|
@@ -31,7 +23,6 @@
|
|
|
31
23
|
"url": "https://github.com/bvsvntv/ga/issues"
|
|
32
24
|
},
|
|
33
25
|
"homepage": "https://github.com/bvsvntv/ga#readme",
|
|
34
|
-
"packageManager": "pnpm@10.25.0",
|
|
35
26
|
"engines": {
|
|
36
27
|
"node": ">=22.x.x",
|
|
37
28
|
"pnpm": ">=10.x.x"
|
|
@@ -46,5 +37,13 @@
|
|
|
46
37
|
"@types/node": "^25.0.3",
|
|
47
38
|
"prettier": "^3.7.4",
|
|
48
39
|
"typescript": "^5.9.3"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"prettier:check": "prettier --check \"./**/*.{js,ts,json}\"",
|
|
43
|
+
"prettier:fix": "prettier --write \"./**/*.{js,ts,json}\"",
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"build": "pnpm run clean && tsc",
|
|
46
|
+
"test": "pnpm run build && node --test ./dist/tests/**/*.js",
|
|
47
|
+
"ga": "pnpm run build && node dist/src/main.js"
|
|
49
48
|
}
|
|
50
|
-
}
|
|
49
|
+
}
|
package/src/ast.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface Expr {
|
|
|
7
7
|
export interface ExprVisitor<T> {
|
|
8
8
|
visitVariableExpr(expr: VariableExpr): T;
|
|
9
9
|
visitLiteralExpr(expr: LiteralExpr): T;
|
|
10
|
+
visitCallExpr(expr: CallExpr): T;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface Stmt {
|
|
@@ -16,6 +17,9 @@ export interface Stmt {
|
|
|
16
17
|
export interface StmtVisitor<T> {
|
|
17
18
|
visitPrintStmt(stmt: PrintStmt): T;
|
|
18
19
|
visitVarStmt(stmt: VarStmt): T;
|
|
20
|
+
visitFunctionStmt(stmt: FunctionStmt): T;
|
|
21
|
+
visitBlockStmt(stmt: BlockStmt): T;
|
|
22
|
+
visitExpressionStmt(stmt: ExpressionStmt): T;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export class VariableExpr implements Expr {
|
|
@@ -42,6 +46,20 @@ export class LiteralExpr implements Expr {
|
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
export class CallExpr implements Expr {
|
|
50
|
+
callee: VariableExpr;
|
|
51
|
+
args: Expr[];
|
|
52
|
+
|
|
53
|
+
constructor(callee: VariableExpr, args: Expr[]) {
|
|
54
|
+
this.callee = callee;
|
|
55
|
+
this.args = args;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
accept<T>(visitor: ExprVisitor<T>): T {
|
|
59
|
+
return visitor.visitCallExpr(this);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
export class PrintStmt implements Stmt {
|
|
46
64
|
expression: Expr;
|
|
47
65
|
|
|
@@ -67,3 +85,43 @@ export class VarStmt implements Stmt {
|
|
|
67
85
|
return visitor.visitVarStmt(this);
|
|
68
86
|
}
|
|
69
87
|
}
|
|
88
|
+
|
|
89
|
+
export class FunctionStmt implements Stmt {
|
|
90
|
+
name: Token;
|
|
91
|
+
params: Token[];
|
|
92
|
+
body: Stmt[];
|
|
93
|
+
|
|
94
|
+
constructor(name: Token, params: Token[], body: Stmt[]) {
|
|
95
|
+
this.name = name;
|
|
96
|
+
this.params = params;
|
|
97
|
+
this.body = body;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
accept<T>(visitor: StmtVisitor<T>): T {
|
|
101
|
+
return visitor.visitFunctionStmt(this);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class BlockStmt implements Stmt {
|
|
106
|
+
statements: Stmt[];
|
|
107
|
+
|
|
108
|
+
constructor(statements: Stmt[]) {
|
|
109
|
+
this.statements = statements;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
accept<T>(visitor: StmtVisitor<T>): T {
|
|
113
|
+
return visitor.visitBlockStmt(this);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class ExpressionStmt implements Stmt {
|
|
118
|
+
expression: Expr;
|
|
119
|
+
|
|
120
|
+
constructor(expression: Expr) {
|
|
121
|
+
this.expression = expression;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
accept<T>(visitor: StmtVisitor<T>): T {
|
|
125
|
+
return visitor.visitExpressionStmt(this);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/interpreter.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import {
|
|
2
|
+
FunctionStmt,
|
|
3
|
+
type BlockStmt,
|
|
4
|
+
type CallExpr,
|
|
5
|
+
type ExpressionStmt,
|
|
6
|
+
type Expr,
|
|
7
|
+
type ExprVisitor,
|
|
8
|
+
type LiteralExpr,
|
|
9
|
+
type PrintStmt,
|
|
10
|
+
type Stmt,
|
|
11
|
+
type StmtVisitor,
|
|
12
|
+
type VariableExpr,
|
|
13
|
+
type VarStmt
|
|
10
14
|
} from './ast.js';
|
|
11
15
|
|
|
12
16
|
export class Interpreter implements ExprVisitor<any>, StmtVisitor<void> {
|
|
@@ -36,6 +40,20 @@ export class Interpreter implements ExprVisitor<any>, StmtVisitor<void> {
|
|
|
36
40
|
this.environment.set(stmt.name.lexeme, value);
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
visitFunctionStmt(stmt: FunctionStmt): void {
|
|
44
|
+
this.environment.set(stmt.name.lexeme, stmt);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
visitBlockStmt(stmt: BlockStmt): void {
|
|
48
|
+
for (const statement of stmt.statements) {
|
|
49
|
+
this.execute(statement);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
visitExpressionStmt(stmt: ExpressionStmt): void {
|
|
54
|
+
this.evaluate(stmt.expression);
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
visitLiteralExpr(expr: LiteralExpr): any {
|
|
40
58
|
if (typeof expr.value === 'string' && expr.value.startsWith('"')) {
|
|
41
59
|
return expr.value.slice(1, -1);
|
|
@@ -52,6 +70,42 @@ export class Interpreter implements ExprVisitor<any>, StmtVisitor<void> {
|
|
|
52
70
|
return value;
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
visitCallExpr(expr: CallExpr): any {
|
|
74
|
+
const func = this.environment.get(expr.callee.name.lexeme);
|
|
75
|
+
if (func === undefined) {
|
|
76
|
+
throw new Error(`Undefined function '${expr.callee.name.lexeme}'`);
|
|
77
|
+
}
|
|
78
|
+
if (!(func instanceof FunctionStmt)) {
|
|
79
|
+
throw new Error(`'${expr.callee.name.lexeme}' is not a function`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Create new environment for function scope
|
|
83
|
+
const prevEnvironment = this.environment;
|
|
84
|
+
this.environment = new Map(this.environment);
|
|
85
|
+
|
|
86
|
+
// Bind arguments to parameters
|
|
87
|
+
if (expr.args.length !== func.params.length) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Expected ${func.params.length} arguments but got ${expr.args.length}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < func.params.length; i++) {
|
|
94
|
+
const value = this.evaluate(expr.args[i]!);
|
|
95
|
+
this.environment.set(func.params[i]!.lexeme, value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Execute function body
|
|
99
|
+
for (const stmt of func.body) {
|
|
100
|
+
this.execute(stmt);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Restore previous environment
|
|
104
|
+
this.environment = prevEnvironment;
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
/*
|
|
56
110
|
* Evaluate an expression
|
|
57
111
|
*/
|
package/src/lexer.ts
CHANGED
|
@@ -96,6 +96,12 @@ export class Lexer {
|
|
|
96
96
|
if (this.isDevnagariChar(char)) {
|
|
97
97
|
if (this.isDevanagariDigit(char)) this.readDevanagariDigit();
|
|
98
98
|
else this.readDevanagariIdentifier();
|
|
99
|
+
} else if (!this.isWhitespace(char)) {
|
|
100
|
+
// Report invalid character
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Invalid character '${char}' at line ${this.line}. ` +
|
|
103
|
+
`Only Devanagari characters, numbers, special characters, and whitespace are allowed.`
|
|
104
|
+
);
|
|
99
105
|
}
|
|
100
106
|
break;
|
|
101
107
|
}
|
|
@@ -161,6 +167,14 @@ export class Lexer {
|
|
|
161
167
|
return '\u{0966}' <= char && char <= '\u{096F}';
|
|
162
168
|
}
|
|
163
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Check if the character is whitespace
|
|
172
|
+
* @param {string} char
|
|
173
|
+
*/
|
|
174
|
+
private isWhitespace(char: string): boolean {
|
|
175
|
+
return char === ' ' || char === '\r' || char === '\t' || char === '\n';
|
|
176
|
+
}
|
|
177
|
+
|
|
164
178
|
/**
|
|
165
179
|
* Read devanagari character sequence as number
|
|
166
180
|
*/
|
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CallExpr,
|
|
3
|
+
ExpressionStmt,
|
|
4
|
+
FunctionStmt,
|
|
2
5
|
LiteralExpr,
|
|
3
6
|
PrintStmt,
|
|
4
7
|
VarStmt,
|
|
@@ -20,6 +23,10 @@ export class Parser {
|
|
|
20
23
|
parse(): Stmt[] {
|
|
21
24
|
const stmts: Stmt[] = [];
|
|
22
25
|
while (!this.isAtEnd()) {
|
|
26
|
+
if (this.check(TokenKind.Illegal)) {
|
|
27
|
+
const token = this.peek();
|
|
28
|
+
throw this.error(token, `Invalid character '${token.lexeme}'`);
|
|
29
|
+
}
|
|
23
30
|
stmts.push(this.parseStmt());
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -35,7 +42,16 @@ export class Parser {
|
|
|
35
42
|
return this.parseVarStmt();
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
if (this.match(TokenKind.Function)) {
|
|
46
|
+
return this.parseFunctionStmt();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.parseExpressionStmt();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private parseExpressionStmt(): ExpressionStmt {
|
|
53
|
+
const expr = this.parseExpression();
|
|
54
|
+
return new ExpressionStmt(expr);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
private parsePrintStmt(): Stmt {
|
|
@@ -59,8 +75,65 @@ export class Parser {
|
|
|
59
75
|
return new VarStmt(name, initializer);
|
|
60
76
|
}
|
|
61
77
|
|
|
78
|
+
private parseFunctionStmt(): FunctionStmt {
|
|
79
|
+
const name = this.consume(
|
|
80
|
+
TokenKind.Identifier,
|
|
81
|
+
"Expect function name after 'कार्य'"
|
|
82
|
+
);
|
|
83
|
+
this.consume(TokenKind.OpenParen, "Expect '(' after function name");
|
|
84
|
+
|
|
85
|
+
const params: Token[] = [];
|
|
86
|
+
if (!this.check(TokenKind.CloseParen)) {
|
|
87
|
+
do {
|
|
88
|
+
params.push(
|
|
89
|
+
this.consume(TokenKind.Identifier, 'Expect parameter name')
|
|
90
|
+
);
|
|
91
|
+
} while (this.match(TokenKind.Comma));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.consume(TokenKind.CloseParen, "Expect ')' after parameters");
|
|
95
|
+
this.consume(TokenKind.OpenCurly, "Expect '{' before function body");
|
|
96
|
+
|
|
97
|
+
const body = this.parseBlock();
|
|
98
|
+
|
|
99
|
+
return new FunctionStmt(name, params, body);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private parseBlock(): Stmt[] {
|
|
103
|
+
const statements: Stmt[] = [];
|
|
104
|
+
|
|
105
|
+
while (!this.check(TokenKind.CloseCurly) && !this.isAtEnd()) {
|
|
106
|
+
statements.push(this.parseStmt());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.consume(TokenKind.CloseCurly, "Expect '}' after block");
|
|
110
|
+
return statements;
|
|
111
|
+
}
|
|
112
|
+
|
|
62
113
|
private parseExpression(): Expr {
|
|
63
|
-
return this.
|
|
114
|
+
return this.parseCall();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private parseCall(): Expr {
|
|
118
|
+
let expr = this.parsePrimary();
|
|
119
|
+
|
|
120
|
+
while (this.match(TokenKind.OpenParen)) {
|
|
121
|
+
const args: Expr[] = [];
|
|
122
|
+
if (!this.check(TokenKind.CloseParen)) {
|
|
123
|
+
do {
|
|
124
|
+
args.push(this.parseExpression());
|
|
125
|
+
} while (this.match(TokenKind.Comma));
|
|
126
|
+
}
|
|
127
|
+
this.consume(TokenKind.CloseParen, "Expect ')' after arguments");
|
|
128
|
+
|
|
129
|
+
if (expr instanceof VariableExpr) {
|
|
130
|
+
expr = new CallExpr(expr, args);
|
|
131
|
+
} else {
|
|
132
|
+
throw this.error(this.previous(), 'Can only call functions');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return expr;
|
|
64
137
|
}
|
|
65
138
|
|
|
66
139
|
private parsePrimary(): Expr {
|
package/tests/lexer.test.ts
CHANGED
|
@@ -85,4 +85,32 @@ describe('Lexer', () => {
|
|
|
85
85
|
assert.equal(actual.length, expected.length);
|
|
86
86
|
assert.deepEqual(actual, expected);
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test('reject non-devanagari characters outside string literals', () => {
|
|
90
|
+
const source = `मानौ x = ५`;
|
|
91
|
+
|
|
92
|
+
const lexer = new Lexer(source);
|
|
93
|
+
|
|
94
|
+
assert.throws(() => {
|
|
95
|
+
lexer.readTokens();
|
|
96
|
+
}, /Invalid character 'x' at line 1/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('allow non-devanagari characters inside string literals', () => {
|
|
100
|
+
const source = `मानौ सन्देश = "Hello World. 123!"`;
|
|
101
|
+
|
|
102
|
+
const lexer = new Lexer(source);
|
|
103
|
+
const actual = lexer.readTokens();
|
|
104
|
+
|
|
105
|
+
const expected = [
|
|
106
|
+
new Token(TokenKind.Let, 'मानौ', 1),
|
|
107
|
+
new Token(TokenKind.Identifier, 'सन्देश', 1),
|
|
108
|
+
new Token(TokenKind.Equal, '=', 1),
|
|
109
|
+
new Token(TokenKind.String, '"Hello World. 123!"', 1),
|
|
110
|
+
new Token(TokenKind.Eof, '', 1)
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
assert.equal(actual.length, expected.length);
|
|
114
|
+
assert.deepEqual(actual, expected);
|
|
115
|
+
});
|
|
88
116
|
});
|
package/tests/parser.test.ts
CHANGED
|
@@ -2,7 +2,13 @@ import test, { describe } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { Lexer } from '../src/lexer.js';
|
|
4
4
|
import { Parser } from '../src/parser.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PrintStmt,
|
|
7
|
+
VarStmt,
|
|
8
|
+
FunctionStmt,
|
|
9
|
+
LiteralExpr,
|
|
10
|
+
VariableExpr
|
|
11
|
+
} from '../src/ast.js';
|
|
6
12
|
|
|
7
13
|
describe('Parser', () => {
|
|
8
14
|
test('parse print statement', () => {
|
|
@@ -85,4 +91,41 @@ describe('Parser', () => {
|
|
|
85
91
|
assert.ok(stmts[0] instanceof VarStmt);
|
|
86
92
|
assert.ok(stmts[1] instanceof PrintStmt);
|
|
87
93
|
});
|
|
94
|
+
|
|
95
|
+
test('parse function declaration without parameters', () => {
|
|
96
|
+
const source: string = `कार्य नमस्कार() {
|
|
97
|
+
छाप("नमस्ते")
|
|
98
|
+
}`;
|
|
99
|
+
const lexer: Lexer = new Lexer(source.trim());
|
|
100
|
+
const tokens = lexer.readTokens();
|
|
101
|
+
const parser: Parser = new Parser(tokens);
|
|
102
|
+
const stmts = parser.parse();
|
|
103
|
+
|
|
104
|
+
assert.equal(stmts.length, 1);
|
|
105
|
+
assert.ok(stmts[0] instanceof FunctionStmt);
|
|
106
|
+
const funcStmt = stmts[0] as FunctionStmt;
|
|
107
|
+
assert.equal(funcStmt.name.lexeme, 'नमस्कार');
|
|
108
|
+
assert.equal(funcStmt.params.length, 0);
|
|
109
|
+
assert.equal(funcStmt.body.length, 1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('parse function declaration with parameters', () => {
|
|
113
|
+
const source: string = `कार्य जोड(अ, ब) {
|
|
114
|
+
छाप(अ)
|
|
115
|
+
छाप(ब)
|
|
116
|
+
}`;
|
|
117
|
+
const lexer: Lexer = new Lexer(source.trim());
|
|
118
|
+
const tokens = lexer.readTokens();
|
|
119
|
+
const parser: Parser = new Parser(tokens);
|
|
120
|
+
const stmts = parser.parse();
|
|
121
|
+
|
|
122
|
+
assert.equal(stmts.length, 1);
|
|
123
|
+
assert.ok(stmts[0] instanceof FunctionStmt);
|
|
124
|
+
const funcStmt = stmts[0] as FunctionStmt;
|
|
125
|
+
assert.equal(funcStmt.name.lexeme, 'जोड');
|
|
126
|
+
assert.equal(funcStmt.params.length, 2);
|
|
127
|
+
assert.equal(funcStmt.params[0]!.lexeme, 'अ');
|
|
128
|
+
assert.equal(funcStmt.params[1]!.lexeme, 'ब');
|
|
129
|
+
assert.equal(funcStmt.body.length, 2);
|
|
130
|
+
});
|
|
88
131
|
});
|