Chapter 1: Basic Compiler¶

AST frontend and LLVM backend¶

This chapter introduces the fundamental components of our compiler: the AST frontend and LLVM backend. We show how to parse Python functions into an intermediate representation (RVSDG-IR) and then compile them to executable code using LLVM.

The chapter covers:

  • How to implement a frontend that converts Python AST to RVSDG-IR
  • How to implement a backend that generates LLVM IR
  • How to JIT compile and execute the generated code

Imports and Setup¶

Import all necessary modules for the basic compiler implementation.

In [1]:
from __future__ import annotations
In [2]:
import builtins
from collections import ChainMap
In [3]:
from llvmlite import binding as llvm
from llvmlite import ir
from sealir import ase, rvsdg
from sealir.llvm_pyapi_backend import (
    CodegenCtx,
    CodegenState,
    JitCallable,
    PythonAPI,
    _codegen_loop,
)
from sealir.rvsdg import grammar as rg
from typing_extensions import TypedDict
In [4]:
from utils import Pipeline, Report

Frontend Implementation¶

The frontend accepts a Python function object, reads its source code, and parses its Abstract Syntax Tree (AST). It then transforms the AST into a Regionalized Value-State Dependence Graph (RVSDG), a representation that simplifies further intermediate representation (IR) processing. The RVSDG uses a data-flow centric encoding in which control-flow constructs are mapped into regions. These regions function much like ordinary operations, with clearly defined sets of input and output ports. Additionally, state is explicitly encoded as I/O, so that every operation maintains a pure operational appearance.

In [5]:
class FrontendOutput(TypedDict):
    rvsdg_expr: object
    dbginfo: object
In [6]:
@Pipeline
def pipeline_frontend(fn, pipeline_report=Report.Sink()) -> FrontendOutput:
    """
    Frontend code is all encapsulated in sealir.rvsdg.restructure_source
    """
    with pipeline_report.nest("Frontend", default_expanded=True) as report:
        rvsdg_expr, dbginfo = rvsdg.restructure_source(fn)
        report.append("Debug Info on RVSDG", dbginfo.show_sources())
        report.append("RVSDG", rvsdg.format_rvsdg(rvsdg_expr))
    return {"rvsdg_expr": rvsdg_expr, "dbginfo": dbginfo}

Simple Frontend Example¶

Here's a simple function to illustrate the frontend

In [7]:
if __name__ == "__main__":

    def exercise_frontend_simple(x, y):
        return x + y

    cres = pipeline_frontend(fn=exercise_frontend_simple)
    # Results are returned as a SimpleNamespace
    print(rvsdg.format_rvsdg(cres.rvsdg_expr))
transformed_exercise_frontend_simple = Func (Args (ArgSpec 'x' (PyNone)) (ArgSpec 'y' (PyNone)))
$0 = Region[239] <- !io x y
{
  $1 = PyBinOp + $0[0] $0[1], $0[2]
} [314] -> !io=$1[0] !ret=$1[1]

Alternatively use Report to display the results

In [8]:
if __name__ == "__main__":
    report = Report("Frontend", default_expanded=True)
    cres = pipeline_frontend(
        fn=exercise_frontend_simple, pipeline_report=report
    )
    report.display()

Frontend

1. Frontend ▶
Frontend
Debug Info on RVSDG ▶
--------------------------------original source---------------------------------
   3|    def exercise_frontend_simple(x, y):
   4|        return x + y
----------------------------------inter source----------------------------------
   1|def transformed_exercise_frontend_simple(x, y):
   2|    """#file: /tmp/ipykernel_3405/4177425945.py"""
   3|    '#loc: 4:8-4:20'
   4|    return x + y
RVSDG ▶
transformed_exercise_frontend_simple = Func (Args (ArgSpec 'x' (PyNone)) (ArgSpec 'y' (PyNone)))
$0 = Region[239] <- !io x y
{
  $1 = PyBinOp + $0[0] $0[1], $0[2]
} [314] -> !io=$1[0] !ret=$1[1]

The function's RVSDG-IR includes a region:

