rkt-pythonize
| (require rkt-pythonize) | package: rkt-pythonize |
rkt-pythonize is an LE-to-Python compiler behind one macro. The library exports #%python-code, whose body is LE and whose value is the Python it compiles to. The compilation happens at expansion time, so the value is a string.
1 Quick start
#lang racket/base (require rkt-pythonize) (define python (#%python-code (define (fact n) (if (= n 0) 1 (* n (fact (- n 1))))) (print (fact 5)))) (displayln python)
# generated by rkt-pythonize |
|
def fact(n): |
return (1 if (n == 0) is not False else (n * fact((n - 1)))) |
|
print(fact(5)) |
syntax
(#%python-code form ...)
2 LE
A statement is what a body, a begin in statement position and the branch of a statement if are made of; an expression is what an if, a call argument and a begin in expression position are made of. Nothing in an expression position is a statement, so a definition cannot hide inside one and mean something else there.
s ::= e an expression, for its value or its effect |
| (define x e) bind a value |
| (define (x x* ...) s* ...) bind a procedure |
| (define (x x* ... . rest) s* ...) bind a procedure; the rest parameter |
| collects the arguments into a list |
| (set! x e) assign |
| (begin s ...) a sequence, of statements here |
| (if e1 s1 s2) a conditional of statements |
|
e ::= x variable, a Python global |
| l self-evaluating literal |
| 'd quoted datum |
| (if e1 e2 e3) conditional |
| (begin e1 e* ...) a sequence, of expressions here |
| (raise e1) raise an exception |
| (with-handler e1 s* ...) run the body with e1 handling what it raises |
| (trampoline s* ...) call what the body returns while it is a procedure |
| (e0 e* ...) application |
|
d ::= int | float | string | boolean | list | tuple | dict |
l ::= int | float | string | boolean | tuple | dict |
A procedure body is a sequence of statements, and its value is the last expression in it. The body of a with-handler or a trampoline is a statement sequence too: when it holds define or set! it becomes a nested def that runs where the form stands, and a lone expression stays an inline lambda.
set! of a name an enclosing procedure binds becomes nonlocal, and of a name the program defines at the top level global; set! of a name the program never defines is a plain Python assignment.
2.1 Data
quote is data: integers, floats, strings, booleans, lists, tuples and dicts. A list is a Python list, a tuple is #(1 2) and becomes (1 ,2), and a dict is #hash(("a" . 1)) and becomes {"a" : 1}, so dict keys are strings. There is no symbol type, so quoting a symbol is a compile error, and there is no eval and no gensym.
2.2 Names
A free identifier is a Python global: (print (len "abc")) becomes print (len ("abc")).
Names are munged into readable Python identifiers: even? is even_p, set-car! is set_car_b, object-ref is object_ref, and a name that is a Python keyword gets a trailing _.
+, -, *, /, quotient, modulo, expt, =, <, >, <=, >=, equal? and eq? are Python operators, and and or are Python’s, and not is Python’s. An operator is not a value: (apply + xs) is refused, so name a procedure when the procedure itself is wanted.
Only #f is false. An if compiles to (then if test is not False else else), so 0, "" and '() are true, as they are in Racket.
2.3 Runtime pieces
A procedure the program names comes with the piece it needs, and nothing else does.
source | Python |
(raise e) | raise and Raised |
(with-handler h e ...) | with_handler |
(trampoline e ...) | trampoline |
in an expression: begin | |
(list 1 2) | |
(apply f xs) | |
(keyword-apply f kw xs) | keyword_apply |
(object-ref xs 0) | object_ref |
(object-set! xs 0 1) | object_set_b |
(object-get-attr xs "append") | object_get_attr |
(object-set-attr! xs "a" 1) | object_set_attr_b |
(object-has-attr? xs "append") | object_has_attr_p |
2.4 Tail calls
Tail calls are explicit. trampoline calls the value of its body and keeps calling while that value is a procedure, so a body that ends in a tail call returns a 0-arity procedure and loops, and a body that returns anything else is done:
(#%python-code (define (count n acc) (trampoline (if (= n 0) acc (count (- n 1) (+ acc 1))))) (print (count 100000 0))) ; 100000, flat
A procedure that drives trampoline is emitted twice: f drives and f_body holds the bounces, so a bounce never re-enters a driver.
2.5 Macros
LE has no macros, because a Racket macro is the better place for sugar: write the sugar in Racket and let it expand to #%python-code, or build the LE forms as data and hand them to the macro.
(define-syntax-rule (python-twice e) (#%python-code (print e) (print e))) (python-twice (quote (1 2))) ; "[1, 2]\n[1, 2]\n"
3 Tests
$ PLTCOLLECTS="$PWD/..:" TMPDIR="$PWD/.tmp" raco test tests/
The tests use the macro directly, look at the Python it renders, and run it with python3. PLTCOLLECTS is only needed when an older copy of the package is installed; with the package installed, raco test -x -p rkt-pythonize is enough.