Minor additions and fixes (preparation for break and continue in the compiler)

This commit is contained in:
nocturn9x 2021-12-11 15:57:28 +01:00
parent 302acbcaae
commit 195045e4f2
3 changed files with 25 additions and 6 deletions

View File

@ -18,11 +18,11 @@ Clox is fine as a toy language, but its limitations are a little too strict for
__Note__: Some languages support so-called "static methods", basically methods that are bound to a _class_ instead of an _instance_ and therefore take no self parameter. It might cause confusion for some people to see a `static fun` inside a class take a self argument, but that's normal: JAPL doesn't support static methods (because they're worthless). The keyword `static` in JAPL has no other meaning than "resolve this name's location at compile time": it is merely a compile-time optimization to speed up name lookup since hash tables are slow
- Loops can be interrupted via `break` and an iteration can be skipped using `continue`. Although JAPL has a triple-pass compiler, we still use the book's method of patching jump offsets because otherwise we'd have to predict how many instructions a given AST node compiles into (something that JAPL might do in the future, though)
- Classes support multiple inheritance. Methods are resolved starting from the first parent until the last one
- Classes support multiple inheritance. Methods are resolved starting from the first parent until the last one, in the order in which they are listed in the class declaration
- Proper exception support, with `try`/`except` handlers (with `finally` and `else` support as well), and obviously `raise`, has been added. The VM will keep a list of active exception hooks which are created with the `BeginTry` instruction. When an exception is raised, the VM will traverse this list backwards to find any matching exception hook according to a set of rules (i.e. taking superclasses and multiple catching into account) and execute their associated code if they do. If the VM can't find any matching handler and it gets to the top of the list, it then writes the message to stderr and exits. Some niceties like `except (exc1, exc2, exc2)` and `exc SomeExc as excName` (`except (SomeExc, Exc2) as name` is also valid) have also been blatantly stolen from python's exception handling system
- JAPL will have an iterator protocol, hence a `foreach` loop has been added to iterate over collections and sequence types
- Closures are now different from regular functions. JAPL will compile a function to be a closure only if it makes use of values that are outside its own scope. In this case, as the book rightly suggests: _"[...] The next easiest approach, then, would be to take any local variable that gets closed over and have it always live on the heap. When the local variable declaration in the surrounding function is executed, the VM would allocate memory for it dynamically. That way it could live as long as needed."_
- Builtin collections similar to Python's have been added: lists, tuples, sets and dictionaries. A notable quirk though is that since brackets are used both for set and dictionary literals and for block statements, and due to the latter's precedence being higher, a bare `{};` creates an empty block scope and leaves a dangling semicolon instead of creating a dictionary object and discarding it immediately. Sets and dictionaries literals can only be defined where an expression is expected (which is fine, because they _are_ expressions), so something like `var a = {1, 2, 3};` is perfectly fine and not ambiguous because the parser only expects expression as values for variable declarations, and block statements are, well, statements
- Builtin collections similar to Python's have been added: lists, tuples, sets and dictionaries. A notable quirk though is that since brackets are used both for set and dictionary literals and for block statements, and due to the latter's precedence being higher, a bare `{};` creates an empty block scope and leaves a dangling semicolon instead of creating a dictionary object and discarding it immediately. Set and dictionary literals can only be defined where an expression is expected (which is fine, because they _are_ expressions), so something like `var a = {1, 2, 3};` is perfectly fine and not ambiguous because the parser only expects expression as values for variable declarations, and block statements are, well, statements
- JAPL supports `yield` statements and expressions, allowing on-the-fly value generation for improved iteration performance and highly-efficient O(1) algorithms, the most basic being infinite counters
- Some handy operators have been added: `is` (and its opppsite `isnot`) checks if two objects refer to the same value, `A of B` returns `true` if A is a subclass of B and `A as B` will call `B(A)` allowing for simple casting, like `"55" as Integer;`, which pushes the Integer `55` onto the stack
- The keyword `this` has been removed and instead JAPL passes the instance's value as the first argument to a bound method (commonly named `self`)
@ -32,7 +32,7 @@ Clox is fine as a toy language, but its limitations are a little too strict for
- Private attributes are not very useful without an import system and modules, a topic that clox doesn't touch at all (probably because it's not that interesting implementation-wise and is only a recipe for trouble in a beginner's book), so JAPL fixes that by having a pretty Python-esque import system with things like `import name;`, `import module.submodule;`, `from module import someComponent;`, `import someModule as someName;`, `import a, b, c;` and basically all variations of the above. Imports in JAPL are "proper", i.e. they don't just copy-paste code like C's `#include` or some other lox implementations: they create a separate namespace and populate it with whatever the imported module decides to export (i.e. all declarations marked as `public`)
- Inline comments in JAPL start with an hashtag and that's the only kind of comment that exists. This is because `//` is used as the binary operator for integer (aka floor) division
- Support for in-place operations has been added (`+=`, `-=`, etc)
- Multithreading and multiprocessing support has been (or more like will be) added. Multiprocessing is easy: just fork() (or CreateProcess on windows) and let the new VM do its thing. Since processes have entirely separate address spaces, there's no race conditions to handle. Multithreading is a bit trickier, requiring a global VM lock. But wait! Unlike Python's GIL, which locks all but one thread from running bytecode at a time, JAPL's lock is only acquired during a garbage collection cycle, which minimizes interference with other threads and lets JAPL achieve true concurrency. Also, due to how JAPL's garbage collector is implemented, collection cycles become rarer and rarer as more and more memory is allocated, which further minimizes the pauses the GC has to issue while it reclaims memory. Since we're on the subject of memory management and we mentioned the fact that JAPL has an exception system, it's worth adding that the GC will try to raise a `OutOfMemoryException` when it runs out of memory (assuming there's enough memory for the exception itself to be allocated, which requires just a few dozen bytes. If it can't do even that, it will just shut down the VM entirely, free every object that it is managing, and print an error message on stderr)
- Multithreading and multiprocessing support has been (or more like will be) added. Multiprocessing is easy: just fork() (or CreateProcess on windows) and let the new VM do its thing. Since processes have entirely separate address spaces, there's no race conditions to handle. Multithreading is a bit trickier, requiring a global VM lock. But wait! Unlike Python's GIL, which locks all but one thread from running bytecode at a time, JAPL's lock is only acquired during a garbage collection cycle, which minimizes interference with other threads and lets JAPL achieve true concurrency. Also, due to how JAPL's garbage collector is implemented, collection cycles become rarer and rarer as more and more memory is allocated, which further minimizes the pauses the GC has to issue while it reclaims memory. Since we're on the subject of memory management and we mentioned the fact that JAPL has an exception system, it's worth adding that the GC will try to raise an `OutOfMemoryException` when it runs out of memory (assuming there's enough memory for the exception itself to be allocated, which requires just a few dozen bytes. If it can't do even that, it will just shut down the VM entirely, free every object that it is managing, and print an error message on stderr)
- JAPL will support native `async` functions using the coroutines model (like Python does) which can be called via `await`. This also allows me to experiment with writing an asynchronous scheduler, if [my other project](https://github.com/giambio) wasn't enough
- JAPL supports `const` declarations, which emit simple `LoadConstant` instructions. For this reason, name resolution specifiers do not apply to constant declarations and they have to be assigned at declaration time using a constant type (i.e. a number or a string). The compiler statically checks assignment to constants and spits out compile errors if it finds an attempt to modify a constant's value

View File

@ -43,7 +43,16 @@ type
depth: int
isPrivate: bool
isConst: bool
Loop = object
## A "loop object" used
## by the compiler to emit
## appropriate jump offsets
## for continue and break
## statements
start: int
stop: int
Compiler* = ref object
## A wrapper around the compiler's state
chunk: Chunk
@ -55,6 +64,7 @@ type
scopeDepth: int
currentFunction: FunDecl
enableOptimizations*: bool
currentLoop: Loop
proc initCompiler*(enableOptimizations: bool = true): Compiler =
@ -169,7 +179,7 @@ proc identifierConstant(self: Compiler, identifier: IdentExpr): array[3, uint8]
proc emitJump(self: Compiler, opcode: OpCode): int =
## Emits a dummy jump offset to be patched later. Assumes
## the largest offset (emits 4 bytes, one for given jump
## the largest offset (emits 4 bytes, one for the given jump
## opcode, while the other 3 are for the jump offset which is set
## to the maximum unsigned 24 bit integer). If the shorter
## 16 bit alternative is later found to be better suited, patchJump
@ -556,7 +566,14 @@ proc ifStmt(self: Compiler, node: IfStmt) =
## Compiles if/else statements for conditional
## execution of code
self.expression(node.condition)
let jump = self.emitJump(JumpIfFalsePop)
var jumpCode: OpCode
if self.enableOptimizations:
jumpCode = JumpIfFalsePop
else:
jumpCode = JumpIfFalse
let jump = self.emitJump(jumpCode)
if not self.enableOptimizations:
self.emitByte(Pop)
self.statement(node.thenBranch)
self.patchJump(jump)
if node.elseBranch != nil:

View File

@ -866,6 +866,7 @@ proc varDecl(self: Parser, isStatic: bool = true, isPrivate: bool = true): ASTNo
proc funDecl(self: Parser, isAsync: bool = false, isStatic: bool = true, isPrivate: bool = true, isLambda: bool = false): ASTNode =
## Parses function and lambda declarations. Note that lambdas count as expressions!
self.checkDecl(isStatic, isPrivate)
let tok = self.peek(-1)
var enclosingFunction = self.currentFunction
var arguments: seq[ASTNode] = @[]
@ -909,6 +910,7 @@ proc funDecl(self: Parser, isAsync: bool = false, isStatic: bool = true, isPriva
proc classDecl(self: Parser, isStatic: bool = true, isPrivate: bool = true): ASTNode =
## Parses class declarations
self.checkDecl(isStatic, isPrivate)
let tok = self.peek(-1)
var parents: seq[ASTNode] = @[]
self.expect(Identifier)