$0 = Region[236] <- !io x y
{
    ...
} [302] -> !io=$1[0] !ret=$1[1] x=$0[1] y=$0[2]

The region has three input ports: !io, x, and y. Its output ports are !io, !ret, x, and y.

Here, !io represents the state. Because Python functions are imperative, each function carries an implicit state that must be updated for any side-effect.

The !ret port holds the function's return value.

The x and y output ports convey the region's internal value state, but the function node ignores these values.

The single operation in the function is:

 $1 = PyBinOp + $0[0] $0[1], $0[2]

This is a Python binary operation. It uses input operands from $0, which is the header of the function's region. In this notation, $0[n] refers to a specific port: $0[0] corresponds to !io, $0[1] to x, and $0[2] to y.

Complex Frontend Example¶

Below is a more intricate example that requires restructuring of the control flow. This is due to the use of the break statement to exit a for-loop.

RVSDG enforces structured control flow. Only three types of control-flow regions are permitted:

  • Linear region, as Region {...};
  • If-Else region, as If {...} Else {...} EndIf;
  • Loop region, as Loop {...} EndLoop.
In [9]:
if __name__ == "__main__":

    def exercise_frontend_loop_if_break(x, y):
        c = 0
        for i in range(x):
            if i > y:
                break
            c += i
        return c

    report = Report("Frontend", default_expanded=True)
    cres = pipeline_frontend(
        fn=exercise_frontend_loop_if_break, pipeline_report=report
    )
    report.display()

Frontend

1. Frontend ▶
Frontend
Debug Info on RVSDG ▶
--------------------------------original source---------------------------------
   3|    def exercise_frontend_loop_if_break(x, y):
   4|        c = 0
   5|        for i in range(x):
   6|            if i > y:
   7|                break
   8|            c += i
   9|        return c
----------------------------------inter source----------------------------------
   1|def transformed_exercise_frontend_loop_if_break(x, y):
   2|    """#file: /tmp/ipykernel_3405/2883463775.py"""
   3|    '#loc: 4:8-4:13'
   4|    c = 0
   5|    '#loc: 5:8-8:18'
   6|    __scfg_iterator_1__ = iter(range(x))
   7|    i = None
   8|    __scfg_loop_cont_1__ = True
   9|    while __scfg_loop_cont_1__:
  10|        __scfg_iter_last_1__ = i
  11|        i = next(__scfg_iterator_1__, '__scfg_sentinel__')
  12|        if i != '__scfg_sentinel__':
  13|            '#loc: 6:12-7:21'
  14|            if i > y:
  15|                __scfg_exit_var_0__ = 1
  16|                __scfg_backedge_var_0__ = 1
  17|            else:
  18|                '#loc: 8:12-8:18'
  19|                c += i
  20|                __scfg_backedge_var_0__ = 0
  21|                __scfg_exit_var_0__ = -1
  22|        else:
  23|            __scfg_exit_var_0__ = 0
  24|            __scfg_backedge_var_0__ = 1
  25|        __scfg_loop_cont_1__ = not __scfg_backedge_var_0__
  26|    if __scfg_exit_var_0__ in (0,):
  27|        i = __scfg_iter_last_1__
  28|    else:
  29|        '#loc: 7:16-7:21'
  30|    '#loc: 9:8-9:16'
  31|    return c
