import compile digest fs load std shell
# Convenience helpers for the common “compile if needed, cache, then execute”
# workflow.
#
# Cached bytecode is stored under `fs.cache_dir()`, scoped by application name.
# If `app:` is omitted, the scope is derived from `shell.program`.
# Derive a default cache application name from `shell.program`.
#
# - script path: basename without the last extension
# - `-m` module: module name string
# - unset / REPL: `dolang`
def default_app()
let p = shell.program
if (p == nil)
"dolang"
else if (type p fs.Path)
(p.stem || p.name || "dolang")
else
p
def normalize_app app
app = (app || default_app()).replace("/", "_").replace("\\", "_")
if (app == "")
throw std.RuntimeError "cache app name must not be empty"
app
def module_name path name
if (name != nil)
name
else
let stem = path.stem
if (stem == nil)
throw std.RuntimeError "can't derive module name from path: $path"
stem
def inject_keys inject
let keys = [...inject.keys()]
if !(keys.iter().all do |k| type k sym)
throw std.TypeError "inject key: expected `sym`"
keys
def cache_key path mode name prelude inject
let state = digest.Blake3()
state.update $ str $ path.absolute().normalize()
state.update "\0"
state.update $mode
state.update "\0"
state.update (name || "")
state.update "\0"
state.update (str prelude)
state.update "\0"
state.update (str $ inject_keys inject)
state.digest().hex()
def cache_root app
let path = (fs.cache_dir() / normalize_app app / "bytecode")
path.create_dir all: true
path
def cache_path path mode name app prelude inject
let key = cache_key $path $mode $name $prelude $inject
(cache_root app / "$key.dolc")
def synthetic_module_name key
"\$$key\$"
def cache_usable cache path
(cache.exists() && path.metadata().modified < cache.metadata().modified)
def compile_prelude prelude synthetic inject
{...prelude, $synthetic: inject_keys inject}
def compile_script path prelude synthetic inject
let source = path.read()
let prelude = compile_prelude $prelude $synthetic $inject
if (prelude == nil)
compile.compile $path $source
else
compile.compile $path $source prelude: $prelude
def compile_module path name prelude synthetic inject
let source = path.read()
let prelude = compile_prelude $prelude $synthetic $inject
if (prelude == nil)
compile.compile $path $source module: $name
else
compile.compile $path $source module: $name prelude: $prelude
def injected_module inject
record ...inject
def run_loaded bytecode synthetic inject
if (synthetic == nil)
load.run $bytecode
else
let module = injected_module $inject
let handle = load.import_handler do |name|
if (name == synthetic)
module
else
throw std.ImportError(name)
try
load.run $bytecode
finally
handle.unregister()
def compile_cached path mode name cache prelude synthetic inject
let compiled = if (mode == "script")
compile_script $path $prelude $synthetic $inject
else
compile_module $path $name $prelude $synthetic $inject
for diag = compiled.diagnostics
echo $diag.render()
if !compiled.ok
throw std.CompileError "failed to compile $(path.relative())"
cache.write $compiled.bytecode
run_loaded $compiled.bytecode $synthetic $inject
def exec_cached path mode name app prelude inject
if !(type inject dict)
throw std.TypeError "inject: expected `dict`"
path = fs.Path $path
let key = cache_key $path $mode $name $prelude $inject
let cache = cache_path $path $mode $name $app $prelude $inject
let synthetic = synthetic_module_name $key
if cache_usable $cache $path
try
return run_loaded (cache.read "b") $synthetic $inject
catch std.BytecodeError: _
nil
compile_cached $path $mode $name $cache $prelude $synthetic $inject
# Compile `path` in script mode if needed, cache the bytecode, and execute it.
#
# `app` scopes the cache directory under `fs.cache_dir()`. When omitted, the
# scope defaults from `shell.program`.
#
# `prelude` is passed through to `compile.compile`.
#
# `inject` supplies globally scoped symbol/value bindings during compilation
# and execution.
#
# # Example
#
# ```
# import exec
#
# exec.run "./tool.dol"
# exec.run "./tool.dol" app: "my-tool"
# exec.run "./tool.dol" inject: {answer: 42}
# ```
pub def run path :app = nil :prelude = {} :inject = {}
exec_cached $path script nil $app $prelude $inject
# Compile `path` in module mode if needed, cache the bytecode, and return the
# loaded module object.
#
# `name` defaults to the source file stem when omitted.
#
# `app` scopes the cache directory under `fs.cache_dir()`. When omitted, the
# scope defaults from `shell.program`.
#
# `prelude` is passed through to `compile.compile`.
#
# `inject` supplies globally scoped symbol/value bindings during compilation
# and execution.
#
# # Example
#
# ```
# import exec
#
# let mod = exec.module "./lib/build.dol"
# mod.main()
#
# let plugin = exec.module "./plugin.dol" name: "plugins.build"
# let injected = exec.module "./plugin.dol" inject: {answer: 42}
# ```
pub def module path :name = nil :app = nil :prelude = {} :inject = {}
exec_cached $path module (module_name (fs.Path path) name) $app $prelude $inject