rkt-pythonize
1 Quick start
#%python-code
2 LE
2.1 Data
2.2 Names
2.3 Runtime pieces
2.4 Tail calls
2.5 Macros
3 Tests
9.3

rkt-pythonize🔗ℹ

zhanghao

 (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

    2 LE

      2.1 Data

      2.2 Names

      2.3 Runtime pieces

      2.4 Tail calls

      2.5 Macros

    3 Tests

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 ...)

The Python that the LE forms compile to, as a string. It is a macro: the rendering happens while the enclosing module is compiled, and there is nothing left of it at run time.

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

begin

in an expression: begin

(list 1 2)

list

(apply f xs)

apply

(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.