RVSDG ▶
transformed_exercise_frontend_loop_if_break = Func (Args (ArgSpec 'x' (PyNone)) (ArgSpec 'y' (PyNone)))
$0 = Region[1350] <- !io x y
{
  $1 = PyLoadGlobal $0[0] 'range'
  $2 = PyCall $1 $0[0] $0[1]
  $3 = PyLoadGlobal $2[0] 'iter'
  $4 = PyCall $3 $2[0] $2[1]
  $5 = Undef __scfg_backedge_var_0__
  $6 = Undef __scfg_exit_var_0__
  $7 = Undef __scfg_iter_last_1__
  $8 = DbgValue '__scfg_iterator_1__' $4[1]
  $9 = PyBool True
  $10 = DbgValue '__scfg_loop_cont_1__' $9
  $11 = PyInt 0
  $12 = DbgValue 'c' $11
  $13 = PyNone
  $14 = DbgValue 'i' $13
  $15 = Loop [2956] <- $4[0] $5 $6 $7 $8 $10 $12 $14 $0[1] $0[2]
    $16 = Region[1544] <- !io __scfg_backedge_var_0__ __scfg_exit_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i x y
    {
      $17 = PyLoadGlobal $16[0] 'next'
      $18 = PyStr '__scfg_sentinel__'
      $19 = PyCall $17 $16[0] $16[4], $18
      $20 = DbgValue '__scfg_iter_last_1__' $16[7]
      $21 = DbgValue 'i' $19[1]
      $22 = PyStr '__scfg_sentinel__'
      $23 = PyBinOp != $19[0] $21, $22
      $24 = If $23[1] <- $19[0] $16[1] $16[2] $20 $16[4] $16[5] $16[6] $21 $16[8] $16[9]
        $25 = Region[1704] <- !io __scfg_backedge_var_0__ __scfg_exit_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i x y
        {
          $26 = PyBinOp > $25[0] $25[7], $25[9]
          $27 = If $26[1] <- $25[0] $25[1] $25[2] $25[3] $25[4] $25[5] $25[6] $25[7] $25[8] $25[9]
            $28 = Region[1792] <- !io __scfg_backedge_var_0__ __scfg_exit_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i x y
            {
              $29 = PyInt 1
              $30 = DbgValue '__scfg_backedge_var_0__' $29
              $31 = PyInt 1
              $32 = DbgValue '__scfg_exit_var_0__' $31
            } [2246] -> !io=$28[0] __scfg_backedge_var_0__=$30 __scfg_exit_var_0__=$32 __scfg_iter_last_1__=$28[3] __scfg_iterator_1__=$28[4] __scfg_loop_cont_1__=$28[5] c=$28[6] i=$28[7] x=$28[8] y=$28[9]
            Else
            $33 = Region[1977] <- !io __scfg_backedge_var_0__ __scfg_exit_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i x y
            {
              $34 = PyInplaceBinOp + $33[0], $33[6], $33[7]
              $35 = PyInt 1
              $36 = PyUnaryOp - $34[0] $35
              $37 = PyInt 0
              $38 = DbgValue '__scfg_backedge_var_0__' $37
              $39 = DbgValue '__scfg_exit_var_0__' $36[1]
            } [2310] -> !io=$36[0] __scfg_backedge_var_0__=$38 __scfg_exit_var_0__=$39 __scfg_iter_last_1__=$33[3] __scfg_iterator_1__=$33[4] __scfg_loop_cont_1__=$33[5] c=$34[1] i=$33[7] x=$33[8] y=$33[9]
          Endif
        } [2689] -> !io=$27[0] __scfg_backedge_var_0__=$27[1] __scfg_exit_var_0__=$27[2] __scfg_iter_last_1__=$27[3] __scfg_iterator_1__=$27[4] __scfg_loop_cont_1__=$27[5] c=$27[6] i=$27[7] x=$27[8] y=$27[9]
        Else
        $40 = Region[2457] <- !io __scfg_backedge_var_0__ __scfg_exit_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i x y
        {
          $41 = PyInt 1
          $42 = DbgValue '__scfg_backedge_var_0__' $41
          $43 = PyInt 0
          $44 = DbgValue '__scfg_exit_var_0__' $43
        } [2753] -> !io=$40[0] __scfg_backedge_var_0__=$42 __scfg_exit_var_0__=$44 __scfg_iter_last_1__=$40[3] __scfg_iterator_1__=$40[4] __scfg_loop_cont_1__=$40[5] c=$40[6] i=$40[7] x=$40[8] y=$40[9]
      Endif
      $45 = PyUnaryOp not $24[0] $24[1]
      $46 = DbgValue '__scfg_loop_cont_1__' $45[1]
    } [2941] -> !_loopcond_0002=$46 !io=$45[0] __scfg_backedge_var_0__=$24[1] __scfg_exit_var_0__=$24[2] __scfg_iter_last_1__=$24[3] __scfg_iterator_1__=$24[4] __scfg_loop_cont_1__=$46 c=$24[6] i=$24[7] x=$24[8] y=$24[9]
  EndLoop
  $47 = PyInt 0
  $48 = PyTuple $47
  $49 = PyBinOp in $15[0] $15[2], $48
  $50 = If $49[1] <- $15[0] $15[1] $15[2] $15[3] $15[4] $15[5] $15[6] $15[7] $15[8] $15[9]
    $51 = Region[3048] <- !io __scfg_backedge_var_0__ __scfg_exit_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i x y
    {
      $52 = DbgValue 'i' $51[3]
    } [3384] -> !io=$51[0] __scfg_backedge_var_0__=$51[1] __scfg_exit_var_0__=$51[2] __scfg_iter_last_1__=$51[3] __scfg_iterator_1__=$51[4] __scfg_loop_cont_1__=$51[5] c=$51[6] i=$52 x=$51[8] y=$51[9]
    Else
    $53 = Region[3202] <- !io __scfg_backedge_var_0__ __scfg_exit_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i x y
    {
    } [3448] -> !io=$53[0] __scfg_backedge_var_0__=$53[1] __scfg_exit_var_0__=$53[2] __scfg_iter_last_1__=$53[3] __scfg_iterator_1__=$53[4] __scfg_loop_cont_1__=$53[5] c=$53[6] i=$53[7] x=$53[8] y=$53[9]
  Endif
} [3602] -> !io=$50[0] !ret=$50[6]

