mrt-lang 0.1.4__py3-none-any.whl
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.
- mrt_lang-0.1.4.dist-info/LICENSE.txt +21 -0
- mrt_lang-0.1.4.dist-info/METADATA +168 -0
- mrt_lang-0.1.4.dist-info/RECORD +12 -0
- mrt_lang-0.1.4.dist-info/WHEEL +5 -0
- mrt_lang-0.1.4.dist-info/entry_points.txt +2 -0
- mrt_lang-0.1.4.dist-info/top_level.txt +1 -0
- src/__init__.py +1 -0
- src/__main__.py +32 -0
- src/ast.py +98 -0
- src/interpreter.py +413 -0
- src/lexer.py +238 -0
- src/parser.py +340 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 MRT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mrt-lang
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: MRT Programming Language - A modern, expressive programming language
|
|
5
|
+
Home-page: https://github.com/rithymeth/MRT
|
|
6
|
+
Author: MRT Team
|
|
7
|
+
Author-email: rithy1337@gmail.com
|
|
8
|
+
Keywords: programming language interpreter compiler
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
|
|
19
|
+
# MRT Programming Language
|
|
20
|
+
|
|
21
|
+
MRT is a modern, expressive programming language designed for simplicity and readability. It combines intuitive syntax with powerful features for both beginners and experienced programmers.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Simple and expressive syntax
|
|
26
|
+
- Dynamic typing with type inference
|
|
27
|
+
- First-class functions
|
|
28
|
+
- Rich built-in functions
|
|
29
|
+
- Comprehensive array operations
|
|
30
|
+
- Powerful string manipulation
|
|
31
|
+
- Modern module system
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
You can install MRT using pip:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install mrt-lang
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
After installation, you can run MRT programs using the `mrt` command:
|
|
42
|
+
```bash
|
|
43
|
+
mrt your_program.mrt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
1. Create a file `hello.mrt`:
|
|
49
|
+
```mrt
|
|
50
|
+
func main() {
|
|
51
|
+
print("Hello, World!")
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
2. Run the program:
|
|
56
|
+
```bash
|
|
57
|
+
mrt hello.mrt
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Language Examples
|
|
61
|
+
|
|
62
|
+
### Variables and Basic Types
|
|
63
|
+
```mrt
|
|
64
|
+
func variables_demo() {
|
|
65
|
+
var name = "Alice"
|
|
66
|
+
var age = 25
|
|
67
|
+
var height = 1.75
|
|
68
|
+
var isStudent = true
|
|
69
|
+
|
|
70
|
+
print("Name: " + name)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Arrays
|
|
75
|
+
```mrt
|
|
76
|
+
func array_demo() {
|
|
77
|
+
var numbers = [1, 2, 3, 4, 5]
|
|
78
|
+
push(numbers, 6)
|
|
79
|
+
print("Length:", len(numbers))
|
|
80
|
+
print("First element:", numbers[0])
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### String Operations
|
|
85
|
+
```mrt
|
|
86
|
+
func string_demo() {
|
|
87
|
+
var text = " Hello, World! "
|
|
88
|
+
print("Trimmed:", trim(text))
|
|
89
|
+
print("Uppercase:", toUpper(text))
|
|
90
|
+
print("Contains 'World':", contains(text, "World"))
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Built-in Functions
|
|
95
|
+
|
|
96
|
+
### Array Operations
|
|
97
|
+
- `len(array)`: Returns array length
|
|
98
|
+
- `push(array, element)`: Adds element to end
|
|
99
|
+
- `pop(array)`: Removes and returns last element
|
|
100
|
+
- `slice(array, start, end)`: Returns array subset
|
|
101
|
+
- `join(array, separator)`: Joins elements into string
|
|
102
|
+
- `indexOf(array, element)`: Finds element index
|
|
103
|
+
|
|
104
|
+
### String Operations
|
|
105
|
+
- `split(str, separator)`: Splits string into array
|
|
106
|
+
- `substring(str, start, end)`: Extracts string portion
|
|
107
|
+
- `toUpper(str)`: Converts to uppercase
|
|
108
|
+
- `toLower(str)`: Converts to lowercase
|
|
109
|
+
- `trim(str)`: Removes whitespace
|
|
110
|
+
- `replace(str, old, new)`: Replaces text
|
|
111
|
+
- `startsWith(str, prefix)`: Checks string start
|
|
112
|
+
- `endsWith(str, suffix)`: Checks string end
|
|
113
|
+
- `contains(str, substr)`: Checks for substring
|
|
114
|
+
|
|
115
|
+
## Project Structure
|
|
116
|
+
|
|
117
|
+
- `src/`: Source code for the MRT interpreter
|
|
118
|
+
- `lexer.py`: Tokenizes source code
|
|
119
|
+
- `parser.py`: Parses tokens into AST
|
|
120
|
+
- `interpreter.py`: Executes MRT programs
|
|
121
|
+
- `ast.py`: Abstract Syntax Tree definitions
|
|
122
|
+
- `docs/`: Comprehensive documentation
|
|
123
|
+
- `language_guide.md`: Complete language reference
|
|
124
|
+
- `examples.md`: Example programs and tutorials
|
|
125
|
+
- `getting_started.md`: Installation and quick start
|
|
126
|
+
- `examples/`: Example MRT programs
|
|
127
|
+
- `tests/`: Test suite
|
|
128
|
+
|
|
129
|
+
## Documentation
|
|
130
|
+
|
|
131
|
+
For more detailed information, check out:
|
|
132
|
+
- [Language Guide](docs/language_guide.md): Complete language reference
|
|
133
|
+
- [Examples](docs/examples.md): Example programs and tutorials
|
|
134
|
+
- [Getting Started](docs/getting_started.md): Installation and quick start
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
To set up the development environment:
|
|
139
|
+
|
|
140
|
+
1. Clone the repository:
|
|
141
|
+
```bash
|
|
142
|
+
git clone <repository-url>
|
|
143
|
+
cd mrt
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
2. Install in development mode:
|
|
147
|
+
```bash
|
|
148
|
+
pip install -e .
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
3. Install development dependencies:
|
|
152
|
+
```bash
|
|
153
|
+
pip install -r requirements-dev.txt
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
We welcome contributions! Whether it's:
|
|
159
|
+
- Bug reports
|
|
160
|
+
- Feature requests
|
|
161
|
+
- Documentation improvements
|
|
162
|
+
- Code contributions
|
|
163
|
+
|
|
164
|
+
Please feel free to open issues and pull requests.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT License - see [LICENSE.txt](LICENSE.txt) for details.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
src/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
2
|
+
src/__main__.py,sha256=Rlx96UbCsW4PcnwUBSnQzbLIRIf5sF_DKZjfgRqEW4I,719
|
|
3
|
+
src/ast.py,sha256=Iu-pj2c6q3iRRH9z-mbTkZEI9ZGinWyPF68jQDZ8Ws4,1533
|
|
4
|
+
src/interpreter.py,sha256=7PvKcJf61D2ogOFBx8EYPdZDB_QvOBtELrgFHrEMtsY,16222
|
|
5
|
+
src/lexer.py,sha256=Y46xheBhAGlWikG3jijrSgiP7Ddm8ALcu2i5qUQnJQo,7716
|
|
6
|
+
src/parser.py,sha256=MJmzfu_EudLZz2MIoqT52fLWxdJp2NERyjQ6zEYaw3I,11350
|
|
7
|
+
mrt_lang-0.1.4.dist-info/LICENSE.txt,sha256=E__hQj7AfnoRYefz32Q0YRjCmqCNiseVw0DbWeylc6k,1079
|
|
8
|
+
mrt_lang-0.1.4.dist-info/METADATA,sha256=CWa5tR76eMWjNWdCCA6OurFClRVNBahWwtzOfbYEvHM,4278
|
|
9
|
+
mrt_lang-0.1.4.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
|
|
10
|
+
mrt_lang-0.1.4.dist-info/entry_points.txt,sha256=W9v9wmK8xQ_y1asNJw0JNTrRr728mft4o7rhfdatgMw,42
|
|
11
|
+
mrt_lang-0.1.4.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
12
|
+
mrt_lang-0.1.4.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
src
|
src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
src/__main__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .lexer import Lexer
|
|
3
|
+
from .parser import Parser
|
|
4
|
+
from .interpreter import Interpreter
|
|
5
|
+
|
|
6
|
+
def run_file(path: str):
|
|
7
|
+
with open(path, 'r') as file:
|
|
8
|
+
source = file.read()
|
|
9
|
+
run(source)
|
|
10
|
+
|
|
11
|
+
def run(source: str):
|
|
12
|
+
# Create lexer and generate tokens
|
|
13
|
+
lexer = Lexer(source)
|
|
14
|
+
tokens = lexer.scan_tokens()
|
|
15
|
+
|
|
16
|
+
# Parse tokens into AST
|
|
17
|
+
parser = Parser(tokens)
|
|
18
|
+
statements = parser.parse()
|
|
19
|
+
|
|
20
|
+
# Interpret the AST
|
|
21
|
+
interpreter = Interpreter()
|
|
22
|
+
interpreter.interpret(statements)
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
if len(sys.argv) != 2:
|
|
26
|
+
print("Usage: python -m mrt <script>")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
run_file(sys.argv[1])
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
src/ast.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Any, Optional
|
|
3
|
+
|
|
4
|
+
# Base class for all AST nodes
|
|
5
|
+
class Expr:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
class Stmt:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Binary(Expr):
|
|
13
|
+
left: Expr
|
|
14
|
+
operator: 'Token'
|
|
15
|
+
right: Expr
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Grouping(Expr):
|
|
19
|
+
expression: Expr
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Literal(Expr):
|
|
23
|
+
value: Any
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Unary(Expr):
|
|
27
|
+
operator: 'Token'
|
|
28
|
+
right: Expr
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Variable(Expr):
|
|
32
|
+
name: 'Token'
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Assign(Expr):
|
|
36
|
+
name: 'Token'
|
|
37
|
+
value: Expr
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Call(Expr):
|
|
41
|
+
callee: Expr
|
|
42
|
+
paren: 'Token'
|
|
43
|
+
arguments: List[Expr]
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Array(Expr):
|
|
47
|
+
elements: List[Expr]
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ArrayAccess(Expr):
|
|
51
|
+
array: Expr
|
|
52
|
+
index: Expr
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ArrayAssign(Expr):
|
|
56
|
+
array: Expr
|
|
57
|
+
index: Expr
|
|
58
|
+
value: Expr
|
|
59
|
+
|
|
60
|
+
# Statement nodes
|
|
61
|
+
@dataclass
|
|
62
|
+
class Expression(Stmt):
|
|
63
|
+
expression: Expr
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Function(Stmt):
|
|
67
|
+
name: 'Token'
|
|
68
|
+
params: List['Token']
|
|
69
|
+
body: List[Stmt]
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class If(Stmt):
|
|
73
|
+
condition: Expr
|
|
74
|
+
then_branch: Stmt
|
|
75
|
+
else_branch: Optional[Stmt]
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Return(Stmt):
|
|
79
|
+
keyword: 'Token'
|
|
80
|
+
value: Optional[Expr]
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class While(Stmt):
|
|
84
|
+
condition: Expr
|
|
85
|
+
body: Stmt
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class Block(Stmt):
|
|
89
|
+
statements: List[Stmt]
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Print(Stmt):
|
|
93
|
+
expression: Expr
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Var(Stmt):
|
|
97
|
+
name: 'Token'
|
|
98
|
+
initializer: Optional[Expr]
|
src/interpreter.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from .ast import *
|
|
3
|
+
from .lexer import Token, TokenType
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
class MRTFunction:
|
|
7
|
+
def __init__(self, declaration: Function, closure: 'Environment'):
|
|
8
|
+
self.declaration = declaration
|
|
9
|
+
self.closure = closure
|
|
10
|
+
|
|
11
|
+
def call(self, interpreter: 'Interpreter', arguments: List[Any]) -> Any:
|
|
12
|
+
environment = Environment(self.closure)
|
|
13
|
+
for param, arg in zip(self.declaration.params, arguments):
|
|
14
|
+
environment.define(param.lexeme, arg)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
interpreter.execute_block(self.declaration.body, environment)
|
|
18
|
+
return None
|
|
19
|
+
except Return as return_value:
|
|
20
|
+
return return_value.value
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"<function {self.declaration.name.lexeme}>"
|
|
24
|
+
|
|
25
|
+
class Return(Exception):
|
|
26
|
+
def __init__(self, value: Any):
|
|
27
|
+
self.value = value
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
class Environment:
|
|
31
|
+
def __init__(self, enclosing: Optional['Environment'] = None):
|
|
32
|
+
self.values: Dict[str, Any] = {}
|
|
33
|
+
self.enclosing = enclosing
|
|
34
|
+
|
|
35
|
+
def define(self, name: str, value: Any):
|
|
36
|
+
self.values[name] = value
|
|
37
|
+
|
|
38
|
+
def get(self, name: Token) -> Any:
|
|
39
|
+
if name.lexeme in self.values:
|
|
40
|
+
return self.values[name.lexeme]
|
|
41
|
+
|
|
42
|
+
if self.enclosing:
|
|
43
|
+
return self.enclosing.get(name)
|
|
44
|
+
|
|
45
|
+
raise RuntimeError(f"Undefined variable '{name.lexeme}'.")
|
|
46
|
+
|
|
47
|
+
def assign(self, name: Token, value: Any):
|
|
48
|
+
if name.lexeme in self.values:
|
|
49
|
+
self.values[name.lexeme] = value
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if self.enclosing:
|
|
53
|
+
self.enclosing.assign(name, value)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
raise RuntimeError(f"Undefined variable '{name.lexeme}'.")
|
|
57
|
+
|
|
58
|
+
class MRTBuiltin:
|
|
59
|
+
@staticmethod
|
|
60
|
+
def len(*args):
|
|
61
|
+
if len(args) != 1:
|
|
62
|
+
raise RuntimeError("len() takes exactly one argument.")
|
|
63
|
+
if not isinstance(args[0], list):
|
|
64
|
+
raise RuntimeError("len() argument must be an array.")
|
|
65
|
+
return float(len(args[0]))
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def push(*args):
|
|
69
|
+
if len(args) != 2:
|
|
70
|
+
raise RuntimeError("push() takes exactly two arguments.")
|
|
71
|
+
if not isinstance(args[0], list):
|
|
72
|
+
raise RuntimeError("First argument to push() must be an array.")
|
|
73
|
+
args[0].append(args[1])
|
|
74
|
+
return args[1]
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def pop(*args):
|
|
78
|
+
if len(args) != 1:
|
|
79
|
+
raise RuntimeError("pop() takes exactly one argument.")
|
|
80
|
+
if not isinstance(args[0], list):
|
|
81
|
+
raise RuntimeError("pop() argument must be an array.")
|
|
82
|
+
if not args[0]:
|
|
83
|
+
raise RuntimeError("Cannot pop from empty array.")
|
|
84
|
+
return args[0].pop()
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def slice(*args):
|
|
88
|
+
if len(args) not in [2, 3]:
|
|
89
|
+
raise RuntimeError("slice() takes 2 or 3 arguments.")
|
|
90
|
+
if not isinstance(args[0], list):
|
|
91
|
+
raise RuntimeError("First argument to slice() must be an array.")
|
|
92
|
+
|
|
93
|
+
arr = args[0]
|
|
94
|
+
start = int(args[1]) if isinstance(args[1], (int, float)) else 0
|
|
95
|
+
end = int(args[2]) if len(args) > 2 and isinstance(args[2], (int, float)) else len(arr)
|
|
96
|
+
|
|
97
|
+
if start < 0:
|
|
98
|
+
start = len(arr) + start
|
|
99
|
+
if end < 0:
|
|
100
|
+
end = len(arr) + end
|
|
101
|
+
|
|
102
|
+
return arr[start:end]
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def join(*args):
|
|
106
|
+
if len(args) not in [1, 2]:
|
|
107
|
+
raise RuntimeError("join() takes 1 or 2 arguments.")
|
|
108
|
+
if not isinstance(args[0], list):
|
|
109
|
+
raise RuntimeError("First argument to join() must be an array.")
|
|
110
|
+
|
|
111
|
+
separator = str(args[1]) if len(args) > 1 else ""
|
|
112
|
+
return separator.join(str(x) for x in args[0])
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def indexOf(*args):
|
|
116
|
+
if len(args) != 2:
|
|
117
|
+
raise RuntimeError("indexOf() takes exactly 2 arguments.")
|
|
118
|
+
if not isinstance(args[0], list):
|
|
119
|
+
raise RuntimeError("First argument to indexOf() must be an array.")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
return float(args[0].index(args[1]))
|
|
123
|
+
except ValueError:
|
|
124
|
+
return -1.0
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def split(*args):
|
|
128
|
+
if len(args) not in [1, 2]:
|
|
129
|
+
raise RuntimeError("split() takes 1 or 2 arguments.")
|
|
130
|
+
if not isinstance(args[0], str):
|
|
131
|
+
raise RuntimeError("First argument to split() must be a string.")
|
|
132
|
+
|
|
133
|
+
separator = str(args[1]) if len(args) > 1 else " "
|
|
134
|
+
return args[0].split(separator)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def substring(*args):
|
|
138
|
+
if len(args) not in [2, 3]:
|
|
139
|
+
raise RuntimeError("substring() takes 2 or 3 arguments.")
|
|
140
|
+
if not isinstance(args[0], str):
|
|
141
|
+
raise RuntimeError("First argument to substring() must be a string.")
|
|
142
|
+
|
|
143
|
+
text = args[0]
|
|
144
|
+
start = int(args[1]) if isinstance(args[1], (int, float)) else 0
|
|
145
|
+
end = int(args[2]) if len(args) > 2 and isinstance(args[2], (int, float)) else len(text)
|
|
146
|
+
|
|
147
|
+
if start < 0:
|
|
148
|
+
start = len(text) + start
|
|
149
|
+
if end < 0:
|
|
150
|
+
end = len(text) + end
|
|
151
|
+
|
|
152
|
+
return text[start:end]
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def toUpper(*args):
|
|
156
|
+
if len(args) != 1:
|
|
157
|
+
raise RuntimeError("toUpper() takes exactly one argument.")
|
|
158
|
+
if not isinstance(args[0], str):
|
|
159
|
+
raise RuntimeError("toUpper() argument must be a string.")
|
|
160
|
+
return args[0].upper()
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def toLower(*args):
|
|
164
|
+
if len(args) != 1:
|
|
165
|
+
raise RuntimeError("toLower() takes exactly one argument.")
|
|
166
|
+
if not isinstance(args[0], str):
|
|
167
|
+
raise RuntimeError("toLower() argument must be a string.")
|
|
168
|
+
return args[0].lower()
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def trim(*args):
|
|
172
|
+
if len(args) != 1:
|
|
173
|
+
raise RuntimeError("trim() takes exactly one argument.")
|
|
174
|
+
if not isinstance(args[0], str):
|
|
175
|
+
raise RuntimeError("trim() argument must be a string.")
|
|
176
|
+
return args[0].strip()
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def replace(*args):
|
|
180
|
+
if len(args) != 3:
|
|
181
|
+
raise RuntimeError("replace() takes exactly 3 arguments.")
|
|
182
|
+
if not isinstance(args[0], str):
|
|
183
|
+
raise RuntimeError("First argument to replace() must be a string.")
|
|
184
|
+
return str(args[0]).replace(str(args[1]), str(args[2]))
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def startsWith(*args):
|
|
188
|
+
if len(args) != 2:
|
|
189
|
+
raise RuntimeError("startsWith() takes exactly 2 arguments.")
|
|
190
|
+
if not isinstance(args[0], str):
|
|
191
|
+
raise RuntimeError("First argument to startsWith() must be a string.")
|
|
192
|
+
return args[0].startswith(str(args[1]))
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def endsWith(*args):
|
|
196
|
+
if len(args) != 2:
|
|
197
|
+
raise RuntimeError("endsWith() takes exactly 2 arguments.")
|
|
198
|
+
if not isinstance(args[0], str):
|
|
199
|
+
raise RuntimeError("First argument to endsWith() must be a string.")
|
|
200
|
+
return args[0].endswith(str(args[1]))
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def contains(*args):
|
|
204
|
+
if len(args) != 2:
|
|
205
|
+
raise RuntimeError("contains() takes exactly 2 arguments.")
|
|
206
|
+
if not isinstance(args[0], str):
|
|
207
|
+
raise RuntimeError("First argument to contains() must be a string.")
|
|
208
|
+
return str(args[1]) in args[0]
|
|
209
|
+
|
|
210
|
+
class Interpreter:
|
|
211
|
+
def __init__(self):
|
|
212
|
+
self.globals = Environment()
|
|
213
|
+
self.environment = self.globals
|
|
214
|
+
self.output = [] # Capture output for web playground
|
|
215
|
+
|
|
216
|
+
# Add built-in functions
|
|
217
|
+
self.globals.define("print", self.print_function)
|
|
218
|
+
self.globals.define("len", MRTBuiltin.len)
|
|
219
|
+
self.globals.define("push", MRTBuiltin.push)
|
|
220
|
+
self.globals.define("pop", MRTBuiltin.pop)
|
|
221
|
+
self.globals.define("slice", MRTBuiltin.slice)
|
|
222
|
+
self.globals.define("join", MRTBuiltin.join)
|
|
223
|
+
self.globals.define("indexOf", MRTBuiltin.indexOf)
|
|
224
|
+
# Add string functions
|
|
225
|
+
self.globals.define("split", MRTBuiltin.split)
|
|
226
|
+
self.globals.define("substring", MRTBuiltin.substring)
|
|
227
|
+
self.globals.define("toUpper", MRTBuiltin.toUpper)
|
|
228
|
+
self.globals.define("toLower", MRTBuiltin.toLower)
|
|
229
|
+
self.globals.define("trim", MRTBuiltin.trim)
|
|
230
|
+
self.globals.define("replace", MRTBuiltin.replace)
|
|
231
|
+
self.globals.define("startsWith", MRTBuiltin.startsWith)
|
|
232
|
+
self.globals.define("endsWith", MRTBuiltin.endsWith)
|
|
233
|
+
self.globals.define("contains", MRTBuiltin.contains)
|
|
234
|
+
|
|
235
|
+
def print_function(self, *args):
|
|
236
|
+
"""Custom print function that captures output"""
|
|
237
|
+
output_str = " ".join(str(arg) for arg in args)
|
|
238
|
+
self.output.append(output_str)
|
|
239
|
+
print(output_str) # Also print to console for local execution
|
|
240
|
+
sys.stdout.flush()
|
|
241
|
+
|
|
242
|
+
def get_output(self):
|
|
243
|
+
"""Get captured output for web playground"""
|
|
244
|
+
return "\n".join(self.output)
|
|
245
|
+
|
|
246
|
+
def clear_output(self):
|
|
247
|
+
"""Clear captured output"""
|
|
248
|
+
self.output = []
|
|
249
|
+
|
|
250
|
+
def interpret(self, statements: List[Stmt]):
|
|
251
|
+
try:
|
|
252
|
+
self.clear_output()
|
|
253
|
+
# First pass: define all functions
|
|
254
|
+
for statement in statements:
|
|
255
|
+
if isinstance(statement, Function):
|
|
256
|
+
self.execute(statement)
|
|
257
|
+
|
|
258
|
+
# Second pass: look for and execute main function
|
|
259
|
+
main_func = None
|
|
260
|
+
try:
|
|
261
|
+
main_token = Token(TokenType.IDENTIFIER, "main", None, 1)
|
|
262
|
+
main_func = self.environment.get(main_token)
|
|
263
|
+
except RuntimeError:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
if main_func and isinstance(main_func, MRTFunction):
|
|
267
|
+
main_func.call(self, [])
|
|
268
|
+
else:
|
|
269
|
+
# If no main function, execute all non-function statements
|
|
270
|
+
for statement in statements:
|
|
271
|
+
if not isinstance(statement, Function):
|
|
272
|
+
self.execute(statement)
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
error_msg = f"Runtime Error: {str(e)}"
|
|
276
|
+
self.output.append(error_msg)
|
|
277
|
+
print(error_msg)
|
|
278
|
+
|
|
279
|
+
def execute(self, stmt: Stmt):
|
|
280
|
+
match stmt:
|
|
281
|
+
case Block():
|
|
282
|
+
self.execute_block(stmt.statements, Environment(self.environment))
|
|
283
|
+
case Expression():
|
|
284
|
+
self.evaluate(stmt.expression)
|
|
285
|
+
case Function():
|
|
286
|
+
function = MRTFunction(stmt, self.environment)
|
|
287
|
+
self.environment.define(stmt.name.lexeme, function)
|
|
288
|
+
case If():
|
|
289
|
+
if self.is_truthy(self.evaluate(stmt.condition)):
|
|
290
|
+
self.execute(stmt.then_branch)
|
|
291
|
+
elif stmt.else_branch:
|
|
292
|
+
self.execute(stmt.else_branch)
|
|
293
|
+
case Print():
|
|
294
|
+
value = self.evaluate(stmt.expression)
|
|
295
|
+
self.print_function(value)
|
|
296
|
+
case Return():
|
|
297
|
+
value = None
|
|
298
|
+
if stmt.value:
|
|
299
|
+
value = self.evaluate(stmt.value)
|
|
300
|
+
raise Return(value)
|
|
301
|
+
case Var():
|
|
302
|
+
value = None
|
|
303
|
+
if stmt.initializer:
|
|
304
|
+
value = self.evaluate(stmt.initializer)
|
|
305
|
+
self.environment.define(stmt.name.lexeme, value)
|
|
306
|
+
case While():
|
|
307
|
+
while self.is_truthy(self.evaluate(stmt.condition)):
|
|
308
|
+
self.execute(stmt.body)
|
|
309
|
+
|
|
310
|
+
def execute_block(self, statements: List[Stmt], environment: Environment):
|
|
311
|
+
previous = self.environment
|
|
312
|
+
try:
|
|
313
|
+
self.environment = environment
|
|
314
|
+
for statement in statements:
|
|
315
|
+
self.execute(statement)
|
|
316
|
+
finally:
|
|
317
|
+
self.environment = previous
|
|
318
|
+
|
|
319
|
+
def evaluate(self, expr: Expr) -> Any:
|
|
320
|
+
match expr:
|
|
321
|
+
case Array():
|
|
322
|
+
return [self.evaluate(element) for element in expr.elements]
|
|
323
|
+
case ArrayAccess():
|
|
324
|
+
array = self.evaluate(expr.array)
|
|
325
|
+
index = self.evaluate(expr.index)
|
|
326
|
+
if not isinstance(array, list):
|
|
327
|
+
raise RuntimeError("Can only index into arrays.")
|
|
328
|
+
if not isinstance(index, (int, float)):
|
|
329
|
+
raise RuntimeError("Array index must be a number.")
|
|
330
|
+
index = int(index)
|
|
331
|
+
if index < 0 or index >= len(array):
|
|
332
|
+
raise RuntimeError("Array index out of bounds.")
|
|
333
|
+
return array[index]
|
|
334
|
+
case ArrayAssign():
|
|
335
|
+
array = self.evaluate(expr.array)
|
|
336
|
+
index = self.evaluate(expr.index)
|
|
337
|
+
if not isinstance(array, list):
|
|
338
|
+
raise RuntimeError("Can only index into arrays.")
|
|
339
|
+
if not isinstance(index, (int, float)):
|
|
340
|
+
raise RuntimeError("Array index must be a number.")
|
|
341
|
+
index = int(index)
|
|
342
|
+
if index < 0 or index >= len(array):
|
|
343
|
+
raise RuntimeError("Array index out of bounds.")
|
|
344
|
+
value = self.evaluate(expr.value)
|
|
345
|
+
array[index] = value
|
|
346
|
+
return value
|
|
347
|
+
case Assign():
|
|
348
|
+
value = self.evaluate(expr.value)
|
|
349
|
+
self.environment.assign(expr.name, value)
|
|
350
|
+
return value
|
|
351
|
+
case Binary():
|
|
352
|
+
left = self.evaluate(expr.left)
|
|
353
|
+
right = self.evaluate(expr.right)
|
|
354
|
+
|
|
355
|
+
match expr.operator.type:
|
|
356
|
+
case TokenType.PLUS:
|
|
357
|
+
# Handle string concatenation
|
|
358
|
+
if isinstance(left, str) or isinstance(right, str):
|
|
359
|
+
return str(left) + str(right)
|
|
360
|
+
return float(left) + float(right)
|
|
361
|
+
case TokenType.MINUS:
|
|
362
|
+
return float(left) - float(right)
|
|
363
|
+
case TokenType.MULTIPLY:
|
|
364
|
+
return float(left) * float(right)
|
|
365
|
+
case TokenType.DIVIDE:
|
|
366
|
+
if float(right) == 0:
|
|
367
|
+
raise RuntimeError("Division by zero.")
|
|
368
|
+
return float(left) / float(right)
|
|
369
|
+
case TokenType.EQUALS:
|
|
370
|
+
return self.is_equal(left, right)
|
|
371
|
+
case TokenType.GREATER:
|
|
372
|
+
return float(left) > float(right)
|
|
373
|
+
case TokenType.LESS:
|
|
374
|
+
return float(left) < float(right)
|
|
375
|
+
case Call():
|
|
376
|
+
callee = self.evaluate(expr.callee)
|
|
377
|
+
arguments = [self.evaluate(arg) for arg in expr.arguments]
|
|
378
|
+
|
|
379
|
+
if isinstance(callee, MRTFunction):
|
|
380
|
+
if len(arguments) != len(callee.declaration.params):
|
|
381
|
+
raise RuntimeError(f"Expected {len(callee.declaration.params)} arguments but got {len(arguments)}.")
|
|
382
|
+
return callee.call(self, arguments)
|
|
383
|
+
|
|
384
|
+
if callable(callee):
|
|
385
|
+
return callee(*arguments)
|
|
386
|
+
|
|
387
|
+
raise RuntimeError("Can only call functions.")
|
|
388
|
+
case Grouping():
|
|
389
|
+
return self.evaluate(expr.expression)
|
|
390
|
+
case Literal():
|
|
391
|
+
return expr.value
|
|
392
|
+
case Unary():
|
|
393
|
+
right = self.evaluate(expr.right)
|
|
394
|
+
|
|
395
|
+
if expr.operator.type == TokenType.MINUS:
|
|
396
|
+
return -float(right)
|
|
397
|
+
case Variable():
|
|
398
|
+
return self.environment.get(expr.name)
|
|
399
|
+
|
|
400
|
+
def is_equal(self, a: Any, b: Any) -> bool:
|
|
401
|
+
"""Check equality between two values"""
|
|
402
|
+
if a is None and b is None:
|
|
403
|
+
return True
|
|
404
|
+
if a is None:
|
|
405
|
+
return False
|
|
406
|
+
return a == b
|
|
407
|
+
|
|
408
|
+
def is_truthy(self, obj: Any) -> bool:
|
|
409
|
+
if obj is None:
|
|
410
|
+
return False
|
|
411
|
+
if isinstance(obj, bool):
|
|
412
|
+
return obj
|
|
413
|
+
return True
|
src/lexer.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
class TokenType(Enum):
|
|
6
|
+
# Keywords
|
|
7
|
+
FUNC = auto()
|
|
8
|
+
RETURN = auto()
|
|
9
|
+
IF = auto()
|
|
10
|
+
ELSE = auto()
|
|
11
|
+
WHILE = auto()
|
|
12
|
+
FOR = auto()
|
|
13
|
+
PRINT = auto()
|
|
14
|
+
VAR = auto()
|
|
15
|
+
TRUE = auto()
|
|
16
|
+
FALSE = auto()
|
|
17
|
+
|
|
18
|
+
# Literals
|
|
19
|
+
IDENTIFIER = auto()
|
|
20
|
+
NUMBER = auto()
|
|
21
|
+
STRING = auto()
|
|
22
|
+
|
|
23
|
+
# Operators
|
|
24
|
+
PLUS = auto()
|
|
25
|
+
MINUS = auto()
|
|
26
|
+
MULTIPLY = auto()
|
|
27
|
+
DIVIDE = auto()
|
|
28
|
+
ASSIGN = auto()
|
|
29
|
+
EQUALS = auto()
|
|
30
|
+
NOT_EQUALS = auto()
|
|
31
|
+
GREATER = auto()
|
|
32
|
+
GREATER_EQUAL = auto()
|
|
33
|
+
LESS = auto()
|
|
34
|
+
LESS_EQUAL = auto()
|
|
35
|
+
|
|
36
|
+
# Delimiters
|
|
37
|
+
LPAREN = auto()
|
|
38
|
+
RPAREN = auto()
|
|
39
|
+
LBRACE = auto()
|
|
40
|
+
RBRACE = auto()
|
|
41
|
+
LBRACKET = auto()
|
|
42
|
+
RBRACKET = auto()
|
|
43
|
+
COMMA = auto()
|
|
44
|
+
SEMICOLON = auto()
|
|
45
|
+
|
|
46
|
+
# Special
|
|
47
|
+
EOF = auto()
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Token:
|
|
51
|
+
type: TokenType
|
|
52
|
+
lexeme: str
|
|
53
|
+
literal: Optional[object]
|
|
54
|
+
line: int
|
|
55
|
+
|
|
56
|
+
class Lexer:
|
|
57
|
+
def __init__(self, source: str):
|
|
58
|
+
self.source = source
|
|
59
|
+
self.tokens: List[Token] = []
|
|
60
|
+
self.start = 0
|
|
61
|
+
self.current = 0
|
|
62
|
+
self.line = 1
|
|
63
|
+
|
|
64
|
+
self.keywords = {
|
|
65
|
+
"func": TokenType.FUNC,
|
|
66
|
+
"return": TokenType.RETURN,
|
|
67
|
+
"if": TokenType.IF,
|
|
68
|
+
"else": TokenType.ELSE,
|
|
69
|
+
"while": TokenType.WHILE,
|
|
70
|
+
"for": TokenType.FOR,
|
|
71
|
+
"print": TokenType.PRINT,
|
|
72
|
+
"var": TokenType.VAR,
|
|
73
|
+
"true": TokenType.TRUE,
|
|
74
|
+
"false": TokenType.FALSE,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def scan_tokens(self) -> List[Token]:
|
|
78
|
+
while not self.is_at_end():
|
|
79
|
+
self.start = self.current
|
|
80
|
+
self.scan_token()
|
|
81
|
+
|
|
82
|
+
self.tokens.append(Token(TokenType.EOF, "", None, self.line))
|
|
83
|
+
return self.tokens
|
|
84
|
+
|
|
85
|
+
def scan_token(self):
|
|
86
|
+
c = self.advance()
|
|
87
|
+
match c:
|
|
88
|
+
case '(': self.add_token(TokenType.LPAREN)
|
|
89
|
+
case ')': self.add_token(TokenType.RPAREN)
|
|
90
|
+
case '{': self.add_token(TokenType.LBRACE)
|
|
91
|
+
case '}': self.add_token(TokenType.RBRACE)
|
|
92
|
+
case '[': self.add_token(TokenType.LBRACKET)
|
|
93
|
+
case ']': self.add_token(TokenType.RBRACKET)
|
|
94
|
+
case ',': self.add_token(TokenType.COMMA)
|
|
95
|
+
case ';': self.add_token(TokenType.SEMICOLON)
|
|
96
|
+
case '+': self.add_token(TokenType.PLUS)
|
|
97
|
+
case '-': self.add_token(TokenType.MINUS)
|
|
98
|
+
case '*': self.add_token(TokenType.MULTIPLY)
|
|
99
|
+
case '/':
|
|
100
|
+
if self.match('/'):
|
|
101
|
+
# Comment goes until end of line
|
|
102
|
+
while self.peek() != '\n' and not self.is_at_end():
|
|
103
|
+
self.advance()
|
|
104
|
+
elif self.match('*'):
|
|
105
|
+
# Multi-line comment
|
|
106
|
+
self.block_comment()
|
|
107
|
+
else:
|
|
108
|
+
self.add_token(TokenType.DIVIDE)
|
|
109
|
+
case ' ' | '\r' | '\t': pass # Ignore whitespace
|
|
110
|
+
case '\n': self.line += 1
|
|
111
|
+
case '"': self.string()
|
|
112
|
+
case '>':
|
|
113
|
+
if self.match('='):
|
|
114
|
+
self.add_token(TokenType.GREATER_EQUAL)
|
|
115
|
+
else:
|
|
116
|
+
self.add_token(TokenType.GREATER)
|
|
117
|
+
case '<':
|
|
118
|
+
if self.match('='):
|
|
119
|
+
self.add_token(TokenType.LESS_EQUAL)
|
|
120
|
+
else:
|
|
121
|
+
self.add_token(TokenType.LESS)
|
|
122
|
+
case '=':
|
|
123
|
+
if self.match('='):
|
|
124
|
+
self.add_token(TokenType.EQUALS)
|
|
125
|
+
else:
|
|
126
|
+
self.add_token(TokenType.ASSIGN)
|
|
127
|
+
case '!':
|
|
128
|
+
if self.match('='):
|
|
129
|
+
self.add_token(TokenType.NOT_EQUALS)
|
|
130
|
+
else:
|
|
131
|
+
raise Exception(f"Unexpected character '!' at line {self.line}")
|
|
132
|
+
case _:
|
|
133
|
+
if self.is_digit(c):
|
|
134
|
+
self.number()
|
|
135
|
+
elif self.is_alpha(c):
|
|
136
|
+
self.identifier()
|
|
137
|
+
else:
|
|
138
|
+
raise Exception(f"Unexpected character '{c}' at line {self.line}")
|
|
139
|
+
|
|
140
|
+
def block_comment(self):
|
|
141
|
+
"""Handle multi-line comments /* ... */"""
|
|
142
|
+
while not self.is_at_end():
|
|
143
|
+
if self.peek() == '*' and self.peek_next() == '/':
|
|
144
|
+
# Consume the closing */
|
|
145
|
+
self.advance() # consume *
|
|
146
|
+
self.advance() # consume /
|
|
147
|
+
return
|
|
148
|
+
if self.peek() == '\n':
|
|
149
|
+
self.line += 1
|
|
150
|
+
self.advance()
|
|
151
|
+
|
|
152
|
+
# If we reach here, the comment was not closed
|
|
153
|
+
raise Exception(f"Unterminated comment starting at line {self.line}")
|
|
154
|
+
|
|
155
|
+
def identifier(self):
|
|
156
|
+
while self.is_alphanumeric(self.peek()):
|
|
157
|
+
self.advance()
|
|
158
|
+
|
|
159
|
+
text = self.source[self.start:self.current]
|
|
160
|
+
token_type = self.keywords.get(text, TokenType.IDENTIFIER)
|
|
161
|
+
|
|
162
|
+
# Handle boolean literals
|
|
163
|
+
if token_type == TokenType.TRUE:
|
|
164
|
+
self.add_token(token_type, True)
|
|
165
|
+
elif token_type == TokenType.FALSE:
|
|
166
|
+
self.add_token(token_type, False)
|
|
167
|
+
else:
|
|
168
|
+
self.add_token(token_type)
|
|
169
|
+
|
|
170
|
+
def number(self):
|
|
171
|
+
while self.is_digit(self.peek()):
|
|
172
|
+
self.advance()
|
|
173
|
+
|
|
174
|
+
# Look for decimal point
|
|
175
|
+
if self.peek() == '.' and self.is_digit(self.peek_next()):
|
|
176
|
+
self.advance() # Consume the "."
|
|
177
|
+
while self.is_digit(self.peek()):
|
|
178
|
+
self.advance()
|
|
179
|
+
|
|
180
|
+
value = float(self.source[self.start:self.current])
|
|
181
|
+
self.add_token(TokenType.NUMBER, value)
|
|
182
|
+
|
|
183
|
+
def string(self):
|
|
184
|
+
# Find the closing quote
|
|
185
|
+
start_line = self.line
|
|
186
|
+
while self.peek() != '"' and not self.is_at_end():
|
|
187
|
+
if self.peek() == '\n':
|
|
188
|
+
self.line += 1
|
|
189
|
+
self.advance()
|
|
190
|
+
|
|
191
|
+
if self.is_at_end():
|
|
192
|
+
raise Exception(f"Unterminated string starting at line {start_line}")
|
|
193
|
+
|
|
194
|
+
# Skip the closing quote
|
|
195
|
+
self.advance()
|
|
196
|
+
|
|
197
|
+
# Get the string value (without quotes)
|
|
198
|
+
value = self.source[self.start + 1:self.current - 1]
|
|
199
|
+
self.add_token(TokenType.STRING, value)
|
|
200
|
+
|
|
201
|
+
def match(self, expected: str) -> bool:
|
|
202
|
+
if self.is_at_end():
|
|
203
|
+
return False
|
|
204
|
+
if self.source[self.current] != expected:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
self.current += 1
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
def peek(self) -> str:
|
|
211
|
+
if self.is_at_end():
|
|
212
|
+
return '\0'
|
|
213
|
+
return self.source[self.current]
|
|
214
|
+
|
|
215
|
+
def peek_next(self) -> str:
|
|
216
|
+
if self.current + 1 >= len(self.source):
|
|
217
|
+
return '\0'
|
|
218
|
+
return self.source[self.current + 1]
|
|
219
|
+
|
|
220
|
+
def is_alpha(self, c: str) -> bool:
|
|
221
|
+
return ('a' <= c <= 'z') or ('A' <= c <= 'Z') or c == '_'
|
|
222
|
+
|
|
223
|
+
def is_digit(self, c: str) -> bool:
|
|
224
|
+
return '0' <= c <= '9'
|
|
225
|
+
|
|
226
|
+
def is_alphanumeric(self, c: str) -> bool:
|
|
227
|
+
return self.is_alpha(c) or self.is_digit(c)
|
|
228
|
+
|
|
229
|
+
def is_at_end(self) -> bool:
|
|
230
|
+
return self.current >= len(self.source)
|
|
231
|
+
|
|
232
|
+
def advance(self) -> str:
|
|
233
|
+
self.current += 1
|
|
234
|
+
return self.source[self.current - 1]
|
|
235
|
+
|
|
236
|
+
def add_token(self, type: TokenType, literal: Optional[object] = None):
|
|
237
|
+
text = self.source[self.start:self.current]
|
|
238
|
+
self.tokens.append(Token(type, text, literal, self.line))
|
src/parser.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from .lexer import Token, TokenType
|
|
3
|
+
from .ast import *
|
|
4
|
+
|
|
5
|
+
class Parser:
|
|
6
|
+
def __init__(self, tokens: List[Token]):
|
|
7
|
+
self.tokens = tokens
|
|
8
|
+
self.current = 0
|
|
9
|
+
|
|
10
|
+
def parse(self) -> List[Stmt]:
|
|
11
|
+
statements = []
|
|
12
|
+
while not self.is_at_end():
|
|
13
|
+
stmt = self.declaration()
|
|
14
|
+
if stmt:
|
|
15
|
+
statements.append(stmt)
|
|
16
|
+
return statements
|
|
17
|
+
|
|
18
|
+
def declaration(self) -> Optional[Stmt]:
|
|
19
|
+
try:
|
|
20
|
+
if self.match(TokenType.FUNC):
|
|
21
|
+
return self.function("function")
|
|
22
|
+
if self.match(TokenType.VAR):
|
|
23
|
+
return self.var_declaration()
|
|
24
|
+
return self.statement()
|
|
25
|
+
except ParseError:
|
|
26
|
+
self.synchronize()
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
def function(self, kind: str) -> Function:
|
|
30
|
+
name = self.consume(TokenType.IDENTIFIER, f"Expect {kind} name.")
|
|
31
|
+
|
|
32
|
+
self.consume(TokenType.LPAREN, f"Expect '(' after {kind} name.")
|
|
33
|
+
parameters = []
|
|
34
|
+
if not self.check(TokenType.RPAREN):
|
|
35
|
+
while True:
|
|
36
|
+
if len(parameters) >= 255:
|
|
37
|
+
self.error(self.peek(), "Can't have more than 255 parameters.")
|
|
38
|
+
parameters.append(
|
|
39
|
+
self.consume(TokenType.IDENTIFIER, "Expect parameter name."))
|
|
40
|
+
if not self.match(TokenType.COMMA):
|
|
41
|
+
break
|
|
42
|
+
self.consume(TokenType.RPAREN, "Expect ')' after parameters.")
|
|
43
|
+
|
|
44
|
+
self.consume(TokenType.LBRACE, f"Expect '{{' before {kind} body.")
|
|
45
|
+
body = self.block()
|
|
46
|
+
return Function(name, parameters, body)
|
|
47
|
+
|
|
48
|
+
def statement(self) -> Stmt:
|
|
49
|
+
if self.match(TokenType.FOR):
|
|
50
|
+
return self.for_statement()
|
|
51
|
+
if self.match(TokenType.IF):
|
|
52
|
+
return self.if_statement()
|
|
53
|
+
if self.match(TokenType.RETURN):
|
|
54
|
+
return self.return_statement()
|
|
55
|
+
if self.match(TokenType.WHILE):
|
|
56
|
+
return self.while_statement()
|
|
57
|
+
if self.match(TokenType.LBRACE):
|
|
58
|
+
return Block(self.block())
|
|
59
|
+
if self.match(TokenType.PRINT):
|
|
60
|
+
return self.print_statement()
|
|
61
|
+
return self.expression_statement()
|
|
62
|
+
|
|
63
|
+
def for_statement(self) -> Stmt:
|
|
64
|
+
self.consume(TokenType.LPAREN, "Expect '(' after 'for'.")
|
|
65
|
+
|
|
66
|
+
# Initializer
|
|
67
|
+
initializer = None
|
|
68
|
+
if self.match(TokenType.SEMICOLON):
|
|
69
|
+
initializer = None
|
|
70
|
+
elif self.match(TokenType.VAR):
|
|
71
|
+
initializer = self.var_declaration()
|
|
72
|
+
else:
|
|
73
|
+
initializer = self.expression_statement()
|
|
74
|
+
|
|
75
|
+
# Condition
|
|
76
|
+
condition = None
|
|
77
|
+
if not self.check(TokenType.SEMICOLON):
|
|
78
|
+
condition = self.expression()
|
|
79
|
+
self.consume(TokenType.SEMICOLON, "Expect ';' after loop condition.")
|
|
80
|
+
|
|
81
|
+
# Increment
|
|
82
|
+
increment = None
|
|
83
|
+
if not self.check(TokenType.RPAREN):
|
|
84
|
+
increment = self.expression()
|
|
85
|
+
self.consume(TokenType.RPAREN, "Expect ')' after for clauses.")
|
|
86
|
+
|
|
87
|
+
body = self.statement()
|
|
88
|
+
|
|
89
|
+
# Desugar for loop into while loop
|
|
90
|
+
if increment:
|
|
91
|
+
body = Block([body, Expression(increment)])
|
|
92
|
+
|
|
93
|
+
if not condition:
|
|
94
|
+
condition = Literal(True)
|
|
95
|
+
body = While(condition, body)
|
|
96
|
+
|
|
97
|
+
if initializer:
|
|
98
|
+
body = Block([initializer, body])
|
|
99
|
+
|
|
100
|
+
return body
|
|
101
|
+
|
|
102
|
+
def if_statement(self) -> If:
|
|
103
|
+
self.consume(TokenType.LPAREN, "Expect '(' after 'if'.")
|
|
104
|
+
condition = self.expression()
|
|
105
|
+
self.consume(TokenType.RPAREN, "Expect ')' after if condition.")
|
|
106
|
+
|
|
107
|
+
then_branch = self.statement()
|
|
108
|
+
else_branch = None
|
|
109
|
+
if self.match(TokenType.ELSE):
|
|
110
|
+
else_branch = self.statement()
|
|
111
|
+
|
|
112
|
+
return If(condition, then_branch, else_branch)
|
|
113
|
+
|
|
114
|
+
def return_statement(self) -> Return:
|
|
115
|
+
keyword = self.previous()
|
|
116
|
+
value = None
|
|
117
|
+
if not self.check(TokenType.SEMICOLON):
|
|
118
|
+
value = self.expression()
|
|
119
|
+
|
|
120
|
+
self.consume(TokenType.SEMICOLON, "Expect ';' after return value.")
|
|
121
|
+
return Return(keyword, value)
|
|
122
|
+
|
|
123
|
+
def while_statement(self) -> While:
|
|
124
|
+
self.consume(TokenType.LPAREN, "Expect '(' after 'while'.")
|
|
125
|
+
condition = self.expression()
|
|
126
|
+
self.consume(TokenType.RPAREN, "Expect ')' after condition.")
|
|
127
|
+
body = self.statement()
|
|
128
|
+
|
|
129
|
+
return While(condition, body)
|
|
130
|
+
|
|
131
|
+
def block(self) -> List[Stmt]:
|
|
132
|
+
statements = []
|
|
133
|
+
while not self.check(TokenType.RBRACE) and not self.is_at_end():
|
|
134
|
+
stmt = self.declaration()
|
|
135
|
+
if stmt:
|
|
136
|
+
statements.append(stmt)
|
|
137
|
+
|
|
138
|
+
self.consume(TokenType.RBRACE, "Expect '}' after block.")
|
|
139
|
+
return statements
|
|
140
|
+
|
|
141
|
+
def expression_statement(self) -> Stmt:
|
|
142
|
+
expr = self.expression()
|
|
143
|
+
self.consume(TokenType.SEMICOLON, "Expect ';' after expression.")
|
|
144
|
+
return Expression(expr)
|
|
145
|
+
|
|
146
|
+
def print_statement(self) -> Print:
|
|
147
|
+
value = self.expression()
|
|
148
|
+
self.consume(TokenType.SEMICOLON, "Expect ';' after value.")
|
|
149
|
+
return Print(value)
|
|
150
|
+
|
|
151
|
+
def expression(self) -> Expr:
|
|
152
|
+
return self.assignment()
|
|
153
|
+
|
|
154
|
+
def assignment(self) -> Expr:
|
|
155
|
+
expr = self.or_expression()
|
|
156
|
+
|
|
157
|
+
if self.match(TokenType.ASSIGN):
|
|
158
|
+
equals = self.previous()
|
|
159
|
+
value = self.assignment()
|
|
160
|
+
|
|
161
|
+
if isinstance(expr, Variable):
|
|
162
|
+
name = expr.name
|
|
163
|
+
return Assign(name, value)
|
|
164
|
+
elif isinstance(expr, ArrayAccess):
|
|
165
|
+
return ArrayAssign(expr.array, expr.index, value)
|
|
166
|
+
|
|
167
|
+
self.error(equals, "Invalid assignment target.")
|
|
168
|
+
|
|
169
|
+
return expr
|
|
170
|
+
|
|
171
|
+
def or_expression(self) -> Expr:
|
|
172
|
+
return self.and_expression()
|
|
173
|
+
|
|
174
|
+
def and_expression(self) -> Expr:
|
|
175
|
+
return self.equality()
|
|
176
|
+
|
|
177
|
+
def equality(self) -> Expr:
|
|
178
|
+
expr = self.comparison()
|
|
179
|
+
|
|
180
|
+
while self.match(TokenType.NOT_EQUALS, TokenType.EQUALS):
|
|
181
|
+
operator = self.previous()
|
|
182
|
+
right = self.comparison()
|
|
183
|
+
expr = Binary(expr, operator, right)
|
|
184
|
+
|
|
185
|
+
return expr
|
|
186
|
+
|
|
187
|
+
def comparison(self) -> Expr:
|
|
188
|
+
expr = self.term()
|
|
189
|
+
|
|
190
|
+
while self.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL):
|
|
191
|
+
operator = self.previous()
|
|
192
|
+
right = self.term()
|
|
193
|
+
expr = Binary(expr, operator, right)
|
|
194
|
+
|
|
195
|
+
return expr
|
|
196
|
+
|
|
197
|
+
def term(self) -> Expr:
|
|
198
|
+
expr = self.factor()
|
|
199
|
+
|
|
200
|
+
while self.match(TokenType.PLUS, TokenType.MINUS):
|
|
201
|
+
operator = self.previous()
|
|
202
|
+
right = self.factor()
|
|
203
|
+
expr = Binary(expr, operator, right)
|
|
204
|
+
|
|
205
|
+
return expr
|
|
206
|
+
|
|
207
|
+
def factor(self) -> Expr:
|
|
208
|
+
expr = self.unary()
|
|
209
|
+
|
|
210
|
+
while self.match(TokenType.MULTIPLY, TokenType.DIVIDE):
|
|
211
|
+
operator = self.previous()
|
|
212
|
+
right = self.unary()
|
|
213
|
+
expr = Binary(expr, operator, right)
|
|
214
|
+
|
|
215
|
+
return expr
|
|
216
|
+
|
|
217
|
+
def unary(self) -> Expr:
|
|
218
|
+
if self.match(TokenType.MINUS):
|
|
219
|
+
operator = self.previous()
|
|
220
|
+
right = self.unary()
|
|
221
|
+
return Unary(operator, right)
|
|
222
|
+
|
|
223
|
+
return self.call()
|
|
224
|
+
|
|
225
|
+
def call(self) -> Expr:
|
|
226
|
+
expr = self.primary()
|
|
227
|
+
|
|
228
|
+
while True:
|
|
229
|
+
if self.match(TokenType.LPAREN):
|
|
230
|
+
expr = self.finish_call(expr)
|
|
231
|
+
elif self.match(TokenType.LBRACKET):
|
|
232
|
+
expr = self.array_access(expr)
|
|
233
|
+
else:
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
return expr
|
|
237
|
+
|
|
238
|
+
def finish_call(self, callee: Expr) -> Expr:
|
|
239
|
+
arguments = []
|
|
240
|
+
if not self.check(TokenType.RPAREN):
|
|
241
|
+
while True:
|
|
242
|
+
if len(arguments) >= 255:
|
|
243
|
+
self.error(self.peek(), "Can't have more than 255 arguments.")
|
|
244
|
+
arguments.append(self.expression())
|
|
245
|
+
if not self.match(TokenType.COMMA):
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
paren = self.consume(TokenType.RPAREN, "Expect ')' after arguments.")
|
|
249
|
+
return Call(callee, paren, arguments)
|
|
250
|
+
|
|
251
|
+
def array_access(self, expr: Expr) -> Expr:
|
|
252
|
+
index = self.expression()
|
|
253
|
+
self.consume(TokenType.RBRACKET, "Expect ']' after array index.")
|
|
254
|
+
return ArrayAccess(expr, index)
|
|
255
|
+
|
|
256
|
+
def primary(self) -> Expr:
|
|
257
|
+
if self.match(TokenType.TRUE):
|
|
258
|
+
return Literal(True)
|
|
259
|
+
if self.match(TokenType.FALSE):
|
|
260
|
+
return Literal(False)
|
|
261
|
+
if self.match(TokenType.NUMBER, TokenType.STRING):
|
|
262
|
+
return Literal(self.previous().literal)
|
|
263
|
+
if self.match(TokenType.IDENTIFIER):
|
|
264
|
+
return Variable(self.previous())
|
|
265
|
+
if self.match(TokenType.LPAREN):
|
|
266
|
+
expr = self.expression()
|
|
267
|
+
self.consume(TokenType.RPAREN, "Expect ')' after expression.")
|
|
268
|
+
return Grouping(expr)
|
|
269
|
+
if self.match(TokenType.LBRACKET):
|
|
270
|
+
elements = []
|
|
271
|
+
if not self.check(TokenType.RBRACKET):
|
|
272
|
+
while True:
|
|
273
|
+
elements.append(self.expression())
|
|
274
|
+
if not self.match(TokenType.COMMA):
|
|
275
|
+
break
|
|
276
|
+
self.consume(TokenType.RBRACKET, "Expect ']' after array elements.")
|
|
277
|
+
return Array(elements)
|
|
278
|
+
|
|
279
|
+
raise self.error(self.peek(), "Expect expression.")
|
|
280
|
+
|
|
281
|
+
def var_declaration(self) -> Var:
|
|
282
|
+
name = self.consume(TokenType.IDENTIFIER, "Expect variable name.")
|
|
283
|
+
|
|
284
|
+
initializer = None
|
|
285
|
+
if self.match(TokenType.ASSIGN):
|
|
286
|
+
initializer = self.expression()
|
|
287
|
+
|
|
288
|
+
self.consume(TokenType.SEMICOLON, "Expect ';' after variable declaration.")
|
|
289
|
+
return Var(name, initializer)
|
|
290
|
+
|
|
291
|
+
def match(self, *types: TokenType) -> bool:
|
|
292
|
+
for type in types:
|
|
293
|
+
if self.check(type):
|
|
294
|
+
self.advance()
|
|
295
|
+
return True
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
def check(self, type: TokenType) -> bool:
|
|
299
|
+
if self.is_at_end():
|
|
300
|
+
return False
|
|
301
|
+
return self.peek().type == type
|
|
302
|
+
|
|
303
|
+
def advance(self) -> Token:
|
|
304
|
+
if not self.is_at_end():
|
|
305
|
+
self.current += 1
|
|
306
|
+
return self.previous()
|
|
307
|
+
|
|
308
|
+
def is_at_end(self) -> bool:
|
|
309
|
+
return self.peek().type == TokenType.EOF
|
|
310
|
+
|
|
311
|
+
def peek(self) -> Token:
|
|
312
|
+
return self.tokens[self.current]
|
|
313
|
+
|
|
314
|
+
def previous(self) -> Token:
|
|
315
|
+
return self.tokens[self.current - 1]
|
|
316
|
+
|
|
317
|
+
def consume(self, type: TokenType, message: str) -> Token:
|
|
318
|
+
if self.check(type):
|
|
319
|
+
return self.advance()
|
|
320
|
+
raise self.error(self.peek(), message)
|
|
321
|
+
|
|
322
|
+
def error(self, token: Token, message: str):
|
|
323
|
+
# TODO: Implement proper error handling
|
|
324
|
+
raise ParseError(f"Error at {token.lexeme}: {message}")
|
|
325
|
+
|
|
326
|
+
def synchronize(self):
|
|
327
|
+
self.advance()
|
|
328
|
+
|
|
329
|
+
while not self.is_at_end():
|
|
330
|
+
if self.previous().type == TokenType.SEMICOLON:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
match self.peek().type:
|
|
334
|
+
case TokenType.FUNC | TokenType.IF | TokenType.RETURN | TokenType.WHILE | TokenType.VAR | TokenType.FOR:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
self.advance()
|
|
338
|
+
|
|
339
|
+
class ParseError(Exception):
|
|
340
|
+
pass
|