I think Lean is neat.

Maybe especially as a programming language. Got lots of fun doodads, cool compiler to fast code, metaprogramming, and then some day maybe you can prove something if you feel like it as a treat.

I made a python library to make lean-python interop a little less painful https://github.com/philzook58/leancall . Cody has also been sniffing around this area and has been poking me to try and build something like this. https://github.com/codyroux/lean-to-python

Here’s what it looks like to use it.

import leancall as lc

mod = lc.from_string("def foo (x : Nat) (s : String) : String := s ++ toString x")
mod.foo(42, "The answer is ")
'The answer is 42'

This is done by basically 3 things:

  1. an extensible singledispatch to_lean to serialize python values to lean.
  2. a lark parser for lean output. I like lark. It also outputs reasonable defaults. none becomes None, String is str, Int and Nat are int, etc.
  3. a grep for defs in the lean file
  4. a LeanFun class that uses these things to take python values to and from lean

You can see an early prototype of this way below.

Here’s some fun examples of using it with interesting libraries

Controlling a Cartpole

It is fast enough to control a gym environment https://gymnasium.farama.org/introduction/basic_usage/ (gym is a library for reinforcement learning)

Cody had a lean pendulum controller that he kept private for some reason.

%%file /tmp/cartpole.lean

def action (obs : List Float) : Nat := 
    let pos := obs[0]!
    let vel := obs[1]!
    let ang := obs[2]!
    let ang_vel := obs[3]!
    -- I fiddled with this until it seems to work
    if pos - 1 * vel + 2.0 * ang + 0.7 * ang_vel < 0.0 then
        0
    else
        1
Overwriting /tmp/cartpole.lean
import leancall as lc
import leancall.numpy
mod = lc.from_file("/tmp/cartpole.lean")
import gymnasium as gym

env = gym.make("CartPole-v1", render_mode="human")
observation, info = env.reset()
episode_over = False
total_reward = 0
try:
    while not episode_over:
        # Choose an action: 0 = push cart left, 1 = push cart right
        action = mod.action(list(observation))
        observation, reward, terminated, truncated, info = env.step(action)

        total_reward += reward
        episode_over = terminated or truncated
finally:
    env.close()

print(f"Episode finished! Total reward: {total_reward}")

It balances! Wowee zowee!

Ray Tracing

My friends were also just sort of winging a bad ray tracer. Using python is nice because you already have image showing libraries, which avoids the need to make some new custom Lean vs code thing or serialize to PPM.

Here was a python thing we whipped out to draw a circle. I do not claim it as a paragon of code excellence.

import numpy as np

img = np.zeros((800,800,3))

for i in range(800):
    for j in range(800):
        if (i-200)**2 + (j-200)**2 < 100**2:
            img[i,j] = [0,255,0]

import matplotlib.pyplot as plt

plt.imshow(img)

png

%%file /tmp/circle.lean