Observations from the RVSDG-IR above:

  • The for-loop is restructured into a Loop composed of If-Else regions.
  • The Loop region is tail-controlled; its first output port acts as the loop condition. See !_loopcond_0002.
  • An extra If-Else follows the loop to adjust the value of i.

The beauty of the structured control-flow is that everything can be encapsulated as a plain data-flow operation, including any region. Everything is just an operation with some input and output ports. This simplifies the rest of the compiler.

Backend Implementation¶

SealIR includes a lightweight LLVM backend that emits the Python C-API, which executes the RVSDG-IR. The example below demonstrates how to use this backend

In [10]:
def _determine_arity(root: ase.SExpr) -> int:
    """Helper function to get the arity of the function node"""
    match root:
        case rg.Func(args=rg.Args() as args):
            return len(args.arguments)
        case _:
            raise TypeError(root._head)
In [11]:
def backend(root, ns=builtins.__dict__, codegen_extension=None):
    """
    Emit LLVM using Python C-API.

    root: the RVSDG expression for the function
    ns: is the dictionary of global names. A JIT is assumed. Object pointer for
        each key is used.
    codegen_extension: Optional. If defined, it is the function to call when an
        unknown operation is encountered by the code generator. This argument
        is used in the later chapters.

    Warning:

    - This is for testing only.
    - Does NOT do proper memory management.
    - Does NOT do proper error handling.
    """

    llvm.initialize()
    llvm.initialize_native_target()
    llvm.initialize_native_asmprinter()

    mod = ir.Module()

    ll_byte = ir.IntType(8)
    ll_pyobject_ptr = ll_byte.as_pointer()
    # Make LLVM function
    arity = _determine_arity(root)

    assert arity >= 1
    actual_num_args = arity
    fnty = ir.FunctionType(
        ll_pyobject_ptr, [ll_pyobject_ptr] * actual_num_args
    )
    fn = ir.Function(mod, fnty, name="foo")

    # init entry block and builder
    builder = ir.IRBuilder(fn.append_basic_block())
    retval_slot = builder.alloca(ll_pyobject_ptr)
    builder.store(ll_pyobject_ptr(None), retval_slot)  # init retval to NULL

    bb_main = builder.append_basic_block()
    builder.branch(bb_main)
    builder.position_at_end(bb_main)

    ctx = CodegenCtx(
        llvm_module=mod,
        llvm_func=fn,
        builder=builder,
        pyapi=PythonAPI(builder),
        global_ns=ChainMap(ns, __builtins__),
        codegen_extension=codegen_extension,
    )

    # Emit the function body
    ase.traverse(root, _codegen_loop, CodegenState(context=ctx))
    return mod

