PurePy (a pure functional subset of Python 3.12): tokenizer, parser, checker and interpreter
moon add marianoguerra/pure-pyimport {
"marianoguerra/pure-py" @purepy,
"marianoguerra/pure-py/ast",
"marianoguerra/pure-py/eval",
"marianoguerra/pure-py/value",
}///|
test "parsing" {
let m = @purepy.parse("print(\"hello\")\n")
inspect(
@ast.dump(m),
content=(
#|Module
#| body:
#| Expr
#| value:
#| Call
#| func:
#| Name id=print ctx=Load
#| args:
#| Constant value='hello'
#| keywords:
#|
),
)
}///|
test "the sieve rejects what PurePy excludes" {
let m = @purepy.parse("for x in [1]:\n print(x)\n")
match @purepy.sieve(m) {
Some(d) => {
inspect(d.message(), content="for loops prohibited")
// 1 for a form PurePy excludes, 2 for one it plans to accept.
inspect(d.exit_code(), content="1")
}
None => fail("a for loop is not PurePy")
}
}
///|
test "the checker decides definite assignment and the capture rules" {
let good = @purepy.parse("x = 1\nprint(x)\n")
inspect(@purepy.check(good) is None, content="true")
let bad = @purepy.parse("x = 1\nf = lambda: x\nx = 2\n")
match @purepy.check(bad) {
Some(d) =>
inspect(
d.message(),
content="'x' captured by previous statement, reassigned here",
)
None => fail("a captured name may not be reassigned")
}
}///|
test "running a program" {
let modules = Map([
("__main__", @purepy.parse("from lib import double\nprint(double(21))\n")),
("lib", @purepy.parse("def double(n):\n return n * 2\n")),
])
let (output, result) = @purepy.run(@purepy.source_tree(modules))
inspect(result is Finished, content="true")
inspect(
output,
content=(
#|42
#|
),
)
}///|
test "an undefined operation" {
let modules = Map([("__main__", @purepy.parse("if 5:\n print(1)\n"))])
let (_, result) = @purepy.run(@purepy.source_tree(modules))
match result {
Undefined(op) => inspect(op, content="an if condition on int")
_ => fail("Python's truthiness is not PurePy's")
}
}///|
test "a guest calling a function the host supplied" {
let sink = @eval.Sink::new()
let host = @eval.Host::new(
write=fn(t) { sink.write(t) },
modules=[
@eval.HostModule::{
name: "clock",
members: [("now", @value.host_fn("clock.now"))],
},
],
call=fn(name, _) {
match name {
"clock.now" => Val(Int(1757260800N))
_ => Stuck("no such host function")
}
},
)
let modules = Map([
("__main__", @purepy.parse("from clock import now\nprint(now())\n")),
])
let tree = @purepy.source_tree(modules)
// The host goes to the checker too, so `from clock import now` resolves
// before anything runs.
inspect(@purepy.check_program(tree, host~) is None, content="true")
inspect(@purepy.run_with(tree, host) is Finished, content="true")
inspect(
sink.text(),
content=(
#|1757260800
#|
),
)
}///|
test "building a tree and printing it" {
let m = @ast.module_of([
@ast.Stmt::dataclass("Point", ["x", "y"]),
@ast.Stmt::expr_stmt(
@ast.Expr::call(@ast.Expr::name("print"), [
@ast.Expr::call(@ast.Expr::name("Point"), [
@ast.Expr::int(1),
@ast.Expr::int(2),
]),
]),
),
])
inspect(
@purepy.unparse(m),
content=(
#|@dataclass
#|class Point:
#| x: Any
#| y: Any
#|print(Point(1, 2))
#|
),
)
}pure-py tokens FILE one token per line
pure-py dump FILE [--pos] the abstract syntax tree
pure-py unparse FILE Python source regenerated from the tree
pure-py parse FILE... reject what PurePy excludes syntactically
pure-py check FILE... decide module well-formedness
pure-py check-program MAIN decide program well-formedness
pure-py run MAIN [ARGS...] evaluate a programerror[purepy::captured-reassignment]: 'x' captured by previous statement, reassigned here
╭─[ shadow_captured.py:6:5 ]
│
4 │ ╭ def g():
5 │ ├ return x
│ ╰─ 'x' captured here
6 │ x = 6
│ ──┬──
│ ╰─ reassigned here
│
├─ help: a closure captured this name, and PurePy has no cell for it to see
│ a later value; bind a new name instead
╰─just quick type-check, format, unit tests, layering, conformance
just conform every oracle, held to its ratchet
just one PATTERN the tests whose name contains PATTERN, in fullInstall
Download zipPurePy (a pure functional subset of Python 3.12): tokenizer, parser, checker and interpreter