├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── bin ├── pyrogue.dart └── smython.dart ├── lib ├── ast_eval.dart ├── parser.dart ├── scanner.dart ├── smython.dart ├── test_runner.dart └── token.dart ├── parser_tests.py ├── pubspec.lock ├── pubspec.yaml └── test └── smython_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .packages 4 | .vscode/ 5 | build/ 6 | doc/api/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Stefan Matthias Aust 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run-coverage: 2 | @dart run coverage:test_with_coverage 3 | @if which -s genhtml ; then \ 4 | genhtml -q coverage/lcov.info -o coverage/html ; \ 5 | else \ 6 | echo "genhtml not found, please 'brew install lcov'" ; \ 7 | fi 8 | @open coverage/html/index.html 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Smython for Dart 2 | ================ 3 | 4 | A tiny incomplete interpreter for a language similar to Python 3 with enough 5 | functionality to parse & interpret the factorial and the fibonacci function. 6 | 7 | This has been hacked together based on a similar interpreter I did some 8 | ten years ago, that time for [Objective-C](https://github.com/sma/Pyphon) 9 | and [Java](https://github.com/sma/smython3). 10 | 11 | ## Running the Code 12 | 13 | To run the included test suite, execute `dart pub get` once and then simply 14 | execute `dart run`. It should print `OK` for all code snippets run. 15 | 16 | ## Using Smython 17 | 18 | Include the `smython/smython.dart` library in your program. 19 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | strict-inference: true 7 | strict-raw-types: true 8 | errors: 9 | todo: ignore 10 | -------------------------------------------------------------------------------- /bin/pyrogue.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:smython/smython.dart'; 4 | 5 | final system = Smython(); 6 | 7 | /// Loads and runs [filename] which must be a valid Smython program. 8 | void run(String filename) { 9 | system.execute(File(filename).readAsStringSync()); 10 | } 11 | 12 | /// Tries to load and parse all files in the `pyrogue` folder. 13 | void testParse(String filename) { 14 | var failures = 0; 15 | for (final file in Directory(filename).listSync().whereType().where((f) => f.path.endsWith('.py'))) { 16 | if (file.path.contains('xref.py')) continue; 17 | try { 18 | parse(file.readAsStringSync()); 19 | } catch (err) { 20 | print('${file.path} -> $err'); 21 | failures++; 22 | } 23 | } 24 | if (failures > 0) print('Failures: $failures'); 25 | } 26 | 27 | void main() { 28 | testParse('pyrogue'); 29 | run('pyrogue/main.py'); 30 | } 31 | -------------------------------------------------------------------------------- /bin/smython.dart: -------------------------------------------------------------------------------- 1 | import 'package:smython/test_runner.dart'; 2 | 3 | void main() { 4 | run('parser_tests.py'); 5 | } 6 | -------------------------------------------------------------------------------- /lib/ast_eval.dart: -------------------------------------------------------------------------------- 1 | /// Abstract syntax tree nodes represent a Smython program and can be 2 | /// recursively evaluated to execute said program. 3 | /// 4 | /// * A [Suite] is a sequential list of statements. 5 | /// * [Stmt] is the abstract superclass for all statements. 6 | /// * [Expr] is the abstract superclass for all expressions. 7 | /// * All nodes have a `evaluate(Frame)` method. 8 | /// * Some [Expr] subclasses also support `assign(Frame,SmyValue)`. 9 | library ast_eval; 10 | 11 | import 'package:smython/smython.dart'; 12 | 13 | // -------- Suite -------- 14 | 15 | /// A suite of [Stmt]s. 16 | final class Suite { 17 | const Suite(this.stmts); 18 | final List stmts; 19 | 20 | /// Executes all [stmts] sequentially on top level, that is [ReturnStmt], 21 | /// [BreakStmt], or [ContinueStmt] are not allowed here. They will throw 22 | /// unhandled exceptions. 23 | SmyValue evaluate(Frame f) { 24 | SmyValue result = SmyValue.none; 25 | for (final stmt in stmts) { 26 | result = stmt.evaluate(f); 27 | } 28 | return result; 29 | } 30 | 31 | /// Executes all [stmts] sequentially as while evaluating a function 32 | /// body. [ReturnStmt] will stop the execution and return its value. 33 | /// [BreakStmt] or [ContinueStmt] are not allowed here. They will throw 34 | /// unhandled exceptions. 35 | SmyValue evaluateAsFunc(Frame f) { 36 | try { 37 | return evaluate(f); 38 | } on _Return catch (e) { 39 | return e.value; 40 | } 41 | } 42 | } 43 | 44 | // -------- Stmt -------- 45 | 46 | /// A statement that can be executed. 47 | sealed class Stmt { 48 | const Stmt(); 49 | 50 | SmyValue evaluate(Frame f); 51 | } 52 | 53 | /// `if test: thenSuite else: elseSuite` 54 | final class IfStmt extends Stmt { 55 | const IfStmt(this.test, this.thenSuite, this.elseSuite); 56 | final Expr test; 57 | final Suite thenSuite; 58 | final Suite elseSuite; 59 | 60 | /// Evaluates [test] and based on the boolean value of its result, 61 | /// either [thenSuite] or [elseSuite] is evaluated. 62 | @override 63 | SmyValue evaluate(Frame f) { 64 | if (test.evaluate(f).boolValue) { 65 | thenSuite.evaluate(f); 66 | } else { 67 | elseSuite.evaluate(f); 68 | } 69 | return SmyValue.none; 70 | } 71 | } 72 | 73 | /// `while test: suite else: elseSuite` 74 | final class WhileStmt extends Stmt { 75 | const WhileStmt(this.test, this.suite, this.elseSuite); 76 | final Expr test; 77 | final Suite suite; 78 | final Suite elseSuite; 79 | 80 | /// Evaluates [test] and if its boolean value is `true`, [suite] is 81 | /// evaluated. Then, it repeats by evaluating [test] again. A [BreakStmt] 82 | /// will stop that this. A [ContinueStmt] will restart the loop. If the 83 | /// loop ends without `break`, [elseSuite] is evaluated. 84 | @override 85 | SmyValue evaluate(Frame f) { 86 | while (test.evaluate(f).boolValue) { 87 | try { 88 | suite.evaluate(f); 89 | } on _Break { 90 | return SmyValue.none; 91 | } on _Continue { 92 | continue; 93 | } 94 | } 95 | return elseSuite.evaluate(f); 96 | } 97 | } 98 | 99 | /// `for target, ... in test, ...: suite else: suite` 100 | final class ForStmt extends Stmt { 101 | const ForStmt(this.target, this.items, this.suite, this.elseSuite); 102 | final Expr target; 103 | final Expr items; 104 | final Suite suite; 105 | final Suite elseSuite; 106 | 107 | /// Evaluates [items], which must result in an iterable, then a loop 108 | /// starts which assigns each element of the iterable to [target] before 109 | /// evaluating [suite]. A [BreakStmt] will stop that this. 110 | /// A [ContinueStmt] will restart the loop. If the loop ends without 111 | /// `break`, [elseSuite] is evaluated. 112 | @override 113 | SmyValue evaluate(Frame f) { 114 | final i = items.evaluate(f).iterable; 115 | for (final value in i) { 116 | target.assign(f, value); 117 | try { 118 | suite.evaluate(f); 119 | } on _Break { 120 | return SmyValue.none; 121 | } on _Continue { 122 | continue; 123 | } 124 | } 125 | return elseSuite.evaluate(f); 126 | } 127 | } 128 | 129 | /// `try: suite finally: suite` 130 | final class TryFinallyStmt extends Stmt { 131 | const TryFinallyStmt(this.suite, this.finallySuite); 132 | final Suite suite, finallySuite; 133 | 134 | /// Evaluates [suite]. Thereafter, before returning, [finallySuite] is 135 | /// evaluated, even if the evaluation of [suite] was aborted because 136 | /// of exceptions. 137 | @override 138 | SmyValue evaluate(Frame f) { 139 | try { 140 | suite.evaluate(f); 141 | } finally { 142 | finallySuite.evaluate(f); 143 | } 144 | return SmyValue.none; 145 | } 146 | } 147 | 148 | /// `try: suite except test as name: suite else: suite` 149 | final class TryExceptStmt extends Stmt { 150 | const TryExceptStmt(this.trySuite, this.excepts, this.elseSuite); 151 | final Suite trySuite, elseSuite; 152 | final List excepts; 153 | 154 | /// Evaluates [trySuite], thereafter, also evaluate [elseSuite]. If a 155 | /// [RaiseStmt] raises an exception to abort the evaluation of [trySuite], 156 | /// we try to match it to one of the [excepts] and evaluate the matching 157 | /// [ExceptClause.suite]. 158 | @override 159 | SmyValue evaluate(Frame f) { 160 | try { 161 | trySuite.evaluate(f); 162 | } on _Raise catch (e) { 163 | for (final except in excepts) { 164 | // TODO search for the right clause 165 | if (except.matches(f, e.value)) { 166 | var ff = f; 167 | if (except.name != null) { 168 | ff = Frame(f, {SmyString(except.name!): e.value}, f.globals, f.builtins, f.system); 169 | } 170 | return except.suite.evaluate(ff); 171 | } 172 | } 173 | rethrow; 174 | } 175 | return elseSuite.evaluate(f); 176 | } 177 | } 178 | 179 | /// `except test as name: suite` (part of [TryExceptStmt]) 180 | final class ExceptClause { 181 | const ExceptClause(this.test, this.name, this.suite); 182 | final Expr? test; 183 | final String? name; 184 | final Suite suite; 185 | 186 | bool matches(Frame f, SmyValue value) { 187 | if (test == null) return true; 188 | final type = test!.evaluate(f); 189 | // TODO implement an instance of operation to match the type 190 | return type == value; 191 | } 192 | } 193 | 194 | /// `def name(param=def, ...): suite` 195 | final class DefStmt extends Stmt { 196 | const DefStmt(this.name, this.params, this.defs, this.suite); 197 | final String name; 198 | final List params; 199 | final List defs; 200 | final Suite suite; 201 | 202 | /// Defines a new function called [name] that expects [params] and uses 203 | /// [defs] to compute the arguments if they aren't given. Hence, [params] 204 | /// and [defs] should have the same length. Use [suite] as the function's 205 | /// body. 206 | @override 207 | SmyValue evaluate(Frame f) { 208 | final n = SmyString.intern(name); 209 | return f.locals[n] = SmyFunc(f, n, params, defs, suite); 210 | } 211 | } 212 | 213 | /// `class name (super): suite` 214 | final class ClassStmt extends Stmt { 215 | const ClassStmt(this.name, this.superExpr, this.suite); 216 | final String name; 217 | final Expr superExpr; 218 | final Suite suite; 219 | 220 | /// Defines a new class called [name] that inherits from [superExpr] 221 | /// which must evaluate to [SmyClass] value. Use [suite] as the body 222 | /// of the class which is evaluated in its context in such a way that 223 | /// [DefStmt] statements will create methods instead of functions. 224 | @override 225 | SmyValue evaluate(Frame f) { 226 | final superclass = superExpr.evaluate(f); 227 | if (superclass != SmyValue.none && superclass is! SmyClass) { 228 | throw _Raise(SmyString('TypeError: superclass is not a class')); 229 | } 230 | final n = SmyString.intern(name); 231 | final cls = SmyClass(n, superclass != SmyValue.none ? superclass as SmyClass : null); 232 | f.locals[n] = cls; 233 | suite.evaluate(Frame(f, cls.methods, f.globals, f.builtins, f.system)); 234 | return SmyValue.none; 235 | } 236 | } 237 | 238 | /// `pass` 239 | final class PassStmt extends Stmt { 240 | const PassStmt(); 241 | 242 | /// Evaluates to `none`. 243 | @override 244 | SmyValue evaluate(Frame f) { 245 | return SmyValue.none; 246 | } 247 | } 248 | 249 | /// `break` 250 | final class BreakStmt extends Stmt { 251 | const BreakStmt(); 252 | 253 | /// Throws an internal exception to break a [WhileStmt] or [ForStmt]. 254 | @override 255 | SmyValue evaluate(Frame f) => throw _Break(); 256 | } 257 | 258 | /// `continue` 259 | final class ContinueStmt extends Stmt { 260 | const ContinueStmt(); 261 | 262 | /// Throws an internal exception to continue a [WhileStmt] or [ForStmt]. 263 | @override 264 | SmyValue evaluate(Frame f) => throw _Continue(); 265 | } 266 | 267 | /// `return`, `return test, ...` 268 | final class ReturnStmt extends Stmt { 269 | const ReturnStmt(this.expr); 270 | final Expr expr; 271 | 272 | /// Evaluates [expr] and throws an internal exception to return the 273 | /// result of that evaluation from a function body. 274 | @override 275 | SmyValue evaluate(Frame f) => throw _Return(expr.evaluate(f)); 276 | } 277 | 278 | /// `raise`, `raise test` 279 | final class RaiseStmt extends Stmt { 280 | const RaiseStmt(this.expr); 281 | final Expr expr; 282 | 283 | /// Throws an internal exception to raise an exception evaluated from [expr]. 284 | @override 285 | SmyValue evaluate(Frame f) => throw _Raise(expr.evaluate(f)); 286 | } 287 | 288 | /// `import NAME, ...` 289 | final class ImportNameStmt extends Stmt { 290 | const ImportNameStmt(this.names); 291 | final List> names; 292 | 293 | @override 294 | SmyValue evaluate(Frame f) { 295 | for (final name in names) { 296 | final moduleName = name.first; 297 | final asName = name.length == 2 ? name.last : moduleName; 298 | final module = f.system.import(moduleName) ?? (throw "ModuleNotFoundError: No module named '$moduleName'"); 299 | f.locals[SmyString.intern(asName)] = module; 300 | } 301 | return none; 302 | } 303 | } 304 | 305 | /// `from NAME import NAME, ...` 306 | final class FromImportStmt extends Stmt { 307 | const FromImportStmt(this.moduleName, this.names); 308 | final String moduleName; 309 | final List> names; 310 | 311 | @override 312 | SmyValue evaluate(Frame f) { 313 | final module = f.system.import(moduleName) ?? (throw "ModuleNotFoundError: No module named '$moduleName'"); 314 | if (names.isEmpty) { 315 | f.locals.addAll(module.globals.values); 316 | } else { 317 | for (final name in names) { 318 | final varName = name.first; 319 | final asName = name.length == 2 ? name.last : varName; 320 | f.locals[SmyString.intern(asName)] = module.getAttr(varName); 321 | } 322 | } 323 | return none; 324 | } 325 | } 326 | 327 | /// `global NAME, ...` 328 | final class GlobalStmt extends Stmt { 329 | const GlobalStmt(this.names); 330 | final List names; 331 | 332 | @override 333 | SmyValue evaluate(Frame f) => throw UnimplementedError(); 334 | } 335 | 336 | /// `assert test`, `assert test, test` 337 | final class AssertStmt extends Stmt { 338 | const AssertStmt(this.expr, this.message); 339 | final Expr expr; 340 | final Expr? message; 341 | 342 | /// Evaluates [expr] and if its boolean value is not `true`, evaluates 343 | /// the optional [message] to raise this as an `AssertionError`. 344 | @override 345 | SmyValue evaluate(Frame f) { 346 | if (!expr.evaluate(f).boolValue) { 347 | final m = message?.evaluate(f).stringValue; 348 | throw _Raise(make(m == null ? 'AssertionError' : 'AssertionError: $m')); 349 | } 350 | return SmyValue.none; 351 | } 352 | } 353 | 354 | /// `expr` 355 | final class ExprStmt extends Stmt { 356 | const ExprStmt(this.expr); 357 | final Expr expr; 358 | 359 | /// Evaluates [expr] ignoring the result, just for its side effect. 360 | @override 361 | SmyValue evaluate(Frame f) => expr.evaluate(f); 362 | } 363 | 364 | /// `target = test, ...` 365 | final class AssignStmt extends Stmt { 366 | const AssignStmt(this.lhs, this.rhs); 367 | final Expr lhs, rhs; 368 | 369 | /// Evaluates [rhs] and assigns the result of that evaluation to [lhs], 370 | /// evaluating it as required being the LHS of an assignment. Also 371 | /// performs deconstruction of arguments so that `a, b = (1, 2)` works. 372 | @override 373 | SmyValue evaluate(Frame f) => lhs.assign(f, rhs.evaluate(f)); 374 | } 375 | 376 | /// Abstract superclass of all `lhs op= rhs` operations. 377 | sealed class AugAssignStmt extends Stmt { 378 | const AugAssignStmt(this.lhs, this.rhs, this.op); 379 | final Expr lhs, rhs; 380 | final SmyValue Function(SmyValue, SmyValue) op; 381 | 382 | @override 383 | SmyValue evaluate(Frame f) => lhs.assign(f, op(lhs.evaluate(f), rhs.evaluate(f))); 384 | } 385 | 386 | /// `target += test` 387 | final class AddAssignStmt extends AugAssignStmt { 388 | const AddAssignStmt(Expr lhs, Expr rhs) : super(lhs, rhs, Expr.add); 389 | } 390 | 391 | /// `target -= test` 392 | final class SubAssignStmt extends AugAssignStmt { 393 | const SubAssignStmt(Expr lhs, Expr rhs) : super(lhs, rhs, Expr.sub); 394 | } 395 | 396 | /// `target *= test` 397 | final class MulAssignStmt extends AugAssignStmt { 398 | const MulAssignStmt(Expr lhs, Expr rhs) : super(lhs, rhs, Expr.mul); 399 | } 400 | 401 | /// `target /= test` 402 | final class DivAssignStmt extends AugAssignStmt { 403 | const DivAssignStmt(Expr lhs, Expr rhs) : super(lhs, rhs, Expr.div); 404 | } 405 | 406 | /// `target %= test, ...` 407 | final class ModAssignStmt extends AugAssignStmt { 408 | const ModAssignStmt(Expr lhs, Expr rhs) : super(lhs, rhs, Expr.mod); 409 | } 410 | 411 | /// `target |= test` 412 | final class OrAssignStmt extends AugAssignStmt { 413 | const OrAssignStmt(Expr lhs, Expr rhs) : super(lhs, rhs, Expr.or); 414 | } 415 | 416 | /// `target &= test` 417 | final class AndAssignStmt extends AugAssignStmt { 418 | const AndAssignStmt(Expr lhs, Expr rhs) : super(lhs, rhs, Expr.and); 419 | } 420 | 421 | // -------- Expr -------- 422 | 423 | /// An expression can be evaluated. 424 | /// 425 | /// It might be [assignable] in which case it can be [assign]ed to. Trying 426 | /// to assign to something not [assignable] will raise an error. 427 | sealed class Expr { 428 | const Expr(); 429 | 430 | /// Returns the result of the evaluation of this node in the context of [f]. 431 | SmyValue evaluate(Frame f); 432 | 433 | /// Assigns [value] to the receiver; by default a `SyntaxError` is raised. 434 | SmyValue assign(Frame f, SmyValue value) => throw _Raise(make("SyntaxError: can't assign")); 435 | 436 | /// Returns whether [assign] can be safely called on this node. 437 | bool get assignable => false; 438 | 439 | // default arithmetic & bit operations 440 | static SmyValue add(SmyValue l, SmyValue r) { 441 | if (l is SmyString && r is SmyString) { 442 | return SmyString(l.stringValue + r.stringValue); 443 | } 444 | return SmyNum(l.numValue + r.numValue); 445 | } 446 | 447 | static SmyValue sub(SmyValue l, SmyValue r) => SmyNum(l.numValue - r.numValue); 448 | static SmyValue mul(SmyValue l, SmyValue r) { 449 | if (l is SmyString) { 450 | return SmyString(l.stringValue * r.intValue); 451 | } 452 | if (l is SmyList) { 453 | final result = []; 454 | var count = r.intValue; 455 | while (count-- > 0) { 456 | result.addAll(l.values); 457 | } 458 | return SmyList(result); 459 | } 460 | return SmyNum(l.numValue * r.numValue); 461 | } 462 | 463 | static SmyValue div(SmyValue l, SmyValue r) => SmyNum(l.numValue / r.numValue); 464 | static SmyValue mod(SmyValue l, SmyValue r) { 465 | if (l is SmyString) return l.format(r); 466 | return SmyNum(l.numValue % r.numValue); 467 | } 468 | 469 | static SmyValue or(SmyValue l, SmyValue r) => SmyNum(l.intValue | r.intValue); 470 | static SmyValue and(SmyValue l, SmyValue r) => SmyNum(l.intValue & r.intValue); 471 | } 472 | 473 | /// _expr_ `if` _test_ `else` _test_ 474 | final class CondExpr extends Expr { 475 | const CondExpr(this.test, this.thenExpr, this.elseExpr); 476 | final Expr test, thenExpr, elseExpr; 477 | 478 | @override 479 | SmyValue evaluate(Frame f) { 480 | return (test.evaluate(f).boolValue ? thenExpr : elseExpr).evaluate(f); 481 | } 482 | 483 | @override 484 | String toString() => '$thenExpr if $test else $elseExpr'; 485 | } 486 | 487 | /// expr `or` expr 488 | final class OrExpr extends Expr { 489 | const OrExpr(this.left, this.right); 490 | final Expr left, right; 491 | 492 | @override 493 | SmyValue evaluate(Frame f) { 494 | return SmyBool(left.evaluate(f).boolValue || right.evaluate(f).boolValue); 495 | } 496 | } 497 | 498 | /// expr `and` expr 499 | final class AndExpr extends Expr { 500 | const AndExpr(this.left, this.right); 501 | final Expr left, right; 502 | 503 | @override 504 | SmyValue evaluate(Frame f) { 505 | return SmyBool(left.evaluate(f).boolValue && right.evaluate(f).boolValue); 506 | } 507 | } 508 | 509 | /// `not expr` 510 | final class NotExpr extends Expr { 511 | const NotExpr(this.expr); 512 | final Expr expr; 513 | 514 | @override 515 | SmyValue evaluate(Frame f) => SmyBool(!expr.evaluate(f).boolValue); 516 | } 517 | 518 | /// `expr < expr`, `expr < expr < expr` 519 | final class CompOp { 520 | CompOp(this.op, this.right); 521 | final bool Function(SmyValue, SmyValue) op; 522 | final Expr right; 523 | 524 | static bool eq(SmyValue l, SmyValue r) => l == r; 525 | static bool ne(SmyValue l, SmyValue r) => !eq(l, r); 526 | static bool lt(SmyValue l, SmyValue r) => l.numValue < r.numValue; 527 | static bool gt(SmyValue l, SmyValue r) => l.numValue > r.numValue; 528 | static bool le(SmyValue l, SmyValue r) => l.numValue <= r.numValue; 529 | static bool ge(SmyValue l, SmyValue r) => l.numValue >= r.numValue; 530 | 531 | static bool in_(SmyValue l, SmyValue r) { 532 | if (l is SmyString && r is SmyString) return r.stringValue.contains(l.stringValue); 533 | throw UnimplementedError(); 534 | } 535 | 536 | static bool notin(SmyValue l, SmyValue r) => !in_(l, r); 537 | static bool is_(SmyValue l, SmyValue r) => throw UnimplementedError(); 538 | static bool notis(SmyValue l, SmyValue r) => !is_(l, r); 539 | } 540 | 541 | final class Comparison extends Expr { 542 | Comparison(this.left, this.ops); 543 | final Expr left; 544 | final List ops; 545 | 546 | @override 547 | SmyValue evaluate(Frame f) { 548 | var l = left.evaluate(f); 549 | for (final op in ops) { 550 | final r = op.right.evaluate(f); 551 | if (!op.op(l, r)) return SmyBool(false); 552 | l = r; 553 | } 554 | return SmyBool(true); 555 | } 556 | } 557 | 558 | /// `expr | expr` 559 | final class BitOrExpr extends Expr { 560 | const BitOrExpr(this.left, this.right); 561 | final Expr left, right; 562 | 563 | @override 564 | SmyValue evaluate(Frame f) { 565 | return Expr.or(left.evaluate(f), right.evaluate(f)); 566 | } 567 | } 568 | 569 | /// `expr & expr` 570 | final class BitAndExpr extends Expr { 571 | const BitAndExpr(this.left, this.right); 572 | final Expr left, right; 573 | 574 | @override 575 | SmyValue evaluate(Frame f) { 576 | return Expr.and(left.evaluate(f), right.evaluate(f)); 577 | } 578 | 579 | @override 580 | String toString() => '$left & $right'; 581 | } 582 | 583 | /// `expr + expr` 584 | final class AddExpr extends Expr { 585 | const AddExpr(this.left, this.right); 586 | final Expr left, right; 587 | 588 | @override 589 | SmyValue evaluate(Frame f) { 590 | return Expr.add(left.evaluate(f), right.evaluate(f)); 591 | } 592 | 593 | @override 594 | String toString() => '$left + $right'; 595 | } 596 | 597 | /// `expr - expr` 598 | final class SubExpr extends Expr { 599 | const SubExpr(this.left, this.right); 600 | final Expr left, right; 601 | 602 | @override 603 | SmyValue evaluate(Frame f) { 604 | return Expr.sub(left.evaluate(f), right.evaluate(f)); 605 | } 606 | 607 | @override 608 | String toString() => '$left - $right'; 609 | } 610 | 611 | /// `expr * expr` 612 | final class MulExpr extends Expr { 613 | const MulExpr(this.left, this.right); 614 | final Expr left, right; 615 | 616 | @override 617 | SmyValue evaluate(Frame f) { 618 | return Expr.mul(left.evaluate(f), right.evaluate(f)); 619 | } 620 | } 621 | 622 | /// `expr / expr` 623 | final class DivExpr extends Expr { 624 | const DivExpr(this.left, this.right); 625 | final Expr left, right; 626 | 627 | @override 628 | SmyValue evaluate(Frame f) { 629 | return Expr.div(left.evaluate(f), right.evaluate(f)); 630 | } 631 | } 632 | 633 | /// `expr % expr` 634 | final class ModExpr extends Expr { 635 | const ModExpr(this.left, this.right); 636 | final Expr left, right; 637 | 638 | @override 639 | SmyValue evaluate(Frame f) { 640 | return Expr.mod(left.evaluate(f), right.evaluate(f)); 641 | } 642 | 643 | @override 644 | String toString() => '$left % $right'; 645 | } 646 | 647 | /// `+expr` 648 | final class PosExpr extends Expr { 649 | const PosExpr(this.expr); 650 | final Expr expr; 651 | 652 | @override 653 | SmyValue evaluate(Frame f) => expr.evaluate(f); 654 | } 655 | 656 | /// `-expr` 657 | final class NegExpr extends Expr { 658 | const NegExpr(this.expr); 659 | final Expr expr; 660 | 661 | @override 662 | SmyValue evaluate(Frame f) => SmyNum(-expr.evaluate(f).numValue); 663 | } 664 | 665 | /// `~expr` 666 | final class InvertExpr extends Expr { 667 | const InvertExpr(this.expr); 668 | final Expr expr; 669 | 670 | @override 671 | SmyValue evaluate(Frame f) => SmyNum(~expr.evaluate(f).intValue); 672 | } 673 | 674 | /// `expr(args, ...)` 675 | final class CallExpr extends Expr { 676 | const CallExpr(this.expr, this.args); 677 | final Expr expr; 678 | final List args; 679 | 680 | @override 681 | SmyValue evaluate(Frame f) { 682 | // print('call: $this'); 683 | return expr.evaluate(f).call(f, args.map((arg) => arg.evaluate(f)).toList()); 684 | } 685 | 686 | @override 687 | String toString() => '$expr(${args.join(', ')})'; 688 | } 689 | 690 | /// `expr[expr]` 691 | final class IndexExpr extends Expr { 692 | const IndexExpr(this.left, this.right); 693 | final Expr left, right; 694 | 695 | @override 696 | SmyValue evaluate(Frame f) { 697 | final value = left.evaluate(f); 698 | final index = right.evaluate(f); 699 | final length = value.length; 700 | if (value is SmyDict) { 701 | return value.values[index] ?? SmyValue.none; 702 | } 703 | if (index is SmyNum) { 704 | var i = index.index; 705 | if (i < 0) i += length; 706 | if (i < 0 || i >= length) throw 'IndexError: index out of range'; 707 | if (value is SmyString) { 708 | return SmyString(value.value[i]); 709 | } 710 | return value.iterable.skip(i).first; 711 | } 712 | final slice = (index as SmyTuple).values; 713 | var i = slice[0] != SmyValue.none ? slice[0].index : 0; 714 | var j = slice[1] != SmyValue.none ? slice[1].index : length; 715 | if (slice[2] != SmyValue.none) throw 'slicing with step not yet implemented'; 716 | if (i < 0) i += length; 717 | if (i < 0) i = 0; 718 | if (i > length) i = length; 719 | if (j < 0) j += length; 720 | if (j < 0) j = 0; 721 | if (j > length) j = length; 722 | if (value is SmyString) { 723 | if (i >= j) return const SmyString(''); 724 | return SmyString(value.value.substring(i, j)); 725 | } 726 | if (value is SmyTuple) { 727 | if (i >= j) return const SmyTuple([]); 728 | return SmyTuple(value.iterable.skip(i).take(j - i).toList()); 729 | } 730 | if (i >= j) return const SmyList([]); 731 | return SmyList(value.iterable.skip(i).take(j - i).toList()); 732 | } 733 | 734 | @override 735 | SmyValue assign(Frame f, value) { 736 | final target = left.evaluate(f); 737 | final index = right.evaluate(f); 738 | if (target is SmyList) { 739 | return target.values[index.index] = value; 740 | } 741 | throw '[]= not implemented yet'; 742 | } 743 | 744 | @override 745 | bool get assignable => true; 746 | 747 | @override 748 | String toString() => '$left[$right]'; 749 | } 750 | 751 | /// `expr.NAME` 752 | final class AttrExpr extends Expr { 753 | const AttrExpr(this.expr, this.name); 754 | final Expr expr; 755 | final String name; 756 | 757 | @override 758 | SmyValue evaluate(Frame f) { 759 | return expr.evaluate(f).getAttr(name); 760 | } 761 | 762 | @override 763 | SmyValue assign(Frame f, value) { 764 | return expr.evaluate(f).setAttr(name, value); 765 | } 766 | 767 | @override 768 | bool get assignable => true; 769 | 770 | @override 771 | String toString() => '$expr.$name'; 772 | } 773 | 774 | /// `NAME` 775 | final class VarExpr extends Expr { 776 | const VarExpr(this.name); 777 | final SmyString name; 778 | 779 | @override 780 | SmyValue evaluate(Frame f) => f.lookup(name); 781 | 782 | @override 783 | SmyValue assign(Frame f, value) => f.set(name, value); 784 | 785 | @override 786 | bool get assignable => true; 787 | 788 | @override 789 | String toString() => name.stringValue; 790 | } 791 | 792 | /// `None`, `True`, `False`, `NUMBER`, `STRING` 793 | final class LitExpr extends Expr { 794 | const LitExpr(this.value); 795 | final SmyValue value; 796 | 797 | @override 798 | SmyValue evaluate(Frame f) => value; 799 | 800 | @override 801 | String toString() => value.toString(); 802 | } 803 | 804 | /// `()`, `(expr,)`, `(expr, ...)` 805 | final class TupleExpr extends Expr { 806 | const TupleExpr(this.exprs); 807 | final List exprs; 808 | 809 | @override 810 | SmyValue evaluate(Frame f) { 811 | return SmyTuple(exprs.map((e) => e.evaluate(f)).toList()); 812 | } 813 | 814 | @override 815 | SmyValue assign(Frame f, SmyValue value) { 816 | final i = value.iterable.iterator; 817 | for (final e in exprs) { 818 | if (!i.moveNext()) throw 'ValueError: not enough values to unpack'; 819 | e.assign(f, i.current); 820 | } 821 | if (i.moveNext()) throw 'ValueError: too many values to unpack'; 822 | return value; 823 | } 824 | 825 | @override 826 | bool get assignable => true; 827 | } 828 | 829 | /// `[]`, `[expr, ...]` 830 | final class ListExpr extends Expr { 831 | const ListExpr(this.exprs); 832 | final List exprs; 833 | 834 | @override 835 | SmyValue evaluate(Frame f) { 836 | if (exprs.isEmpty) return SmyList([]); 837 | return SmyList(exprs.map((e) => e.evaluate(f)).toList()); 838 | } 839 | } 840 | 841 | /// `{}`, `{expr: expr, ...}` 842 | final class DictExpr extends Expr { 843 | const DictExpr(this.exprs); 844 | final List exprs; 845 | 846 | @override 847 | SmyValue evaluate(Frame f) { 848 | final dict = {}; 849 | for (var i = 0; i < exprs.length; i += 2) { 850 | dict[exprs[i].evaluate(f)] = exprs[i + 1].evaluate(f); 851 | } 852 | return SmyDict(dict); 853 | } 854 | } 855 | 856 | /// `{expr, ...}` 857 | final class SetExpr extends Expr { 858 | const SetExpr(this.exprs); 859 | final List exprs; 860 | 861 | @override 862 | SmyValue evaluate(Frame f) => SmySet(exprs.map((e) => e.evaluate(f)).toSet()); 863 | } 864 | 865 | /// Implements breaking loops. 866 | final class _Break {} 867 | 868 | /// Implements continuing loops. 869 | final class _Continue {} 870 | 871 | /// Implements returning from functions. 872 | final class _Return { 873 | _Return(this.value); 874 | final SmyValue value; 875 | } 876 | 877 | /// Implements raising exceptions. 878 | final class _Raise { 879 | _Raise(this.value); 880 | final SmyValue value; 881 | 882 | @override 883 | String toString() => '$value'; 884 | } 885 | -------------------------------------------------------------------------------- /lib/parser.dart: -------------------------------------------------------------------------------- 1 | /// Parses Smython source code, a programming language similar to a subset 2 | /// of Python 3 that is barely capable of running the factorial function. 3 | /// 4 | /// Here is a simple example: 5 | /// 6 | /// ``` 7 | /// def fac(n): 8 | /// if n == 0: return 1 9 | /// return n * fac(n - 1) 10 | /// print(fac(10)) 11 | /// ``` 12 | /// 13 | /// Syntax differences to Python 3.7 (or later versions): 14 | /// 15 | /// Smython has no decorators, `async` functions, typed function parameters, 16 | /// function keyword arguments, argument spatting with `*` or `**`, no 17 | /// `del`, `import`, `global`, `nonlocal`, or `yield` statements, 18 | /// no `@=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `//=` or `**=`, 19 | /// no `from` clause in `raise`, no `with` statement, no combined 20 | /// `try`/`except`/`finally`, no multiple inheritance in classes, no 21 | /// lambdas, no `<>`, `@`, `//`, `^`, `<<`, or `>>` operators, 22 | /// no `await`, no list or dict comprehension, no `...`, no list in `[ ` 23 | /// but only a single value or slice, no tripple-quoted, byte, or raw 24 | /// strings, only unicode ones. 25 | /// 26 | /// Also, indentation must use exactly four spaces. TABs are not allowed. 27 | /// 28 | /// ## EBNF Grammar: 29 | /// ``` 30 | /// file_input: {NEWLINE | stmt} ENDMARKER 31 | /// 32 | /// stmt: simple_stmt | compound_stmt 33 | /// simple_stmt: small_stmt {';' small_stmt} [';'] NEWLINE 34 | /// small_stmt: expr_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | assert_stmt 35 | /// expr_stmt: testlist [('+=' | '-=' | '*=' | '/=' | '%=' | '|=' | '&=' | '=') testlist] 36 | /// pass_stmt: 'pass' 37 | /// flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt 38 | /// break_stmt: 'break' 39 | /// continue_stmt: 'continue' 40 | /// return_stmt: 'return' [testlist] 41 | /// raise_stmt: 'raise' [test] 42 | /// import_stmt: import_name | import_from 43 | /// import_name: 'import' import_as_names 44 | /// import_from: 'from' NAME 'import' ('*' | import_as_names) 45 | /// import_as_names: import_as_name {',' import_as_name} [','] 46 | /// import_as_name: NAME ['as' NAME] 47 | /// global_stmt: 'global' NAME {',' NAME} 48 | /// assert_stmt: 'assert' test [',' test] 49 | /// compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef 50 | /// if_stmt: 'if' test ':' suite {'elif' test ':' suite} ['else' ':' suite] 51 | /// while_stmt: 'while' test ':' suite ['else' ':' suite] 52 | /// for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] 53 | /// exprlist: expr {',' expr} [','] 54 | /// try_stmt: 'try' ':' suite (except_cont | finally_cont) 55 | /// except_cont: except_clause {except_clause} ['else' ':' suite] 56 | /// except_clause: 'except' [test ['as' NAME]] ':' suite 57 | /// finally_cont: 'finally' ':' suite 58 | /// funcdef: 'def' NAME parameters ':' suite 59 | /// parameters: '(' [parameter {',' parameter} [',']] ')' 60 | /// parameter: NAME ['=' test] 61 | /// classdef: 'class' NAME ['(' [test] ')'] ':' suite 62 | /// 63 | /// suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT 64 | /// 65 | /// test: or_test ['if' or_test 'else' test] 66 | /// or_test: and_test {'or' and_test} 67 | /// and_test: not_test {'and' not_test} 68 | /// not_test: 'not' not_test | comparison 69 | /// comparison: expr {('<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is' ['not']) expr} 70 | /// expr: and_expr {'|' and_expr} 71 | /// and_expr: arith_expr {'&' arith_expr} 72 | /// arith_expr: term {('+'|'-') term} 73 | /// term: factor {('*'|'/'|'%') factor} 74 | /// factor: ('+'|'-'|'~') factor | power 75 | /// power: atom {trailer} 76 | /// trailer: '(' [testlist] ')' | '[' subscript ']' | '.' NAME 77 | /// subscript: test | [test] ':' [test] [':' [test]] 78 | /// atom: '(' [testlist] ')' | '[' [testlist] ']' | '{' [dictorsetmaker] '}' | NAME | NUMBER | STRING+ 79 | /// dictorsetmaker: test ':' test {',' test ':' test} [','] | testlist 80 | /// 81 | /// testlist: test {',' test} [','] 82 | /// ``` 83 | /// 84 | /// Parsing may throw a [SyntaxError]. 85 | library parser; 86 | 87 | import 'ast_eval.dart'; 88 | import 'scanner.dart'; 89 | import 'smython.dart'; 90 | 91 | /// Parses [source] into an AST. See library header for details. 92 | Suite parse(String source) { 93 | return Parser(tokenize(source).iterator).parseFileInput(); 94 | } 95 | 96 | /// Parses a sequence of tokens into an AST. 97 | /// 98 | /// This is a handcrafted LL(1) recursive descent parser. 99 | /// See library header for the grammar which has been implemented. 100 | class Parser { 101 | Parser(this._iter) { 102 | advance(); 103 | } 104 | 105 | final Iterator _iter; 106 | 107 | // -------- Helper -------- 108 | 109 | /// Returns the current token (not consuming it). 110 | Token get token => _iter.current; 111 | 112 | /// Consumes the curent token and advances to the next token. 113 | void advance() => _iter.moveNext(); 114 | 115 | /// Consumes the current token if and only if its value is [value]. 116 | bool at(String value) { 117 | if (token.value == value) { 118 | advance(); 119 | return true; 120 | } 121 | return false; 122 | } 123 | 124 | /// Consumes the current token if and only if its value is [value] and 125 | /// throws a [SyntaxError] otherwise. 126 | void expect(String value) { 127 | if (!at(value)) throw syntaxError('expected ${value == '\n' ? 'NEWLINE' : value}'); 128 | } 129 | 130 | /// Constructs a syntax error with [message] and the current token. 131 | /// It should also announce the current token's line. Hopefully the 132 | /// current token is never a synthesized token. 133 | SyntaxError syntaxError(String message) { 134 | return SyntaxError('$message but found ${token == Token.eof ? 'end of input' : '$token'} at line ${token.line}'); 135 | } 136 | 137 | // -------- Suite parsing -------- 138 | 139 | /// `file_input: {NEWLINE | stmt} ENDMARKER` 140 | Suite parseFileInput() { 141 | final stmts = []; 142 | while (!at(Token.eof.value)) { 143 | if (!at('\n')) stmts.addAll(parseStmt()); 144 | } 145 | return Suite(stmts); 146 | } 147 | 148 | /// `suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT` 149 | Suite parseSuite() { 150 | if (at('\n')) { 151 | expect(Token.indent.value); 152 | final stmts = []; 153 | while (!at(Token.dedent.value)) { 154 | stmts.addAll(parseStmt()); 155 | } 156 | return Suite(stmts); 157 | } 158 | return Suite(parseSimpleStmt()); 159 | } 160 | 161 | // -------- Statement parsing -------- 162 | 163 | /// `stmt: simple_stmt | compound_stmt` 164 | List parseStmt() { 165 | final stmt = parseCompoundStmtOpt(); 166 | if (stmt != null) return [stmt]; 167 | return parseSimpleStmt(); 168 | } 169 | 170 | // -------- Compount statement parsing -------- 171 | 172 | /// `compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef` 173 | Stmt? parseCompoundStmtOpt() { 174 | if (at('if')) return parseIfStmt(); 175 | if (at('while')) return parseWhileStmt(); 176 | if (at('for')) return parseForStmt(); 177 | if (at('try')) return parseTryStmt(); 178 | if (at('def')) return parseFuncDef(); 179 | if (at('class')) return parseClassDef(); 180 | return null; 181 | } 182 | 183 | /// `if_stmt: 'if' test ':' suite {'elif' test ':' suite} ['else' ':' suite]` 184 | Stmt parseIfStmt() { 185 | final test = parseTest(); 186 | expect(':'); 187 | return IfStmt(test, parseSuite(), _parseIfStmtCont()); 188 | } 189 | 190 | /// private: `['elif' test ':' suite | 'else' ':' suite]` 191 | Suite _parseIfStmtCont() { 192 | if (at('elif')) { 193 | final test = parseTest(); 194 | expect(':'); 195 | return Suite([IfStmt(test, parseSuite(), _parseIfStmtCont())]); 196 | } 197 | return _parseElse(); 198 | } 199 | 200 | /// private: `['else' ':' suite]` 201 | Suite _parseElse() { 202 | if (at('else')) { 203 | expect(':'); 204 | return parseSuite(); 205 | } 206 | return Suite([const PassStmt()]); 207 | } 208 | 209 | /// `while_stmt: 'while' test ':' suite ['else' ':' suite]` 210 | Stmt parseWhileStmt() { 211 | final test = parseTest(); 212 | expect(':'); 213 | return WhileStmt(test, parseSuite(), _parseElse()); 214 | } 215 | 216 | /// `for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]` 217 | Stmt parseForStmt() { 218 | final target = parseExprOrListAsTuple(); 219 | expect('in'); 220 | final iter = parseTestOrListAsTuple(); 221 | expect(':'); 222 | return ForStmt(target, iter, parseSuite(), _parseElse()); 223 | } 224 | 225 | /// `exprlist: expr {',' expr} [',']` 226 | Expr parseExprOrListAsTuple() { 227 | final expr = parseExpr(); 228 | if (!at(',')) return expr; 229 | final exprs = [expr]; 230 | while (hasTest) { 231 | exprs.add(parseExpr()); 232 | if (!at(',')) break; 233 | } 234 | return TupleExpr(exprs); 235 | } 236 | 237 | /// `try_stmt: 'try' ':' suite (except_clause {except_clause} ['else' ':' suite] | 'finally' ':' suite)` 238 | Stmt parseTryStmt() { 239 | expect(':'); 240 | final trySuite = parseSuite(); 241 | if (at('finally')) { 242 | expect(':'); 243 | return TryFinallyStmt(trySuite, parseSuite()); 244 | } 245 | expect('except'); 246 | final excepts = [_parseExceptClause()]; 247 | while (at('except')) { 248 | excepts.add(_parseExceptClause()); 249 | } 250 | return TryExceptStmt(trySuite, excepts, _parseElse()); 251 | } 252 | 253 | /// `except_clause: 'except' [test ['as' NAME]] ':' suite` 254 | ExceptClause _parseExceptClause() { 255 | Expr? test; 256 | String? name; 257 | if (!at(':')) { 258 | test = parseTest(); 259 | if (at('as')) { 260 | name = parseName(); 261 | } 262 | expect(':'); 263 | } 264 | return ExceptClause(test, name, parseSuite()); 265 | } 266 | 267 | /// `funcdef: 'def' NAME parameters ':' suite` 268 | Stmt parseFuncDef() { 269 | final name = parseName(); 270 | final defExprs = []; 271 | final params = parseParameters(defExprs); 272 | expect(':'); 273 | return DefStmt(name, params, defExprs, parseSuite()); 274 | } 275 | 276 | /// `parameters: '(' [parameter {',' parameter} [',']] ')'` 277 | List parseParameters(List defExprs) { 278 | final params = []; 279 | expect('('); 280 | if (at(')')) return params; 281 | params.add(parseParameter(defExprs)); 282 | while (at(',')) { 283 | if (at(')')) return params; 284 | params.add(parseParameter(defExprs)); 285 | } 286 | expect(')'); 287 | return params; 288 | } 289 | 290 | /// `parameter: NAME ['=' test]` 291 | String parseParameter(List defExprs) { 292 | if (at('*')) return '*${parseName()}'; 293 | final name = parseName(); 294 | if (at('=')) defExprs.add(parseTest()); 295 | return name; 296 | } 297 | 298 | /// `classdef: 'class' NAME ['(' [test] ')'] ':' suite` 299 | Stmt parseClassDef() { 300 | final name = parseName(); 301 | Expr superExpr = const LitExpr(SmyValue.none); 302 | if (at('(')) { 303 | if (!at(')')) { 304 | superExpr = parseTest(); 305 | expect(')'); 306 | } 307 | } 308 | expect(':'); 309 | return ClassStmt(name, superExpr, parseSuite()); 310 | } 311 | 312 | // -------- Simple statement parsing -------- 313 | 314 | /// `simple_stmt: small_stmt {';' small_stmt} [';'] NEWLINE` 315 | List parseSimpleStmt() { 316 | final stmts = [parseSmallStmt()]; 317 | while (at(';')) { 318 | if (at('\n')) return stmts; 319 | stmts.add(parseSmallStmt()); 320 | } 321 | expect('\n'); 322 | return stmts; 323 | } 324 | 325 | /// `small_stmt: expr_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | assert_stmt` 326 | /// `flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt` 327 | /// `import_stmt: import_name | import_from` 328 | Stmt parseSmallStmt() { 329 | if (at('pass')) return const PassStmt(); 330 | if (at('break')) return const BreakStmt(); 331 | if (at('continue')) return const ContinueStmt(); 332 | if (at('return')) return parseReturnStmt(); 333 | if (at('raise')) return parseRaiseStmt(); 334 | if (at('import')) return parseImportName(); 335 | if (at('from')) return parseFromImport(); 336 | if (at('global')) return parseGlobalStmt(); 337 | if (at('assert')) return parseAssertStmt(); 338 | return parseExprStmt(); 339 | } 340 | 341 | /// `return_stmt: 'return' [testlist]` 342 | Stmt parseReturnStmt() { 343 | return ReturnStmt(hasTest ? parseTestOrListAsTuple() : const LitExpr(SmyValue.none)); 344 | } 345 | 346 | /// `raise_stmt: 'raise' [test]` 347 | Stmt parseRaiseStmt() { 348 | return RaiseStmt(hasTest ? parseTest() : const LitExpr(SmyValue.none)); 349 | } 350 | 351 | /// `import_name: 'import' import_as_names` 352 | Stmt parseImportName() { 353 | return ImportNameStmt(_parseImportAsNames()); 354 | } 355 | 356 | /// `import_from: 'from' NAME 'import' ('*' | import_as_names)` 357 | Stmt parseFromImport() { 358 | final module = parseName(); 359 | expect('import'); 360 | if (at('*')) { 361 | return FromImportStmt(module, []); 362 | } 363 | return FromImportStmt(module, _parseImportAsNames()); 364 | } 365 | 366 | /// `import_as_names: import_as_name {',' import_as_name} [',']` 367 | List> _parseImportAsNames() { 368 | final names = [_parseImportAsName()]; 369 | while (at(',')) { 370 | if (token.value == '\n') break; 371 | names.add(_parseImportAsName()); 372 | } 373 | return names; 374 | } 375 | 376 | /// `import_as_name: NAME ['as' NAME]` 377 | List _parseImportAsName() { 378 | final name = [parseName()]; 379 | if (at('as')) { 380 | name.add(parseName()); 381 | } 382 | return name; 383 | } 384 | 385 | /// `global_stmt: 'global' NAME {',' NAME}` 386 | Stmt parseGlobalStmt() { 387 | final names = [parseName()]; 388 | while (at(',')) { 389 | names.add(parseName()); 390 | } 391 | return GlobalStmt(names); 392 | } 393 | 394 | /// `assert_stmt: 'assert' test [',' test]` 395 | Stmt parseAssertStmt() { 396 | final test = parseTest(); 397 | return AssertStmt(test, at(',') ? parseTest() : null); 398 | } 399 | 400 | /// `expr_stmt: testlist [('+=' | '-=' | '*=' | '/=' | '%=' | '|=' | '&=' | '=') testlist]` 401 | Stmt parseExprStmt() { 402 | if (hasTest) { 403 | final expr = parseTestOrListAsTuple(); 404 | if (at('=')) return AssignStmt(expr, parseTestOrListAsTuple()); 405 | if (at('+=')) return AddAssignStmt(expr, parseTest()); 406 | if (at('-=')) return SubAssignStmt(expr, parseTest()); 407 | if (at('*=')) return MulAssignStmt(expr, parseTest()); 408 | if (at('/=')) return DivAssignStmt(expr, parseTest()); 409 | if (at('%=')) return ModAssignStmt(expr, parseTest()); 410 | if (at('|=')) return OrAssignStmt(expr, parseTest()); 411 | if (at('&=')) return AndAssignStmt(expr, parseTest()); 412 | return ExprStmt(expr); 413 | } 414 | throw syntaxError('expected statement'); 415 | } 416 | 417 | // -------- Expression parsing -------- 418 | 419 | /// `test: or_test ['if' or_test 'else' test]` 420 | Expr parseTest() { 421 | final expr = parseOrTest(); 422 | if (at('if')) { 423 | final test = parseOrTest(); 424 | expect('else'); 425 | return CondExpr(test, expr, parseTest()); 426 | } 427 | return expr; 428 | } 429 | 430 | /// `or_test: and_test {'or' and_test}` 431 | Expr parseOrTest() { 432 | var expr = parseAndTest(); 433 | while (at('or')) { 434 | expr = OrExpr(expr, parseAndTest()); 435 | } 436 | return expr; 437 | } 438 | 439 | /// `and_test: not_test {'and' not_test}` 440 | Expr parseAndTest() { 441 | var expr = parseNotTest(); 442 | while (at('and')) { 443 | expr = AndExpr(expr, parseNotTest()); 444 | } 445 | return expr; 446 | } 447 | 448 | /// `not_test: 'not' not_test | comparison` 449 | Expr parseNotTest() { 450 | if (at('not')) return NotExpr(parseNotTest()); 451 | return parseComparison(); 452 | } 453 | 454 | /// `comparison: expr {('<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is' ['not']) expr}` 455 | Expr parseComparison() { 456 | final expr = parseExpr(); 457 | final ops = []; 458 | while (true) { 459 | if (at('<')) { 460 | ops.add(CompOp(CompOp.lt, parseExpr())); 461 | } else if (at('>')) { 462 | ops.add(CompOp(CompOp.gt, parseExpr())); 463 | } else if (at('==')) { 464 | ops.add(CompOp(CompOp.eq, parseExpr())); 465 | } else if (at('>=')) { 466 | ops.add(CompOp(CompOp.ge, parseExpr())); 467 | } else if (at('<=')) { 468 | ops.add(CompOp(CompOp.le, parseExpr())); 469 | } else if (at('!=') || at('<>')) { 470 | ops.add(CompOp(CompOp.ne, parseExpr())); 471 | } else if (at('in')) { 472 | ops.add(CompOp(CompOp.in_, parseExpr())); 473 | } else if (at('not')) { 474 | expect('in'); 475 | ops.add(CompOp(CompOp.notin, parseExpr())); 476 | } else if (at('is')) { 477 | if (at('not')) { 478 | ops.add(CompOp(CompOp.notis, parseExpr())); 479 | } else { 480 | ops.add(CompOp(CompOp.is_, parseExpr())); 481 | } 482 | } else { 483 | break; 484 | } 485 | } 486 | if (ops.isNotEmpty) return Comparison(expr, ops); 487 | return expr; 488 | } 489 | 490 | /// `expr: and_expr {'|' and_expr}` 491 | Expr parseExpr() { 492 | var expr = parseAndExpr(); 493 | while (at('|')) { 494 | expr = BitOrExpr(expr, parseAndExpr()); 495 | } 496 | return expr; 497 | } 498 | 499 | /// `and_expr: arith_expr {'&' arith_expr}` 500 | Expr parseAndExpr() { 501 | var expr = parseArithExpr(); 502 | while (at('&')) { 503 | expr = BitAndExpr(expr, parseArithExpr()); 504 | } 505 | return expr; 506 | } 507 | 508 | /// `arith_expr: term {('+'|'-') term}` 509 | Expr parseArithExpr() { 510 | var expr = parseTerm(); 511 | while (true) { 512 | if (at('+')) { 513 | expr = AddExpr(expr, parseTerm()); 514 | } else if (at('-')) { 515 | expr = SubExpr(expr, parseTerm()); 516 | } else { 517 | break; 518 | } 519 | } 520 | return expr; 521 | } 522 | 523 | /// `term: factor {('*'|'/'|'%') factor}` 524 | Expr parseTerm() { 525 | var expr = parseFactor(); 526 | while (true) { 527 | if (at('*')) { 528 | expr = MulExpr(expr, parseFactor()); 529 | } else if (at('/')) { 530 | expr = DivExpr(expr, parseFactor()); 531 | } else if (at('%')) { 532 | expr = ModExpr(expr, parseFactor()); 533 | } else { 534 | break; 535 | } 536 | } 537 | return expr; 538 | } 539 | 540 | /// `factor: ('+'|'-'|'~') factor | power` 541 | Expr parseFactor() { 542 | if (at('+')) return PosExpr(parseFactor()); 543 | if (at('-')) return NegExpr(parseFactor()); 544 | if (at('~')) return InvertExpr(parseFactor()); 545 | return parsePower(); 546 | } 547 | 548 | /// `power: atom {trailer}` 549 | Expr parsePower() { 550 | var expr = parseAtom(); 551 | // trailer: '(' [testlist] ')' | '[' subscript ']' | '.' NAME 552 | while (true) { 553 | if (at('(')) { 554 | expr = CallExpr(expr, parseTestListOpt()); 555 | expect(')'); 556 | } else if (at('[')) { 557 | expr = IndexExpr(expr, parseSubscript()); 558 | expect(']'); 559 | } else if (at('.')) { 560 | expr = AttrExpr(expr, parseName()); 561 | } else { 562 | break; 563 | } 564 | } 565 | return expr; 566 | } 567 | 568 | /// `subscript: test | [test] ':' [test] [':' [test]]` 569 | Expr parseSubscript() { 570 | Expr start; 571 | final none = const LitExpr(SmyValue.none); 572 | if (hasTest) { 573 | start = parseTest(); 574 | if (!at(':')) return start; 575 | } else { 576 | start = none; 577 | expect(':'); 578 | } 579 | final stop = hasTest ? parseTest() : none; 580 | final step = at(':') && hasTest ? parseTest() : none; 581 | return CallExpr(const VarExpr(SmyString('slice')), [start, stop, step]); 582 | } 583 | 584 | /// `atom: '(' [testlist] ')' | '[' [testlist] ']' | '{' [dictorsetmaker] '}' | NAME | NUMBER | STRING+` 585 | Expr parseAtom() { 586 | if (at('(')) return _parseTupleMaker(); 587 | if (at('[')) return _parseListMaker(); 588 | if (at('{')) return _parseDictOrSetMaker(); 589 | final t = token; 590 | if (t.isName) { 591 | advance(); 592 | final name = t.value; 593 | if (name == 'True') return const LitExpr(SmyValue.trueValue); 594 | if (name == 'False') return const LitExpr(SmyValue.falseValue); 595 | if (name == 'None') return const LitExpr(SmyValue.none); 596 | return VarExpr(SmyString(name)); 597 | } 598 | if (t.isNumber) { 599 | advance(); 600 | return LitExpr(SmyNum(t.number)); 601 | } 602 | if (t.isString) { 603 | final buffer = StringBuffer(); 604 | while (token.isString) { 605 | buffer.write(token.string); 606 | advance(); 607 | } 608 | return LitExpr(SmyString(buffer.toString())); 609 | } 610 | throw syntaxError('expected (, [, {, NAME, NUMBER, or STRING'); 611 | } 612 | 613 | Expr _parseTupleMaker() { 614 | if (at(')')) return const TupleExpr([]); 615 | final expr = parseTest(); 616 | if (at(')')) return expr; 617 | expect(','); 618 | final exprs = [expr] + parseTestListOpt(); 619 | expect(')'); 620 | return TupleExpr(exprs); 621 | } 622 | 623 | Expr _parseListMaker() { 624 | final exprs = parseTestListOpt(); 625 | expect(']'); 626 | return ListExpr(exprs); 627 | } 628 | 629 | /// `dictorsetmaker: test ':' test {',' test ':' test} [','] | testlist` 630 | Expr _parseDictOrSetMaker() { 631 | if (at('}')) return const DictExpr([]); 632 | final expr = parseTest(); 633 | if (at(':')) { 634 | // dictionary 635 | final exprs = [expr, parseTest()]; 636 | while (at(',')) { 637 | if (at('}')) return DictExpr(exprs); 638 | exprs.add(parseTest()); 639 | expect(':'); 640 | exprs.add(parseTest()); 641 | } 642 | expect('}'); 643 | return DictExpr(exprs); 644 | } else { 645 | // set 646 | final exprs = [expr]; 647 | if (at(',')) exprs.addAll(parseTestListOpt()); 648 | expect('}'); 649 | return SetExpr(exprs); 650 | } 651 | } 652 | 653 | /// `NAME` 654 | String parseName() { 655 | final t = token; 656 | if (t.isName) { 657 | advance(); 658 | return t.value; 659 | } 660 | throw syntaxError('expected NAME'); 661 | } 662 | 663 | // -------- Expression list parsing -------- 664 | 665 | /// `testlist: test {',' test} [',']` 666 | Expr parseTestOrListAsTuple() { 667 | final test = parseTest(); 668 | if (!at(',')) return test; 669 | final tests = [test]; 670 | if (hasTest) tests.addAll(parseTestListOpt()); 671 | return TupleExpr(tests); 672 | } 673 | 674 | /// `testlist: test {',' test} [',']` 675 | List parseTestListOpt() { 676 | final exprs = []; 677 | if (hasTest) { 678 | exprs.add(parseTest()); 679 | while (at(',')) { 680 | if (!hasTest) break; 681 | exprs.add(parseTest()); 682 | } 683 | } 684 | return exprs; 685 | } 686 | 687 | /// Returns whether the current token is a valid start of a `test`. 688 | /// It must be either a name, a number, a string, a prefix `+` or `-` or 689 | /// `~`, the `not` keyword, or `(`, `[`, and `{`. 690 | bool get hasTest { 691 | // final t = token; 692 | // return t.isName || t.isNumber || "+-~([{\"'".contains(t.value[0]) || t.value == "not"; 693 | return token.value.startsWith(RegExp('[-+~\'"\\d([{]')) || token.isName || token.value == 'not'; 694 | } 695 | } 696 | 697 | /// Denotes a parsing error. 698 | class SyntaxError implements Exception { 699 | SyntaxError(this.message); 700 | final String message; 701 | 702 | @override 703 | String toString() => 'SyntaxError: $message'; 704 | } 705 | -------------------------------------------------------------------------------- /lib/scanner.dart: -------------------------------------------------------------------------------- 1 | /// Splits Smython source code into tokens. 2 | library scanner; 3 | 4 | import 'token.dart'; 5 | export 'token.dart'; 6 | 7 | /// Returns an iterable of [Token]s generated from [source]. 8 | /// 9 | /// Restrictions compared to Python: 10 | /// * Code must be indented by exactly four spaces. 11 | /// * TABs are not allowed. 12 | /// * Lines ending with `\` are joined with the next line and mess up 13 | /// line numbers in error messages. 14 | Iterable tokenize(String source) sync* { 15 | // keep track of indentation 16 | var curIndent = 0; 17 | var newIndent = 0; 18 | 19 | // keep track of open parens and brackets to supress indentation 20 | var parens = 0; 21 | 22 | // combine lines with trailing backslashes with following lines 23 | source = source.replaceAll('\\\n', ''); 24 | 25 | // assure that the source ends with a newline 26 | source += '\n'; 27 | 28 | // compile the regular expression to tokenize the source 29 | final regex = RegExp( 30 | '^ *(?:#.*)?\n|#.*\$|(' // whitespace and comments 31 | '^ +|' // indentation 32 | '\n|' // newline 33 | '\\d+(?:\\.\\d*)?|' // numbers 34 | '\\w+|' // names 35 | '[()\\[\\]{}:.,;]|' // syntax 36 | '[+\\-*/%<>=|&]=?|!=|~|' // operators 37 | "'(?:\\\\[n'\"\\\\]|[^'])*'|" // single-quoted strings 38 | '"(?:\\\\[n\'"\\\\]|[^"])*"' // double-quoted strings 39 | ')', 40 | multiLine: true, 41 | ); 42 | 43 | for (final match in regex.allMatches(source)) { 44 | // did we get a match (empty lines and comments are ignored)? 45 | final s = match[1]; 46 | if (s == null) continue; 47 | if (s[0] == ' ') { 48 | // compute new indentation which is applied before the next non-whitespace token 49 | if (parens == 0) newIndent = s.length ~/ 4; 50 | } else { 51 | if (s[0] == '\n') { 52 | // reset indentation 53 | if (parens == 0) newIndent = 0; 54 | } else { 55 | // found a non-whitespace token, apply new indentation 56 | while (curIndent < newIndent) { 57 | yield Token.indent; 58 | curIndent++; 59 | } 60 | while (curIndent > newIndent) { 61 | yield Token.dedent; 62 | curIndent--; 63 | } 64 | } 65 | if (parens > 0 && s == '\n') continue; 66 | if (s == '(' || s == '[' || s == '{') parens++; 67 | if (s == ')' || s == ']' || s == '}') parens--; 68 | // add newline or non-whitespace token to result 69 | yield Token(match.input, match.start, match.end); 70 | } 71 | } 72 | 73 | // balance pending INDENTs 74 | while (curIndent > 0) { 75 | yield Token.dedent; 76 | curIndent--; 77 | } 78 | 79 | // append EOF 80 | yield Token.eof; 81 | } 82 | -------------------------------------------------------------------------------- /lib/smython.dart: -------------------------------------------------------------------------------- 1 | /// The runtime system for Smython. 2 | /// 3 | /// Create a new [Smython] instance to run code. It has a small but 4 | /// extendable number of builtin function. Use [Smython.builtin] to add 5 | /// your own. Use [Smython.execute] to run Smython code. 6 | /// 7 | /// Example: 8 | /// ``` 9 | /// Smython().execute('print(3+4)'); 10 | /// ``` 11 | /// 12 | /// To learn more about the supported syntax, see `parser.dart`. 13 | /// 14 | /// See [SmyValue] for how Smython values are represented in Dart. Use 15 | /// [make] to convert a Dart value into a [SmyValue] instance. This might 16 | /// throw if there is no Smython value of tha given Dart value. 17 | library smython; 18 | 19 | import 'dart:io'; 20 | import 'dart:math'; 21 | 22 | import 'ast_eval.dart' show Expr, Suite; 23 | import 'parser.dart' show parse; 24 | 25 | export 'parser.dart' show parse; 26 | 27 | typedef SmythonBuiltin = SmyValue Function(Frame f, List args); 28 | 29 | /// Main entry point. 30 | class Smython { 31 | final builtins = {}; 32 | final globals = {}; 33 | final modules = {}; 34 | 35 | Smython() { 36 | builtin('print', (f, args) { 37 | print(args.map((v) => '$v').join(' ')); 38 | return none; 39 | }); 40 | builtin('len', (f, args) { 41 | if (args.length != 1) throw 'TypeError: len() takes 1 argument (${args.length} given)'; 42 | return SmyNum(args[0].length); 43 | }); 44 | builtin('slice', (f, args) { 45 | if (args.length != 3) throw 'TypeError: slice() takes 3 arguments (${args.length} given)'; 46 | return SmyTuple(args); 47 | }); 48 | builtin('del', (f, args) { 49 | if (args.length != 2) throw 'TypeError: del() takes 2 arguments (${args.length} given)'; 50 | final value = args[0]; 51 | final index = args[1]; 52 | if (value is SmyList) { 53 | if (index is SmyNum) { 54 | return value.values.removeAt(index.index); 55 | } 56 | if (index is SmyTuple) { 57 | final length = value.values.length; 58 | var start = index.values[0].isNone ? 0 : index.values[0].index; 59 | var end = index.values[1].isNone ? value.values.length : index.values[1].index; 60 | if (start < 0) start += length; 61 | if (end < 0) end += length; 62 | if (start >= end) return none; 63 | value.values.removeRange(start, end); 64 | return none; 65 | } 66 | throw 'TypeError: invalid index'; 67 | } 68 | if (value is SmyDict) { 69 | return value.values.remove(index) ?? SmyValue.none; 70 | } 71 | throw 'TypeError: Unsupported item deletion'; 72 | }); 73 | builtin('range', (f, args) { 74 | if (args.isEmpty) { 75 | throw 'TypeError: range expected at least 1 argument, got ${args.length}'; 76 | } 77 | if (args.length > 3) { 78 | throw 'TypeError: range expected at most 3 arguments, got ${args.length}'; 79 | } 80 | var begin = 0; 81 | var end = args[0].intValue; 82 | var step = 1; 83 | if (args.length == 2) { 84 | begin = end; 85 | end = args[1].intValue; 86 | } 87 | if (args.length == 3) { 88 | step = args[2].intValue; 89 | } 90 | if (step == 0) throw 'ValueError: range() arg 3 must not be zero'; 91 | if (step < 0) { 92 | return SmyList([for (var i = begin; i > end; i += step) SmyNum(i)]); 93 | } 94 | return SmyList([for (var i = begin; i < end; i += step) SmyNum(i)]); 95 | }); 96 | builtin('hasattr', (f, args) { 97 | if (args.length != 2) throw 'TypeError: hasattr() takes 2 arguments (${args.length} given)'; 98 | final value = args[0]; 99 | final key = args[1]; 100 | if (value is SmyList) { 101 | return SmyBool(value.values.contains(key)); 102 | } 103 | if (value is SmyDict) { 104 | return SmyBool(value.values.containsKey(key)); 105 | } 106 | if (value is SmyModule) { 107 | return SmyBool(value.globals.values.containsKey(key)); 108 | } 109 | throw 'TypeError: Unsupported hasattr()'; 110 | }); 111 | builtin('chr', (f, args) { 112 | if (args.length != 1) throw 'TypeError: chr() takes 1 argument (${args.length} given)'; 113 | return SmyString(String.fromCharCode(args[0].intValue)); 114 | }); 115 | builtin('ord', (f, args) { 116 | if (args.length != 1) throw 'TypeError: ord() takes 1 argument (${args.length} given)'; 117 | return SmyNum(args[0].stringValue.codeUnitAt(0)); 118 | }); 119 | } 120 | 121 | /// Adds [func] as a new builtin function [name] to the system. 122 | void builtin(String name, SmythonBuiltin func) { 123 | final bname = SmyString.intern(name); 124 | builtins[bname] = SmyBuiltin(bname, func); 125 | } 126 | 127 | SmyModule? import(String moduleName) { 128 | final name = SmyString.intern(moduleName); 129 | final module = modules[name]; 130 | if (module is SmyModule) return module; 131 | 132 | if (moduleName == 'sys') { 133 | return modules[name] = SmyModule( 134 | name, 135 | SmyDict({ 136 | SmyString('modules'): SmyDict(modules), 137 | }), 138 | ); 139 | } 140 | 141 | if (moduleName == 'os') { 142 | return modules[name] = SmyModule( 143 | name, 144 | SmyDict({ 145 | SmyString('getlogin'): SmyBuiltin(SmyString('getlogin'), (cf, args) { 146 | return SmyString(Platform.environment['USER'] ?? ''); 147 | }), 148 | SmyString('getpid'): SmyBuiltin(SmyString('getpid'), (cf, args) { 149 | return SmyNum(pid); 150 | }), 151 | }), 152 | ); 153 | } 154 | 155 | if (moduleName == 'random') { 156 | var random = Random(); 157 | return modules[name] = SmyModule( 158 | name, 159 | SmyDict({ 160 | SmyString('seed'): SmyBuiltin(SmyString('seed'), (cf, args) { 161 | random = Random(args[0].intValue); 162 | return none; 163 | }), 164 | SmyString('randint'): SmyBuiltin(SmyString('randint'), (cf, args) { 165 | final min = args[0].intValue; 166 | final max = args[1].intValue; 167 | return SmyNum(random.nextInt(max - min) + min); 168 | }), 169 | }), 170 | ); 171 | } 172 | 173 | String source; 174 | if (moduleName == 'curses') { 175 | source = ''' 176 | class Curses: 177 | def clear(self): pass 178 | def clrtoeol(self): pass 179 | def getkey(self): return '?' 180 | def move(self, row, col): pass 181 | def inch(self, row, col): return 0 182 | def refresh(self): pass 183 | def standout(self): pass 184 | def standend(self): pass 185 | def addch(self, *args): pass 186 | def addstr(self, *args): pass 187 | def cbreak(): pass 188 | def noecho(): pass 189 | def nonl(): pass 190 | def endwin(): pass 191 | def beep(): pass 192 | def initscr(): return Curses() 193 | '''; 194 | } else if (moduleName == 'atexit') { 195 | source = ''' 196 | def register(func): pass 197 | '''; 198 | } else if (moduleName == 'copy') { 199 | source = ''' 200 | def copy(obj): return obj 201 | '''; 202 | } else if (moduleName == 'time') { 203 | source = ''' 204 | '''; 205 | } else { 206 | final file = File('pyrogue/$moduleName.py'); 207 | if (!file.existsSync()) return null; 208 | source = file.readAsStringSync(); 209 | } 210 | final globals = {}; 211 | modules[name] = SmyModule(name, SmyDict(globals)); 212 | parse(source).evaluate(Frame(null, globals, globals, builtins, this)); 213 | return modules[name] as SmyModule; 214 | } 215 | 216 | /// Runs [source]. 217 | void execute(String source) { 218 | parse(source).evaluate(Frame(null, globals, globals, builtins, this)); 219 | } 220 | } 221 | 222 | /// The global value representing no other value. 223 | const none = SmyValue.none; 224 | 225 | /// Returns the Smython value for a Dart [value]. 226 | SmyValue make(Object? value) { 227 | if (value == null) return SmyNone(); 228 | if (value is SmyValue) return value; 229 | if (value is bool) return SmyBool(value); 230 | if (value is num) return SmyNum(value); 231 | if (value is String) return SmyString(value); 232 | if (value is List) return SmyList(value); 233 | if (value is List) return make([...value.map(make)]); 234 | if (value is Map) return SmyDict(value); 235 | if (value is Map) return make(value.map((dynamic key, dynamic value) => MapEntry(make(key), make(value)))); 236 | if (value is Set) return SmySet(value); 237 | if (value is Set) return make(value.map(make).toSet()); 238 | throw "TypeError: alien value '$value'"; 239 | } 240 | 241 | /// Everything in Smython is a value. 242 | /// 243 | /// There are a lot of subclasses: 244 | /// - [SmyNone] represents `None` 245 | /// - [SmyBool] represents `True` and `False` 246 | /// - [SmyNum] represents integer and double numbers 247 | /// - [SmyString] represents strings 248 | /// - [SmyTuple] represents tuples (immutable fixed-size arrays) 249 | /// - [SmyList] represents lists (mutable growable arrays) 250 | /// - [SmyDict] represents dicts (mutable hash maps) 251 | /// - [SmySet] represents sets (mutable hash sets) 252 | /// - [SmyClass] represents classes 253 | /// - [SmyObject] represents objects (instances of classes) 254 | /// - [SmyMethod] represents methods (functions bound to instances) 255 | /// - [SmyFunc] represents user defined functions 256 | /// - [SmyBuiltin] represents built-in functions 257 | /// 258 | /// Each value knows whether it is the [none] singleton. 259 | /// Each value has an associated boolean value ([boolValue]). 260 | /// Each value has a print string ([toString]). 261 | /// Some values have associated [intValue] or [doubleValue]. 262 | /// Some values are even strings ([stringValue]). 263 | /// Some values are callable ([call]). 264 | /// Some values are iterable ([iterable]). 265 | /// Those values also have an associated [length]. 266 | /// Some values have attributes which can be get, set and/or deleted. 267 | /// Some values are representable as a Dart map ([mapValue]). 268 | /// Some values can be used as a list index ([index]). 269 | /// 270 | /// For efficency, [SmyString.intern] can be used to create unique strings, 271 | /// so called symbols which are used in [Frame] objects to lookup values. 272 | /// 273 | /// There are threee singletons: [none], [trueValue], and [falseValue]. 274 | sealed class SmyValue { 275 | const SmyValue(); 276 | 277 | bool get isNone => false; 278 | bool get boolValue => false; 279 | num get numValue => throw 'TypeError: Not a number'; 280 | int get intValue => numValue.toInt(); 281 | double get doubleValue => numValue.toDouble(); 282 | String get stringValue => throw 'TypeError: Not a string'; 283 | SmyValue call(Frame cf, List args) => throw 'TypeError: Not callable'; 284 | Iterable get iterable => throw 'TypeError: Not iterable'; 285 | int get length => iterable.length; 286 | 287 | Never attributeError(String name) => throw "AttributeError: No attribute '$name'"; 288 | SmyValue getAttr(String name) => attributeError(name); 289 | SmyValue setAttr(String name, SmyValue value) => attributeError(name); 290 | SmyValue delAttr(String name) => attributeError(name); 291 | 292 | Map get mapValue => throw 'TypeError: Not a dict'; 293 | 294 | int get index => throw 'TypeError: list indices must be integers'; 295 | 296 | static const SmyNone none = SmyNone._(); 297 | static const SmyBool trueValue = SmyBool._(true); 298 | static const SmyBool falseValue = SmyBool._(false); 299 | } 300 | 301 | /// `None` (singleton, equatable, hashable) 302 | final class SmyNone extends SmyValue { 303 | factory SmyNone() => SmyValue.none; 304 | 305 | const SmyNone._(); 306 | 307 | @override 308 | bool operator ==(Object other) => other is SmyNone; 309 | 310 | @override 311 | int get hashCode => 18736098234; 312 | 313 | @override 314 | String toString() => 'None'; 315 | 316 | @override 317 | bool get isNone => true; 318 | 319 | @override 320 | bool get boolValue => false; 321 | } 322 | 323 | /// `True` or `False` (singletons, equatable, hashable) 324 | final class SmyBool extends SmyValue { 325 | factory SmyBool(bool value) => value ? SmyValue.trueValue : SmyValue.falseValue; 326 | 327 | const SmyBool._(this.value); 328 | 329 | final bool value; 330 | 331 | @override 332 | bool operator ==(Object other) => other is SmyBool && value == other.value; 333 | 334 | @override 335 | int get hashCode => value.hashCode; 336 | 337 | @override 338 | String toString() => value ? 'True' : 'False'; 339 | 340 | @override 341 | bool get boolValue => value; 342 | } 343 | 344 | /// `NUMBER` (equatable, hashable) 345 | final class SmyNum extends SmyValue { 346 | const SmyNum(this.value); 347 | final num value; 348 | 349 | @override 350 | bool operator ==(Object other) => other is SmyNum && value == other.value; 351 | 352 | @override 353 | int get hashCode => value.hashCode; 354 | 355 | @override 356 | String toString() => '$value'; 357 | 358 | @override 359 | bool get boolValue => value != 0; 360 | 361 | @override 362 | num get numValue => value; 363 | 364 | @override 365 | int get index { 366 | return intValue.toInt(); 367 | } 368 | } 369 | 370 | /// `STRING` (equatable, hashable) 371 | final class SmyString extends SmyValue { 372 | const SmyString(this.value); 373 | final String value; 374 | 375 | static final Map _interns = {}; 376 | static SmyString intern(String value) => _interns.putIfAbsent(value, () => SmyString(value)); 377 | 378 | @override 379 | bool operator ==(Object other) => other is SmyString && value == other.value; 380 | 381 | @override 382 | int get hashCode => value.hashCode; 383 | 384 | @override 385 | String toString() => value; 386 | 387 | @override 388 | bool get boolValue => value.isNotEmpty; 389 | 390 | @override 391 | String get stringValue => value; 392 | 393 | @override 394 | int get length => value.length; 395 | 396 | SmyString format(SmyValue args) { 397 | if (args is SmyTuple) { 398 | var index = 0; 399 | return SmyString(stringValue.replaceAllMapped(RegExp(r'%%|%(\d+)?([sd])'), (match) { 400 | if (match[0] == '%%') return '%'; 401 | String value; 402 | if (match[2] == 's') { 403 | value = '${args.values[index++]}'; 404 | } else { 405 | value = '${args.values[index++].intValue}'; 406 | } 407 | if (match[1] != null) { 408 | final width = int.parse(match[1]!); 409 | value = value.padLeft(width); 410 | } 411 | return value; 412 | })); 413 | } 414 | if (args is SmyList) return format(SmyTuple(args.values)); 415 | return format(SmyTuple([args])); 416 | } 417 | } 418 | 419 | /// `(expr, ...)` 420 | final class SmyTuple extends SmyValue { 421 | const SmyTuple(this.values); 422 | final List values; 423 | 424 | @override 425 | String toString() { 426 | if (values.isEmpty) return '()'; 427 | if (values.length == 1) return '(${values[0]},)'; 428 | return '(${values.map((v) => '$v').join(', ')})'; 429 | } 430 | 431 | @override 432 | bool get boolValue => values.isNotEmpty; 433 | 434 | @override 435 | Iterable get iterable => values; 436 | 437 | @override 438 | int get length => values.length; 439 | } 440 | 441 | /// `[expr, ...]` 442 | final class SmyList extends SmyValue { 443 | const SmyList(this.values); 444 | final List values; 445 | 446 | @override 447 | String toString() { 448 | if (values.isEmpty) return '[]'; 449 | return '[${values.map((v) => '$v').join(', ')}]'; 450 | } 451 | 452 | @override 453 | bool get boolValue => values.isNotEmpty; 454 | 455 | @override 456 | Iterable get iterable => values; 457 | 458 | @override 459 | int get length => values.length; 460 | 461 | @override 462 | SmyValue getAttr(String name) { 463 | if (name == 'append') { 464 | return SmyMethod( 465 | this, 466 | SmyBuiltin(SmyString.intern('append'), (f, args) { 467 | if (args.length != 2) throw 'TypeError: list.append() takes exactly one argument (${args.length - 1} given)'; 468 | values.add(args[1]); 469 | return none; 470 | }), 471 | ); 472 | } 473 | return super.getAttr(name); 474 | } 475 | } 476 | 477 | /// `{expr: expr, ...}` 478 | final class SmyDict extends SmyValue { 479 | const SmyDict(this.values); 480 | final Map values; 481 | 482 | @override 483 | String toString() { 484 | if (values.isEmpty) return '{}'; 485 | return '{${values.entries.map((e) => '${e.key}: ${e.value}').join(', ')}}'; 486 | } 487 | 488 | @override 489 | bool get boolValue => values.isNotEmpty; 490 | 491 | @override 492 | Map get mapValue => values; 493 | 494 | @override 495 | Iterable get iterable => values.entries.map((e) => SmyTuple([e.key, e.value])); 496 | 497 | @override 498 | int get length => values.length; 499 | 500 | @override 501 | SmyValue getAttr(String name) { 502 | if (name == 'values') { 503 | return SmyMethod( 504 | this, 505 | SmyBuiltin(SmyString.intern('values'), (f, args) { 506 | if (args.length != 1) throw 'TypeError: dict.values() takes no arguments (${args.length - 1} given)'; 507 | return SmyList(values.values.toList()); 508 | }), 509 | ); 510 | } 511 | if (name == 'update') { 512 | return SmyMethod( 513 | this, 514 | SmyBuiltin(SmyString.intern('update'), (f, args) { 515 | if (args.length != 2) throw 'TypeError: dict.update() takes one argument (${args.length - 1} given)'; 516 | values.addAll(args[1].mapValue); 517 | return none; 518 | }), 519 | ); 520 | } 521 | return super.getAttr(name); 522 | } 523 | } 524 | 525 | /// `{expr, ...}` 526 | final class SmySet extends SmyValue { 527 | const SmySet(this.values); 528 | final Set values; 529 | 530 | @override 531 | String toString() { 532 | if (values.isEmpty) return 'set()'; 533 | return '{${values.join(', ')}}'; 534 | } 535 | 536 | @override 537 | bool get boolValue => values.isNotEmpty; 538 | 539 | @override 540 | Iterable get iterable => values; 541 | 542 | @override 543 | int get length => values.length; 544 | } 545 | 546 | /// `class name (super): ...` 547 | final class SmyClass extends SmyValue { 548 | SmyClass(this._name, this._superclass); 549 | 550 | final SmyString _name; 551 | final SmyClass? _superclass; 552 | final SmyDict _dict = SmyDict({}); 553 | 554 | Map get methods => _dict.values; 555 | 556 | SmyValue? findAttr(String name) { 557 | final n = SmyString(name); 558 | for (SmyClass? cls = this; cls != null; cls = cls._superclass) { 559 | final value = cls._dict.values[n]; 560 | if (value != null) return value; 561 | } 562 | return null; 563 | } 564 | 565 | @override 566 | String toString() => ""; 567 | 568 | /// Calling a class creates a new instance of that class. 569 | @override 570 | SmyValue call(Frame cf, List args) { 571 | final object = SmyObject(this); 572 | final init = findAttr('__init__'); 573 | if (init is SmyFunc) { 574 | init.call(cf, [object] + args); 575 | } 576 | return object; 577 | } 578 | 579 | @override 580 | SmyValue getAttr(String name) { 581 | if (name == '__name__') return _name; 582 | if (name == '__superclass__') return _superclass ?? SmyValue.none; 583 | if (name == '__dict__') return _dict; 584 | final value = _dict.values[SmyString(name)]; 585 | if (value != null) return value; 586 | return super.getAttr(name); 587 | } 588 | 589 | @override 590 | SmyValue setAttr(String name, SmyValue value) { 591 | return _dict.values[SmyString(name)] = value; 592 | } 593 | } 594 | 595 | /// class instance 596 | final class SmyObject extends SmyValue { 597 | SmyObject(this._class); 598 | 599 | final SmyClass _class; 600 | final SmyDict _dict = SmyDict({}); 601 | 602 | @override 603 | String toString() => '<${_class._name} object $hashCode>'; 604 | 605 | @override 606 | SmyValue getAttr(String name) { 607 | if (name == '__class__') return _class; 608 | if (name == '__dict__') return _dict; 609 | // returns a user-defined property 610 | final value1 = _dict.values[SmyString(name)]; 611 | if (value1 != null) return value1; 612 | // returns a class property and bind functions as methods 613 | final value = _class.findAttr(name); 614 | if (value != null) { 615 | if (value is SmyFunc) { 616 | return SmyMethod(this, value); 617 | } 618 | return value; 619 | } 620 | return super.getAttr(name); 621 | } 622 | 623 | @override 624 | SmyValue setAttr(String name, SmyValue value) { 625 | return _dict.values[SmyString(name)] = value; 626 | } 627 | } 628 | 629 | /// instance method 630 | final class SmyMethod extends SmyValue { 631 | SmyMethod(this.self, this.func); 632 | 633 | final SmyValue self; 634 | final SmyValue func; 635 | 636 | @override 637 | SmyValue call(Frame cf, List args) { 638 | return func.call(cf, [self] + args); 639 | } 640 | } 641 | 642 | /// `def name(param, ...): ...` 643 | final class SmyFunc extends SmyValue { 644 | const SmyFunc(this.df, this.name, this.params, this.defExprs, this.suite); 645 | 646 | final Frame df; 647 | final SmyString name; 648 | final List params; 649 | final List defExprs; 650 | final Suite suite; 651 | 652 | @override 653 | String toString() => ''; 654 | 655 | @override 656 | SmyValue call(Frame cf, List args) { 657 | final f = Frame(df, {}, df.globals, df.builtins, df.system); 658 | for (var i = 0, j = 0; i < params.length; i++) { 659 | final param = params[i]; 660 | if (param.startsWith('*')) { 661 | f.locals[SmyString.intern(param.substring(1))] = SmyTuple(args.sublist(i)); 662 | break; 663 | } 664 | f.locals[SmyString.intern(param)] = i < args.length ? args[i] : defExprs[j++].evaluate(df); 665 | } 666 | return suite.evaluateAsFunc(f); 667 | } 668 | } 669 | 670 | /// Builtin function like `print` or `len`. 671 | final class SmyBuiltin extends SmyValue { 672 | const SmyBuiltin(this.name, this.func); 673 | 674 | final SmyString name; 675 | final SmythonBuiltin func; 676 | 677 | @override 678 | String toString() => ''; 679 | 680 | @override 681 | SmyValue call(Frame cf, List args) => func(cf, args); 682 | } 683 | 684 | final class SmyModule extends SmyValue { 685 | const SmyModule(this.name, this.globals); 686 | 687 | final SmyString name; 688 | final SmyDict globals; 689 | 690 | @override 691 | String toString() => ''; 692 | 693 | @override 694 | SmyValue getAttr(String name) { 695 | if (name == '__name__') return this.name; 696 | if (name == '__dict__') return globals; 697 | final value = globals.values[SmyString(name)]; 698 | if (value != null) return value; 699 | return super.getAttr(name); 700 | } 701 | } 702 | 703 | // -------- Runtime -------- 704 | 705 | /// Runtime state passed to all AST nodes while evaluating them. 706 | final class Frame { 707 | Frame(this.parent, this.locals, this.globals, this.builtins, this.system); 708 | 709 | /// Links to the parent frame, a.k.a. sender. 710 | final Frame? parent; 711 | 712 | /// Private bindings local to this frame, a.k.a. local variables. 713 | final Map locals; 714 | 715 | /// Shared bindings global to this frame, a.k.a. global variables. 716 | final Map globals; 717 | 718 | /// Shared bindings global to this frame, not overwritable. 719 | final Map builtins; 720 | 721 | /// Shared reference to the runtime system. 722 | final Smython system; 723 | 724 | /// Returns the value bound to [name] by first searching [locals], 725 | /// then searching [globals], and last but not least searching the 726 | /// [builtins]. Throws a `NameError` if [name] is unbound. 727 | SmyValue lookup(SmyString name) { 728 | return locals[name] ?? 729 | parent?.lookup(name) ?? 730 | globals[name] ?? 731 | builtins[name] ?? 732 | (throw "NameError: name '$name' is not defined"); 733 | } 734 | 735 | SmyValue set(SmyString name, SmyValue value) { 736 | for (Frame? f = this; f != null; f = f.parent) { 737 | if (f.locals.containsKey(name)) { 738 | return f.locals[name] = value; 739 | } 740 | } 741 | return locals[name] = value; 742 | } 743 | } 744 | -------------------------------------------------------------------------------- /lib/test_runner.dart: -------------------------------------------------------------------------------- 1 | /// A test runner for Smython source code. 2 | /// 3 | /// Lines starting with `#` and empty lines are ignored. Lines starting 4 | /// with `>>>` or `...` are stripped from the prefix, are combined and then 5 | /// executed as Symthon code, comparing the result of the last expression 6 | /// to the next non-empty, non-comment line without a prefix, after using 7 | /// [repr] to convert the result of the evaluation into a string 8 | /// representation for easy comparison. 9 | /// 10 | /// Then either `OK` is printed or both the actual and the expected value. 11 | /// 12 | /// After running all tests, the number of failures is printed. If the 13 | /// output ends with `OK`, there are no failure and everything is shiny. 14 | library test_runner; 15 | 16 | import 'dart:io'; 17 | 18 | import 'smython.dart'; 19 | 20 | /// Runs the Smython test suite loaded from [filename]. 21 | bool run(String filename) { 22 | var failures = 0; 23 | final report = stdout; 24 | final buffer = StringBuffer(); 25 | 26 | for (final line in File(filename).readAsLinesSync()) { 27 | if (line.isEmpty || line.startsWith('#')) continue; 28 | if (line.startsWith('>>> ') || line.startsWith('... ')) { 29 | buffer.writeln(line.substring(4)); 30 | } else { 31 | final expected = line; 32 | final source = buffer.toString(); 33 | // report.writeln('----------'); 34 | // report.write(source); 35 | 36 | String actual; 37 | try { 38 | final system = Smython(); 39 | final suite = parse(source); 40 | final frame = Frame(null, {}, {}, system.builtins, system); 41 | actual = repr(suite.evaluate(frame)); 42 | } catch (e) { 43 | actual = '$e'; 44 | } 45 | if (actual == expected) { 46 | // report.writeln('OK'); 47 | } else { 48 | report.writeln('----------'); 49 | report.write(source); 50 | report.writeln('Actual..: $actual'); 51 | report.writeln('Expected: $expected'); 52 | failures++; 53 | } 54 | 55 | buffer.clear(); 56 | } 57 | } 58 | if (failures > 0) { 59 | report.writeln('----------'); 60 | report.writeln('$failures failure(s)'); 61 | } 62 | return failures == 0; 63 | } 64 | 65 | /// Returns a canonical string prepresentation of the given [value] that 66 | /// can be used to compare a computed value to a stringified value from the 67 | /// the suite. Strings are therefore displayed always with single quotes. 68 | /// Sets and dictionaries are displayed after sorting their keys first. 69 | /// Then sets, dictionaries, and lists are recursively displayed using 70 | /// [repr]. All other values are displayed using their `toString` method. 71 | String repr(SmyValue? value) { 72 | if (value == null) throw 'missing value'; 73 | if (value is SmyString) { 74 | return "'${value.value.replaceAll('\\', '\\\\').replaceAll('\'', '\\\'').replaceAll('\n', '\\n')}'"; 75 | } else if (value is SmyTuple) { 76 | return '(${value.values.map(repr).join(', ')}${value.length == 1 ? ',' : ''})'; 77 | } else if (value is SmyList) { 78 | return '[${value.values.map(repr).join(', ')}]'; 79 | } else if (value is SmySet) { 80 | return '{${(value.values.map(repr).toList()..sort()).join(', ')}}'; 81 | } else if (value is SmyDict) { 82 | final v = value.values.entries.map((e) => MapEntry(repr(e.key), repr(e.value))).toList(); 83 | return '{${(v..sort((a, b) => a.key.compareTo(b.key))).map((e) => '${e.key}: ${e.value}').join(', ')}}'; 84 | } 85 | return '$value'; 86 | } 87 | -------------------------------------------------------------------------------- /lib/token.dart: -------------------------------------------------------------------------------- 1 | /// The [Token] class is used by the scanner to split Smython source code 2 | /// into bits and pieces which are then processed by the parser. 3 | library token; 4 | 5 | /// Represents a piece of source code. 6 | /// 7 | /// Tokens are either keywords, NAMEs, NUMBERs, STRINGs, operators, syntax, 8 | /// or synthesized INDENT, DEDENT, NEWLINE, or EOF tokens. Currently, these 9 | /// synthesized tokens have no valid line number. 10 | /// 11 | /// Tokens are spans of source code and do not allocate. 12 | class Token { 13 | const Token(this._source, this._start, this._end); 14 | 15 | final String _source; 16 | final int _start; 17 | final int _end; 18 | 19 | /// Returns the piece of source code this token represents. 20 | String get value => _source.substring(_start, _end); 21 | 22 | /// Returns whether this token is a reserved Smython keyword. 23 | bool get isKeyword => _keywords.contains(value); 24 | 25 | /// Returns whether this token is a NAME but not also a keyword. 26 | bool get isName => value.startsWith(RegExp('[A-Za-z_]')) && !isKeyword; 27 | 28 | /// Returns whether this token is a (positive) NUMBER. 29 | bool get isNumber => value.startsWith(RegExp('[0-9]')); 30 | 31 | /// Returns whether this token is a quoted STRING. 32 | bool get isString => _source[_start] == '"' || _source[_start] == "'"; 33 | 34 | /// Returns the token's numeric value (only valid if [isNumber] is true). 35 | num get number => num.parse(value); 36 | 37 | /// Returns the token's string value (only valid if [isString] is true). 38 | String get string => _unescape(_source.substring(_start + 1, _end - 1)); 39 | 40 | /// Returns the line of the source code this token is at (1-based). 41 | /// 42 | /// This is computed on the fly by counting newline characters in front 43 | /// of the token, so don't call it too often. Synthesized tokens have no 44 | /// valid line, so don't call this for [indent], [dedent], or [eof]. 45 | int get line { 46 | var line = 1; 47 | for (var i = 0; i < _start; i++) { 48 | if (_source[i] == '\n') line++; 49 | } 50 | return line; 51 | } 52 | 53 | @override 54 | bool operator ==(Object other) { 55 | return other is Token && value == other.value; 56 | } 57 | 58 | @override 59 | int get hashCode => value.hashCode; 60 | 61 | @override 62 | String toString() => value == '\n' ? 'NEWLINE' : value; 63 | 64 | /// A synthetic token representing an indentation before the next line. 65 | static const indent = Token('!INDENT', 0, 7); 66 | 67 | /// A synthetic token representing a dedentation before the next line. 68 | static const dedent = Token('!DEDENT', 0, 7); 69 | 70 | /// A synthetic token representing the end of the input. 71 | static const eof = Token('!EOF', 0, 4); 72 | 73 | /// Replaces `\n`, `\'`, `\"`, or `\\` in [s]. 74 | static String _unescape(String s) { 75 | // see scanner.dart for which string escapes are supported 76 | return s.replaceAllMapped(RegExp('\\\\([n\'"\\\\])'), (match) { 77 | final s = match.group(1)!; 78 | return s == 'n' ? '\n' : s; 79 | }); 80 | } 81 | 82 | static const _keywords = { 83 | 'and', 84 | 'as', 85 | 'assert', 86 | 'break', 87 | 'class', 88 | 'continue', 89 | 'def', 90 | //'del', 91 | 'elif', 92 | 'else', 93 | 'except', 94 | 'exec', 95 | 'finally', 96 | 'for', 97 | 'from', 98 | 'global', 99 | 'if', 100 | 'import', 101 | 'in', 102 | 'is', 103 | 'lambda', 104 | 'not', 105 | 'or', 106 | 'pass', 107 | 'raise', 108 | 'return', 109 | 'try', 110 | 'while', 111 | 'with', 112 | 'yield', 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /parser_tests.py: -------------------------------------------------------------------------------- 1 | # simple tests 2 | >>> 1 3 | 1 4 | >>> a=1 5 | >>> a 6 | 1 7 | >>> a=1 8 | >>> b=2 9 | >>> a+b 10 | 3 11 | >>> 4.8 12 | 4.8 13 | 14 | # arthmetic 15 | >>> 1+3 16 | 4 17 | >>> 5-4 18 | 1 19 | >>> -5 20 | -5 21 | >>> 2*3 22 | 6 23 | >>> 9/3 24 | 3.0 25 | >>> 4 % 3 26 | 1 27 | >>> 1+2*3 28 | 7 29 | >>> (1+2)*3 30 | 9 31 | >>> 3==3 32 | True 33 | >>> 3!=3 34 | False 35 | >>> 3 & 2 36 | 2 37 | >>> 1 | 2 38 | 3 39 | >>> ~0 40 | -1 41 | >>> ~5 42 | -6 43 | >>> ~-6 44 | 5 45 | 46 | # parallel assignment 47 | >>> a, b = 2, 3 48 | >>> a, b 49 | (2, 3) 50 | >>> a, b = 2, 3 51 | >>> a, b = b, a 52 | >>> a, b 53 | (3, 2) 54 | >>> a = 1, 2 55 | >>> a, (b, c) = 0, a 56 | >>> a, b, c 57 | (0, 1, 2) 58 | 59 | # while loop 60 | >>> a = 0 61 | >>> while a < 3: 62 | ... a = a + 1 63 | ... else: 64 | ... b = 1 65 | >>> a, b 66 | (3, 1) 67 | >>> a = 0 68 | >>> while a < 3: 69 | ... a = a + 1 70 | ... if a == 1: break 71 | ... else: 72 | ... a = 0 73 | >>> a 74 | 1 75 | >>> a = 0 76 | ... while True: 77 | ... a = a + 1 78 | ... if a == 1: continue 79 | ... break 80 | ... a 81 | 2 82 | 83 | # for loop 84 | >>> s = 0 85 | >>> for i in 1, 2, 3: 86 | ... s = s + i 87 | ... else: 88 | ... s = -s 89 | >>> s 90 | -6 91 | >>> s = 0 92 | >>> for i in 1, 2, 3: 93 | ... s = s + i 94 | ... if i == 2: 95 | ... break 96 | ... else: s = 0 97 | >>> s 98 | 3 99 | >>> s = 0 100 | ... for i in 1, 2, 3: 101 | ... s = 1 102 | ... continue 103 | ... s = 2 104 | ... s 105 | 1 106 | 107 | # complex if 108 | >>> a=1 109 | >>> if a == 0: 110 | ... a = a + 1 111 | ... elif a == 1: 112 | ... a = a + 3 113 | ... else: 114 | ... a = a + 5 115 | >>> a 116 | 4 117 | >>> a = 3; a = (1 if a > 2 else 4); a 118 | 1 119 | 120 | # constants 121 | >>> True, False, None 122 | (True, False, None) 123 | 124 | # function 125 | >>> def f(): return 1 126 | >>> f() 127 | 1 128 | >>> def f(n): return n+1 129 | >>> f(2) 130 | 3 131 | 132 | # function with default parameters 133 | >>> def f(x=2): return x 134 | >>> f() 135 | 2 136 | >>> def f(x=2): return x 137 | >>> f(3) 138 | 3 139 | 140 | # strings 141 | >>> "Hallo, Welt" 142 | 'Hallo, Welt' 143 | >>> "'" '"' 144 | '\'"' 145 | >>> "\n" 146 | '\n' 147 | >>> '' 148 | '' 149 | >>> a = "abc" 150 | >>> len(a) 151 | 3 152 | >>> 'abc'[0] 153 | 'a' 154 | >>> ''[-2] 155 | IndexError: index out of range 156 | >>> 'abc'[1:] 157 | 'bc' 158 | >>> 'abc'[:-2] 159 | 'a' 160 | 161 | # lists 162 | >>> [] 163 | [] 164 | >>> a = [1, [2], 3]; a[1:], a[:1] 165 | ([[2], 3], [1]) 166 | >>> len([]), len([1]) 167 | (0, 1) 168 | 169 | # tuples 170 | >>> () 171 | () 172 | >>> a = (1, (2,), 3); a[2:], a[:2] 173 | ((3,), (1, (2,))) 174 | >>> len(()), len((3,)), len(((), ())) 175 | (0, 1, 2) 176 | 177 | # dicts 178 | >>> {} 179 | {} 180 | >>> a = {'a': 3, 'b': 4} 181 | >>> len(a), a['a'], a['b'], a['c'] 182 | (2, 3, 4, None) 183 | 184 | # sets 185 | >>> {1} 186 | {1} 187 | >>> {1,2,2,1} 188 | {1, 2} 189 | 190 | # in 191 | >>> 3 in [1, 2, 3], 3 not in [1, 2] 192 | UnimplementedError 193 | >>> 3 in (1, 2, 3), 3 not in (1, 2) 194 | UnimplementedError 195 | >>> 3 in {1, 2, 3}, 3 not in {1, 2} 196 | UnimplementedError 197 | >>> 3 in {1: '1', 2: '2', 3: '3'}, 3 not in {1: 1, 2: 2} 198 | UnimplementedError 199 | 200 | # complex for 201 | >>> kk, vv = 0, 0 202 | >>> for k,v in {3: 1, 4: 2}: 203 | ... kk = kk + k 204 | ... vv = vv + v 205 | >>> (kk, vv) 206 | (7, 3) 207 | 208 | # logic 209 | >>> False and False 210 | False 211 | >>> True and False 212 | False 213 | >>> False and True 214 | False 215 | >>> True and True 216 | True 217 | >>> False or False 218 | False 219 | >>> True or False 220 | True 221 | >>> False or True 222 | True 223 | >>> True or True 224 | True 225 | >>> not True, not False 226 | (False, True) 227 | >>> not not True 228 | True 229 | 230 | # exceptions 231 | >>> a = 0 232 | >>> try: 233 | ... raise 234 | ... a = 4 235 | ... except: 236 | ... a = 1 237 | ... else: 238 | ... a = a + 1 239 | >>> a 240 | 1 241 | >>> a = 0 242 | >>> try: 243 | ... try: 244 | ... raise 245 | ... a = 4 246 | ... finally: 247 | ... a = 1 248 | ... except: 249 | ... a = a + 1 250 | >>> a 251 | 2 252 | >>> a = 0 253 | >>> try: 254 | ... a = 4 255 | ... except: 256 | ... a = 1 257 | ... else: 258 | ... a = a + 1 259 | >>> a 260 | 5 261 | >>> a = 0 262 | ... try: 263 | ... raise 2 264 | ... except 1: 265 | ... a = 1 266 | ... except 2 as b: 267 | ... a = b 268 | ... a 269 | 2 270 | 271 | # classes & instances 272 | >>> class A: 273 | ... def m(self): return 1 274 | >>> class B(A): 275 | ... def n(self): 276 | ... return 2 277 | >>> a, b = A(), B() 278 | >>> a.m(), b.m(), b.n() 279 | (1, 1, 2) 280 | >>> class A: pass 281 | >>> class B (A): pass 282 | >>> A, B.__superclass__, B.__superclass__.__superclass__ 283 | (, , None) 284 | >>> class C: 285 | ... def __init__(self, x): self.x = x 286 | ... def m(self): return self.x + 1 287 | >>> c = C(7) 288 | >>> c.x, c.m() 289 | (7, 8) 290 | 291 | # get/set/del 292 | >>> a = {1: 2} 293 | >>> b = len(a) 294 | >>> del(a, 1) 295 | >>> b, len(a) 296 | (1, 0) 297 | 298 | # factorial 299 | >>> def fac(n): 300 | ... if n == 0: 301 | ... return 1 302 | ... return n * fac(n - 1) 303 | >>> fac(11) 304 | 39916800 305 | 306 | # fibonacci 307 | >>> def fib(n): 308 | ... if n <= 2: return 1 309 | ... return fib(n - 1) + fib(n - 2) 310 | >>> fib(20) 311 | 6765 312 | 313 | # syntax errors 314 | >>> if 1 315 | SyntaxError: expected : but found NEWLINE at line 1 316 | >>> break 1 317 | SyntaxError: expected NEWLINE but found 1 at line 1 318 | >>> class "A" 319 | SyntaxError: expected NAME but found "A" at line 1 320 | >>> global a, b, 321 | SyntaxError: expected NAME but found NEWLINE at line 1 322 | >>> a = 323 | SyntaxError: expected (, [, {, NAME, NUMBER, or STRING but found NEWLINE at line 1 324 | 325 | # no INDENT/DEDENT/NEWLINE inside of parentheses 326 | >>> a = [1, 327 | ... 2] 328 | ... a 329 | [1, 2] 330 | >>> a = { 331 | ... 1: 2, 332 | ... } 333 | {1: 2} 334 | 335 | # assert 336 | >>> assert True 337 | >>> assert True, "message" 338 | >>> assert False 339 | AssertionError 340 | >>> assert False, "message" 341 | AssertionError: message 342 | 343 | # augmented assigns 344 | >>> a, b, c, d = 1, 2, 4, 8 345 | ... a += 5 346 | ... b -= 5 347 | ... c *= 3 348 | ... d /= 2 349 | ... (a, b, c, d) 350 | (6, -3, 12, 4.0) 351 | >>> a = 17; a %= 7; a 352 | 3 353 | >>> a = 192; a &= 224; a |= 130; a 354 | 194 355 | 356 | # imports 357 | >>> import a 358 | ModuleNotFoundError: No module named 'a' 359 | >>> import a as x 360 | ModuleNotFoundError: No module named 'a' 361 | >>> import a, b, 362 | ModuleNotFoundError: No module named 'a' 363 | >>> import a, b as x 364 | ModuleNotFoundError: No module named 'a' 365 | >>> from a import * 366 | ModuleNotFoundError: No module named 'a' 367 | >>> from a import a 368 | ModuleNotFoundError: No module named 'a' 369 | >>> from a import a, b as x, c, 370 | ModuleNotFoundError: No module named 'a' 371 | 372 | # complex comparison 373 | >>> 1 < 4 < 5 374 | True 375 | >>> 1 < 1 < 5, 1 < 5 < 5 376 | (False, False) 377 | >>> 4 >= 3 378 | True 379 | 380 | # global 381 | >>> x = 1 382 | ... def f(x): 383 | ... global x 384 | ... return x 385 | ... f(2) 386 | UnimplementedError 387 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "67.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.4.1" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.18.0" 52 | convert: 53 | dependency: transitive 54 | description: 55 | name: convert 56 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.1.1" 60 | coverage: 61 | dependency: "direct dev" 62 | description: 63 | name: coverage 64 | sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.7.2" 68 | crypto: 69 | dependency: transitive 70 | description: 71 | name: crypto 72 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "3.0.3" 76 | file: 77 | dependency: transitive 78 | description: 79 | name: file 80 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "7.0.0" 84 | frontend_server_client: 85 | dependency: transitive 86 | description: 87 | name: frontend_server_client 88 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "3.2.0" 92 | glob: 93 | dependency: transitive 94 | description: 95 | name: glob 96 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "2.1.2" 100 | http_multi_server: 101 | dependency: transitive 102 | description: 103 | name: http_multi_server 104 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "3.2.1" 108 | http_parser: 109 | dependency: transitive 110 | description: 111 | name: http_parser 112 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "4.0.2" 116 | io: 117 | dependency: transitive 118 | description: 119 | name: io 120 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.0.4" 124 | js: 125 | dependency: transitive 126 | description: 127 | name: js 128 | sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.7.1" 132 | lints: 133 | dependency: "direct dev" 134 | description: 135 | name: lints 136 | sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "3.0.0" 140 | logging: 141 | dependency: transitive 142 | description: 143 | name: logging 144 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.2.0" 148 | matcher: 149 | dependency: transitive 150 | description: 151 | name: matcher 152 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "0.12.16+1" 156 | meta: 157 | dependency: transitive 158 | description: 159 | name: meta 160 | sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.12.0" 164 | mime: 165 | dependency: transitive 166 | description: 167 | name: mime 168 | sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.0.5" 172 | node_preamble: 173 | dependency: transitive 174 | description: 175 | name: node_preamble 176 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "2.0.2" 180 | package_config: 181 | dependency: transitive 182 | description: 183 | name: package_config 184 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "2.1.0" 188 | path: 189 | dependency: transitive 190 | description: 191 | name: path 192 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "1.9.0" 196 | pool: 197 | dependency: transitive 198 | description: 199 | name: pool 200 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "1.5.1" 204 | pub_semver: 205 | dependency: transitive 206 | description: 207 | name: pub_semver 208 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "2.1.4" 212 | shelf: 213 | dependency: transitive 214 | description: 215 | name: shelf 216 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.4.1" 220 | shelf_packages_handler: 221 | dependency: transitive 222 | description: 223 | name: shelf_packages_handler 224 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "3.0.2" 228 | shelf_static: 229 | dependency: transitive 230 | description: 231 | name: shelf_static 232 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "1.1.2" 236 | shelf_web_socket: 237 | dependency: transitive 238 | description: 239 | name: shelf_web_socket 240 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "1.0.4" 244 | source_map_stack_trace: 245 | dependency: transitive 246 | description: 247 | name: source_map_stack_trace 248 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "2.1.1" 252 | source_maps: 253 | dependency: transitive 254 | description: 255 | name: source_maps 256 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "0.10.12" 260 | source_span: 261 | dependency: transitive 262 | description: 263 | name: source_span 264 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "1.10.0" 268 | stack_trace: 269 | dependency: transitive 270 | description: 271 | name: stack_trace 272 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "1.11.1" 276 | stream_channel: 277 | dependency: transitive 278 | description: 279 | name: stream_channel 280 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "2.1.2" 284 | string_scanner: 285 | dependency: transitive 286 | description: 287 | name: string_scanner 288 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "1.2.0" 292 | term_glyph: 293 | dependency: transitive 294 | description: 295 | name: term_glyph 296 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "1.2.1" 300 | test: 301 | dependency: "direct dev" 302 | description: 303 | name: test 304 | sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "1.25.2" 308 | test_api: 309 | dependency: transitive 310 | description: 311 | name: test_api 312 | sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "0.7.0" 316 | test_core: 317 | dependency: transitive 318 | description: 319 | name: test_core 320 | sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "0.6.0" 324 | typed_data: 325 | dependency: transitive 326 | description: 327 | name: typed_data 328 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "1.3.2" 332 | vm_service: 333 | dependency: transitive 334 | description: 335 | name: vm_service 336 | sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "14.0.0" 340 | watcher: 341 | dependency: transitive 342 | description: 343 | name: watcher 344 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.1.0" 348 | web: 349 | dependency: transitive 350 | description: 351 | name: web 352 | sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "0.5.0" 356 | web_socket_channel: 357 | dependency: transitive 358 | description: 359 | name: web_socket_channel 360 | sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "2.4.4" 364 | webkit_inspection_protocol: 365 | dependency: transitive 366 | description: 367 | name: webkit_inspection_protocol 368 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "1.2.1" 372 | yaml: 373 | dependency: transitive 374 | description: 375 | name: yaml 376 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "3.1.2" 380 | sdks: 381 | dart: ">=3.3.0 <4.0.0" 382 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: smython 2 | description: | 3 | A subset of Python 3, just large enough to run `fac`. 4 | Written by Stefan Matthias Aust 5 | version: 0.2.0 6 | repository: https://github.com/sma/dart_smython 7 | environment: 8 | sdk: '>=3.3.0 <4.0.0' 9 | 10 | dev_dependencies: 11 | coverage: ^1.6.3 12 | lints: ^3.0.0 13 | test: ^1.24.0 14 | -------------------------------------------------------------------------------- /test/smython_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:smython/test_runner.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('test runner', () { 6 | expect(run('parser_tests.py'), isTrue); 7 | }); 8 | } 9 | --------------------------------------------------------------------------------