JIT Compilation¶

The following function takes a LLVM module and JIT compile it for execution:

In [12]:
def jit_compile(mod, rvsdg_expr):
    """JIT compile LLVM module into an executable function for this process."""
    llvm_ir = str(mod)

    # Create JIT
    lljit = llvm.create_lljit_compiler()
    rt = (
        llvm.JITLibraryBuilder()
        .add_ir(llvm_ir)
        .export_symbol("foo")
        .add_current_process()
        .link(lljit, "foo")
    )
    ptr = rt["foo"]
    arity = _determine_arity(rvsdg_expr)
    return JitCallable.from_pointer(rt, ptr, arity)

Compiler Pipeline¶

The following is a simple compiler pipeline:

In [13]:
class BackendOutput(TypedDict):
    llmod: object
In [14]:
@pipeline_frontend.extend
def pipeline_backend(
    rvsdg_expr, pipeline_report=Report.Sink()
) -> BackendOutput:
    with pipeline_report.nest("Backend", default_expanded=True) as report:
        llmod = backend(rvsdg_expr)
        report.append("LLVM", llmod)
    return {"llmod": llmod}
In [15]:
class JITOutput(TypedDict):
    jit_func: object
In [16]:
@pipeline_backend.extend
def compiler_pipeline(llmod, rvsdg_expr) -> JITOutput:
    jit_func = jit_compile(llmod, rvsdg_expr)
    return {"jit_func": jit_func}

Testing Framework¶

Define a testing framework to verify compiler correctness.

In [17]:
def run_test(fn, jt, args, *, verbose=False, equal=lambda x, y: x == y):
    res = jt(*args)
    got = fn(*args)

    if verbose:
        report = Report("Testing report", default_expanded=False)
        report.append("Args", args)
        report.append("JIT output", res)
        report.append("Expected output", got)
        report.display()

    assert equal(res, got), (res, got)
    return res

Complete Example¶

The following puts everything together. Running the frontend to generate RVSDG-IR. Then, emitting LLVM using the backend. Finally, JIT'ing it into executable code and verifying it.

In [18]:
if __name__ == "__main__":

    def sum_ints(n):
        c = 0
        for i in range(n):
            c += i
        return c

    report = Report("Compiler Pipeline", default_expanded=True)
    jt = compiler_pipeline(fn=sum_ints, pipeline_report=report).jit_func
    report.display()
    run_test(sum_ints, jt, (12,), verbose=True)

Compiler Pipeline

1. Frontend ▶
Frontend
Debug Info on RVSDG ▶
--------------------------------original source---------------------------------
   3|    def sum_ints(n):
   4|        c = 0
   5|        for i in range(n):
   6|            c += i
   7|        return c
----------------------------------inter source----------------------------------
   1|def transformed_sum_ints(n):
   2|    """#file: /tmp/ipykernel_3405/1628022400.py"""
   3|    '#loc: 4:8-4:13'
   4|    c = 0
   5|    '#loc: 5:8-6:18'
   6|    __scfg_iterator_1__ = iter(range(n))
   7|    i = None
   8|    __scfg_loop_cont_1__ = True
   9|    while __scfg_loop_cont_1__:
  10|        __scfg_iter_last_1__ = i
  11|        i = next(__scfg_iterator_1__, '__scfg_sentinel__')
  12|        if i != '__scfg_sentinel__':
  13|            '#loc: 6:12-6:18'
  14|            c += i
  15|            __scfg_backedge_var_0__ = 0
  16|        else:
  17|            __scfg_backedge_var_0__ = 1
  18|        __scfg_loop_cont_1__ = not __scfg_backedge_var_0__
  19|    i = __scfg_iter_last_1__
  20|    '#loc: 7:8-7:16'
  21|    return c
