426 lines
18 KiB
Nim
426 lines
18 KiB
Nim
# Copyright 2024 Mattia Giambirtone & All Contributors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import config
|
|
import util/fmterr
|
|
import util/symbols
|
|
import frontend/parsing/lexer
|
|
import frontend/parsing/parser
|
|
import frontend/compiler/typechecker
|
|
import backend/bytecode/codegen/generator
|
|
import backend/bytecode/tooling/serializer
|
|
import backend/bytecode/opcodes
|
|
import backend/bytecode/tooling/debugger
|
|
import backend/bytecode/vm
|
|
|
|
|
|
import std/os
|
|
import std/parseopt
|
|
import std/strutils
|
|
import std/terminal
|
|
import std/strformat
|
|
import std/times
|
|
|
|
|
|
# Thanks art <3
|
|
#[
|
|
import jale/editor as ed
|
|
import jale/templates
|
|
import jale/plugin/defaults
|
|
import jale/plugin/editor_history
|
|
import jale/keycodes
|
|
import jale/multiline
|
|
|
|
proc getLineEditor: LineEditor =
|
|
result = newLineEditor()
|
|
result.prompt = "=> "
|
|
result.populateDefaults()
|
|
let history = result.plugHistory()
|
|
result.bindHistory(history)
|
|
]#
|
|
|
|
|
|
proc `$`(self: TypedNode): string =
|
|
if self.node.isConst():
|
|
var self = TypedExpr(self)
|
|
return &"{self.node}: {self.kind[]}"
|
|
case self.node.kind:
|
|
of varDecl, typeDecl, funDecl:
|
|
var self = TypedDecl(self)
|
|
result = &"{self.name[]}: {self.name.valueType[]}"
|
|
of identExpr, binaryExpr, unaryExpr:
|
|
var self = TypedExpr(self)
|
|
result &= &"{self.node}: {self.kind[]}"
|
|
else:
|
|
result = &"{self.node}: ? ({self.node.kind})"
|
|
|
|
|
|
proc runFile(filename: string, fromString: bool = false, dump: bool = true, generate: bool = true,
|
|
parse: bool = true, typeCheck: bool = true, breakpoints: seq[uint64] = @[],
|
|
disabledWarnings: seq[WarningKind] = @[], mismatches: bool = false, run: bool = true,
|
|
backend: PeonBackend = PeonBackend.Bytecode, output: string, cacheDir: string) =
|
|
var
|
|
tokens: seq[Token]
|
|
tree: ParseTree
|
|
typedNodes: seq[TypedNode]
|
|
tokenizer = newLexer()
|
|
parser = newParser()
|
|
typeChecker = newTypeChecker()
|
|
input: string
|
|
filename = filename
|
|
isBinary = false
|
|
output = output
|
|
if output == "":
|
|
output = filename.replace(".pn", "")
|
|
tokenizer.fillSymbolTable()
|
|
try:
|
|
if not fromString and filename.endsWith(".pbc"):
|
|
isBinary = true
|
|
if fromString:
|
|
input = filename
|
|
filename = "<string>"
|
|
else:
|
|
input = readFile(filename)
|
|
if not isBinary:
|
|
tokens = tokenizer.lex(input, filename)
|
|
if tokens.len() == 0:
|
|
return
|
|
if debugLexer:
|
|
styledEcho fgCyan, "Tokenizer output:"
|
|
for i, token in tokens:
|
|
if i == tokens.high():
|
|
# Who cares about EOF?
|
|
break
|
|
styledEcho fgGreen, "\t", $token
|
|
echo ""
|
|
if not parse:
|
|
return
|
|
tree = parser.parse(tokens, filename, tokenizer.getLines(), input)
|
|
if tree.len() == 0:
|
|
return
|
|
if debugParser:
|
|
styledEcho fgCyan, "Parser output:"
|
|
for node in tree:
|
|
styledEcho fgGreen, "\t", $node
|
|
echo ""
|
|
if not typeCheck:
|
|
return
|
|
typedNodes = typeChecker.check(tree, filename, tokenizer.getSource(), mismatches, disabledWarnings)
|
|
if debugTypeChecker:
|
|
styledEcho fgCyan, "Typechecker output:"
|
|
for typedNode in typedNodes:
|
|
case typedNode.node.kind:
|
|
of exprStmt:
|
|
# *Technically* an expression statement has no type, but that isn't really useful for debug
|
|
# purposes, so we print the type of the expression within it instead
|
|
let exprNode = TypedExprStmt(typedNode).expression
|
|
styledEcho fgGreen, &"\t{typedNode.node} (inner) -> {typeChecker.stringify(exprNode.kind)}\n"
|
|
else:
|
|
styledEcho fgGreen, &"\t{typedNode.node} -> {typeChecker.stringify(typedNode)}\n"
|
|
if not generate:
|
|
return
|
|
case backend:
|
|
of PeonBackend.Bytecode:
|
|
var
|
|
debugger = newBytecodeDebugger()
|
|
generator = newBytecodeGenerator()
|
|
serializer = newBytecodeSerializer()
|
|
vm = newPeonVM()
|
|
chunk: Chunk = newChunk()
|
|
serialized: SerializedBytecode
|
|
if not isBinary:
|
|
chunk = generator.generate(typedNodes, typeChecker)
|
|
serialized = serializer.loadBytes(serializer.dumpBytes(chunk, filename))
|
|
else:
|
|
serialized = serializer.loadFile(filename)
|
|
chunk = serialized.chunk
|
|
if dump and not fromString:
|
|
if not output.endsWith(".pbc"):
|
|
output.add(".pbc")
|
|
if not dirExists(cacheDir):
|
|
createDir(cacheDir)
|
|
serializer.dumpFile(chunk, joinPath(cacheDir, filename), output)
|
|
if debugCompiler:
|
|
styledEcho fgCyan, "Disassembler output below"
|
|
debugger.disassembleChunk(chunk, filename)
|
|
if debugSerializer:
|
|
styledEcho fgCyan, "Serializer checks: "
|
|
styledEcho fgBlue, "\t- Peon version: ", fgYellow, &"{serialized.version.major}.{serialized.version.minor}.{serialized.version.patch}", fgBlue, " (commit ", fgYellow, serialized.commit[0..8], fgBlue, ") on branch ", fgYellow, serialized.branch
|
|
stdout.styledWriteLine(fgBlue, "\t- Compilation date & time: ", fgYellow, fromUnix(serialized.compileDate).format("d/M/yyyy HH:mm:ss"))
|
|
stdout.styledWriteLine(fgBlue, "\t- Total binary size: ", fgYellow, formatSize(serialized.size))
|
|
|
|
stdout.styledWrite(fgBlue, &"\t- Constants segment: ")
|
|
if serialized.chunk.consts == chunk.consts:
|
|
styledEcho fgGreen, "OK"
|
|
else:
|
|
styledEcho fgRed, "Corrupted"
|
|
stdout.styledWrite(fgBlue, &"\t- Code segment: ")
|
|
if serialized.chunk.code == chunk.code:
|
|
styledEcho fgGreen, "OK"
|
|
else:
|
|
styledEcho fgRed, "Corrupted"
|
|
stdout.styledWrite(fgBlue, "\t- Line info segment: ")
|
|
if serialized.chunk.lines == chunk.lines:
|
|
styledEcho fgGreen, "OK"
|
|
else:
|
|
styledEcho fgRed, "Corrupted"
|
|
stdout.styledWrite(fgBlue, "\t- Functions segment: ")
|
|
if serialized.chunk.functions == chunk.functions:
|
|
styledEcho fgGreen, "OK"
|
|
else:
|
|
styledEcho fgRed, "Corrupted"
|
|
stdout.styledWrite(fgBlue, "\t- Modules segment: ")
|
|
if serialized.chunk.modules == chunk.modules:
|
|
styledEcho fgGreen, "OK"
|
|
else:
|
|
styledEcho fgRed, "Corrupted"
|
|
if run:
|
|
vm.run(chunk, breakpoints, repl=false)
|
|
else:
|
|
discard
|
|
except LexingError as exc:
|
|
print(exc)
|
|
except ParseError as exc:
|
|
print(exc)
|
|
except TypeCheckError as exc:
|
|
print(exc)
|
|
except CodeGenError as exc:
|
|
var file = exc.file
|
|
if file notin ["<string>", ""]:
|
|
file = relativePath(file, getCurrentDir())
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error while generating code for ", fgYellow, file, fgDefault, &": {exc.msg}")
|
|
except SerializationError as exc:
|
|
var file = exc.file
|
|
if file notin ["<string>", ""]:
|
|
file = relativePath(file, getCurrentDir())
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error while (de-)serializing ", fgYellow, file, fgDefault, &": {exc.msg}")
|
|
except IOError as exc:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error while trying to read ", fgYellow, filename, fgDefault, &": {exc.msg}")
|
|
except OSError as exc:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error while trying to read ", fgYellow, filename, fgDefault, &": {exc.msg} ({osErrorMsg(osLastError())})",
|
|
fgRed, "[errno ", fgYellow, $osLastError(), fgRed, "]")
|
|
|
|
#[
|
|
proc repl(warnings: seq[WarningKind] = @[], showMismatches: bool = false) =
|
|
var
|
|
keep = true
|
|
tokens: seq[Token]
|
|
tree: ParseTree
|
|
typeChecker = newTypeChecker()
|
|
lexer = newLexer()
|
|
parser = newParser()
|
|
editor = getLineEditor()
|
|
input: string
|
|
lexer.fillSymbolTable()
|
|
editor.bindEvent(jeQuit):
|
|
stdout.styledWriteLine(fgGreen, "Goodbye!")
|
|
keep = false
|
|
input = ""
|
|
editor.bindKey("ctrl+a"):
|
|
editor.content.home()
|
|
editor.bindKey("ctrl+e"):
|
|
editor.content.`end`()
|
|
while keep:
|
|
try:
|
|
input = editor.read()
|
|
if input == "#clear":
|
|
stdout.write("\x1Bc")
|
|
continue
|
|
elif input == "":
|
|
continue
|
|
tokens = lexer.lex(input, "stdin")
|
|
if tokens.len() == 0:
|
|
continue
|
|
if debugLexer:
|
|
styledEcho fgCyan, "Tokenizer output:"
|
|
for i, token in tokens:
|
|
if i == tokens.high():
|
|
# Who cares about EOF?
|
|
break
|
|
styledEcho fgGreen, "\t", $token
|
|
echo ""
|
|
tree = parser.parse(tokens, "stdin", lexer.getLines(), input, persist=true)
|
|
if tree.len() == 0:
|
|
continue
|
|
if debugParser:
|
|
styledEcho fgCyan, "Parser output:"
|
|
for node in tree:
|
|
styledEcho fgGreen, "\t", $node
|
|
echo ""
|
|
if debugTypeChecker:
|
|
styledEcho fgCyan, "Typechecker output:"
|
|
for typedNode in typeChecker.validate(parser.parse(lexer.lex(input, "<stdin>"), lexer.getFile(), lexer.getLines(), lexer.getSource()),
|
|
lexer.getFile(), lexer.getSource(), showMismatches=showMismatches, disabledWarnings=warnings):
|
|
if debugTypeChecker:
|
|
styledEcho fgGreen, &"\t{typedNode.node} -> {typeChecker.stringify(typedNode)}\n"
|
|
echo ""
|
|
except LexingError:
|
|
print(LexingError(getCurrentException()))
|
|
except ParseError:
|
|
print(ParseError(getCurrentException()))
|
|
except TypeCheckError:
|
|
print(TypeCheckError(getCurrentException()))
|
|
quit(0)
|
|
]#
|
|
|
|
|
|
when isMainModule:
|
|
setControlCHook(proc () {.noconv.} = quit(0))
|
|
var
|
|
optParser = initOptParser(commandLineParams())
|
|
file: string
|
|
fromString: bool
|
|
dump = true
|
|
warnings: seq[WarningKind] = @[]
|
|
showMismatches = false
|
|
cachePath: string = ".buildcache"
|
|
#mode: CompileMode = CompileMode.Debug
|
|
run = true
|
|
generateCode = true
|
|
parseCode = true
|
|
typeCheck = true
|
|
backend: PeonBackend
|
|
output: string
|
|
breakpoints: seq[uint64]
|
|
for kind, key, value in optParser.getopt():
|
|
case kind:
|
|
of cmdArgument:
|
|
file = key
|
|
of cmdLongOption:
|
|
case key:
|
|
#[
|
|
of "mode":
|
|
if value.toLowerAscii() == "release":
|
|
mode = CompileMode.Release
|
|
elif value.toLowerAscii() == "debug":
|
|
discard
|
|
else:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, "invalid value for option 'mode' (valid options are: debug, release)")
|
|
quit()
|
|
]#
|
|
of "help":
|
|
echo HELP_MESSAGE
|
|
quit()
|
|
of "version":
|
|
echo PEON_VERSION_STRING
|
|
quit()
|
|
of "string":
|
|
file = key
|
|
fromString = true
|
|
of "noGen":
|
|
generateCode = false
|
|
of "noTC", "noTypeCheck":
|
|
typeCheck = false
|
|
of "noParse":
|
|
parseCode = false
|
|
of "noDump":
|
|
dump = false
|
|
of "warnings":
|
|
if value.toLowerAscii() in ["yes", "on"]:
|
|
warnings = @[]
|
|
elif value.toLowerAscii() in ["no", "off"]:
|
|
for warning in WarningKind:
|
|
warnings.add(warning)
|
|
else:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, "invalid value for option 'warnings' (valid options are: yes, on, no, off)")
|
|
quit()
|
|
of "showMismatches":
|
|
showMismatches = true
|
|
of "noWarn":
|
|
case value:
|
|
of "UserWarning":
|
|
warnings.add(UserWarning)
|
|
else:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, "invalid warning name for option 'noWarn'")
|
|
quit()
|
|
of "listWarns":
|
|
echo "Currently supported warnings: "
|
|
for warning in WarningKind:
|
|
echo &" - {warning}"
|
|
quit(0)
|
|
of "debugTypeChecker":
|
|
debugTypeChecker = true
|
|
of "debugCompiler":
|
|
debugCompiler = true
|
|
of "debugSerializer":
|
|
debugSerializer = true
|
|
of "compile":
|
|
run = false
|
|
of "output":
|
|
output = value
|
|
of "backend":
|
|
case value:
|
|
of "bytecode":
|
|
backend = PeonBackend.Bytecode
|
|
of "c":
|
|
backend = PeonBackend.NativeC
|
|
of "debug-dump":
|
|
debugSerializer = true
|
|
of "debugLexer":
|
|
debugLexer = true
|
|
of "debugParser":
|
|
debugParser = true
|
|
of "cachePath":
|
|
cachePath = value
|
|
of "breakpoints":
|
|
when debugVM:
|
|
for point in value.strip(chars={' '}).split(","):
|
|
try:
|
|
breakpoints.add(parseBiggestUInt(point))
|
|
except ValueError:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, &"error: invalid breakpoint value '{point}'")
|
|
quit()
|
|
when not debugVM:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, "VM debugging is off, cannot set breakpoints (recompile with -d:debugVM to fix this)")
|
|
quit()
|
|
else:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, &"unkown long option '{key}'")
|
|
quit()
|
|
of cmdShortOption:
|
|
case key:
|
|
of "o":
|
|
output = value
|
|
of "h":
|
|
echo HELP_MESSAGE
|
|
quit()
|
|
of "v":
|
|
echo PEON_VERSION_STRING
|
|
quit()
|
|
of "s":
|
|
file = key
|
|
fromString = true
|
|
of "n":
|
|
dump = false
|
|
of "w":
|
|
if value.toLowerAscii() in ["yes", "on"]:
|
|
warnings = @[]
|
|
elif value.toLowerAscii() in ["no", "off"]:
|
|
for warning in WarningKind:
|
|
warnings.add(warning)
|
|
else:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, "invalid value for option 'w' (valid options are: yes, on, no, off)")
|
|
quit()
|
|
of "c":
|
|
run = false
|
|
else:
|
|
stderr.styledWriteLine(fgRed, styleBright, "Error: ", fgDefault, &"unkown short option '{key}'")
|
|
quit()
|
|
else:
|
|
echo "usage: peon [options] [filename.pn]"
|
|
quit()
|
|
if file == "":
|
|
echo "Sorry, the REPL is broken :("
|
|
# repl(warnings, showMismatches, backend, dump)
|
|
else:
|
|
runFile(file, fromString, dump, generateCode, parseCode, typeCheck, breakpoints, warnings, showMismatches, run, backend, output, cachePath) |