Sign in

    marianoguerra/pure-py/eval does not have a README file

    Dispatch

    What a match statement does with a value.

    Host

    What the host supplies: everything about a PurePy run that is not pure.

    PurePy is a pure language, but a RUN is not a pure thing: it prints, it reads its arguments, it loads modules, and -- when it is embedded -- it calls out to whatever the application wants to expose. Those four are the whole of it, and they are all here, so an embedder can see the surface at once instead of discovering it a call at a time.

    • Output. write receives what print produced, one call per print, while the run is still going. A host that streams to a terminal writes it out; a host that wants a transcript accumulates.
    • Arguments. argv is what sys.argv reads. argv[0] is the program's own name, as in Python.
    • Modules. modules are importable modules the host defines, beyond the five the specification predefines. Their members are values, and a member may be a function the host implements.
    • Foreign calls. call answers a call to one of those functions. It is given the name the host registered and the arguments the guest passed, and returns an Outcome: a value, an abort, or -- for anything it does not recognise or cannot do -- an operation the semantics does not cover.

    Everything else about a run is already a value: the program is a SourceTree, so a host that keeps its guest code in a database, a zip file or a string never touches the filesystem.

    Note what is NOT here. There is no way for the host to mutate a guest value, because there is no mutation; no way to install a callback the guest invokes implicitly, because there are no hooks; and no ambient authority at all -- a guest can reach exactly the modules the host handed it. A host that gives no modules gives a program that can only compute and print.

    Host::member_names

    fn Host::member_names(self : Host) -> Map[String, Array[String]]

    The names of each host module's members.

    The CHECKER needs these and not the values: a from hostmod import f has to type-check before it can run, and the checker has no business knowing what f is. This is the one place the two sides of a host module meet.

    Host::module_names

    fn Host::module_names(self : Host) -> Array[String]

    The modules a program may import that have no source: the five the specification predefines, and the host's own.

    Host::new

    fn Host::new(write? : (String) -> Unit, argv? : Array[String], modules? : Array[HostModule], call? : (String, Array[
    Value
    ]) ->
    Outcome
    ) -> Host

    A host.

    Every part has a default that does nothing observable: output is dropped, sys.argv is empty, no modules are defined, and a foreign call is an operation the semantics does not cover. A host supplies the parts it wants.

    HostModule

    pub(all) struct HostModule {
    name : String
    members : Array[(String,
    Value
    )]
    }

    A module the host defines: a name the guest can import, and its members.

    A member is any Value. @value.host_fn("name") makes one the guest can call and the host answers by name.

    Interp

    pub struct Interp {
    tree :
    SourceTree

    host : Host
    loaded : Map[String,
    HashMap
    [String,
    Value
    ]]
    loading : Array[String]
    depth : Int
    max_depth : Int
    }

    Interp::apply

    eval-call-lambda, eval-call-def, eval-call-prim and their arity rules.

    Interp::dispatch

    dispatch(ρ, v, p⃗, s⃗): the first case whose pattern matches, with its bindings; no bindings and pass if none does.

    Interp::eval_expr

    Expression evaluation: Figures 4.5 to 4.10.

    Every rule that needs a condition -- if, and, or, a conditional expression, assert -- requires an actual True or False. Python's truthiness is not PurePy's, so if 5: has no rule and the run is undefined. That is one of the conformance suite's dynamically excluded tests, and it is the reason Stuck is threaded everywhere.

    Interp::eval_exprs

    A sequence of expressions, left to right, stopping at the first that does not yield a value (Figure 4.7).

    Interp::eval_quals

    Comprehension qualifiers (Figure 4.10): a list of environments, one per binding the generators produce.

    The enclosing environment is not extended by the result: a comprehension's variables are local to it.

    Interp::eval_seq

    Statement evaluation: Figures 4.3 and 4.4.

    A sequence's result is the bindings the SEQUENCE made, not the whole environment: eval-seq composes assigns ρ' with what follows, and the caller overrides its own environment with the result. That is what keeps a function body's locals out of its caller.

    Interp::load

    Module and program evaluation: Figures 4.11 to 4.13.

    Loading mirrors the static rules: the import prefix first, then the body, under builtins. An import loads the ancestors of its target from the root down, each before its descendant.

    The one departure is the cache, and it is about OUTPUT rather than values. The spec says loading is deterministic and needs no cache, which is true of what a module loads TO; it is not true of what loading PRINTS. Two imports of a module that prints would print twice, and CPython prints once. The suite has a test for it.

    Interp::match_pattern

    Pattern matching: Figure 4.2.

    The rules are narrower than "anything else fails", and the narrowness is deliberate. eval-pat-list-no fires when the value is NEITHER a list nor a tuple, or is a list of the wrong length -- so a list pattern against a TUPLE is covered by no rule at all and the run is undefined, while a tuple pattern against an integer is an honest no-match. A literal pattern against a list reaches eq between unrelated kinds and is undefined too. The conformance suite has a test for each of the three.

    Interp::new

    fn Interp::new(tree :
    SourceTree
    , host? : Host, max_depth? : Int) -> Interp

    Interp::predefined

    The environment a predefined module loads to, or None if q is not one.

    typing exposes Any alone. Figure 2.7 also lists Callable; the reference checker does not, and the signature and the environment have to agree or a from-import would type-check and then fail to run.

    Interp::resolve_class

    resolve-class(ρ, q): the class a qualified name stands for.

    Interp::run

    fn Interp::run(self : Interp) -> RunResult

    ⇒ κ (Figure 4.13): evaluate a program by loading its main module.

    RunResult

    pub(all) enum RunResult {
    Finished
    Terminated(
    Termination
    )
    Undefined(String)
    } derive(
    Debug
    )

    How a run ended.

    Sink

    pub struct Sink {
    buf : StringBuilder
    }

    Somewhere to put output when the caller just wants the transcript.

    run_program uses one of these; run_with does not, because a host that supplies its own write has somewhere better to put it.

    Sink::new

    fn Sink::new() -> Sink

    Sink::text

    fn Sink::text(self : Sink) -> String

    Sink::write

    fn Sink::write(self : Sink, text : String) -> Unit

    default_max_depth

    let default_max_depth : Int

    How deep a PurePy call stack may go by default.

    Twice Python's own limit of a thousand, and a third of where this evaluator's stack gives out on a native thread's default 8 MB. A PurePy program is pure and recursion is how it loops, so the default is generous.

    It is a parameter and not a constant because the ceiling is the HOST's, not this evaluator's: a JavaScript engine's stack holds far fewer frames than a native thread's, and a caller running there wants a smaller number.

    region_bindings

    {f⃗: v⃗}: every definition of a region, as closures over the same environment.

    run_program

    fn run_program(tree :
    SourceTree
    , argv? : Array[String], max_depth? : Int) -> (String, RunResult)

    Evaluate a program and answer with its transcript and how it ended.

    The simple path: output is collected and handed back. A caller that wants output as it happens, its own modules, or its own functions supplies a Host and uses run_with.

    run_with

    fn run_with(tree :
    SourceTree
    , host : Host, max_depth? : Int) -> RunResult

    Evaluate a program against a host.

    Output goes wherever the host sends it, so nothing is returned but how the run ended.