RVSDG ▶
transformed_sum_ints = Func (Args (ArgSpec 'n' (PyNone)))
$0 = Region[931] <- !io n
{
  $1 = PyLoadGlobal $0[0] 'range'
  $2 = PyCall $1 $0[0] $0[1]
  $3 = PyLoadGlobal $2[0] 'iter'
  $4 = PyCall $3 $2[0] $2[1]
  $5 = Undef __scfg_backedge_var_0__
  $6 = Undef __scfg_iter_last_1__
  $7 = DbgValue '__scfg_iterator_1__' $4[1]
  $8 = PyBool True
  $9 = DbgValue '__scfg_loop_cont_1__' $8
  $10 = PyInt 0
  $11 = DbgValue 'c' $10
  $12 = PyNone
  $13 = DbgValue 'i' $12
  $14 = Loop [1816] <- $4[0] $5 $6 $7 $9 $11 $13 $0[1]
    $15 = Region[1115] <- !io __scfg_backedge_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i n
    {
      $16 = PyLoadGlobal $15[0] 'next'
      $17 = PyStr '__scfg_sentinel__'
      $18 = PyCall $16 $15[0] $15[3], $17
      $19 = DbgValue '__scfg_iter_last_1__' $15[6]
      $20 = DbgValue 'i' $18[1]
      $21 = PyStr '__scfg_sentinel__'
      $22 = PyBinOp != $18[0] $20, $21
      $23 = If $22[1] <- $18[0] $15[1] $19 $15[3] $15[4] $15[5] $20 $15[7]
        $24 = Region[1263] <- !io __scfg_backedge_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i n
        {
          $25 = PyInplaceBinOp + $24[0], $24[5], $24[6]
          $26 = PyInt 0
          $27 = DbgValue '__scfg_backedge_var_0__' $26
        } [1589] -> !io=$25[0] __scfg_backedge_var_0__=$27 __scfg_iter_last_1__=$24[2] __scfg_iterator_1__=$24[3] __scfg_loop_cont_1__=$24[4] c=$25[1] i=$24[6] n=$24[7]
        Else
        $28 = Region[1418] <- !io __scfg_backedge_var_0__ __scfg_iter_last_1__ __scfg_iterator_1__ __scfg_loop_cont_1__ c i n
        {
          $29 = PyInt 1
          $30 = DbgValue '__scfg_backedge_var_0__' $29
        } [1641] -> !io=$28[0] __scfg_backedge_var_0__=$30 __scfg_iter_last_1__=$28[2] __scfg_iterator_1__=$28[3] __scfg_loop_cont_1__=$28[4] c=$28[5] i=$28[6] n=$28[7]
      Endif
      $31 = PyUnaryOp not $23[0] $23[1]
      $32 = DbgValue '__scfg_loop_cont_1__' $31[1]
    } [1803] -> !_loopcond_0002=$32 !io=$31[0] __scfg_backedge_var_0__=$23[1] __scfg_iter_last_1__=$23[2] __scfg_iterator_1__=$23[3] __scfg_loop_cont_1__=$32 c=$23[5] i=$23[6] n=$23[7]
  EndLoop
} [1953] -> !io=$14[0] !ret=$14[5]
2. Backend ▶
Backend
LLVM ▶
; ModuleID = ""
target triple = "unknown-unknown-unknown"
target datalayout = ""

