Browse Source

Implemented forward declarations

master
Mattia Giambirtone 4 weeks ago
parent
commit
ce136a7a3c
  1. 29
      README.md
  2. 2
      docs/bytecode.md
  3. 26
      docs/manual.md
  4. 60
      src/frontend/compiler.nim

29
README.md

@ -77,37 +77,40 @@ Type system:
- Custom types [ ]
- Intrinsics [X]
- Generics [ ] -> WIP
- Generics [X]
- Functions [X]
- Closures [X]
Misc:
- Pragmas [ ] -> WIP (Some pragmas implemented)
- Pragmas [X]
- Attribute resolution [ ]
- method-like call syntax without actual methods (dispatched at compile-time) [ ]
- ... More?
- UFCS [ ]
- Import system with namespaces and visibility control [X]
## Features
Aside from the obvious basics like exceptions, a true import system with namespaces and a standard library (duh), here's a
random list of high-level features I plan peon to have and that I think are kinda neat:
- References not being nullable by default (must use `#pragma[nullable]`)
## Feature wishlist
Here's a random list of high-level features I would like peon to have and that I think are kinda neat (some may
have been implemented alredady):
- Reference types are not nullable by default (must use `#pragma[nullable]`)
- Easy C/Nim interop via FFI
- C/C++ backend
- Nim backend (maybe)
- Nim backend
- Structured concurrency
- Capability-based programming (i.e. functions are passed objects to act on the real world)
- Parametric Polymorphism (with untagged typed unions and maybe interfaces)
- Parametric Polymorphism (with untagged typed unions) [X]
- Simple OOP (with multiple dispatch!)
- RTTI, with methods that dispatch at runtime based on the true type of a value (maybe)
- RTTI, with methods that dispatch at runtime based on the true type of a value
- Limited compile-time evaluation (embed the Peon VM in the C/C++/Nim backend and use that to execute peon code at compile time)
## The name
The name for peon comes from my and [Productive2's](https://git.nocturn9x.space/prod2) genius and is a result of shortening
the name of the fastest animal on earth: the **Pe**regrine Falc**on**. I guess I wanted this to mean peon will be blazing fast
The name for peon comes from [Productive2's](https://git.nocturn9x.space/prod2) genius cute brain, and is a result of shortening
the name of the fastest animal on earth: the **Pe**regrine Falc**on**. I guess I wanted this to mean peon will be blazing fast (I
certainly hope so!)
# Peon needs you.

2
docs/bytecode.md

@ -87,7 +87,7 @@ them whatsoever: it is the code that, at runtime, loads each constant (whose typ
the stack accordingly. For example, a 32 bit integer constant would be encoded as a sequence of 4 bytes, which would
then be loaded by the appropriate `LoadInt32` instruction at runtime. The segment's size is fixed and is encoded at
the beginning as a sequence of 4 bytes (i.e. a single 32 bit integer). The constant segment may be empty, although in
real-world scenarios likely won't.
real-world scenarios it likely won't be.
## Code segment

26
docs/manual.md

@ -80,6 +80,15 @@ type Foo = object { # Can also be "ref object" for reference types (managed auto
}
```
### Enumeration types
```
type SomeEnum = enum { # Can be mapped to an integer
KindOne,
KindTwo
}
```
### Operator overloading
```
@ -92,8 +101,9 @@ Foo(fieldOne: 1, fieldTwo: 3) + Foo(fieldOne: 2, fieldTwo: 3); # Foo(fieldOne:
__Note__: Custom operators (e.g. `foo`) can also be defined! The backticks around the plus sign serve to mark it
as an identifier instead of a symbol (which is a requirement for function names, since operators are basically
functions). In fact, even the built-in peon operators are implemented partially in peon (well, their forward
declarations are) and they are then specialized in the compiler to emit a single bytecode instruction.
functions in peon). In fact, even the built-in peon operators are implemented partially in peon (actually, just
their stubs are) and they are then specialized in the compiler to emit a single bytecode instruction to get rid
of unnecessary function call overhead.
### Function calls
@ -101,14 +111,14 @@ declarations are) and they are then specialized in the compiler to emit a single
foo(1, 2 + 3, 3.14, bar(baz));
```
__Note__: Operators can be called as functions too. Just wrap their name in backticks, like so:
__Note__: Operators can be called as functions; If their name is a symbol, just wrap it in backticks like so:
```
`+`(1, 2)
`+`(1, 2) # Identical to 1 + 2
```
__Note__: Code the likes of `a.b()` is desugared to `b(a)` if there exists a function `b` whose
signature is compatible with the value of `a` (assuming `a` doesn't have a field named `b`, in
which case the attribute resolution takes precedence)
__Note__: Code the likes of `a.b()` is (actually, will be) desugared to `b(a)` if there exists a function
`b` whose signature is compatible with the value of `a` (assuming `a` doesn't have a field named `b`,
in which case the attribute resolution takes precedence)
### Generics
@ -128,7 +138,7 @@ genericSum(1'u8, 250'u8);
#### More generics!
```
fn genericSth[T: someInterface, K: someInterface2](a: T, b: K) { # Note: no return type == void function!
fn genericSth[T: someTyp, K: someTyp2](a: T, b: K) { # Note: no return type == void function!
# code...
}

60
src/frontend/compiler.nim

@ -69,6 +69,7 @@ type
children: seq[Type]
parent: Type
retJumps: seq[int]
forwarded: bool
of CustomType:
fields: TableRef[string, Type]
of Reference, Pointer:
@ -211,6 +212,10 @@ type
parser: Parser
# Are we compiling the main module?
isMainModule: bool
# Stores the call offsets for forward
# declarations so that we can patch them
# later
forwarded: seq[tuple[name: Name, pos: int]]
CompileError* = ref object of PeonException
compiler*: Compiler
node*: ASTNode
@ -228,7 +233,7 @@ proc peek(self: Compiler, distance: int = 0): ASTNode
proc identifier(self: Compiler, node: IdentExpr)
proc varDecl(self: Compiler, node: VarDecl, name: Name)
proc specialize(self: Compiler, name: Name, args: seq[Expression]): Name
proc matchImpl(self: Compiler, name: string, kind: Type, node: ASTNode = nil): Name
proc matchImpl(self: Compiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true): Name
proc infer(self: Compiler, node: LiteralExpr, allowGeneric: bool = false): Type
proc infer(self: Compiler, node: Expression, allowGeneric: bool = false): Type
proc inferOrError[T: LiteralExpr | Expression](self: Compiler, node: T, allowGeneric: bool = false): Type
@ -270,6 +275,7 @@ proc newCompiler*(replMode: bool = false): Compiler =
result.parser = newParser()
result.isMainModule = false
result.closures = @[]
result.forwarded = @[]
## Public getters for nicer error formatting
@ -943,7 +949,6 @@ proc findByName(self: Compiler, name: string): seq[Name] =
## with the given name. Returns all objects that apply.
## As with resolve(), this will cause type and function
## declarations to be compiled on-the-fly
for obj in reversed(self.names):
if obj.ident.token.lexeme == name:
if obj.owner != self.currentModule:
@ -991,11 +996,11 @@ proc findAtDepth(self: Compiler, name: string, depth: int): seq[Name] {.used.} =
result.add(obj)
proc matchImpl(self: Compiler, name: string, kind: Type, node: ASTNode = nil): Name =
proc matchImpl(self: Compiler, name: string, kind: Type, node: ASTNode = nil, allowFwd: bool = true): Name =
## Tries to find a matching function implementation
## compatible with the given type and returns its
## name object
let impl = self.findByType(name, kind)
var impl = self.findByType(name, kind)
if impl.len() == 0:
var msg = &"cannot find a suitable implementation for '{name}'"
let names = self.findByName(name)
@ -1019,11 +1024,17 @@ proc matchImpl(self: Compiler, name: string, kind: Type, node: ASTNode = nil): N
msg &= &", first mismatch at position {i + 1}: expected argument of type '{self.typeToStr(name.valueType.args[i].kind)}', got '{self.typeToStr(arg.kind)}' instead"
break
self.error(msg, node)
elif impl.len() > 1:
var msg = &"multiple matching implementations of '{name}' found:\n"
for fn in reversed(impl):
msg &= &"- in module '{fn.owner}' at line {fn.line} of type {self.typeToStr(fn.valueType)}\n"
self.error(msg, node)
if impl.len() > 1:
# Forward declarations don't count when looking for a function
impl = filterIt(impl, not it.valueType.forwarded)
if impl.len() > 1:
# If it's *still* more than one match, then it's an error
var msg = &"multiple matching implementations of '{name}' found:\n"
for fn in reversed(impl):
msg &= &"- in module '{fn.owner}' at line {fn.line} of type {self.typeToStr(fn.valueType)}\n"
self.error(msg, node)
if impl[0].valueType.forwarded and not allowFwd:
self.error(&"expecting an implementation for function '{impl[0].ident.token.lexeme}' declared in module '{impl[0].owner}' at line {impl[0].ident.token.line} of type '{self.typeToStr(impl[0].valueType)}'")
result = impl[0]
@ -1142,6 +1153,26 @@ proc flattenImpl(self: Type, to: var seq[Type]) =
proc flatten(self: Type): seq[Type] = flattenImpl(self, result)
proc patchForwardDeclarations(self: Compiler) =
## Patches forward declarations and looks
## for their implementations so that calls
## to them work properly
var impl: Name
var pos: array[8, uint8]
for (forwarded, position) in self.forwarded:
impl = self.matchImpl(forwarded.ident.token.lexeme, forwarded.valueType, allowFwd=false)
if position == 0:
continue
pos = impl.codePos.toLong()
self.chunk.consts[position] = pos[0]
self.chunk.consts[position + 1] = pos[1]
self.chunk.consts[position + 2] = pos[2]
self.chunk.consts[position + 3] = pos[3]
self.chunk.consts[position + 4] = pos[4]
self.chunk.consts[position + 5] = pos[5]
self.chunk.consts[position + 6] = pos[6]
self.chunk.consts[position + 7] = pos[7]
proc endScope(self: Compiler) =
## Ends the current local scope
@ -1292,7 +1323,8 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false) =
returnType: nil, # We check it later
args: @[],
fun: node,
children: @[]),
children: @[],
forwarded: node.body.isNil()),
ident: node.name,
node: node,
isLet: false,
@ -1358,6 +1390,7 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false) =
if name == n:
continue
elif (name.kind == NameKind.Var and name.depth == self.depth) or name.kind in [NameKind.Module, NameKind.CustomType, NameKind.Enum]:
# We don't check for clashing functions here: self.matchImpl() takes care of that
self.error(&"attempt to redeclare '{name.ident.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}")
@ -1443,6 +1476,7 @@ proc patchReturnAddress(self: Compiler, pos: int) =
proc terminateProgram(self: Compiler, pos: int) =
## Utility to terminate a peon program
self.patchForwardDeclarations()
self.endScope()
self.emitByte(OpCode.Return, self.peek().token.line)
self.emitByte(0, self.peek().token.line) # Entry point has no return value (TODO: Add easter eggs, cuz why not)
@ -1766,6 +1800,8 @@ proc generateCall(self: Compiler, fn: Name, args: seq[Expression], line: int) =
self.emitBytes(self.chunk.writeConstant(fn.codePos.toLong()), line)
else:
discard
if fn.valueType.forwarded:
self.forwarded.add((fn, self.chunk.consts.high() - 7))
self.emitByte(LoadUInt64, line)
self.emitBytes(self.chunk.writeConstant(0.toLong()), line)
let pos = self.chunk.consts.len() - 8
@ -2188,7 +2224,7 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
self.currentFunction.valueType.children.add(name.valueType)
name.valueType.parent = function.valueType
self.currentFunction = name
if not node.body.isNil():
if not node.body.isNil(): # We ignore forward declarations
# A function's code is just compiled linearly
# and then jumped over
jmp = self.emitJump(JumpForwards, node.token.line)
@ -2262,7 +2298,7 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
# the jump offset
self.patchJump(jmp)
else:
discard # TODO: Forward declarations
self.forwarded.add((name, 0))
# Restores the enclosing function (if any).
# Makes nested calls work (including recursion)
self.currentFunction = function

Loading…
Cancel
Save