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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.45.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mrt = src.__main__:main
@@ -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