define i8* @"foo"(i8* %".1")
{
.3:
  %".4" = alloca i8*
  store i8* null, i8** %".4"
  br label %".6"
.6:
  br label %".8"
.8:
  %"global.range" = inttoptr i64 94822853635008 to i8*
  %".10" = call i8* (i8*, ...) @"PyObject_CallFunctionObjArgs"(i8* %"global.range", i8* %".1", i8* null)
  %"global.iter" = inttoptr i64 140717372066528 to i8*
  %".11" = call i8* (i8*, ...) @"PyObject_CallFunctionObjArgs"(i8* %"global.iter", i8* %".10", i8* null)
  %".12" = call i8* @"PyLong_FromSsize_t"(i64 1)
  %".13" = call i8* @"PyLong_FromSsize_t"(i64 0)
  call void @"Py_IncRef"(i8* @"_Py_NoneStruct")
  br label %"loopbody"
loopbody:
  %".16" = phi  i8* [null, %".8"], [%".37", %"endif"]
  %".17" = phi  i8* [null, %".8"], [%".38", %"endif"]
  %".18" = phi  i8* [%".11", %".8"], [%".39", %"endif"]
  %".19" = phi  i8* [%".12", %".8"], [%".46", %"endif"]
  %".20" = phi  i8* [%".13", %".8"], [%".41", %"endif"]
  %".21" = phi  i8* [@"_Py_NoneStruct", %".8"], [%".42", %"endif"]
  %".22" = phi  i8* [%".1", %".8"], [%".43", %"endif"]
  %"global.next" = inttoptr i64 140717372067008 to i8*
  %".23" = bitcast [18 x i8]* @"const_string" to i8*
  %".24" = call i8* @"PyUnicode_FromString"(i8* %".23")
  %".25" = call i8* (i8*, ...) @"PyObject_CallFunctionObjArgs"(i8* %"global.next", i8* %".18", i8* %".24", i8* null)
  %".26" = bitcast [18 x i8]* @"const_string.1" to i8*
  %".27" = call i8* @"PyUnicode_FromString"(i8* %".26")
  %".28" = call i8* @"PyObject_RichCompare"(i8* %".25", i8* %".27", i32 3)
  %".29" = call i32 @"PyObject_IsTrue"(i8* %".28")
  %".30" = icmp ne i32 0, %".29"
  br i1 %".30", label %"then", label %"else"
endloop:
  ret i8* %".41"
then:
  %".32" = call i8* @"PyNumber_InPlaceAdd"(i8* %".20", i8* %".25")
  %".33" = call i8* @"PyLong_FromSsize_t"(i64 0)
  br label %"endif"
else:
  %".35" = call i8* @"PyLong_FromSsize_t"(i64 1)
  br label %"endif"
endif:
  %".37" = phi  i8* [%".33", %"then"], [%".35", %"else"]
  %".38" = phi  i8* [%".21", %"then"], [%".21", %"else"]
  %".39" = phi  i8* [%".18", %"then"], [%".18", %"else"]
  %".40" = phi  i8* [%".19", %"then"], [%".19", %"else"]
  %".41" = phi  i8* [%".32", %"then"], [%".20", %"else"]
  %".42" = phi  i8* [%".25", %"then"], [%".25", %"else"]
  %".43" = phi  i8* [%".22", %"then"], [%".22", %"else"]
  %".44" = call i32 @"PyObject_Not"(i8* %".37")
  %".45" = zext i32 %".44" to i64
  %".46" = call i8* @"PyBool_FromLong"(i64 %".45")
  %".47" = call i32 @"PyObject_IsTrue"(i8* %".46")
  %".48" = icmp ne i32 0, %".47"
  br i1 %".48", label %"loopbody", label %"endloop"
}

declare i8* @"PyObject_CallFunctionObjArgs"(i8* %".1", ...)

declare i8* @"PyLong_FromSsize_t"(i64 %".1")

@"_Py_NoneStruct" = external global i8
declare void @"Py_IncRef"(i8* %".1")

@"const_string" = internal constant [18 x i8] c"__scfg_sentinel__\00"
declare i8* @"PyUnicode_FromString"(i8* %".1")

@"const_string.1" = internal constant [18 x i8] c"__scfg_sentinel__\00"
declare i8* @"PyObject_RichCompare"(i8* %".1", i8* %".2", i32 %".3")

declare i32 @"PyObject_IsTrue"(i8* %".1")

declare i8* @"PyNumber_InPlaceAdd"(i8* %".1", i8* %".2")

declare i32 @"PyObject_Not"(i8* %".1")

declare i8* @"PyBool_FromLong"(i64 %".1")

Testing report

1. Args ▶
(12,)
2. JIT output ▶
66
3. Expected output ▶
66