Skip to content

Instantly share code, notes, and snippets.

@Vindaar
Created September 22, 2023 10:20
Show Gist options
  • Save Vindaar/c3ed38659559aebe40f82906b38bdafa to your computer and use it in GitHub Desktop.
Save Vindaar/c3ed38659559aebe40f82906b38bdafa to your computer and use it in GitHub Desktop.
Dynlib based Nim REPL using compiler API, clean up a bit
import std / [strutils, strformat, tables, dynlib, os]
import noise, shell
import compiler/[llstream, renderer, types, magicsys, ast,
transf, # for code transformation (for -> while etc)
injectdestructors, # destructor injection
pathutils, # AbsoluteDir
modulegraphs] # getBody
import ./nimeval_dynlib_clean
# probably need to import `pragmas` and `wordrecg` to get
# `hasPragma` working
import hnimast
import typetraits
proc setupInterpreter(moduleName = "/t/script.nim"): Interpreter =
let std = findNimStdLibCompileTime()
var paths = newSeq[string]()
paths.add std
paths.add std & "/pure"
paths.add std & "/core"
paths.add std & "/pure/collections"
paths.add std & "/posix"
paths.add "/home/basti/.nimble/pkgs"
#paths.add "/home/basti/CastData/ExternCode/units/src"
result = createInterpreter(moduleName, paths, defines = @[])
proc printHelp() = echo ""
const procTmpl = """
{.push cdecl, exportc, dynlib.}
$#
{.pop.}
"""
const exprTmpl = """
$# # <- insert code to importc &
{.push cdecl, exportc, dynlib.}
proc tmp() =
$#
{.pop.}
"""
type
Repl = object
intr: Interpreter
#ctx: JitContext
# Table of all precompiled functions
# Maps function name to the compiled result ptr
fnTab: Table[string, (string, string)]
imports: string
#stream: PLLStream
#streamOpened = false
buffer: string
InputKind = enum
ikProcDef, ikStatement, ikImport, ikExpression
proc callTmp(fname, procName: string) =
## XXX: do not unload lib!
echo "Loading: ", fname, " name: ", procName
let lib = loadLib(fname)
doAssert lib != nil
let foo = cast[(proc() {.nimcall.})](lib.symAddr(procName))
doAssert foo != nil
echo "Succesfully loaded foo : ", foo != nil
foo()
unloadLib(lib)
# maps known functions to their dynlib
#var fnTab = initTable[string, (string, string)]()
proc loadIt(fn, signature, lib: string): string =
result = signature & "{.importc: \"" & fn & "\", dynlib: \"" & lib & "\".}"
proc codeToLoad(repl: Repl): string =
for fn, (file, signature) in repl.fnTab:
result.add loadIt(fn, signature, file) & "\n"
proc inputKind(line: string): InputKind =
## XXX: this will be improved obv
if line.strip.startsWith("proc"): result = ikProcDef
# we need to ask the nim compiler what the resulting type is!
# Question: how do we deal with the *nim compiler* knowing the state? I.e. referencing a variable
# `foo` 10 REPL statements after? Does that happen "automatically"?
#elif
elif line.strip.startsWith("import"): result = ikImport
else: result = ikStatement
proc getFnName(line: string): string =
result = line.strip()
result.removePrefix("proc")
let idx = result.find("(")
result.delete(idx, result.len)
result = result.strip()
echo "FN NAME: ", result, " from ", line
proc withStream(intr: var Interpreter, code, outfile: string): string =
let stream = llStreamOpen(code)
intr.evalScript(stream, outfile)
llStreamClose(stream)
result = outfile.parentDir / "lib" & outfile.extractFilename.replace(".nim", ".so")
var counter = 0
proc writeCompile(repl: var Repl, fn, content: string): string =
let fname = "tmp_file_$#.nim" % $counter
let file = "/t/$#" % fname
writeFile(file, content)
echo "\tWrote:\n", content
inc counter
# compile as lib
shell:
nim c "--app:lib --verbosity:0" ($file)
result = ("/t/lib" & fname).replace(".nim", ".so")
proc onlyWrite(repl: var Repl, fn, content: string): string =
#let fname = "tmp_file_$#.nim" % $counter
let fname = "script.nim" #"tmp_file.nim"
let file = "/t/$#" % fname
writeFile(file, content)
echo "\tWrote:\n", content
#inc counter
result = file
proc handleProcDef(repl: var Repl, line: string) =
## MORE STUFF
let fnName = getFnName(line)
# make sure `fn` exported
var line = line
if "*" notin line:
line = line.replace(fnName, fnName & "*")
repl.buffer.add line & "\n"
# signature
var signature = line
signature = signature.split("=")[0] ## XXX: Better extract real proc signature!!!
let content = procTmpl % line
let outfile = repl.onlyWrite(fnName, content)
let libfile = repl.intr.withStream(content, outfile)
let newname = libfile.replace(".so", "_" & $counter & ".so")
copyFile(libfile, newname)
inc counter
#callTmp(libfile, fnName)
repl.fnTab[fnName] = (newname, signature)
#withReplStream(repl.buffer)
#let t = repl.intr.selectRoutine(fnName)
#echo "Jit it"
#repl.compileOnly(t.ast, fnName)
proc handleStatement(repl: var Repl, line: string) =
## Statement: place in temporary proc and jit & run
#var gn = "wrapper_fn_" & $counter
#inc counter
#var body = &"{repl.imports}\nproc {gn}*() =\n {line}"
#echo "Body: ", body
#repl.buffer.add body & "\n"
let imports = repl.imports & "\n"
let loadCode = repl.codeToLoad()
let content = exprTmpl % [imports & loadCode, line]
#let outfile = repl.writeCompile("tmp", content)
let outfile = repl.onlyWrite("tmp", content)
let libfile = repl.intr.withStream(content, outfile)
## call tmp function
callTmp(libfile, "tmp")
proc handleImport(repl: var Repl, line: string) =
## Imports for now are just appended to the import header, which is prefixed
## globally to an a statement
repl.imports.add line & "\n"
proc handleUserInput(repl: var Repl, line: string) =
# pass code through nim compiler
case line.inputKind
of ikProcDef:
# just JIT compile the proc!
repl.handleProcDef(line)
of ikStatement:
# JIT compile the body and run
repl.handleStatement(line)
of ikImport:
# handle imports
repl.handleImport(line)
of ikExpression: doAssert false
proc repl(repl: var Repl) =
var noise = Noise.init()
let prompt = Styler.init(fgRed, "Red ", fgGreen, "nim> ")
noise.setPrompt(prompt)
when promptPreloadBuffer:
noise.preloadBuffer("")
when promptHistory:
var file = "history"
discard noise.historyLoad(file)
when promptCompletion:
proc completionHook(noise: var Noise, text: string): int =
const words = ["apple", "diamond", "diadem", "diablo", "horse", "home", "quartz", "quit"]
for w in words:
if w.find(text) != -1:
noise.addCompletion w
noise.setCompletionHook(completionHook)
while true:
let ok = noise.readLine()
if not ok: break
## XXX: figure out how to input multiple lines
let line = noise.getLine
case line
of ".help": printHelp()
of ".quit": break
else:
if line.len > 0:
repl.handleUserInput(line.strip)
when promptHistory:
if line.len > 0:
noise.historyAdd(line)
discard noise.historySave(file)
when promptHistory:
discard noise.historySave(file)
proc setupRepl =
echo "setting up interpreter"
var intr = setupInterpreter()
# add Unchained
# [X] Adding at runtime works just fine after the interpreter is constructed!
intr.graph.config.searchPaths.add(AbsoluteDir "/home/basti/CastData/ExternCode/units/src")
## ^--- this way we can add more paths when user imports libraries!
### XXX: add `extract import from user input` and then add those imports like this
## set up gcc jit context
#let jitCtx = initJitContext(intr, true)
#var repl = Repl(intr: intr, ctx: jitCtx)
var repl = Repl(intr: intr, fnTab: initTable[string, (string, string)]())
repl(repl)
proc main() =
setupRepl()
when isMainModule:
import cligen
dispatch main
#
#
# The Nim Compiler
# (c) Copyright 2018 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## exposes the Nim VM to clients.
import compiler / [
ast, modules, condsyms,
options, llstream, lineinfos, vm,
vmdef, modulegraphs, idents, pathutils,
scriptconfig, cgen, extccomp, cgendata, ropes,
passes
]
import std/[compilesettings, os, tables]
import compiler / pipelines
when defined(nimPreviewSlimSystem):
import std/[assertions, syncio]
type
Interpreter* = ref object ## Use Nim as an interpreter with this object
mainModule: PSym
graph*: ModuleGraph
scriptName: string
idgen*: IdGenerator
iterator exportedSymbols*(i: Interpreter): PSym =
assert i != nil
assert i.mainModule != nil, "no main module selected"
for s in modulegraphs.allSyms(i.graph, i.mainModule):
yield s
proc selectUniqueSymbol*(i: Interpreter; name: string;
symKinds: set[TSymKind] = {skLet, skVar}): PSym =
## Can be used to access a unique symbol of ``name`` and
## the given ``symKinds`` filter.
assert i != nil
assert i.mainModule != nil, "no main module selected"
let n = getIdent(i.graph.cache, name)
var it: ModuleIter
var s = initModuleIter(it, i.graph, i.mainModule, n)
result = nil
while s != nil:
if s.kind in symKinds:
if result == nil: result = s
else: return nil # ambiguous
s = nextModuleIter(it, i.graph)
proc selectRoutine*(i: Interpreter; name: string): PSym =
## Selects a declared routine (proc/func/etc) from the main module.
## The routine needs to have the export marker ``*``. The only matching
## routine is returned and ``nil`` if it is overloaded.
result = selectUniqueSymbol(i, name, {skTemplate, skMacro, skFunc,
skMethod, skProc, skConverter})
proc callRoutine*(i: Interpreter; routine: PSym; args: openArray[PNode]): PNode =
assert i != nil
result = vm.execProc(PCtx i.graph.vm, routine, args)
proc getGlobalValue*(i: Interpreter; letOrVar: PSym): PNode =
result = vm.getGlobalValue(PCtx i.graph.vm, letOrVar)
proc setGlobalValue*(i: Interpreter; letOrVar: PSym, val: PNode) =
## Sets a global value to a given PNode, does not do any type checking.
vm.setGlobalValue(PCtx i.graph.vm, letOrVar, val)
proc implementRoutine*(i: Interpreter; pkg, module, name: string;
impl: proc (a: VmArgs) {.closure, gcsafe.}) =
assert i != nil
let vm = PCtx(i.graph.vm)
vm.registerCallback(pkg & "." & module & "." & name, impl)
proc findNimStdLib*(): string =
## Tries to find a path to a valid "system.nim" file.
## Returns "" on failure.
try:
let nimexe = os.findExe("nim")
# this can't work with choosenim shims, refs https://github.com/dom96/choosenim/issues/189
# it'd need `nim dump --dump.format:json . | jq -r .libpath`
# which we should simplify as `nim dump --key:libpath`
if nimexe.len == 0: return ""
result = nimexe.splitPath()[0] /../ "lib"
if not fileExists(result / "system.nim"):
when defined(unix):
result = nimexe.expandSymlink.splitPath()[0] /../ "lib"
if not fileExists(result / "system.nim"): return ""
except OSError, ValueError:
return ""
proc findNimStdLibCompileTime*(): string =
## Same as `findNimStdLib` but uses source files used at compile time,
## and asserts on error.
result = querySetting(libPath)
doAssert fileExists(result / "system.nim"), "result:" & result
import std / [os, strutils]
var
first = true
ropesArray: array[TCFileSection, Rope]
ropesTable = initTable[string, array[TCFileSection, Rope]]()
proc commandCompileToC(graph: ModuleGraph) =
let conf = graph.config
#extccomp.initVars(conf)
setPipeLinePass(graph, SemPass)
setPipeLinePass(graph, CGenPass)
compileProject(graph)
## write the C code to files (in `~/.cache/nim/foo_r`)
cgenWriteModules(graph.backend, conf)
## Call the C compiler to generate the shared lib
extccomp.callCCompiler(conf)
## Other potential things I tried to "reset" without having to redo everything
#graph.markDirty(conf.projectMainIdx)
#graph.clearPasses()
#graph.resetForBackend()
# reset all modules also resets the main module, i.e. everything
#graph.resetAllModules()
proc setupModuleGraph(conf: ConfigRef): ModuleGraph =
var cache = newIdentCache()
var graph = newModuleGraph(cache, conf)
connectPipelineCallbacks(graph)
result = graph
proc setupConfig(scriptName: string, searchPaths: openArray[string | AbsoluteDir]): ConfigRef =
var conf = newConfigRef()
initDefines(conf.symbols)
#for define in defines: ## <-- don't define any symbols at the moment
# defineSymbol(conf.symbols, define[0], define[1])
conf.selectedGC = gcOrc ## <-- this is what's needed to get destructors working!
defineSymbol(conf.symbols, "gcorc")
defineSymbol(conf.symbols, "gcdestructors")
incl conf.globalOptions, optSeqDestructors
incl conf.globalOptions, optTinyRtti
incl conf.globalOptions, optGenDynLib ## <- we want to produce a shared lib, this is equivalent to `nim c --app:lib`
defineSymbol(conf.symbols, "nimSeqsV2")
defineSymbol(conf.symbols, "nimV2")
defineSymbol(conf.symbols, "danger")
defineSymbol(conf.symbols, "release")
for p in searchPaths:
conf.searchPaths.add(AbsoluteDir p)
if conf.libpath.isEmpty: conf.libpath = AbsoluteDir p
extccomp.initVars(conf)
conf.outfile = RelativeFile scriptName
result = conf
proc setupModule(scriptName: string, graph: ModuleGraph): (PSym, IdGenerator) =
var m = graph.makeModule(scriptName)
incl(m.flags, sfMainModule) ## <-- I don't think we need this, but maybe we should after all
var idgen = idGeneratorFromModule(m)
var vm = newCtx(m, graph.cache, graph, idgen)
#vm.mode = emRepl
# vm.features = flags ## <-- We don't use any flags at the moment
if true: # registerOps:
vm.registerAdditionalOps() # Required to register parts of stdlib modules
graph.vm = vm
#setPipeLinePass(graph, SemPass) ## <-- This would only do the "nim compilation", i.e. processing macros, injecting destructors etc
setPipeLinePass(graph, CgenPass) ## <-- this tells the compiler to do everything, incl compiling to C
graph.compilePipelineSystemModule()
result = (m, idgen)
proc evalScript*(i: Interpreter; scriptStream: PLLStream = nil, file: string) =
## This can also be used to *reload* the script.
## All this commented out code is me trying to manually fix the state of the module graph
## after the first script evaluation to make it work, if we don't start from scratch.
## In particular the `s` field of the `BModule`, contained in the `BModuleList`, which is
## the `graph.backend` contain all the strings (`Rope` type in compiler) that actually
## just store the literal C code that is emitted in the headers, types etc. So to avoid
## duplicates, I tried to reset these to the state they were in after / before the first
## compilation
#block:
# #i.graph.backend = newModuleList(i.graph)
# #var graph = newModuleGraph(i.graph.cache, i.graph.config)
# #connectPipelineCallbacks(graph)
#
# #var backend = cast[BModuleList](graph.backend)
# #backend.mainModProcs = Rope""
# #backend.mainModInit = Rope""
# #backend.otherModsInit = Rope""
# #backend.mainDatInit = Rope""
# #graph.backend = backend
# echo i.graph.config.nimMainPrefix
#
# for m in cgenModules(backend):
# m.s[cfsInitProc] = newRopeAppender()
# # for x in mitems(m.s):
# # x = Rope""
#
# i.graph = graph
# #i.graph.config.nimMainPrefix = ""
# #i.graph.backend = backend
# var m = i.graph.makeModule(file.replace(".nim", "_script.nim"))
# #incl(m.flags, sfMainModule)
# var idgen = idGeneratorFromModule(m)
# var vm = newCtx(m, cache, i.graph, idgen)
# #vm.mode = emRepl
# # vm.features = {}
# vm.registerAdditionalOps() # Required to register parts of stdlib modules
# i.graph.vm = vm
# #setPipeLinePass(graph, SemPass)
#
# #setPipeLinePass(graph, SemPass)
# setPipeLinePass(i.graph, CgenPass)
# i.graph.compilePipelineSystemModule()
# i.mainModule = m
#var m = i.graph.makeModule(file)
#if first:
# # save the ropes for the fields
# #discard processPipelineModule(i.graph, i.mainModule, i.idgen, scriptStream)
# var backend = cast[BModuleList](i.graph.backend)
# for m in cgenModules(backend):
# ropesTable[m.filename.string] = default(array[TCFileSection, Rope])
# for k in TCFileSection:
# ropesTable[m.filename.string][k] = m.s[k]
#
# #if m.filename.string.endsWith("system.nim"): # == "/t/tmp_file.nim":
# # for k in TCFileSection:
# # ropesArray[k] = m.s[k]
# # for x in mitems(m.s):
# # x = Rope""
# first = false
#else:
# # reset the ropes
# var backend = cast[BModuleList](i.graph.backend)
# for m in cgenModules(backend):
# if m.filename.string.endsWith("system.nim"): #string == "/t/tmp_file.nim":
# for k in TCFileSection:
# #if k == cfsInitProc:
# echo k, "=====================\n\n"
# echo m.s[k]
#
# for k in TCFileSection:
# if k == cfsInitProc: #k != cfsTypes:
# m.s[k] = ropesTable[m.filename.string][k]
## Reset by fully reconstructing module graph etc.
## v-- If you comment this out, it will be fast, but break.
if not first:
let conf = setupConfig(file, i.graph.config.searchPaths)
let graph = setupModuleGraph(conf)
let (m, idgen) = setupModule(file, graph)
i.graph = graph
i.mainModule = m
i.idgen = idgen
else:
first = false
## Ideas:
## We could test (I did that, but maybe did it wrong) making sure to create a *different*
## module for each case. I.e. `setupModule` for a secondary module that we recompile.
## Maybe somehow like that we can make something work.
## Alternative:
## Have one ModuleGraph that only does the `SemPass`. Then have a second one that also does
## the `CGenPass`. Let the `SemPass` one process the new code. Deep copy data from the SemPass
## one to the `CGenPass` version and generate C code. That way the CGenPass one only ever
## produces C code once? Huge hack, but could maybe work?
## I mean maybe we can generally separate these things without any hacks, but I doubt.
discard processPipelineModule(i.graph, i.mainModule, i.idgen, scriptStream)
## This resetting can also be done by `resetForBackend` I think
assert i != nil
assert i.mainModule != nil, "no main module selected"
initStrTables(i.graph, i.mainModule)
i.graph.cacheSeqs.clear()
i.graph.cacheCounters.clear()
i.graph.cacheTables.clear()
i.mainModule.ast = nil
i.graph.config.outDir = AbsoluteDir(file.parentDir)
i.graph.config.outFile = RelativeFile("lib" & file.extractFilename.replace(".nim", ".so"))
i.graph.config.projectPath = AbsoluteDir(file.parentDir)
i.graph.config.projectName = file.extractFilename
i.graph.config.projectFull = AbsoluteFile file
echo "File : ", file
echo i.graph.config.outFile
echo i.graph.config.projectPath
echo "--------------------------"
commandCompileToC(i.graph)
import compiler/passes
proc createInterpreter*(scriptName: string;
searchPaths: openArray[string];
flags: TSandboxFlags = {},
defines = @[("nimscript", "false")],
registerOps = true): Interpreter =
let conf = setupConfig(scriptName, searchPaths)
let graph = setupModuleGraph(conf)
let (m, idgen) = setupModule(scriptName, graph)
result = Interpreter(mainModule: m, graph: graph, scriptName: scriptName, idgen: idgen)
proc destroyInterpreter*(i: Interpreter) =
## destructor.
discard "currently nothing to do."
proc registerErrorHook*(i: Interpreter, hook:
proc (config: ConfigRef; info: TLineInfo; msg: string;
severity: Severity) {.gcsafe.}) =
i.graph.config.structuredErrorHook = hook
proc runRepl*(r: TLLRepl;
searchPaths: openArray[string];
supportNimscript: bool) =
## deadcode but please don't remove... might be revived
var conf = newConfigRef()
var cache = newIdentCache()
var graph = newModuleGraph(cache, conf)
for p in searchPaths:
conf.searchPaths.add(AbsoluteDir p)
if conf.libpath.isEmpty: conf.libpath = AbsoluteDir p
conf.cmd = cmdInteractive # see also `setCmd`
conf.setErrorMaxHighMaybe
initDefines(conf.symbols)
defineSymbol(conf.symbols, "nimscript")
if supportNimscript: defineSymbol(conf.symbols, "nimconfig")
when hasFFI: defineSymbol(graph.config.symbols, "nimffi")
var m = graph.makeStdinModule()
incl(m.flags, sfMainModule)
var idgen = idGeneratorFromModule(m)
if supportNimscript: graph.vm = setupVM(m, cache, "stdin", graph, idgen)
setPipeLinePass(graph, InterpreterPass)
graph.compilePipelineSystemModule()
discard processPipelineModule(graph, m, idgen, llStreamOpenStdIn(r))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment