///|
pub(all) enum ProgramRunResult {
ProgramCompleted(Value, Array[String])
ProgramFailed(Array[Diagnostic], Array[String])
}
///|
/// Per-operation resource limits for a persistent `Vm`.
pub(all) struct VmLimits {
max_steps : Int
max_call_depth : Int
max_output_characters : Int
max_modules : Int
max_objects : Int
max_source_characters : Int
}
///|
/// Automatic collection policy evaluated between host operations. Collection
/// never interrupts a running Wren call, so temporary host return values remain
/// valid until the next VM operation unless retained with a handle.
pub(all) struct VmGcPolicy {
minimum_objects : Int
growth_percent : Int
}
///|
pub fn VmGcPolicy::new(
minimum_objects : Int,
growth_percent : Int,
) -> VmGcPolicy {
{ minimum_objects, growth_percent, }
}
///|
pub fn VmLimits::new(
max_steps : Int,
max_call_depth : Int,
max_output_characters : Int,
max_modules : Int,
max_objects : Int,
max_source_characters : Int,
) -> VmLimits {
{
max_steps,
max_call_depth,
max_output_characters,
max_modules,
max_objects,
max_source_characters,
}
}
///|
/// Limits suitable for ordinary embedded scripts.
pub fn VmLimits::standard(max_steps : Int) -> VmLimits {
VmLimits::new(max_steps, 64, 1000000, 1024, 1000000, 10000000)
}
///|
/// An opaque strong root owned by a persistent VM.
pub struct VmHandle {
vm_identity : Int
identifier : Int
generation : Int
}
///|
pub(all) enum VmHandleResult {
VmHandleCreated(VmHandle)
VmHandleFailed(Diagnostic)
}
///|
/// Internal strong-root entry retained by a `Vm`.
pub struct VmHandleEntry {
value : Value
generation : Int
mut alive : Bool
}
///|
/// Persistent contexts are hidden behind integer identities so the public VM
/// API does not expose evaluator implementation types.
let persistent_vm_contexts : Array[RuntimeContext?] = []
///|
/// Append-only identity tokens prevent handles from being accepted by another
/// VM even when a closed runtime slot is reused.
let persistent_vm_identities : Array[Unit] = []
///|
priv struct PreservedBinding {
name : String
value : Value
}
///|
/// A long-lived Wren runtime. Modules, globals, classes, closures, Fibers and
/// foreign objects remain alive across host calls until `close` or a successful
/// transactional reload replaces the runtime generation.
pub struct Vm {
identity : Int
context_identifier : Int
mut sources : Array[ModuleSource]
roots : Array[String]
handles : Array[VmHandleEntry]
mut generation : Int
mut closed : Bool
limits : VmLimits
fallback_loader : (String, String) -> ModuleLoadResult
foreign_methods : Array[ForeignMethodBinding]
foreign_classes : Array[ForeignClassBinding]
should_cancel : () -> Bool
mut automatic_gc : Bool
mut gc_minimum_objects : Int
mut gc_growth_percent : Int
mut next_gc_objects : Int
mut last_gc_stats : VmGcStats?
}
///|
fn Vm::runtime(self : Vm) -> RuntimeContext {
match persistent_vm_contexts[self.context_identifier] {
Some(context) => context
None => abort("closed VM has no runtime context")
}
}
///|
fn copy_output(output : Array[String]) -> Array[String] {
let copied : Array[String] = []
for line in output {
copied.push(line)
}
copied
}
///|
fn copy_sources(sources : Array[ModuleSource]) -> Array[ModuleSource] {
let copied : Array[ModuleSource] = []
for source in sources {
copied.push(ModuleSource::new(source.name, source.source))
}
copied
}
///|
fn copy_foreign_methods(
bindings : Array[ForeignMethodBinding],
) -> Array[ForeignMethodBinding] {
let copied : Array[ForeignMethodBinding] = []
for binding in bindings {
copied.push(binding)
}
copied
}
///|
fn copy_foreign_classes(
bindings : Array[ForeignClassBinding],
) -> Array[ForeignClassBinding] {
let copied : Array[ForeignClassBinding] = []
for binding in bindings {
copied.push(binding)
}
copied
}
///|
fn find_source_index(sources : Array[ModuleSource], name : String) -> Int? {
for index = 0; index < sources.length(); index = index + 1 {
if sources[index].name == name {
return Some(index)
}
}
None
}
///|
fn make_vm_context(
sources : Array[ModuleSource],
limits : VmLimits,
fallback_loader : (String, String) -> ModuleLoadResult,
foreign_methods : Array[ForeignMethodBinding],
foreign_classes : Array[ForeignClassBinding],
should_cancel : () -> Bool,
) -> RuntimeContext {
let loader = fn(importer : String, specifier : String) {
match find_module_source(sources, specifier) {
Some(source) => ModuleLoaded(specifier, source)
None => fallback_loader(importer, specifier)
}
}
RuntimeContext::new_with_limits(
limits.max_steps,
limits.max_call_depth,
limits.max_output_characters,
limits.max_modules,
limits.max_objects,
limits.max_source_characters,
"",
loader,
foreign_methods,
foreign_classes,
should_cancel,
)
}
///|
pub fn Vm::new(
limits : VmLimits,
loader : (String, String) -> ModuleLoadResult,
foreign_methods : Array[ForeignMethodBinding],
foreign_classes : Array[ForeignClassBinding],
) -> Vm {
Vm::new_cancellable(limits, loader, foreign_methods, foreign_classes, fn() {
false
})
}
///|
pub fn Vm::new_cancellable(
limits : VmLimits,
loader : (String, String) -> ModuleLoadResult,
foreign_methods : Array[ForeignMethodBinding],
foreign_classes : Array[ForeignClassBinding],
should_cancel : () -> Bool,
) -> Vm {
let sources : Array[ModuleSource] = []
let methods = copy_foreign_methods(foreign_methods)
let classes = copy_foreign_classes(foreign_classes)
let context = make_vm_context(
sources, limits, loader, methods, classes, should_cancel,
)
let mut context_identifier = -1
for index = 0; index < persistent_vm_contexts.length(); index = index + 1 {
if context_identifier < 0 {
match persistent_vm_contexts[index] {
None => context_identifier = index
Some(_) => ()
}
}
}
if context_identifier < 0 {
context_identifier = persistent_vm_contexts.length()
persistent_vm_contexts.push(Some(context))
} else {
persistent_vm_contexts[context_identifier] = Some(context)
}
let identity = persistent_vm_identities.length()
persistent_vm_identities.push(())
{
identity,
context_identifier,
sources,
roots: [],
handles: [],
generation: 0,
closed: false,
limits,
fallback_loader: loader,
foreign_methods: methods,
foreign_classes: classes,
should_cancel,
automatic_gc: true,
gc_minimum_objects: 10000,
gc_growth_percent: 100,
next_gc_objects: 10000,
last_gc_stats: None,
}
}
///|
fn Vm::root_values(self : Vm) -> Array[Value] {
let roots : Array[Value] = []
for handle in self.handles {
if handle.alive && handle.generation == self.generation {
roots.push(handle.value)
}
}
roots
}
///|
fn Vm::record_collection(self : Vm, stats : VmGcStats) -> Unit {
self.last_gc_stats = Some(stats)
let grown = stats.reachable_objects * (100 + self.gc_growth_percent) / 100 + 1
self.next_gc_objects = if grown < self.gc_minimum_objects {
self.gc_minimum_objects
} else {
grown
}
}
///|
fn Vm::prepare_operation(self : Vm) -> Unit {
if self.closed {
return
}
let context = self.runtime()
if context.gc_requested ||
(self.automatic_gc && context.allocated_objects() >= self.next_gc_objects) {
let stats = collect_runtime_garbage(context, self.root_values())
context.gc_requested = false
self.record_collection(stats)
}
}
///|
/// Enable automatic collection between host operations. Invalid policies are
/// rejected without changing the active configuration.
pub fn Vm::configure_automatic_gc(self : Vm, policy : VmGcPolicy) -> Bool {
if policy.minimum_objects <= 0 || policy.growth_percent < 0 {
return false
}
self.automatic_gc = true
self.gc_minimum_objects = policy.minimum_objects
self.gc_growth_percent = policy.growth_percent
self.next_gc_objects = policy.minimum_objects
true
}
///|
pub fn Vm::disable_automatic_gc(self : Vm) -> Unit {
self.automatic_gc = false
}
///|
pub fn Vm::last_automatic_gc(self : Vm) -> VmGcStats? {
self.last_gc_stats
}
///|
fn invalid_vm_state(message : String, source_name : String) -> Diagnostic {
Diagnostic::new(Runtime, ExecutionLimit, message, SourceSpan::empty()).with_source(
source_name,
)
}
///|
fn validate_vm_limits(limits : VmLimits, source_name : String) -> Diagnostic? {
if limits.max_steps <= 0 ||
limits.max_call_depth <= 0 ||
limits.max_output_characters <= 0 ||
limits.max_modules <= 0 ||
limits.max_objects <= 0 ||
limits.max_source_characters <= 0 {
Some(invalid_vm_state("all VM limits must be positive", source_name))
} else {
None
}
}
///|
fn Vm::operation_failure(
self : Vm,
diagnostic : Diagnostic,
) -> ProgramRunResult {
ProgramFailed([diagnostic], copy_output(self.runtime().output))
}
///|
fn object_limit_issue(
context : RuntimeContext,
source_name : String,
) -> Diagnostic? {
if context.allocated_objects() > context.max_objects {
Some(
Diagnostic::new(
Runtime,
ExecutionLimit,
"runtime object limit exceeded",
SourceSpan::empty(),
).with_source(source_name),
)
} else {
None
}
}
///|
/// Interpret and retain one named root module. Reusing an existing name is an
/// explicit reload operation so accidental cache hits cannot hide new source.
pub fn Vm::interpret(
self : Vm,
source_name : String,
source : String,
) -> ProgramRunResult {
if self.closed {
return ProgramFailed([invalid_vm_state("VM is closed", source_name)], [])
}
self.prepare_operation()
let context = self.runtime()
context.begin_operation()
match validate_vm_limits(self.limits, source_name) {
Some(issue) => return self.operation_failure(issue)
None => ()
}
if find_source_index(self.sources, source_name) is Some(_) {
return self.operation_failure(
invalid_vm_state(
"module '\{source_name}' already exists; use reload_module",
source_name,
),
)
}
self.sources.push(ModuleSource::new(source_name, source))
match execute_module_source(source_name, source, context) {
ModuleExecutionFailed(issues) =>
ProgramFailed(issues, copy_output(context.output))
ModuleExecuted(_, value) => {
match object_limit_issue(context, source_name) {
Some(issue) =>
return ProgramFailed([issue], copy_output(context.output))
None => ()
}
self.roots.push(source_name)
ProgramCompleted(value, copy_output(context.output))
}
}
}
///|
fn Vm::module_value(
self : Vm,
module_name : String,
variable : String,
) -> VmHandleResult {
if self.closed {
return VmHandleFailed(invalid_vm_state("VM is closed", module_name))
}
let context = self.runtime()
match find_runtime_module(context, module_name) {
None =>
VmHandleFailed(
Diagnostic::new(
Runtime,
ModuleNotFound,
"module '\{module_name}' is not loaded",
SourceSpan::empty(),
).with_source(module_name),
)
Some(index) =>
match context.modules[index].environment.get(variable) {
None =>
VmHandleFailed(
Diagnostic::new(
Runtime,
UndefinedExport,
"module '\{module_name}' does not define '\{variable}'",
SourceSpan::empty(),
).with_source(module_name),
)
Some(value) => {
let identifier = self.handles.length()
self.handles.push({ value, generation: self.generation, alive: true, })
VmHandleCreated({
vm_identity: self.identity,
identifier,
generation: self.generation,
})
}
}
}
}
///|
/// Create a strong host root for a module variable.
pub fn Vm::capture(
self : Vm,
module_name : String,
variable : String,
) -> VmHandleResult {
self.module_value(module_name, variable)
}
///|
/// Read a module variable without creating a strong host root. Runtime-backed
/// values should be passed to `retain` before an explicit collection pass.
pub fn Vm::read(
self : Vm,
module_name : String,
variable : String,
) -> ProgramRunResult {
if self.closed {
return ProgramFailed([invalid_vm_state("VM is closed", module_name)], [])
}
self.prepare_operation()
let context = self.runtime()
context.begin_operation()
match self.module_value(module_name, variable) {
VmHandleFailed(issue) => ProgramFailed([issue], copy_output(context.output))
VmHandleCreated(handle) => {
let value = match self.resolve_handle(handle) {
Some(value) => value
None =>
return self.operation_failure(
invalid_vm_state("VM handle is invalid", module_name),
)
}
ignore(self.release(handle))
ProgramCompleted(value, copy_output(context.output))
}
}
}
///|
/// Retain a value returned by a VM call as a strong host root. Values that
/// refer to runtime objects must be retained before the next explicit GC pass.
pub fn Vm::retain(self : Vm, value : Value) -> VmHandleResult {
if self.closed {
return VmHandleFailed(invalid_vm_state("VM is closed", ""))
}
let identifier = self.handles.length()
self.handles.push({ value, generation: self.generation, alive: true, })
VmHandleCreated({
vm_identity: self.identity,
identifier,
generation: self.generation,
})
}
///|
fn Vm::resolve_handle(self : Vm, handle : VmHandle) -> Value? {
if self.closed ||
handle.vm_identity != self.identity ||
handle.generation != self.generation ||
handle.identifier < 0 ||
handle.identifier >= self.handles.length() {
return None
}
let entry = self.handles[handle.identifier]
if !entry.alive || entry.generation != self.generation {
None
} else {
Some(entry.value)
}
}
///|
/// Release a strong host root. Released and pre-reload handles cannot be used.
pub fn Vm::release(self : Vm, handle : VmHandle) -> Bool {
if handle.vm_identity != self.identity ||
handle.generation != self.generation ||
handle.identifier < 0 ||
handle.identifier >= self.handles.length() {
return false
}
let entry = self.handles[handle.identifier]
if !entry.alive {
false
} else {
entry.alive = false
true
}
}
///|
fn Vm::invoke_host(
self : Vm,
handle : VmHandle,
method_name : String,
arguments : Array[Value],
) -> ProgramRunResult {
if self.closed {
return ProgramFailed([invalid_vm_state("VM is closed", "")], [])
}
self.prepare_operation()
let context = self.runtime()
context.begin_operation()
let receiver = match self.resolve_handle(handle) {
Some(value) => value
None =>
return self.operation_failure(
invalid_vm_state("VM handle is invalid or released", ""),
)
}
context.current_source = ""
match
invoke_method(
receiver,
method_name,
arguments,
SourceSpan::empty(),
context,
) {
EvaluationSucceeded(value) =>
match object_limit_issue(context, "") {
Some(issue) => ProgramFailed([issue], copy_output(context.output))
None => ProgramCompleted(value, copy_output(context.output))
}
EvaluationFailed(issue) =>
ProgramFailed([issue.with_source("")], copy_output(context.output))
}
}
///|
/// Call a captured Wren function from MoonBit.
pub fn Vm::call(
self : Vm,
handle : VmHandle,
arguments : Array[Value],
) -> ProgramRunResult {
self.invoke_host(handle, "call", arguments)
}
///|
/// Call a method or getter on a captured Wren value from MoonBit.
pub fn Vm::call_method(
self : Vm,
handle : VmHandle,
method_name : String,
arguments : Array[Value],
) -> ProgramRunResult {
self.invoke_host(handle, method_name, arguments)
}
///|
/// Read a Wren getter or built-in property from a captured value.
pub fn Vm::get(
self : Vm,
handle : VmHandle,
property_name : String,
) -> ProgramRunResult {
if self.closed {
return ProgramFailed([invalid_vm_state("VM is closed", "")], [])
}
self.prepare_operation()
let context = self.runtime()
context.begin_operation()
let receiver = match self.resolve_handle(handle) {
Some(value) => value
None =>
return self.operation_failure(
invalid_vm_state("VM handle is invalid or released", ""),
)
}
context.current_source = ""
match read_property(receiver, property_name, SourceSpan::empty(), context) {
EvaluationSucceeded(value) =>
match object_limit_issue(context, "") {
Some(issue) => ProgramFailed([issue], copy_output(context.output))
None => ProgramCompleted(value, copy_output(context.output))
}
EvaluationFailed(issue) =>
ProgramFailed([issue.with_source("")], copy_output(context.output))
}
}
///|
fn portable_reload_value(value : Value) -> Bool {
match value {
NullValue
| BoolValue(_)
| NumberValue(_)
| StringValue(_)
| RawStringValue(_)
| RangeValue(_, _, _) => true
_ => false
}
}
///|
fn invalidate_vm_handles(vm : Vm) -> Unit {
for handle in vm.handles {
handle.alive = false
}
}
///|
/// Transactionally rebuild the VM from its root modules using a replacement
/// source. Selected primitive globals are migrated after the replacement
/// module initializes and before dependent roots execute. A failed rebuild
/// leaves the current VM and all handles unchanged.
pub fn Vm::reload_module(
self : Vm,
module_name : String,
source : String,
preserve : Array[String],
) -> ProgramRunResult {
if self.closed {
return ProgramFailed([invalid_vm_state("VM is closed", module_name)], [])
}
self.prepare_operation()
let context = self.runtime()
context.begin_operation()
if source.length() > self.limits.max_source_characters {
return self.operation_failure(
invalid_vm_state(
"replacement source exceeds the source size limit", module_name,
),
)
}
let preserved : Array[PreservedBinding] = []
match find_runtime_module(context, module_name) {
Some(module_index) =>
for name in preserve {
match context.modules[module_index].environment.get(name) {
None =>
return self.operation_failure(
Diagnostic::new(
Runtime,
UndefinedExport,
"module '\{module_name}' does not define preserved global '\{name}'",
SourceSpan::empty(),
).with_source(module_name),
)
Some(value) =>
if portable_reload_value(value) {
preserved.push({ name, value, })
} else {
return self.operation_failure(
invalid_vm_state(
"preserved global '\{name}' is not a portable primitive value",
module_name,
),
)
}
}
}
None =>
if !preserve.is_empty() {
return self.operation_failure(
invalid_vm_state("module is not loaded", module_name),
)
}
}
let candidate_sources = copy_sources(self.sources)
match find_source_index(candidate_sources, module_name) {
Some(index) =>
candidate_sources[index] = ModuleSource::new(module_name, source)
None => candidate_sources.push(ModuleSource::new(module_name, source))
}
let candidate_methods = copy_foreign_methods(self.foreign_methods)
let candidate_classes = copy_foreign_classes(self.foreign_classes)
let candidate = make_vm_context(
candidate_sources,
self.limits,
self.fallback_loader,
candidate_methods,
candidate_classes,
self.should_cancel,
)
candidate.begin_operation()
let target_result = execute_module_source(module_name, source, candidate)
match target_result {
ModuleExecutionFailed(issues) => {
finalize_foreign_instances(candidate)
return ProgramFailed(issues, copy_output(candidate.output))
}
ModuleExecuted(environment, _) =>
for binding in preserved {
ignore(environment.assign(binding.name, binding.value))
}
}
let mut last = Value::null()
for root in self.roots {
if root == module_name {
continue
}
let root_source = match find_module_source(candidate_sources, root) {
Some(root_source) => root_source
None => {
finalize_foreign_instances(candidate)
return ProgramFailed(
[
invalid_vm_state(
"root module '\{root}' has no retained source",
root,
),
],
copy_output(candidate.output),
)
}
}
match execute_module_source(root, root_source, candidate) {
ModuleExecutionFailed(issues) => {
finalize_foreign_instances(candidate)
return ProgramFailed(issues, copy_output(candidate.output))
}
ModuleExecuted(_, value) => last = value
}
}
match object_limit_issue(candidate, module_name) {
Some(issue) => {
finalize_foreign_instances(candidate)
return ProgramFailed([issue], copy_output(candidate.output))
}
None => ()
}
finalize_foreign_instances(context)
invalidate_vm_handles(self)
self.generation = self.generation + 1
self.sources = candidate_sources
persistent_vm_contexts[self.context_identifier] = Some(candidate)
ProgramCompleted(last, copy_output(candidate.output))
}
///|
/// Trace VM roots and release unreachable instances. Suspended opaque
/// continuations conservatively retain instances until the Fiber resumes.
pub fn Vm::collect_garbage(self : Vm) -> VmGcStats {
if self.closed {
return {
before_objects: 0,
reachable_objects: 0,
reclaimed_objects: 0,
before_instances: 0,
reachable_instances: 0,
reclaimed_instances: 0,
finalized_foreign_instances: 0,
}
}
let context = self.runtime()
let stats = collect_runtime_garbage(context, self.root_values())
context.gc_requested = false
self.record_collection(stats)
stats
}
///|
/// Finalize remaining foreign instances and permanently close the VM.
pub fn Vm::close(self : Vm) -> Unit {
if self.closed {
return
}
finalize_foreign_instances(self.runtime())
invalidate_vm_handles(self)
self.closed = true
persistent_vm_contexts[self.context_identifier] = None
}
///|
fn invalid_execution_limit(source_name : String) -> ProgramRunResult {
ProgramFailed(
[
Diagnostic::new(
Runtime,
ExecutionLimit,
"execution step limit must be positive",
SourceSpan::empty(),
).with_source(source_name),
],
[],
)
}
///|
/// Run one named entry module and resolve imports through an embedding callback.
pub fn run_with_loader(
source_name : String,
source : String,
max_steps : Int,
loader : (String, String) -> ModuleLoadResult,
) -> ProgramRunResult {
run_with_foreign_bindings(source_name, source, max_steps, loader, [], [])
}
///|
/// Run a named entry module with host implementations for declared foreign
/// methods. Bindings are matched by module, class, static flag, and signature.
pub fn run_with_foreign_methods(
source_name : String,
source : String,
max_steps : Int,
loader : (String, String) -> ModuleLoadResult,
foreign_methods : Array[ForeignMethodBinding],
) -> ProgramRunResult {
run_with_foreign_bindings(
source_name,
source,
max_steps,
loader,
foreign_methods,
[],
)
}
///|
/// Run one named entry module with host implementations for foreign methods
/// and host-owned foreign class state.
pub fn run_with_foreign_bindings(
source_name : String,
source : String,
max_steps : Int,
loader : (String, String) -> ModuleLoadResult,
foreign_methods : Array[ForeignMethodBinding],
foreign_classes : Array[ForeignClassBinding],
) -> ProgramRunResult {
if max_steps <= 0 {
return invalid_execution_limit(source_name)
}
let context = RuntimeContext::new_with_loader(
max_steps, source_name, loader, foreign_methods, foreign_classes,
)
let result = match execute_module_source(source_name, source, context) {
ModuleExecuted(_, value) => ProgramCompleted(value, context.output)
ModuleExecutionFailed(issues) => ProgramFailed(issues, context.output)
}
finalize_foreign_instances(context)
result
}
///|
fn find_module_source(modules : Array[ModuleSource], name : String) -> String? {
for module_source in modules {
if module_source.name == name {
return Some(module_source.source)
}
}
None
}
///|
/// Run an entry module from a complete in-memory source collection.
/// Import specifiers are matched against module names exactly.
pub fn run_modules(
entry_name : String,
modules : Array[ModuleSource],
max_steps : Int,
) -> ProgramRunResult {
let entry_source = match find_module_source(modules, entry_name) {
Some(source) => source
None =>
return ProgramFailed(
[
Diagnostic::new(
Runtime,
ModuleNotFound,
"entry module '\{entry_name}' was not provided",
SourceSpan::empty(),
).with_source(entry_name),
],
[],
)
}
let loader = fn(_importer : String, specifier : String) -> ModuleLoadResult {
match find_module_source(modules, specifier) {
Some(source) => ModuleLoaded(specifier, source)
None => ModuleLoadFailed("module '\{specifier}' was not provided")
}
}
run_with_loader(entry_name, entry_source, max_steps, loader)
}
///|
/// Run a source file with a configurable guard against unbounded loops.
pub fn run_with_limit(source : String, max_steps : Int) -> ProgramRunResult {
run_with_loader("", source, max_steps, fn(_, name) {
ModuleLoadFailed("module '\{name}' is not available")
})
}
///|
/// Run a source file using a conservative default step budget.
pub fn run(source : String) -> ProgramRunResult {
run_with_limit(source, 100000)
}