<?xml version="1.0" encoding="utf-8"?>
<feed version="0.3" xmlns="http://purl.org/atom/ns#">
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/"/>

<title>Jeff Epler's blog</title>
<modified>2026-03-09T20:55:10Z</modified>
<tagline>Photos, electronics, cnc, and more</tagline>
<author><name>Jeff Epler</name><email>jepler@unpythonic.net</email></author>
<entry>
<title>radix40 encoding</title>
<issued>2026-03-09T20:55:10Z</issued>
<modified>2026-03-09T20:55:10Z</modified>
<id>https://gamma.unpythonic.net/01773089710</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01773089710"/>
<content type="text/html" mode="escaped">



&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css&quot;&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/python.min.js&quot;&gt;&lt;/script&gt;
&lt;style&gt;
.junkpre { border: 1px solid black; max-width: 132ex; max-height: 45lh; overflow: auto; }
&lt;/style&gt;
&lt;script&gt;
const do_embed = function(container, url) {
    const request = new Request(url);
    fetch(request)
      .then((response) =&gt; response.json())
      .then((data) =&gt; {
        container.textContent = atob(data['content']);
        hljs.highlightElement(container)    
    })
    .catch((error) =&gt; {
       console.error(error);
    });
}
const do_embed_here = function(url, classes) {
    const id = &quot;embed&quot; + (((1 + Math.random()) * 0x1000000) | 0).toString(16).substring(1);
    document.write(&quot;&lt;pre class=junkpre&gt;&lt;code id=&quot; + id + &quot; class=junkembed&gt;&lt;/code&gt;&lt;/pre&gt;&quot;)
    const el = document.getElementById(id)
    if (classes) {
        el.className = el.className + &quot; &quot; + classes
    }
    do_embed(el, url)
}
&lt;/script&gt;



&lt;p&gt;&lt;br&gt;I was inspired to design an original(?) text encoding for tiny embedded
computers. It is, however, similar to &lt;a href=&quot;https://en.wikipedia.org/wiki/DEC_RADIX_50&quot;&gt;DEC RADIX 50&lt;/a&gt; from 1965. (That's 50₈=40₁₀).
Since 40³&amp;lt;65536, it is possible to store 3 symbols in each 16 bit word.

&lt;p&gt;In radix 40 you get the 26 basic alphabetic characters, 10 digits, and 4
additional symbols. I chose:
&lt;ul&gt;
&lt;li&gt;End of string
&lt;li&gt;Space (ASCII 32)
&lt;li&gt;Exclamation point (ASCII 33)
&lt;li&gt;Double quote (ASCII 34)
&lt;/ul&gt;

&lt;p&gt;The choice of 3 characters that are adjacent in ASCII saved code size on the
decoder; initially I thought maybe &amp;quot;-&amp;quot; and &amp;quot;.&amp;quot; would be useful choices.

&lt;p&gt;Unlike RADIX 50, the encoding is arranged so that no division or
remainder operation is needed. Instead, at each step of decoding, a
24 bit temporary value is multiplied by 40 and the top byte gives the
output code. In the assembler vesion, the multiplication is coded as 
&lt;tt&gt;x&lt;-x*8; tmp&lt;-x; x&lt;-x*4; x&lt;=x+tmp&lt;/tt&gt;) since the MC6800 has no multiply
instruction.

&lt;p&gt;Here are the not quite matching Python encoder