abbrev Image := Array (Array (Array UInt8))
partial def doit (N : Nat) := Id.run do
  let mut img : Image := Array.replicate N (Array.replicate N #[0, 0, 0])
  for i in [0:N] do
    for j in [0:N] do
          if ((i : Int)- N/2)^2 + ((j : Int) - N/2)^2 < (N/4)^2 -- This Int conversion sucks. But I'd have a similar problem in many other languages
          then
            img := img.modify i fun row => row.set! j #[0, 0, 255]
  return img
Writing /tmp/circle.lean
import leancall as lc
mod = lc.from_file("/tmp/ray.lean")
plt.imshow(mod.doit(300))

png

I found the lark parsing to be a bit slow (printing a giant array out is a somewhat ludicrous thing to be doing). Using numpy.loadtxt is quite a bit faster. I wrapped it in leancall.numpy.parse

import leancall.numpy as lcnp
N = 600
plt.imshow(lcnp.parse(mod.doit(N, parse=None)).reshape((N,N,3)).astype(np.uint8))

png

This is a larger ray tracer. Again, not a paragon of anything. AI produced this largely from our python version. You may also want to check out https://github.com/kmill/lean4-raytracer and https://raytracing.github.io/books/RayTracingInOneWeekend.html

%%file /tmp/ray.lean

abbrev Pixel := UInt8 × UInt8 × UInt8
abbrev Image : Type := Array (Array Pixel)

structure Vec3 where
  x : Float
  y : Float
  z : Float
deriving Repr, Inhabited, BEq

instance : Add Vec3 where
  add a b := a.x + b.x, a.y + b.y, a.z + b.z

instance : Sub Vec3 where
  sub a b := a.x - b.x, a.y - b.y, a.z - b.z


instance : HMul Float Vec3 Vec3 where
  hMul s v := s * v.x, s * v.y, s * v.z

instance : HDiv Vec3 Float Vec3 where
  hDiv v s := v.x / s, v.y / s, v.z / s

def dot (a b : Vec3) : Float :=
  a.x * b.x + a.y * b.y + a.z * b.z

def norm (v : Vec3) : Float :=
  Float.sqrt (dot v v)

def normalize (v : Vec3) : Vec3 :=
  let n := norm v
  if n == 0 then v else v / n

def clamp01 (x : Float) : Float :=
  if x < 0 then 0 else if x > 1 then 1 else x

def hadamard (a b : Vec3) : Vec3 :=
  a.x * b.x, a.y * b.y, a.z * b.z

def floatToByte (x : Float) : UInt8 :=
  let y := clamp01 x
  UInt8.ofNat (UInt64.toNat (Float.toUInt64 (y * 255.0)))

def colorToPixel (c : Vec3) : Pixel :=
  (floatToByte c.x, floatToByte c.y, floatToByte c.z)

structure Sphere where
  center : Vec3
  radius : Float
  color : Vec3
deriving Repr, Inhabited, BEq

structure Ray where
  origin : Vec3
  direction : Vec3
deriving Repr, Inhabited, BEq

structure Light where
  position : Vec3
  color : Vec3
deriving Repr, Inhabited, BEq


def Sphere.intersect (s : Sphere) (r : Ray) : Option (Float × Vec3) :=
  let oc := r.origin - s.center
  let a := dot r.direction r.direction
  let b := 2.0 * dot oc r.direction
  let c := dot oc oc - s.radius * s.radius
  let discriminant := b * b - 4 * a * c
  if discriminant < 0 then
    none
  else
    let t1 := (-b - Float.sqrt discriminant) / (2.0 * a)
    let t2 := (-b + Float.sqrt discriminant) / (2.0 * a)
    if t1 > 0 then
      let pos := r.origin + t1 * r.direction
      let norm := (pos - s.center) / s.radius
      some (t1, norm)
    else if t2 > 0 then
      let pos := r.origin + t2 * r.direction
      let norm := (pos - s.center) / s.radius
      some (t2, norm)
    else
      none

def lights : List Light :=
  [ { position := 50, 50, 0, color := 1, 0.1, 0.4 } ]

def objects : List Sphere :=
  [ { center := 0, 10, 50, radius := 10, color := 1, 0.1, 0.1 }
  , { center := 0, 0, 100, radius := 10, color := 0.1, 1, 0.1 }
  , { center := -20, 0, 100, radius := 10, color := 0.1, 0.1, 1 } ]

def render (N : Nat) : Image := Id.run do
  let mut img : Image := Array.replicate N (Array.replicate N (0, 0, 0))
  let half := Float.ofNat (N / 2)
  let nFloat := Float.ofNat N
  for i in [0:N] do
    for j in [0:N] do
      let dir := (Float.ofNat i - half) / nFloat, (Float.ofNat j - half) / nFloat, 1
      let ray := Ray.mk 0, 0, 0 dir
      let mut closestT : Float := 1.0e30
      let mut pixelColor : Pixel := (135, 206, 235) -- sky color
      for obj in objects do
        match obj.intersect ray with
        | some (t, norm) =>
          if t < closestT then
            closestT := t
            let mut col : Vec3 := 0, 0, 0
            let hitPos := ray.origin + t * ray.direction
            for light in lights do
              let lightDir := normalize (light.position - hitPos)
              let diff := dot norm lightDir
              if diff > 0 then
                col := col + diff * (hadamard obj.color light.color)
            pixelColor := colorToPixel col
        | none => ()
      img := img.modify i fun row => row.set! j pixelColor
  img
Overwriting /tmp/ray.lean
mod = lc.from_file("/tmp/ray.lean")
plt.imshow(mod.render(400))

png

Bits and Bobbles

https://www.youtube.com/watch?v=c5LOYzZx-0c The Best New Programming Language is a Proof Assistant by Harry Goldstein

https://www.philipzucker.com/dirty_lean/ Doing Lean Dirty: Lean as a Jupyter Notebook Replacement

https://www.philipzucker.com/lean_smt/ Using Lean like an External SMT Solver from Python

Feel no guilt putting unsafe and partial everywhere imo. It’s the analog of just getting clone happy in Rust. Just do it at the first sight of trouble. I absolve you.

You pay quite an overhead to serialize and deserialize. I also added an experimental direct FFI thing. It was pretty vibe coded so I dunno.

Another thing that could actually be useful is to use Lean with cocotb, which is a python verilog test driver thing

Maybe leancall should just be a module in leaninteract.

Also, I like using python. I defend it to my PL nerd friends and shit on it to my python friends. It has a vast number of useful libraries. It’s nice for ripping ideas out.

Once you get past the standard library and mathlib, Lean’s library ecosystem is not great yet. Using python as a shim to get pictures and file formats and stuff is not that crazy. Using Lean as a backend to python libraries is also not that crazy of an idea also I think. It is faster (almost everything is), it exposes a C ABI. Some projects already do it (https://github.com/leanprover/KLR https://github.com/alerad/leancert )

https://github.com/augustepoiroux/LeanInteract Lean interact is a project that gives you a lean repl as a python library. It’s main intent is probably for machine learning proof stuff. This library really supplies the heaviest lifting and what I added was some ergonomics shellac.

I thought it would be fun to use it to build a python lean bridge where you can use lean functions fairly painlessly as python functions.

Because people are interested in

One thing that isn’t great about lean is it’s library ecosystem. It’s a young language still. Mathlib is remarkable, the standard library has made great strides, but there isn’t much else.

I am a dirty dog. I do not like lifting little ideas out into projects of their own. I have a giant folder of files that I tinker on. I want to install garbage willy nilly and puke all over my computer. This is against best software engineering practice.

The one way I have to make this work is to just have my garvage folder have a lakefile at the top level specifying a version of lean and mathlib. I keep it updating to the latest versio nevery couple of months. Libraries get left behind.

Actually Lean stresses me out. Too many smart people working hard. What’s left for little ole me?

import leancall as lc
import leancall.numpy

mod = lc.from_string("def foo x := x + 1")
type(mod.foo(3))

Does leancall change how I would go about pcode?

%%file /tmp/app.lean
import Lean.Data.Json
structure App where
    f : String
    args : List App
deriving Repr, Inhabited, BEq, Lean.ToJson, Lean.FromJson

def idapp (a : App) : Lean.Json := Lean.toJson a

partial def is_subterm (a b : App) : Bool :=
  if a == b then
    true
  else
    match a with
    | App.mk _ args => args.any (fun arg => is_subterm arg b)


Writing /tmp/app.lean
import leancall as lc
import leancall.numpy
from dataclasses import dataclass
@dataclass(frozen=True)
class App:
    f: str
    args: tuple["App", ...] = ()
    def of_dict(d):
        return App(d['f'], [App.of_dict(a) for a in d['args']])


@lc.register_from_lean("App")
def _(x) -> App:
    return x
mod = lc.from_file("/tmp/app.lean")
App.of_dict(mod.idapp(App("foo", [])))
x = App("x")
y = App("y")
f = lambda x: App("f", (x,))

#mod.is_subterm(f(x), x)  # true


calllean look at other similar solutions? prologs? ocaml? clr https://pythonnet.github.io/pythonnet/python.html

https://github.com/codyroux/simple-prolog/blob/master/SimpleProlog/Basic.lean



Overwriting /tmp/cartpole.lean
import numpy as np
mod = lc.from_file("/tmp/cartpole.lean")
mod.action([0.01234567, -0.00987654,  0.02345678,  0.01456789])
1
import gymnasium as gym

env = gym.make("CartPole-v1", render_mode="human")
observation, info = env.reset()

print(f"Starting observation: {observation}")
# Example output: [ 0.01234567 -0.00987654  0.02345678  0.01456789]
# [cart_position, cart_velocity, pole_angle, pole_angular_velocity]

print(type(observation))
episode_over = False
total_reward = 0
try:
    while not episode_over:
        # Choose an action: 0 = push cart left, 1 = push cart right
        action = mod.action(list(observation))
        observation, reward, terminated, truncated, info = env.step(action)
        # reward: +1 for each step the pole stays upright
        # terminated: True if pole falls too far (agent failed)
        # truncated: True if we hit the time limit (500 steps)

        total_reward += reward
        episode_over = terminated or truncated
finally:
    env.close()

print(f"Episode finished! Total reward: {total_reward}")

Starting observation: [-0.04921084 -0.01602873  0.02273309 -0.01083545]
<class 'numpy.ndarray'>
Episode finished! Total reward: 500.0

Lean interact

def foo1(i):
    s = 0
    for _ in range(i):
        s += i
    return s

%%timeit
foo1(100000)
2.18 ms ± 5.57 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%file /tmp/foo.lean
def foo (i : Nat) : Nat := Id.run do
    let mut s := 0
    for _ in [0:i] do
        s := s + i
    return s
Overwriting /tmp/foo.lean
mod = cl.from_file("/tmp/foo.lean")

Yea, it’s constant time

mod.foo(100000000)
10000000000000000
%%timeit
mod.foo(100000000)
191 μs ± 65.4 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)
from lean_interact import LeanREPLConfig, LeanServer, Command

config = LeanREPLConfig(verbose=True) # download and build Lean REPL
server = LeanServer(config) # start Lean REPL
server.run(Command(cmd="theorem ex (n : Nat) : n = 5 → n = 5 := id"))

server.run(Command(cmd="def mysucc x := x + 1", env=0))
CommandResponse(env=1)
server.run(Command(cmd="#eval mysucc 41", env=1))
CommandResponse(env=2, messages=[Message(start_pos=Pos(line=1, column=0), severity='info', data='42', end_pos=Pos(line=1, column=5))])
"""
def to_lean(x : object) -> str:
    if isinstance(x, int):
        return str(x)
    elif isinstance(x, bool):
        return "true" if x else "false"
    elif isinstance(x, str):
        return f'"{x}"'
    elif isinstance(x, list):
        return "[" + ", ".join(map(to_lean, x)) + "]"
    elif isinstance(x, tuple):
        return "(" + ", ".join(map(to_lean, x)) + ")"
    elif isinstance(x, float):
        return str(x)
    # dict -> json or hashmap?
    else:
        raise Exception(f"Cannot convert {x} to Lean")
"""

Could have lean reach into python process and directly access memory for numpy / buffers That’d be nasssssty.


from lean_interact import LeanREPLConfig, LeanServer, Command
from functools import singledispatch

config = LeanREPLConfig(verbose=True) # download and build Lean REPL
server = LeanServer(config) # start Lean REPL

@singledispatch
def to_lean(x : object) -> str:
    raise Exception(f"Cannot convert {x} to Lean")
@to_lean.register
def _(x : int) -> str:
    return str(x)
@to_lean.register
def _(x : bool) -> str:
    return "true" if x else "false"
@to_lean.register
def _(x : str) -> str:
    return f'"{x}"'
@to_lean.register
def _(x : float) -> str:
    return str(x)
@to_lean.register(list)
def _(x : list) -> str:
    return "[" + ", ".join(map(to_lean, x)) + "]"
@to_lean.register(tuple)
def _(x : tuple) -> str:
    return "(" + ", ".join(map(to_lean, x)) + ")"

def from_lean(x : str, typ : str) -> object:
    if typ == "Nat" or typ == "Int":
        return int(x)
    elif typ == "Bool":
        assert x in ["true", "false"]
        return x == "true"
    elif typ == "String":
        return x.strip('"')
    elif typ == "Float":
        return float(x)
    elif typ == "Unit":
        assert x == "()"
        return ()
    elif typ.startswith('Option '):
        if x == "none":
            return None
        elif x.startswith("some "):
            inner_typ = typ[len('Option '):]
            return from_lean(x[len("some "):], inner_typ)
    else:
        # hail mary
        # may work for lists and tuples of simple elements
        return eval(x)
    
class LeanFun():
    def __init__(self, code, env=None):
        codesplit = code.split()
        assert codesplit[0] == "def"
        self.name = codesplit[1]
        self.code = code
        res = server.run(Command(cmd=code, env=env))
        if hasattr(res, "messages") and len(res.messages) > 0:
            for message in res.messages:
                if message.severity == "error":
                    raise Exception(message.data)
        self.env = res.env
        # get return type for deserialization
        res = server.run(Command(cmd=f"#check {self.name}", env=self.env))
        self.res_type = res.messages[0].data.split(":")[-1].strip()

    @classmethod
    def from_string(cls, code : str, env=None):
        ...

    def from_file(cls, filename : str, names : list[str], env=None):
        # load from file
        # return list of LeanFun objects
        ...

    def __call__(self, *args, **kwargs):
        res = server.run(Command(cmd=f"#eval {self.name} " + " ".join(map(to_lean, args)), env=self.env))
        assert len(res.messages) == 1
        message = res.messages[0]
        if message.severity == "info":
            return from_lean(message.data, self.res_type)
        else:
            raise Exception(message.data)
        
f = LeanFun("def mysucc (x y : Int) (s : String) (p : Nat × Nat) (r : List Int) (f : Float) := (f + 1.3, x + y + s.length + p.fst + r[0]!) |> some")
print(f.res_type)
f(1,2, "foo", (4,5), [1,2,3], 7.4)

Lake version 5.0.0-src+d8204c9 (Lean version 4.26.0)
Build completed successfully (26 jobs).
Option (Float × Int)





(8.7, 11)
%%prun -s cumulative
for i in range(1000):
    f(1,2, "foo", (4,5))
         216676 function calls (202767 primitive calls) in 0.260 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1000    0.004    0.000    0.263    0.000 2346713141.py:90(__call__)
        1    0.001    0.001    0.259    0.259 <string>:1(<module>)
     1000    0.004    0.000    0.247    0.000 server.py:327(run)
     1000    0.003    0.000    0.211    0.000 server.py:242(run_dict)
     1000    0.003    0.000    0.189    0.000 server.py:173(_execute_cmd_in_repl)
     1000    0.002    0.000    0.173    0.000 threading.py:1115(join)
     1000    0.002    0.000    0.150    0.000 threading.py:1153(_wait_for_tstate_lock)
     1000    0.012    0.000    0.129    0.000 threading.py:1016(_bootstrap)
     1000    0.001    0.000    0.117    0.000 threading.py:1056(_bootstrap_inner)
     1000    0.003    0.000    0.114    0.000 ipkernel.py:744(run_closure)
     1000    0.001    0.000    0.086    0.000 threading.py:999(run)
     1000    0.003    0.000    0.084    0.000 server.py:198(reader)
     7000    0.078    0.000    0.079    0.000 {method 'readline' of '_io.TextIOWrapper' objects}
     1000    0.021    0.000    0.021    0.000 {built-in method _thread.start_new_thread}
     1000    0.002    0.000    0.014    0.000 server.py:298(_augment_request)
     1000    0.002    0.000    0.012    0.000 threading.py:973(start)
     2000    0.003    0.000    0.012    0.000 main.py:384(model_copy)
3000/2000    0.010    0.000    0.011    0.000 {method 'validate_python' of 'pydantic_core._pydantic_core.SchemaValidator' objects}
     1000    0.001    0.000    0.010    0.000 main.py:677(model_validate)
3000/2000    0.002    0.000    0.009    0.000 {method 'join' of 'str' objects}
     2000    0.001    0.000    0.009    0.000 main.py:240(__init__)
     2000    0.004    0.000    0.008    0.000 main.py:955(__copy__)
     1000    0.001    0.000    0.008    0.000 ipkernel.py:768(init_closure)
     1000    0.001    0.000    0.008    0.000 server.py:220(_parse_repl_output)
     1000    0.001    0.000    0.008    0.000 interface.py:564(__init__)
6000/4000    0.003    0.000    0.008    0.000 functools.py:904(wrapper)
     1000    0.001    0.000    0.007    0.000 __init__.py:299(loads)
     1000    0.001    0.000    0.007    0.000 __init__.py:183(dumps)
     1000    0.002    0.000    0.007    0.000 threading.py:882(__init__)
     1000    0.001    0.000    0.006    0.000 threading.py:616(set)
     1000    0.001    0.000    0.006    0.000 decoder.py:332(decode)
     1000    0.001    0.000    0.005    0.000 encoder.py:183(encode)
     1000    0.002    0.000    0.005    0.000 threading.py:1043(_set_tstate_lock)
     1001    0.001    0.000    0.004    0.000 threading.py:424(notify_all)
     1000    0.000    0.000    0.004    0.000 main.py:418(model_dump)
     6000    0.002    0.000    0.004    0.000 copy.py:61(copy)
     1000    0.004    0.000    0.004    0.000 encoder.py:205(iterencode)
     1000    0.001    0.000    0.004    0.000 threading.py:1079(_stop)
     1000    0.003    0.000    0.003    0.000 {method 'to_python' of 'pydantic_core._pydantic_core.SchemaSerializer' objects}
     1000    0.000    0.000    0.003    0.000 server.py:111(is_alive)
     1001    0.001    0.000    0.003    0.000 threading.py:394(notify)
     2000    0.002    0.000    0.003    0.000 threading.py:855(_maintain_shutdown_locks)
     1000    0.001    0.000    0.003    0.000 2346713141.py:22(_)
     1000    0.003    0.000    0.003    0.000 {method 'write' of '_io.TextIOWrapper' objects}
     1000    0.000    0.000    0.003    0.000 subprocess.py:1233(poll)
     1000    0.003    0.000    0.003    0.000 decoder.py:343(raw_decode)
     1000    0.001    0.000    0.003    0.000 subprocess.py:1973(_internal_poll)
     6000    0.001    0.000    0.002    0.000 functools.py:818(dispatch)
     1000    0.001    0.000    0.002    0.000 threading.py:588(__init__)
     4001    0.002    0.000    0.002    0.000 {method 'release' of '_thread.lock' objects}
     1000    0.001    0.000    0.002    0.000 <frozen codecs>:319(decode)
        1    0.000    0.000    0.001    0.001 {method 'execute' of 'sqlite3.Connection' objects}
  8003/78    0.022    0.000    0.001    0.000 {method 'acquire' of '_thread.lock' objects}
     6000    0.001    0.000    0.001    0.000 weakref.py:414(__getitem__)
     1000    0.001    0.000    0.001    0.000 {built-in method posix.waitpid}
     2000    0.001    0.000    0.001    0.000 {method 'match' of 're.Pattern' objects}
     2001    0.001    0.000    0.001    0.000 threading.py:314(_is_owned)
     7000    0.001    0.000    0.001    0.000 {method 'endswith' of 'str' objects}
     1000    0.001    0.000    0.001    0.000 threading.py:1106(_delete)
     1000    0.001    0.000    0.001    0.000 threading.py:277(__init__)
     2003    0.001    0.000    0.001    0.000 threading.py:299(__enter__)
     2003    0.001    0.000    0.001    0.000 threading.py:302(__exit__)
7143/7139    0.001    0.000    0.001    0.000 {built-in method builtins.isinstance}
     8004    0.001    0.000    0.001    0.000 {method 'get' of 'dict' objects}
        3    0.000    0.000    0.001    0.000 base_events.py:1910(_run_once)
     4000    0.001    0.000    0.001    0.000 2346713141.py:10(_)
     2000    0.001    0.000    0.001    0.000 {method 'difference_update' of 'set' objects}
     2000    0.001    0.000    0.001    0.000 threading.py:1483(current_thread)
     5007    0.001    0.000    0.001    0.000 {method '__exit__' of '_thread.lock' objects}
     1000    0.001    0.000    0.001    0.000 _weakrefset.py:85(add)
     1000    0.001    0.000    0.001    0.000 threading.py:837(_newname)
     1000    0.000    0.000    0.001    0.000 _weakrefset.py:39(_remove)
     3000    0.001    0.000    0.001    0.000 threading.py:1234(daemon)
     1000    0.000    0.000    0.001    0.000 threading.py:1036(_set_ident)
     5000    0.001    0.000    0.001    0.000 {built-in method _thread.get_ident}
     1000    0.000    0.000    0.001    0.000 threading.py:1040(_set_native_id)
      5/3    0.000    0.000    0.001    0.000 events.py:86(_run)
     5010    0.001    0.000    0.001    0.000 {built-in method builtins.hasattr}
     4000    0.001    0.000    0.001    0.000 {method 'copy' of 'dict' objects}
     1000    0.001    0.000    0.001    0.000 {built-in method _codecs.utf_8_decode}
     1000    0.001    0.000    0.001    0.000 2346713141.py:45(from_lean)
     3001    0.000    0.000    0.000    0.000 {method '__exit__' of '_thread.RLock' objects}
     2000    0.000    0.000    0.000    0.000 {method 'update' of 'set' objects}
     1000    0.000    0.000    0.000    0.000 {built-in method _thread._set_sentinel}
     1001    0.000    0.000    0.000    0.000 threading.py:311(_acquire_restore)
     4000    0.000    0.000    0.000    0.000 {method 'locked' of '_thread.lock' objects}
     2002    0.000    0.000    0.000    0.000 {method '__enter__' of '_thread.lock' objects}
      4/2    0.000    0.000    0.000    0.000 {method 'run' of '_contextvars.Context' objects}
     1000    0.000    0.000    0.000    0.000 {built-in method _thread.get_native_id}
     3020    0.000    0.000    0.000    0.000 {built-in method builtins.len}
     2000    0.000    0.000    0.000    0.000 {method 'add' of 'set' objects}
     3000    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 zmqstream.py:573(_handle_events)
     2001    0.000    0.000    0.000    0.000 {built-in method _thread.allocate_lock}
     1001    0.000    0.000    0.000    0.000 threading.py:308(_release_save)
     2000    0.000    0.000    0.000    0.000 {built-in method __new__ of type object at 0xa43b40}
     2000    0.000    0.000    0.000    0.000 {method 'copy' of 'set' objects}
        1    0.000    0.000    0.000    0.000 asyncio.py:200(_handle_events)
        1    0.000    0.000    0.000    0.000 kernelbase.py:302(poll_control_queue)
        1    0.000    0.000    0.000    0.000 _base.py:537(set_result)
        1    0.000    0.000    0.000    0.000 _base.py:337(_invoke_callbacks)
     1000    0.000    0.000    0.000    0.000 threading.py:1354(_make_invoke_excepthook)
  1000/13    0.001    0.000    0.000    0.000 threading.py:637(wait)
     1000    0.000    0.000    0.000    0.000 {method 'startswith' of 'str' objects}
        2    0.000    0.000    0.000    0.000 zmqstream.py:614(_handle_recv)
     1000    0.000    0.000    0.000    0.000 {method 'strip' of 'str' objects}
  1000/13    0.001    0.000    0.000    0.000 threading.py:323(wait)
     2000    0.000    0.000    0.000    0.000 {method 'end' of 're.Match' objects}
     2000    0.000    0.000    0.000    0.000 threading.py:1196(ident)
        2    0.000    0.000    0.000    0.000 zmqstream.py:546(_run_callback)
     2000    0.000    0.000    0.000    0.000 {method 'items' of 'dict' objects}
     1000    0.000    0.000    0.000    0.000 {method 'flush' of '_io.TextIOWrapper' objects}
        1    0.000    0.000    0.000    0.000 futures.py:394(_call_set_state)
     1000    0.000    0.000    0.000    0.000 {method 'remove' of 'collections.deque' objects}
     2000    0.000    0.000    0.000    0.000 threading.py:601(is_set)
        2    0.000    0.000    0.000    0.000 iostream.py:157(_handle_event)
        2    0.000    0.000    0.000    0.000 iostream.py:276(<lambda>)
     1000    0.000    0.000    0.000    0.000 {method 'discard' of 'set' objects}
     1000    0.000    0.000    0.000    0.000 encoder.py:105(__init__)
     1000    0.000    0.000    0.000    0.000 2346713141.py:16(_)
        1    0.000    0.000    0.000    0.000 decorator.py:232(fun)
        1    0.000    0.000    0.000    0.000 base_events.py:838(call_soon_threadsafe)
        1    0.000    0.000    0.000    0.000 history.py:93(only_when_enabled)
        1    0.000    0.000    0.000    0.000 selector_events.py:141(_write_to_self)
        1    0.000    0.000    0.000    0.000 {method 'send' of '_socket.socket' objects}
     1007    0.000    0.000    0.000    0.000 {method 'append' of 'collections.deque' objects}
     1005    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
     1000    0.000    0.000    0.000    0.000 threading.py:1220(is_alive)
        1    0.000    0.000    0.000    0.000 history.py:1025(writeout_cache)
        3    0.000    0.000    0.000    0.000 zmqstream.py:653(_rebuild_io_state)
        2    0.000    0.000    0.000    0.000 iostream.py:278(_really_send)
        2    0.000    0.000    0.000    0.000 socket.py:700(send_multipart)
       14    0.000    0.000    0.000    0.000 socket.py:623(send)
        3    0.000    0.000    0.000    0.000 zmqstream.py:676(_update_handler)
        2    0.000    0.000    0.000    0.000 traitlets.py:708(__set__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        2    0.000    0.000    0.000    0.000 traitlets.py:3631(set)
        5    0.000    0.000    0.000    0.000 attrsettr.py:43(__getattr__)
        1    0.000    0.000    0.000    0.000 ioloop.py:742(_run_callback)
       15    0.000    0.000    0.000    0.000 enum.py:1551(__or__)
        1    0.000    0.000    0.000    0.000 zmqstream.py:684(<lambda>)
        2    0.000    0.000    0.000    0.000 traitlets.py:689(set)
       66    0.000    0.000    0.000    0.000 enum.py:1544(_get_value)
        1    0.000    0.000    0.000    0.000 decorator.py:200(fix)
        7    0.000    0.000    0.000    0.000 enum.py:1562(__and__)
        5    0.000    0.000    0.000    0.000 attrsettr.py:66(_get_attr_opt)
        2    0.000    0.000    0.000    0.000 traitlets.py:718(_validate)
        1    0.000    0.000    0.000    0.000 queues.py:186(put)
        2    0.000    0.000    0.000    0.000 socket.py:771(recv_multipart)
        1    0.000    0.000    0.000    0.000 queues.py:209(put_nowait)
        1    0.000    0.000    0.000    0.000 history.py:1009(_writeout_input_cache)
       29    0.000    0.000    0.000    0.000 enum.py:726(__call__)
        2    0.000    0.000    0.000    0.000 {method '__exit__' of 'sqlite3.Connection' objects}
        1    0.000    0.000    0.000    0.000 inspect.py:3237(bind)
        1    0.000    0.000    0.000    0.000 selector_events.py:129(_read_from_self)
        2    0.000    0.000    0.000    0.000 typing.py:1194(__instancecheck__)
        2    0.000    0.000    0.000    0.000 base_events.py:785(call_soon)
        1    0.000    0.000    0.000    0.000 inspect.py:3102(_bind)
        3    0.000    0.000    0.000    0.000 base_events.py:814(_call_soon)
        1    0.000    0.000    0.000    0.000 asyncio.py:225(add_callback)
        2    0.000    0.000    0.000    0.000 {method 'recv' of '_socket.socket' objects}
        1    0.000    0.000    0.000    0.000 traitlets.py:1512(_notify_trait)
        2    0.000    0.000    0.000    0.000 typing.py:1465(__subclasscheck__)
        2    0.000    0.000    0.000    0.000 traitlets.py:3474(validate)
        1    0.000    0.000    0.000    0.000 concurrent.py:182(future_set_result_unless_cancelled)
        1    0.000    0.000    0.000    0.000 traitlets.py:1523(notify_change)
        6    0.000    0.000    0.000    0.000 typing.py:392(inner)
        3    0.000    0.000    0.000    0.000 zmqstream.py:532(sending)
        3    0.000    0.000    0.000    0.000 events.py:36(__init__)
        2    0.000    0.000    0.000    0.000 traitlets.py:727(_cross_validate)
        1    0.000    0.000    0.000    0.000 queues.py:225(get)
        2    0.000    0.000    0.000    0.000 {built-in method builtins.issubclass}
       29    0.000    0.000    0.000    0.000 enum.py:1129(__new__)
        3    0.000    0.000    0.000    0.000 selector_events.py:750(_process_events)
        3    0.000    0.000    0.000    0.000 queue.py:97(empty)
        1    0.000    0.000    0.000    0.000 traitlets.py:1527(_notify_observers)
        4    0.000    0.000    0.000    0.000 traitlets.py:676(__get__)
        2    0.000    0.000    0.000    0.000 {method 'set_result' of '_asyncio.Future' objects}
      2/1    0.000    0.000    0.000    0.000 selectors.py:451(select)
        5    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:1390(_handle_fromlist)
        1    0.000    0.000    0.000    0.000 base_events.py:1895(_add_callback)
        1    0.000    0.000    0.000    0.000 contextlib.py:299(helper)
        8    0.000    0.000    0.000    0.000 {built-in method builtins.next}
        4    0.000    0.000    0.000    0.000 base_events.py:734(time)
        1    0.000    0.000    0.000    0.000 history.py:1017(_writeout_output_cache)
        2    0.000    0.000    0.000    0.000 <frozen abc>:121(__subclasscheck__)
        2    0.000    0.000    0.000    0.000 traitlets.py:3624(validate_elements)
        4    0.000    0.000    0.000    0.000 typing.py:1258(__hash__)
        1    0.000    0.000    0.000    0.000 inspect.py:2918(apply_defaults)
        2    0.000    0.000    0.000    0.000 iostream.py:216(_check_mp_mode)
        1    0.000    0.000    0.000    0.000 queues.py:317(__put_internal)
        1    0.000    0.000    0.000    0.000 inspect.py:2865(args)
        1    0.000    0.000    0.000    0.000 contextlib.py:141(__exit__)
        4    0.000    0.000    0.000    0.000 traitlets.py:629(get)
        1    0.000    0.000    0.000    0.000 threading.py:627(clear)
        1    0.000    0.000    0.000    0.000 contextlib.py:132(__enter__)
        1    0.000    0.000    0.000    0.000 contextlib.py:104(__init__)
        1    0.000    0.000    0.000    0.000 queues.py:256(get_nowait)
        2    0.000    0.000    0.000    0.000 queues.py:322(_consume_expired)
        6    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
      2/1    0.000    0.000    0.000    0.000 {method 'poll' of 'select.epoll' objects}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.max}
        2    0.000    0.000    0.000    0.000 iostream.py:213(_is_master_process)
        2    0.000    0.000    0.000    0.000 {built-in method _abc._abc_subclasscheck}
        2    0.000    0.000    0.000    0.000 traitlets.py:2304(validate)
        1    0.000    0.000    0.000    0.000 inspect.py:2888(kwargs)
        1    0.000    0.000    0.000    0.000 queues.py:312(_put)
        3    0.000    0.000    0.000    0.000 queue.py:209(_qsize)
        8    0.000    0.000    0.000    0.000 {method 'popleft' of 'collections.deque' objects}
        5    0.000    0.000    0.000    0.000 {method 'upper' of 'str' objects}
        2    0.000    0.000    0.000    0.000 {built-in method posix.getpid}
       20    0.000    0.000    0.000    0.000 typing.py:2154(cast)
       10    0.000    0.000    0.000    0.000 inspect.py:2777(kind)
        1    0.000    0.000    0.000    0.000 queues.py:173(qsize)
        2    0.000    0.000    0.000    0.000 {built-in method _contextvars.copy_context}
        4    0.000    0.000    0.000    0.000 {built-in method time.monotonic}
        3    0.000    0.000    0.000    0.000 {method 'items' of 'mappingproxy' objects}
        1    0.000    0.000    0.000    0.000 {built-in method _asyncio.get_running_loop}
        2    0.000    0.000    0.000    0.000 history.py:1066(hold)
        2    0.000    0.000    0.000    0.000 traitlets.py:3486(validate_elements)
        1    0.000    0.000    0.000    0.000 {built-in method math.ceil}
        5    0.000    0.000    0.000    0.000 zmqstream.py:528(receiving)
        4    0.000    0.000    0.000    0.000 {built-in method builtins.hash}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.iter}
        1    0.000    0.000    0.000    0.000 {method 'values' of 'mappingproxy' objects}
        5    0.000    0.000    0.000    0.000 base_events.py:2005(get_debug)
        1    0.000    0.000    0.000    0.000 {method '__enter__' of '_thread.RLock' objects}
        1    0.000    0.000    0.000    0.000 selectors.py:275(_key_from_fd)
        3    0.000    0.000    0.000    0.000 base_events.py:539(_check_closed)
        1    0.000    0.000    0.000    0.000 queues.py:309(_get)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.min}
        1    0.000    0.000    0.000    0.000 unix_events.py:81(_process_self_data)
        1    0.000    0.000    0.000    0.000 queues.py:177(empty)
        2    0.000    0.000    0.000    0.000 iostream.py:255(closed)
        4    0.000    0.000    0.000    0.000 inspect.py:2765(name)
        4    0.000    0.000    0.000    0.000 inspect.py:3058(parameters)
        1    0.000    0.000    0.000    0.000 locks.py:224(clear)
        2    0.000    0.000    0.000    0.000 {method 'cancelled' of '_asyncio.Future' objects}
        1    0.000    0.000    0.000    0.000 {method 'done' of '_asyncio.Future' objects}
        1    0.000    0.000    0.000    0.000 base_events.py:720(is_closed)
        1    0.000    0.000    0.000    0.000 inspect.py:2857(__init__)
        1    0.000    0.000    0.000    0.000 queues.py:59(_set_timeout)
        2    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method '_is_owned' of '_thread.RLock' objects}
def mysucc(x, y):
    return x + y
%%timeit
mysucc(41, 43)
24 ns ± 0.404 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

https://github.com/leanprover-community/repl

Hmm. Ingest response via microlean? Use simp

l.lean_simp

from kdrag.all import *
kd.lean("1 + 1.0")
import kdrag.printers.lean as plean

plean.of_expr
<function kdrag.printers.lean.of_expr(e: z3.z3.ExprRef)>