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

    Env

    An environment: ρ. Immutable, because a closure captures one by value.

    DefClosure

    pub(all) struct DefClosure {
    env :
    HashMap
    [String, Value]
    region : Array[
    Stmt
    ]
    index : Int
    in_module : String
    } derive(
    Debug
    )

    LamClosure

    pub(all) struct LamClosure {
    env :
    HashMap
    [String, Value]
    params : Array[String]
    defaults : Array[Value]
    body :
    Expr

    in_module : String
    } derive(
    Debug
    )

    in_module is not part of LamΓ: the spec's closures carry an environment and a body, and nothing about where they were written. It is here because a position without a file is a lie the moment a program has two modules -- a helper.py function that aborts on its line 4 would be read as line 4 of __main__. The evaluator's import stack cannot supply it, because during a call it names the module that IMPORTED the callee, not the one that defined it. Only the closure knows.

    MatchResult

    pub(all) enum MatchResult {
    Match(
    HashMap
    [String, Value])
    NoMatch
    MatchStuck(String)
    } derive(
    Debug
    )

    Whether a pattern matched, and what it bound.

    MatchResult::then

    fn MatchResult::then(self : MatchResult, other : MatchResult) -> MatchResult

    m ⊗ m': a match result composes by union of bindings, and no-match is absorbing.

    Outcome

    pub(all) enum Outcome {
    Val(Value)
    Aborts(Termination)
    Stuck(String)
    } derive(
    Debug
    )

    The outcome of evaluating an expression: a value, an abort, or an operation the semantics leaves undefined.

    Primitive

    pub(all) enum Primitive {
    Print
    Len
    Range
    Exit
    Sqrt
    Exp
    Log
    Sin
    Cos
    Tan
    MathFloor
    MathCeil
    Abs
    All
    Any
    DivMod
    Enumerate
    ToFloat
    ToInt
    ToList
    Max
    Min
    Repr
    Reversed
    Round
    Sorted
    ToStr
    Sum
    ToTuple
    Zip
    Opaque(String)
    Foreign(String)
    } derive(Eq,
    Debug
    )

    The predefined functions of Figure 2.7.

    Opaque is for the three members that are values only so that importing them binds something: typing.Any, typing.Callable and dataclasses.dataclass are never called.

    Primitive::name

    fn Primitive::name(self : Primitive) -> String

    What to call a primitive, for an embedder that has to render one.

    A Prim has no printable form -- Value::str and Value::repr answer None for one, because Python prints an address no implementation can reproduce -- so a host that wants to show something in its place has to name it. Without this it spells all thirty out, which is a fourth table agreeing with the three this library already keeps in line, and the only one of the four that upstream cannot test.

    The answer comes from two different places, and the difference is worth knowing before you render it.

    • A primitive the SPECIFICATION defines answers with the name it is bound under. MathFloor is floor and not math.floor: the name is the member's, and which module it came from is the environment's business. lib/eval/predefined_wbtest.mbt asserts exactly that, for every predefined module under every profile -- so this is not a fourth table, it is the same table read back, and a builtin some later profile adds fails that test rather than quietly leaving a consumer's list short.

    • A primitive the HOST defines answers with the name the host registered, which is @value.host_fn's argument and NOT necessarily the name it is bound under. A host is free to bind record to host_fn("host.record") -- docs/embedding.mbt.md does -- and then this answers host.record. That is the useful answer rather than an accident of it: a qualified name is the spelling a reader would have to type, and the host is the only party that knows what qualification means to it.

    So a host that wants its own functions rendered a particular way chooses that when it registers them, not when it renders them.

    SliceBounds

    pub(all) struct SliceBounds {
    lower : Value?
    upper : Value?
    step : Value?
    }

    A slice's three parts, each absent unless the source wrote it.

    A Value and not an @ast.Slice, because lib/value is where the rule lives and an evaluator should hand it what it evaluated rather than what it parsed. None is "the source left it out", which is not the same as None the PurePy value: xs[None:] is a TypeError in Python and is one here.

    StmtResult

    pub(all) enum StmtResult {
    Assigns(
    HashMap
    [String, Value])
    Returns(Value)
    ResultAborts(Termination)
    ResultStuck(String)
    } derive(
    Debug
    )

    The result of evaluating a statement.

    StmtResult::outcome

    fn StmtResult::outcome(self : StmtResult) -> Outcome

    outcome(r): a statement's result read as the outcome of calling it.

    StmtResult::then

    fn StmtResult::then(self : StmtResult, other : StmtResult) -> StmtResult

    Sequential composition of results: assigns ρ ⊗ r.

    Termination

    pub(all) enum Termination {
    TypeError
    IndexError
    KeyError
    ZeroDivisionError
    AttributeError
    AssertionError(String?)
    SystemExit(
    BigInt
    )
    } derive(Eq,
    Debug
    )

    How a run ends. Every kind but SystemExit is named after the exception the same program raises under Python.

    Termination::name

    fn Termination::name(self : Termination) -> String

    The name Python prints for a termination, and the message after it.

    Termination::to_text

    fn Termination::to_text(self : Termination) -> String

    AssertionError: message when there is one, the bare name otherwise -- the last line of the traceback Python prints.

    Value

    Runtime values, environments and evaluation results: Figure 4.1.

    Two things about this type are the whole shape of the language:

    • Nothing is mutable. A list, a tuple, a dictionary and an object are built once and never changed, so an environment can be shared by every closure that captures it and nothing has to be copied defensively.
    • Stuck is a result, not a crash. The semantics leaves some operations undefined, and Chapter 1 allows an implementation either to abort or to produce Python's answer. This one aborts, with the operation named, so that the conformance suite's dynamically excluded bucket is CHECKED rather than merely tolerated.

    Value::kind_name

    fn Value::kind_name(self : Value) -> String

    What kind of value this is, for a message naming an undefined operation.

    Value::repr

    fn Value::repr(self : Value) -> String?

    Value::str

    fn Value::str(self : Value) -> String?

    Python's str and repr of a value.

    The two differ in exactly one place -- a string is itself under str and quoted under repr -- and print uses str while a container prints its elements with repr. Everything else is the same function.

    A closure, a module, a class or a primitive has no printable form here. Python prints one with an address in it, which no implementation can reproduce, so printing one is an undefined operation and is reported as such rather than invented.

    big_to_double

    fn big_to_double(n :
    BigInt
    ) -> Double

    A BigInt as a double, correctly rounded -- which is what Python's float(n) gives. MoonBit's BigInt has no conversion, so it goes through the decimal text, which parse_double rounds correctly.

    binop

    The operators: ⊙̂(v, v') and ⊖̂(v).

    Arithmetic is Python's, with three things this port has to supply that MoonBit does not:

    • // and % floor. MoonBit's / and % on BigInt and Int64 truncate toward zero; Python's floor toward negative infinity, so the remainder takes the sign of the DIVISOR: -7 // 2 is -4 and 7 % -2 is -1.
    • bool is not a number. True + 1, True < 2 and -False are UNDEFINED, even though Python accepts all three. Undefined and not TypeError: a termination kind is "named after the exception the same program raises under Python in the same circumstances" (operational-semantics.tex), and Python raises nothing for True + 1 -- it says 2. This is the same rule that makes True == 1 undefined, and the conformance suite has a test for that.
    • An integer and a float compare exactly, not by converting the integer, so a large integer is not made equal to the double nearest it.

    + and * are tried over sequences first, because they are not arithmetic there. The spec's arithmetic table is a \todo reading "Arithmetic, as in Python ... aborts TypeError where an operand is not a number", and the two halves disagree about "a" + "b": the sentence was written for 1 + "a", which the conformance suite tests and which IS a TypeError in Python. Concatenation is not, and CPython is the oracle for what run prints, so "as in Python" is the half that decides.

    build_size

    How many elements binop would BUILD for these operands, as a BigInt so that a count too large for an index is still a number a caller can refuse.

    Zero for every operation that builds nothing. A caller that meters a run asks this before calling binop, because [0] * 10 ** 9 is a single operation and metering it afterwards is metering it too late.

    compare

    ==, !=, in, not in and the four orderings.

    contains

    fn contains(haystack : Value, needle : Value) -> Bool?

    Whether needle is in haystack: an element of a list or tuple, a key of a dictionary, a substring of a string.

    contains_elems

    fn contains_elems(xs : Array[Value], needle : Value) -> Bool?

    dict_method

    fn dict_method(recv : Array[(String, Value)], name : String, args : Array[Value]) -> Outcome?

    d.get(k), d.keys(): a dictionary, read and never written.

    keys, values and items answer with a list rather than a view, as range answers with a list rather than a range. Everything that consumes one agrees; printing one directly does not.

    elems

    fn elems(v : Value) -> Array[Value]?

    The elements of a list, a tuple or a string. A string's elements are its CODE POINTS, which is what Python iterates.

    empty_env

    entries

    fn entries(base : Array[(String, Value)], more : Array[(String, Value)]) -> Array[(String, Value)]

    entries(δ, δ'): the left entries extended by the right ones in order.

    env_of

    fn env_of(entries : Array[(String, Value)]) ->
    HashMap
    [String, Value]

    fn eq(a : Value, b : Value) -> Bool?

    The functions on values of Annex A.3: equality, membership, elements, iteration, subscripting and the dictionary builders.

    Equality is PARTIAL, and the order in which elements are compared is therefore observable: eq short-circuits at the first pair that decides the answer, so an undefined comparison later in a sequence does not make the whole comparison undefined. [1, "a"] == [1, 3] is stuck and [1, "a"] == [2, 3] is False, and the difference is deliberate.

    Every function that can be undefined returns a Bool? or an Outcome; None and Stuck mean "no rule", not "false".

    eq_elems

    fn eq_elems(xs : Array[Value], ys : Array[Value]) -> Bool?

    Elementwise equality, stopping at the first pair that decides it.

    exact_integer

    fn exact_integer(d : Double) ->
    BigInt
    ?

    The integer a double exactly equals, or None when it is not one.

    Read off the bit pattern rather than through decimal text, so it is exact at every magnitude: a double is mantissa * 2^exponent, and when it is integral the shift below loses nothing.

    extend_env

    ρ ⊲ ρ': extension, which differs from override only on modules -- a loaded module is preferred to a stub of the same module, and two loadings of one module merge member by member.

    extend_value

    fn extend_value(a : Value, b : Value) -> Value

    float_mod

    fn float_mod(x : Double, y : Double) -> Double

    Python's % on floats: the result takes the sign of the divisor.

    floor_div

    Python's // on integers: floor, not truncation.

    floor_mod

    Python's % on integers: the remainder takes the sign of the divisor.

    getitem

    fn getitem(v : Value, key : Value) -> Outcome

    Subscripting: a dictionary by a string key, a sequence by an integer index counting from the end when negative.

    getslice

    fn getslice(v : Value, bounds : SliceBounds) -> Outcome

    xs[i:j:k], by CPython's own rule.

    Slicing is total where indexing is partial: an index past the end is an IndexError and a slice past the end is empty, which is why this cannot borrow getitem's arithmetic. The clamping below is PySlice_AdjustIndices written out -- a negative bound counts from the end and then clamps, and which end it clamps to depends on the sign of the step.

    k == 0 is a ValueError in Python. PurePy does not model ValueError, so it is undefined here rather than pretending to be one of the seven terminations the semantics has.

    host_fn

    fn host_fn(name : String) -> Value

    A function the host supplies, as a value the guest can call.

    The host puts one of these in a module (@eval.HostModule) and answers calls to it by name.

    iter

    fn iter(v : Value) -> Array[Value]?

    What a generator draws from a value: the KEYS of a dictionary, and otherwise its elements.

    order

    fn order(a : Value, b : Value) -> Int?

    Three-way comparison, None where the two values have no order.

    Numbers with numbers, strings by code point, lists with lists and tuples with tuples lexicographically. A nan anywhere makes the pair unordered, which is how Python's comparisons all come out False.

    override_env

    ρ ⊗ ρ': the right-hand side wins outright.
    fn print_text(args : Array[Value]) -> String?

    What print writes for these arguments: str of each, joined by a single space, and a newline. None if one of them has no printable form, which is the operation print leaves undefined.

    A host that REDEFINES builtins.print is handed the arguments and not the text, and most of them still want the text for something -- a transcript beside the values, a fallback for a shape they do not recognise. This is how they get it without writing the join a second time and drifting from the oracle on the first argument that renders unusually.

    seq_method

    fn seq_method(recv : Array[Value], name : String, args : Array[Value]) -> Outcome?

    xs.index(x), xs.count(x): what a list and a tuple share, which is everything about them that does not mutate.

    str_method

    fn str_method(recv : String, name : String, args : Array[Value]) -> Outcome?

    s.upper(), s.split(","), and the rest of str.

    unop

    not, unary + and unary -.

    update

    fn update(entries : Array[(String, Value)], key : String, v : Value) -> Array[(String, Value)]

    update(δ, w, v): the entries with w bound to v, in place if it was already there and appended otherwise.