&lt;script&gt;
do_embed_here(&quot;https://codeberg.org/api/v1/repos/jepler/junkdrawer/contents/epvenhla/b40.py?ref=main&quot;, &quot;language-python&quot;);
&lt;/script&gt;


&lt;p&gt;And decoder/test program in m6800 assembly:

&lt;script&gt;
do_embed_here(&quot;https://codeberg.org/api/v1/repos/jepler/junkdrawer/contents/epvenhla/b40.asm?ref=main&quot;);
&lt;/script&gt;


&lt;p&gt;The implementation costs 90 bytes of code and 6 bytes of zero-page (which can
be used for other purposes when the routine is not running). I estimate you'd
need somewhat above 320 characters of text in your program for it to be a benefit.

&lt;p&gt;The m6800 decoder can over-read the data by 1 byte, which seldom poses a problem in such environments.
</content>
</entry>
<entry>
<title>"interlz5" in Python -- Apple II text adventure adventures</title>
<issued>2025-12-15T02:34:43Z</issued>
<modified>2025-12-15T02:34:43Z</modified>
<id>https://gamma.unpythonic.net/01765766083</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01765766083"/>
<content type="text/html" mode="escaped">
On the socials, someone asked whether anyone knew where the source for a
tool called &amp;quot;interlz5&amp;quot; lived; they had found a binary but not the original
(presumably C or C++) source. (Update: S. V. Nickolas's source code was right
in front of us this whole time, a nested zip inside &lt;a href=&quot;https://www.kansasfest.org/wp-content/uploads/apple_ii_inform_demo_files.zip&quot;&gt;apple_ii_inform_demo_files.zip&lt;/a&gt;
and now attached here as well)

&lt;p&gt;This tool is described in &lt;a href=&quot;https://16kram.com/wp-content/uploads/2017/07/apple_ii_inform2.pdf&quot;&gt;An Apple II Build Chain for Inform&lt;/a&gt;
by Michael Sternberg.

&lt;p&gt;Depending on the game, it is stored as 1 or 2 sides of a disk. The first
side consists of the interpreter (16KiB) followed by up to 98.5KiB of the
story. The second side consists of the remaining story, up to 157KiB.

&lt;p&gt;Since I could run the original &amp;quot;interlz5&amp;quot; tool, I was able to confirm what
it did: It copies the interpreter binary as-is, then re-arranges the
first part of the story according to an &amp;quot;interleave&amp;quot; rule. If the game
is a small one, that's all and you're done! (well, you need to save it as a
&amp;quot;.do&amp;quot; file or your emulator may perform a second interleaving step on the data!)

&lt;p&gt;Now, are you ready for the surprise? As Sternberg wrote, &amp;quot;If the story file is
larger than 100,864 bytes, the remainder of the Z-code is stored on a second
18-sector disk image.&amp;quot; interlz5 writes this as a &amp;quot;nib&amp;quot; format file with no
header.

&lt;p&gt;Why 100,864 bytes aka 98.5KiB? This appears to be how much can be loaded
into the RAM of a 128KiB Apple IIe while leaving room for the interpreter &amp;amp;
other required structures. Why is the special format only used on &amp;quot;side b&amp;quot;?
Since there is already always spare space on &amp;quot;side a&amp;quot;, no special encoding was
needed. However, one does wonder whether the initial load time was better with
the interleaved 16 sector tracks compared to if they had used the &amp;quot;18-sector&amp;quot;
format.

&lt;p&gt;Oh, but what exactly is the format? Sternberg's document doesn't contain any
detail, and at the time I didn't have S. V. Nickolas's interlz5 source
to refer to.

&lt;p&gt;I'm aware that 18-sector tracks were used by some other games (The term
RWTS18 comes up) but there seem to be multiple different forms. In the
case of these Z5 disks, each track is actually one big sector containing 4608
bytes of useful data encoded like so:

&lt;p&gt;&lt;ul&gt;
&lt;li&gt; The special sequence &amp;quot;d5 aa ad&amp;quot;
&lt;li&gt; The 0-based track number encoded as two bytes of gcr4
&lt;li&gt; 18 groups of 343 &amp;quot;gcr6&amp;quot; nibbles, organized just like a 256-byte sector
&lt;li&gt; Padding with &amp;quot;ff&amp;quot; flux patterns to the end of the track
&lt;/ul&gt;

&lt;p&gt;The main reason that more data can be stored is because the extra space between
sectors is removed. This lets 18×256 bytes be stored instead of just 16×256
bytes. This is inconvenient if the disk were to be written, because you'd have
to rewrite the whole track. But in normal use, the game disk is read-only.

&lt;p&gt;The groups of 343 &amp;quot;gcr6&amp;quot; nibbles all decode to 256 bytes exactly
as the standard disk encoding, with the first 86 bytes encoding the two
least significant bits of each byte and the remaining 256 bytes encoding
the high 6 bits. Just like normal sector encoding, there's a byte
sometimes called the &amp;quot;checksum&amp;quot; that is initialized to 0 and xor'd with
the outgoing value before the gcr table lookup. The last gcr6 nibble is
the final checksum value. This checksum is reset to 0 at each 256-byte boundary.

&lt;p&gt;My program produces nearly the same output as interlz5, except for differences
that I think stem from use of undefined data in the original compiled version.
This means my files are not bit-for-bit identical. There are two specific
causes:
&lt;ul&gt;
&lt;li&gt;If the input z5 file does not exactly fill a track, interlz5
appears to fill with uninitialized value; I fill with zeros
&lt;li&gt;Two bits in each &amp;quot;twobit&amp;quot; area are unused. interlz5 appears to use
a value from the first byte of the next sector, or an uninitialized byte at the end of data.
I use the next byte if one exists, otherwise zero.
&lt;/ul&gt;

&lt;p&gt;Due to the xor/checksum feature, any difference in data being encoded actually
affects a subsequent gcr byte as well.

&lt;p&gt;Compatibility? I had success with a specific pair of files:
&lt;pre&gt;
9bec6046eca15a720a40e56522fef7624124b54e871b0a31ff9d5f754155ef00  interp.bin
6179b5d5b17d692ec83fe64101ff8e4092390166c2b05063e7310eb976b93ea0  Advent.z5
&lt;/pre&gt;
With files output by either tool, I could successfully boot the game in AppleWin and go NORTH into the forest.

&lt;p&gt;Sadly, I did not have luck with Hitchhiker's Guide or Beyond Zork &amp;quot;.z5&amp;quot; files I
obtained from the internet, with either tool.

&lt;p&gt;interl5.py is licensed GPL-3.0 and is tested with Python 3.13. It requires no
packages outside the Python standard library. I have no plans to further develop it.

&lt;p&gt;&lt;p&gt;&lt;b&gt;Files currently attached to this page:&lt;/b&gt;
&lt;table cellpadding=5 style=&quot;width:auto!important; clear:none!important&quot;&gt;&lt;col&gt;&lt;col style=&quot;text-align: right&quot;&gt;&lt;tr bgcolor=#eeeeee&gt;&lt;td&gt;&lt;a href=&quot;https://media.unpythonic.net/emergent-files/01765766083/Advent.z5&quot;&gt;Advent.z5&lt;/a&gt;&lt;/td&gt;&lt;td&gt;135.0kB&lt;/td&gt;&lt;/tr&gt;&lt;tr bgcolor=#dddddd&gt;&lt;td&gt;&lt;a href=&quot;https://media.unpythonic.net/emergent-files/01765766083/apple_ii_inform2.pdf&quot;&gt;apple_ii_inform2.pdf&lt;/a&gt;&lt;/td&gt;&lt;td&gt;763.3kB&lt;/td&gt;&lt;/tr&gt;&lt;tr bgcolor=#eeeeee&gt;&lt;td&gt;&lt;a href=&quot;https://media.unpythonic.net/emergent-files/01765766083/interl5.py&quot;&gt;interl5.py&lt;/a&gt;&lt;/td&gt;&lt;td&gt;3.5kB&lt;/td&gt;&lt;/tr&gt;&lt;tr bgcolor=#dddddd&gt;&lt;td&gt;&lt;a href=&quot;https://media.unpythonic.net/emergent-files/01765766083/interlz5-001.zip&quot;&gt;interlz5-001.zip&lt;/a&gt;&lt;/td&gt;&lt;td&gt;33.9kB&lt;/td&gt;&lt;/tr&gt;&lt;tr bgcolor=#eeeeee&gt;&lt;td&gt;&lt;a href=&quot;https://media.unpythonic.net/emergent-files/01765766083/interp.bin&quot;&gt;interp.bin&lt;/a&gt;&lt;/td&gt;&lt;td&gt;16.0kB&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;p&gt;
</content>
</entry>
<entry>
<title>Junk drawer & embedding</title>
<issued>2025-11-26T16:09:19Z</issued>
<modified>2025-11-26T16:09:19Z</modified>
<id>https://gamma.unpythonic.net/01764173359</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01764173359"/>
<content type="text/html" mode="escaped">



&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css&quot;&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/python.min.js&quot;&gt;&lt;/script&gt;
&lt;style&gt;
.junkpre { border: 1px solid black; max-width: 132ex; max-height: 45lh; overflow: auto; }
&lt;/style&gt;
&lt;script&gt;
const do_embed = function(container, url) {
    const request = new Request(url);
    fetch(request)
      .then((response) =&gt; response.json())
      .then((data) =&gt; {
        container.textContent = atob(data['content']);
        hljs.highlightElement(container)    
    })
    .catch((error) =&gt; {
       console.error(error);
    });
}
const do_embed_here = function(url, classes) {
    const id = &quot;embed&quot; + (((1 + Math.random()) * 0x1000000) | 0).toString(16).substring(1);
    document.write(&quot;&lt;pre class=junkpre&gt;&lt;code id=&quot; + id + &quot; class=junkembed&gt;&lt;/code&gt;&lt;/pre&gt;&quot;)
    const el = document.getElementById(id)
    if (classes) {
        el.className = el.className + &quot; &quot; + classes
    }
    do_embed(el, url)
}
&lt;/script&gt;


&lt;p&gt;I have been working on leaving github.

&lt;p&gt;One thing I liked from github was gist, including command-line upload and the ability to embed it.
I want to replace this but with codeberg. And, I think I've gotten close.
More polish wouldn't hurt, but ehhh...

&lt;p&gt;First, here's the &lt;a href=&quot;shttps://codeberg.org/api/v1/repos/jepler/junkdrawer/contents/aygdu2e9/junk.py?ref=main&quot;&gt;script for uploading&lt;/a&gt;:

&lt;p&gt;
&lt;script&gt;
do_embed_here(&quot;https://codeberg.org/api/v1/repos/jepler/junkdrawer/contents/aygdu2e9/junk.py?ref=main&quot;, &quot;language-python&quot;);
&lt;/script&gt;

&lt;p&gt;And here's an &lt;a href=&quot;https://codeberg.org/jepler/junkdrawer/src/branch/main/he58aqtn/junkembed.html&quot;&gt;example of embedding&lt;/a&gt;:

&lt;script&gt;
do_embed_here(&quot;https://codeberg.org/api/v1/repos/jepler/junkdrawer/contents/he58aqtn/junkembed.html?ref=main&quot;, &quot;language-html&quot;);
&lt;/script&gt;
</content>
</entry>
<entry>
<title>Thick As A Brick / St Cleve Crossword solution</title>
<issued>2025-10-10T20:56:55Z</issued>
<modified>2025-10-10T20:56:55Z</modified>
<id>https://gamma.unpythonic.net/01760129815</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01760129815"/>
<content type="text/html" mode="escaped">
Has the crossword from the fake newspaper in the Jethro Tull album
Thick as a Brick, titled St Cleve Crossword or Saint Cleve Crossword,
ever been solved? Is it actually a UK style cryptic? Or is it just nonsense?

&lt;p&gt;This is from a copy of the album booklet that I found at 
&lt;a href=&quot;https://world-enlightenment.com/THICKASABRICK/TAAB/ARTICLES/BRICK06.HTM&quot;&gt;world-enlightenment.com&lt;/a&gt; copied here for posterity.

&lt;p&gt;&lt;img src=&quot;https://media.unpythonic.net/emergent-files/01760129815/brick0603.jpg&quot;&gt;
</content>
</entry>
<entry>
<title>Migrating from github to codeberg: existing readthedocs projects</title>
<issued>2025-09-08T15:06:13Z</issued>
<modified>2025-09-08T15:06:13Z</modified>
<id>https://gamma.unpythonic.net/01757343973</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01757343973"/>
<content type="text/html" mode="escaped">

&lt;p&gt;Recently, I made the decision to migrate select projects of mine from github to codeberg.&lt;/p&gt;
&lt;p&gt;Today I started migrating &lt;a href=&quot;https://codeberg.org/jepler/wwvbpy&quot;&gt;wwvbpy&lt;/a&gt;, which has an existing readthedocs configuration. In readthedocs &amp;quot;edit project&amp;quot; page, the &amp;quot;repository URL&amp;quot; field was greyed out and uneditable.&lt;/p&gt;
&lt;p&gt;It took me a few minutes but I eventually realized that before I could set the project URL I had to clear the &amp;quot;connected repository&amp;quot; and save the settings.&lt;/p&gt;
&lt;p&gt;After that, I was able to manually edit the URL and then manually configure the outgoing webhooks, so that pushes to codeberg would trigger doc builds.&lt;/p&gt;

</content>
</entry>
<entry>
<title>Variations on 'if TYPE_CHECKING'</title>
<issued>2025-08-26T13:18:49Z</issued>
<modified>2025-08-26T13:18:49Z</modified>
<id>https://gamma.unpythonic.net/01756214329</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01756214329"/>
<content type="text/html" mode="escaped">

&lt;p&gt;Suggested by mypy documentation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from typing import …
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Works in mypy, pyright, pyrefly. Found in setuptool_scm generated &lt;code&gt;__version__.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import …
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Works in mypy, pyright, pyrefly. Best variant for CircuitPython?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def const(x): return x
TYPE_CHECKING = const(0)
if TYPE_CHECKING:
    from typing import …
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Works in mypy only. Does not work in pyright, pyrefly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if False:
    from typing import …
&lt;/code&gt;&lt;/pre&gt;

</content>
</entry>
<entry>
<title>Dear Julian</title>
<issued>2025-05-15T14:33:44Z</issued>
<modified>2025-05-15T14:33:44Z</modified>
<id>https://gamma.unpythonic.net/01747319624</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01747319624"/>
<content type="text/html" mode="escaped">
Julian,

&lt;p&gt;I just found the message you wrote in 2018.

&lt;p&gt;I miss you and I wish you were in my life.

&lt;p&gt;That is all, that's the message.
</content>
</entry>
<entry>
<title>Mutual Tail Recursion in Python, fully mypy-strict type checked</title>
<issued>2024-12-30T19:06:41Z</issued>
<modified>2024-12-30T19:06:41Z</modified>
<id>https://gamma.unpythonic.net/01735585601</id>
<link rel="alternate" type="text/html" href="https://gamma.unpythonic.net/01735585601"/>
<content type="text/html" mode="escaped">
&lt;p&gt;As one does, I was thinking about how Python is criticized for lacking tail recursion optimization.&lt;/p&gt;
&lt;p&gt;I came up with an idea of how to implement this without new language features, by using a decorator around the tail recursive function that catches a special &lt;code&gt;Recur&lt;/code&gt; exception and then turns around and calls the same function with the new arguments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Recur(BaseException, Generic[P]):
    args: P.args
    kwargs: P.kwargs

    def __init__(self, *args: P.args, **kwargs: P.kwargs):
        super().__init__(*args)
        self.kwargs = kwargs


def recurrent(f: Callable[P, T]) -&amp;gt; Callable[P, T]:
    @functools.wraps(f)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -&amp;gt; T:
        while True:
            try:
                return f(*args, **kwargs)
            except Recur as r:
                args = r.args
                kwargs = r.kwargs

    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It can be used like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@recurrent
def gcd(a: int, b: int) -&amp;gt; int:
    print(f&amp;quot;gcd({a}, {b})&amp;quot;)
    if b == 0:
        return a
    raise Recur(b, a % b)


print(gcd(1071, 462))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not an original idea. It has been documented before e.g., by &lt;a href=&quot;https://chrispenner.ca/posts/python-tail-recursion&quot;&gt;Chris Penner&lt;/a&gt;. This code all properly type-checks under &lt;code&gt;mypy --strict&lt;/code&gt; (some context not shown). However, it doesn't allow mutual tail recursion.&lt;/p&gt;
&lt;p&gt;I'll be honest: I didn't find any well-motivated examples for mutual tail recursion! Everyone uses the same awful poorly-motivated example of &lt;code&gt;is-odd&lt;/code&gt; and &lt;code&gt;is-even&lt;/code&gt;. But, because it was a challenge to placate the mypy type checker, I wanted to implement it anyway.&lt;/p&gt;
&lt;p&gt;The problem lies in the implementation of the &lt;code&gt;wrapper&lt;/code&gt; function: &lt;code&gt;args&lt;/code&gt; and &lt;code&gt;kwargs&lt;/code&gt; have the types given in the initial recurrent call, and the types can't change just because &lt;code&gt;f&lt;/code&gt; changes.&lt;/p&gt;
&lt;p&gt;The solution, which I realized a few weeks later, was to move the responsibility to actually dispatch the recurrent call into the &lt;code&gt;Recur&lt;/code&gt; instance. There can be many &lt;code&gt;Recur&lt;/code&gt; instances, but there inside the wrapper function they all simply have the same type: &lt;code&gt;Recur&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;Here's the full implementation, which type checks clean with &lt;code&gt;mypy --strict&lt;/code&gt; (1.11.2) and runs in python 3.11.2:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from __future__ import annotations
import functools
from typing import Callable, ParamSpec, TypeVar, Generic, NoReturn

P = ParamSpec(&amp;quot;P&amp;quot;)
T = TypeVar(&amp;quot;T&amp;quot;)


class Recur(BaseException, Generic[P, T]):
    f: Callable[P, T]
    args: P.args
    kwargs: P.kwargs

    def __init__(self, f: Callable[P, T], args: P.args, kwargs: P.kwargs):
        super().__init__()
        self.f = f.f if isinstance(f, Recurrent) else f
        self.args = args
        self.kwargs = kwargs

    def __call__(self) -&amp;gt; T:
        return self.f(*self.args, **self.kwargs)

    def __repr__(self) -&amp;gt; str:
        if self.kwargs:
            return f&amp;quot;&amp;lt;Recur {self.f.__name__}(*{self.args}, **{self.kwargs})&amp;gt;&amp;quot;
        return f&amp;quot;&amp;lt;Recur {self.f.__name__}{self.args})&amp;gt;&amp;quot;
    __str__ = __repr__

class Recurrent(Generic[P, T]):
    f: Callable[P, T]

    def __init__(self, f: Callable[P, T]) -&amp;gt; None:
        self.f = f

    def __call__(self, *args: P.args, **kwargs: P.kwargs) -&amp;gt; T:
        r = Recur(self.f, args, kwargs)
        while True:
            try:
                return r()
            except Recur as exc:
                r = exc

    def recur(self, *args: P.args, **kwargs: P.kwargs) -&amp;gt; NoReturn:
        raise Recur(self.f, args, kwargs)

    def __repr__(self) -&amp;gt; str:
        return f&amp;quot;&amp;lt;Recurrent {self.f.__name__}&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here's an example use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys
from recur import Recurrent

@Recurrent
def gcd(a: int, b: int) -&amp;gt; int:
    print(f&amp;quot;gcd({a}, {b})&amp;quot;)
    if b == 0:
        return a
    gcd.recur(b, a % b)


@Recurrent
def is_even(a: int) -&amp;gt; bool:
    assert a &amp;gt;= 0
    if a == 0:
        return True
    is_sum_odd.recur(a, -1)


@Recurrent
def is_sum_odd(a: int, b: int) -&amp;gt; bool:
    c = a + b
    assert c &amp;gt;= 0
    if c == 0:
        return False
    is_even.recur(c - 1)


print(gcd)
print(gcd(1071, 462))
print(is_even(sys.getrecursionlimit() * 2))
print(is_even(sys.getrecursionlimit() * 2 + 1))
&lt;/code&gt;&lt;/pre&gt;

</content>
</entry>
</feed>
