pub struct Interpreter<'a> {
pub program: &'a Program,
pub sources: &'a SourceMap,
pub hosts: &'a HostRegistry,
/* private fields */
}Expand description
Executes a resolved program.
One interpreter runs one body on one thread: the entry, or the body of a
spawned task. Everything shared with the rest of the run is reached
through the Runtime it borrows, which is what a spawn hands to the
thread it starts.
§Ownership of the run’s crate::budget::Budget
The Budget is owned by the HostRegistry this interpreter borrows,
not by Interpreter itself: a host installs it once with
HostRegistry::set_budget, and every task thread charges that one budget
at its own safepoints, through a crate::budget::Meter taken from it
where the run begins. ADR 0008 draws a task’s fuel from the run’s budget,
so there is exactly one authoritative count of what the run spent,
whichever thread spent it. Call depth is the exception and is counted
here, because a task has a stack of its own.
Fields§
§program: &'a Program§sources: &'a SourceMap§hosts: &'a HostRegistryImplementations§
Source§impl<'a> Interpreter<'a>
impl<'a> Interpreter<'a>
Sourcepub fn new(runtime: &'a Runtime) -> Self
pub fn new(runtime: &'a Runtime) -> Self
An interpreter for the entry of runtime’s run.
The run’s budget is bound here, which is the one lock this takes and
the last one a safepoint of this interpreter will be behind. It is
sound to bind it this early because a budget cannot be installed once
an interpreter exists: HostRegistry::set_budget needs
&mut HostRegistry and this borrows the registry shared for 'a. The
one other way a budget is installed is HostRegistry::begin_run,
which is reached only through Interpreter::invoke_within and its
siblings, each of which rebinds.
Sourcepub fn heap_stats(&self) -> HeapStats
pub fn heap_stats(&self) -> HeapStats
What this run’s heaps have done so far: allocation, collections, live heap, peak live heap, and total pause.
The counters come from every heap retired so far, folded into the
Runtime as each task’s thread ended. The live figures come from
this interpreter’s own heap, which at the end of a run is the only one
left: every task’s heap went with its thread, and summing what those
last measured would report memory that no longer exists.
Sourcepub fn allocate_vector(&mut self, elements: Vec<Value>) -> Value
pub fn allocate_vector(&mut self, elements: Vec<Value>) -> Value
Allocates growable vector storage in this task’s heap.
Every Vector a Cove program can reach is created here, which is what
makes the heap’s table of objects complete.
Sourcepub fn collect(&mut self) -> Collection
pub fn collect(&mut self) -> Collection
Marks and sweeps this task’s heap, and records what it did.
The interpreter calls this at safepoints; a host may call it directly to observe the heap at a chosen moment.
Sourcepub fn assertion_failure(&self) -> Option<(Span, &str)>
pub fn assertion_failure(&self) -> Option<(Span, &str)>
Where the most recent failed assertion was written, together with the
message it produced, or None when no assertion has failed.
A caller compares the message against the error it is reporting: an assertion that failed and was then handled inside the program is not the reason a later error was returned.
Sourcepub fn run_entry(
&mut self,
module: &str,
name: &str,
args: Vec<Rc<str>>,
) -> Result<Value, RuntimeError>
pub fn run_entry( &mut self, module: &str, name: &str, args: Vec<Rc<str>>, ) -> Result<Value, RuntimeError>
Calls the host-selected entry function, and records how the run came out.
args are the process arguments; they are passed as an
Array<String> when the entry declares a parameter for them.
Every path a command takes into a Cove program passes through here —
cove run, cove test, cove generate, cove replay, and a cove build binary — because a command has strings to hand over and nothing
else. A host that has a value instead calls Interpreter::invoke,
which is the same run with a different way in.
It wraps Interpreter::enter rather than living inside it so that a
run that never reached its entry — one that named a function this
package does not declare, say — still ends with an event saying so.
§Run this on a thread with at least STACK_SIZE bytes
The interpreter is a recursive tree walker, so a Cove program spends
native stack as it nests calls, and MAX_CALL_DEPTH stops it before
that stack runs out. What “before” means depends on how much stack
there is. The runtime sizes every thread it creates itself, so a
spawned task and everything the toolchain runs are covered; a thread
an embedder created is the one it cannot size, and on it the limit is
only as good as the stack underneath.
So an embedder calls this from inside on_cove_stack, building the
interpreter there too. A Value is Rc-based and cannot cross a
thread boundary in either direction, so the whole run happens inside
the closure and only what the embedder wants to keep comes back:
let failure: Option<String> = cove_runtime::on_cove_stack(|| {
Interpreter::new(&runtime)
.run_entry("app", "main", Vec::new())
.err()
.map(|error| error.message)
})
.map_err(|e| format!("no thread to run Cove on: {e}"))?;An embedder that would rather manage the thread itself gives it
.stack_size(cove_runtime::STACK_SIZE) and builds the interpreter
inside it, which is the same arrangement by hand.
On a smaller stack than that, a deep enough Cove program ends the process with a stack overflow instead of returning the depth limit as an error. That is a boundary of what this runtime can promise rather than a bug in it: the size of a thread somebody else created is not something the interpreter can read or change.
Sourcepub fn invoke(
&mut self,
module: &str,
name: &str,
args: Vec<Value>,
) -> Result<Value, RuntimeError>
pub fn invoke( &mut self, module: &str, name: &str, args: Vec<Value>, ) -> Result<Value, RuntimeError>
Calls module.name with the arguments args, and records how the run
came out.
This is the other public way into a Cove program, and the one
Interpreter::run_entry is not: an entry takes the process
arguments, which are strings, so run_entry is how a command speaks
to a program and this is how an application does. A rule engine’s
evaluate(pr: PullRequest) -> Decision is invoked here with a
Value the host built, and answers the Decision the host reads —
which the entry’s result already allowed, so this is the way in
catching up with the way out. See issue #150.
Vm::invoke takes the same three things and
answers the same way, exactly as the two run_entrys do.
§What holds the arguments to anything
Everything the checker settled, and nothing else. Before the first instruction runs:
- the declaration must be one a host can call at all — no type
parameter, no
varparameter, no variadic one; argsmust be exactly as long as the declared parameter list, a parameter with a default included;- each value must be one its declared type admits, followed as deeply as the type goes, with a nominal type checked by the name the value carries.
A capability is not checked here, because an invocation grants
nothing: what the called function may reach is what the
HostRegistry it runs against was granted, exactly as for any other
run.
Every one of those refusals is a RuntimeError carrying the rule it
broke, the span of the parameter it was about, and the signature the
checker resolved.
§Run this on a thread with at least STACK_SIZE bytes
For the reason Interpreter::run_entry gives, and in the same way.
Sourcepub fn invoke_within(
&mut self,
budget: Budget,
module: &str,
name: &str,
args: Vec<Value>,
) -> Result<Value, RuntimeError>
pub fn invoke_within( &mut self, budget: Budget, module: &str, name: &str, args: Vec<Value>, ) -> Result<Value, RuntimeError>
The same call, bounded by budget and by nothing else.
§What a budget belongs to
A Budget used to belong to the HostRegistry: set_budget needs
&mut HostRegistry, a backend holds the registry by shared reference
for as long as it exists, and so every limit it carried — fuel, the
deadline, max_host_calls, max_tasks — was spent over the whole life
of the backend. For a cove run that is exactly right, because a run
is one invocation and [run.<name>]’s limits bound it. For an
embedding it is not: compile-once/invoke-many is the point, an
application wants to bound one request, and the only way to get that
was to build a registry, a Runtime and a backend per request — which
is 168 allocations of table-building against a request’s own 237, and
is the thing compiling once was for not doing.
A budget belongs to an invocation. It still lives on the registry,
because ADR 0008 draws a spawned task’s fuel from the run’s budget and
a task thread reaches the budget through the Arc<Runtime> it carries;
a task’s charges are still the invocation’s. What this changes is when
it is put there and how long it stands: budget is installed as this
call is entered, bounds everything the invocation and its tasks do, and
is left behind afterwards holding what the invocation spent — the same
state a finished cove run leaves and reads its --stats out of. The
next invoke_within replaces it.
The deadline runs from here, not from wherever budget was built.
A budget built to bound an invocation that has not begun would
otherwise spend it waiting for its turn. Every count starts at zero for
the same reason. A Cancellation is the one thing not reset: a
caller that wants to stop this invocation from another thread builds
the budget with
Budget::with_cancellation and
keeps the handle, and a flag already raised stays raised.
§Why this takes &mut self and there is no way to install a budget
that does not
ADR 0024 states each way a run can be stopped as a bound that holds
over the run, in that backend’s own fuel. A budget that could be
replaced while the run it bounds was executing would make every one of
those bounds a claim about something that had changed underneath it,
and the ADR’s argument would have to be revisited to say what a bound
even meant. So the registry has no public way to install one: this and
its three siblings are the only doors, each takes &mut self on the
backend, and a backend running an invocation is mutably borrowed for
its whole duration. The shape is what forbids it rather than a rule in
a comment.
Everything Interpreter::invoke says about what holds the arguments
holds here unchanged, and so does the refusal: the argument check runs
before the budget is installed, so a call refused for a wrong argument
spends none of it.
Sourcepub fn run_entry_within(
&mut self,
budget: Budget,
module: &str,
name: &str,
args: Vec<Rc<str>>,
) -> Result<Value, RuntimeError>
pub fn run_entry_within( &mut self, budget: Budget, module: &str, name: &str, args: Vec<Rc<str>>, ) -> Result<Value, RuntimeError>
Interpreter::run_entry, bounded by budget and by nothing else.
The command-shaped way in, bounded the way
Interpreter::invoke_within bounds the application-shaped one, and
that method’s documentation is the description of both.
Trait Implementations§
Source§impl Callable for Interpreter<'_>
impl Callable for Interpreter<'_>
Source§fn call_value(
&mut self,
callee: &Value,
args: &mut Vec<Value>,
span: Span,
) -> Result<Value, RuntimeError>
fn call_value( &mut self, callee: &Value, args: &mut Vec<Value>, span: Span, ) -> Result<Value, RuntimeError>
The caller’s vector is drained rather than consumed, so that a
higher-order builtin can hand the same one down for every element it
walks. See crate::builtins::Callable::call_value for why, and
builtins::walk_with for what pays for it.
One vector is still built per call here, because a slot carries a
label and a span beside its value and the interpreter binds
parameters out of that shape. It is one of the several this backend
builds per call — an Env, the parameter names, the label
assignment — rather than the only one, unlike the linear-memory
backend, whose calling convention needs no vector at all: an
argument already lives in the slot a frame reserved for it before the
call began.