Project P116

Deadlock detector

Threads taking and releasing locks by a schedule, turn by turn, with the wait-for cycle that is a deadlock; every interleaving counted, the serial ones that never deadlock, and one lock order that removes it.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-16

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

Threads take and release locks in the order a schedule gives them turns. A thread whose next lock is held waits; when every thread in a cycle waits for a lock the next one holds, none of them can ever move.

  • main.eml - the menu, the threads, one schedule turn by turn, and every interleaving counted
  • locks.eml - the four sets of threads, a turn, the wait-for cycle, a run of a schedule, and the count over every interleaving

How each part works:

  • A thread is a list of steps: lock A, lock B, unlock B, unlock A. On its turn it takes its next step. A lock held by another thread makes it wait, and it tries the same step again on its next turn.
  • The wait-for graph has an arrow from each waiting thread to the thread that holds its lock. A cycle in it is a deadlock: each thread in it waits for the next, so no turn of theirs can move. Two threads can already be stuck before either waits: after 1 2, T1 holds A and T2 holds B, and the program runs the rest to show it ends in deadlock.
  • As in the corpus case deadlock-by-lock-order, every interleaving of the two opposite threads is run, the rest run out at the end: - 40 of the 70 deadlock, exactly those that start 1 2 or 2 1; - the 2 serial ones, one thread finishing before the other starts, never do, and they are all that a test without forced overlap sees.
  • Taking locks in one order, A before B before C, removes the deadlock on every interleaving. Three threads in a ring deadlock on 14,400 of 34,650 interleavings; with T3 taking A before C, on none. Those two counts were run in the cross-check; the program states them.

What is checked: menu choices 1 to 5; a schedule of thread numbers from 1 to the number of threads, separated by spaces, up to 30 turns; one of the four sets of threads. An empty answer cancels.

Sessions: sessions/basic.in gives: - the opposite threads one after the other, then 1 2 1 2 (a deadlock), then 1 2 (no one waits yet, and the rest deadlock), and the count; - the ordered threads with 1 2 1 2 1 2 1 2 (T2 waits, and both finish) and the count; - the ring with 1 2 3 1 2 3 (a deadlock of three) and its count; - the ordered ring with turns 1 2 3 four times, and its count.

sessions/bad-input.in gives: - menu choices 0 and x; - schedules: a cancel, 1 3, 1 a, 1 12 and 31 turns; - other threads: a cancel and circle; - the schedule 2 2 2 2 2, a turn for T2 after it has finished.

Built on the verified corpus case deadlock-by-lock-order (two threads taking two locks in opposite orders, and one global order that removes the deadlock).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Deadlock detector ==
Threads taking and releasing locks by a schedule; a thread waits while its next lock is held, and a cycle of waiting is a deadlock.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 0
Pick a number from 1 to 5.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> x
Pick a number from 1 to 5.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 
Cancelled.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 3
Type thread numbers from 1 to 2, separated by spaces.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 a
Type thread numbers from 1 to 2, separated by spaces.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 12
Type thread numbers from 1 to 2, separated by spaces.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Up to 30 turns.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 4
threads (opposite, ordered, ring or ring-ordered)> 
Cancelled.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 4
threads (opposite, ordered, ring or ring-ordered)> circle
Type opposite, ordered, ring or ring-ordered.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 2 2 2 2 2
  T2 takes B
  T2 takes A
  T2 releases A
  T2 releases B
  T2 has finished
No deadlock; 1 thread still to finish, and running the rest, lowest thread first, finishes every thread.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 5
Bye.
What was typed (19 lines)
0
x
2

2
1 3
2
1 a
2
1 12
2
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
4

4
circle
2
2 2 2 2 2
5

basic

interpreter: byte-equal
== Deadlock detector ==
Threads taking and releasing locks by a schedule; a thread waits while its next lock is held, and a cycle of waiting is a deadlock.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 1
Threads (opposite):
  T1: lock A, lock B, unlock B, unlock A
  T2: lock B, lock A, unlock A, unlock B

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 1 1 1 2 2 2 2
  T1 takes A
  T1 takes B
  T1 releases B
  T1 releases A
  T2 takes B
  T2 takes A
  T2 releases A
  T2 releases B
Every thread finished and every lock is free.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 2 1 2
  T1 takes A
  T2 takes B
  T1 waits for B, held by T2
  T2 waits for A, held by T1
Deadlock: T1 waits for B held by T2; T2 waits for A held by T1. No turn of theirs can ever move.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 2
  T1 takes A
  T2 takes B
No deadlock yet; 2 threads still to finish, and running the rest, lowest thread first, ends in deadlock.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 3
Every interleaving of the opposite threads, each blocked thread trying again on its next turn and the rest run out at the end:
  70 interleavings, 40 end in deadlock, 30 with every lock free.
  Serial ones, one thread finishing before the other starts: 2, of which 0 deadlock.
A test that does not force the threads to overlap sees only the serial schedules, and they never deadlock.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 4
threads (opposite, ordered, ring or ring-ordered)> ordered
Threads (ordered):
  T1: lock A, lock B, unlock B, unlock A
  T2: lock A, lock B, unlock B, unlock A

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 2, up to 30 turns).
schedule> 1 2 1 2 1 2 1 2
  T1 takes A
  T2 waits for A, held by T1
  T1 takes B
  T2 waits for A, held by T1
  T1 releases B
  T2 waits for A, held by T1
  T1 releases A
  T2 takes A
No deadlock; 1 thread still to finish, and running the rest, lowest thread first, finishes every thread.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 3
Every interleaving of the ordered threads, each blocked thread trying again on its next turn and the rest run out at the end:
  70 interleavings, 0 end in deadlock, 70 with every lock free.
  Serial ones, one thread finishing before the other starts: 2, of which 0 deadlock.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 4
threads (opposite, ordered, ring or ring-ordered)> ring
Threads (ring):
  T1: lock A, lock B, unlock B, unlock A
  T2: lock B, lock C, unlock C, unlock B
  T3: lock C, lock A, unlock A, unlock C

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 3, up to 30 turns).
schedule> 1 2 3 1 2 3
  T1 takes A
  T2 takes B
  T3 takes C
  T1 waits for B, held by T2
  T2 waits for C, held by T3
  T3 waits for A, held by T1
Deadlock: T1 waits for B held by T2; T2 waits for C held by T3; T3 waits for A held by T1. No turn of theirs can ever move.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 3
Three threads of four steps have 34,650 interleavings, too many to run here. Every one was run in the cross-check:
  14,400 of them end in deadlock; the 6 serial ones do not.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 4
threads (opposite, ordered, ring or ring-ordered)> ring-ordered
Threads (ring-ordered):
  T1: lock A, lock B, unlock B, unlock A
  T2: lock B, lock C, unlock C, unlock B
  T3: lock A, lock C, unlock C, unlock A

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 2
A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to 3, up to 30 turns).
schedule> 1 2 3 1 2 3 1 2 3 1 2 3
  T1 takes A
  T2 takes B
  T3 waits for A, held by T1
  T1 waits for B, held by T2
  T2 takes C
  T3 waits for A, held by T1
  T1 waits for B, held by T2
  T2 releases C
  T3 waits for A, held by T1
  T1 waits for B, held by T2
  T2 releases B
  T3 waits for A, held by T1
No deadlock; 2 threads still to finish, and running the rest, lowest thread first, finishes every thread.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 3
Three threads of four steps have 34,650 interleavings, too many to run here. Every one was run in the cross-check:
  none of them ends in deadlock: every thread takes its locks in the order A, B, C.

1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit
choice> 5
Bye.
What was typed (24 lines)
1
2
1 1 1 1 2 2 2 2
2
1 2 1 2
2
1 2
3
4
ordered
2
1 2 1 2 1 2 1 2
3
4
ring
2
1 2 3 1 2 3
3
4
ring-ordered
2
1 2 3 1 2 3 1 2 3 1 2 3
3
5

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P116 deadlock detector: threads taking locks, one schedule step by step
# with its wait-for graph, every interleaving counted, and one global lock
# order that removes the deadlock.
import locks

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def program_text(p):
    "" => out
    for s in p:
        if out != "":
            out + ", " => out
        out + s[0] + " " + s[1] => out
    return out

def show(name, progs):
    ("Threads (" + name + "):") ^0
    for t in [0:len(progs) - 1]:
        ("  T" + str(t + 1) + ": " + program_text(progs[t])) ^0

def run_one(progs):
    ("A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to " + str(len(progs)) + ", up to 30 turns).") ^0
    plain(input("schedule> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    [] => sched
    "" => w
    for c in t + " ":
        if c == " ":
            if w != "":
                if len(w) != 1 or w[0] < "1" or w[0] > str(len(progs)):
                    ("Type thread numbers from 1 to " + str(len(progs)) + ", separated by spaces.") ^0
                    return
                sched + [int(w) - 1] => sched
            "" => w
        else:
            w + c => w
    if len(sched) == 0 or len(sched) > 30:
        "Up to 30 turns." ^0
        return
    locks.run(progs, sched, False) => r
    for e in r[0]:
        "T" + str(e[0] + 1) => who
        if e[2] == "done":
            ("  " + who + " has finished") ^0
        else:
            progs[e[0]][e[1]] => s
            if e[2] == "take":
                ("  " + who + " takes " + s[1]) ^0
            elif e[2] == "release":
                ("  " + who + " releases " + s[1]) ^0
            else:
                ("  " + who + " waits for " + s[1] + ", held by T" + str(e[3] + 1)) ^0
    r[1] => st
    locks.cycle(progs, st) => c
    if len(c) > 0:
        "" => line
        for i in [0:len(c) - 1]:
            c[i] => x
            c[(i + 1) % len(c)] => y
            if line != "":
                line + "; " => line
            line + "T" + str(x + 1) + " waits for " + st[2][x] + " held by T" + str(y + 1) => line
        ("Deadlock: " + line + ". No turn of theirs can ever move.") ^0
        return
    0 => left
    for i in [0:len(progs) - 1]:
        if st[0][i] < len(progs[i]):
            left + 1 => left
    if left == 0:
        "Every thread finished and every lock is free." ^0
        return
    locks.run(progs, sched, True) => after
    if after[2]:
        ("No deadlock yet; " + str(left) + " thread" + plural(left) + " still to finish, and running the rest, lowest thread first, ends in deadlock.") ^0
    else:
        ("No deadlock; " + str(left) + " thread" + plural(left) + " still to finish, and running the rest, lowest thread first, finishes every thread.") ^0

def plural(n):
    if n == 1:
        return ""
    return "s"

def census(name, progs):
    if len(progs) > 2:
        "Three threads of four steps have 34,650 interleavings, too many to run here. Every one was run in the cross-check:" ^0
        if name == "ring":
            "  14,400 of them end in deadlock; the 6 serial ones do not." ^0
        else:
            "  none of them ends in deadlock: every thread takes its locks in the order A, B, C." ^0
        return
    locks.census(progs) => c
    ("Every interleaving of the " + name + " threads, each blocked thread trying again on its next turn and the rest run out at the end:") ^0
    ("  " + str(c[0]) + " interleavings, " + str(c[1]) + " end in deadlock, " + str(c[0] - c[1]) + " with every lock free.") ^0
    ("  Serial ones, one thread finishing before the other starts: " + str(c[2]) + ", of which " + str(c[3]) + " deadlock.") ^0
    if c[1] > 0:
        "A test that does not force the threads to overlap sees only the serial schedules, and they never deadlock." ^0

"opposite" => name
locks.presets(name) => progs

"== Deadlock detector ==" ^0
"Threads taking and releasing locks by a schedule; a thread waits while its next lock is held, and a cycle of waiting is a deadlock." ^0
True => running
while running:
    "" ^0
    "1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show(name, progs)
    elif choice == "2":
        run_one(progs)
    elif choice == "3":
        census(name, progs)
    elif choice == "4":
        plain(input("threads (opposite, ordered, ring or ring-ordered)> ")) => n
        if n == "":
            "Cancelled." ^0
        elif len(locks.presets(n)) == 0:
            "Type opposite, ordered, ring or ring-ordered." ^0
        else:
            n => name
            locks.presets(n) => progs
            show(name, progs)
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import locks

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def program_text(p):
    out = ""
    for s in p:
        if out != "":
            out = out + ", "
        out = out + s[0] + " " + s[1]
    return out

def show(name, progs):
    print("Threads (" + name + "):")
    for t in range(0, len(progs)):
        print("  T" + str(t + 1) + ": " + program_text(progs[t]))

def run_one(progs):
    print("A schedule: whose turn it is, such as 1 2 1 2 (threads 1 to " + str(len(progs)) + ", up to 30 turns).")
    t = plain(input("schedule> "))
    if t == "":
        print("Cancelled.")
        return
    sched = []
    w = ""
    for c in t + " ":
        if c == " ":
            if w != "":
                if len(w) != 1 or w[0] < "1" or w[0] > str(len(progs)):
                    print("Type thread numbers from 1 to " + str(len(progs)) + ", separated by spaces.")
                    return
                sched = sched + [int(w) - 1]
            w = ""
        else:
            w = w + c
    if len(sched) == 0 or len(sched) > 30:
        print("Up to 30 turns.")
        return
    r = locks.run(progs, sched, False)
    for e in r[0]:
        who = "T" + str(e[0] + 1)
        if e[2] == "done":
            print("  " + who + " has finished")
        else:
            s = progs[e[0]][e[1]]
            if e[2] == "take":
                print("  " + who + " takes " + s[1])
            elif e[2] == "release":
                print("  " + who + " releases " + s[1])
            else:
                print("  " + who + " waits for " + s[1] + ", held by T" + str(e[3] + 1))
    st = r[1]
    c = locks.cycle(progs, st)
    if len(c) > 0:
        line = ""
        for i in range(0, len(c)):
            x = c[i]
            y = c[(i + 1) % len(c)]
            if line != "":
                line = line + "; "
            line = line + "T" + str(x + 1) + " waits for " + st[2][x] + " held by T" + str(y + 1)
        print("Deadlock: " + line + ". No turn of theirs can ever move.")
        return
    left = 0
    for i in range(0, len(progs)):
        if st[0][i] < len(progs[i]):
            left = left + 1
    if left == 0:
        print("Every thread finished and every lock is free.")
        return
    after = locks.run(progs, sched, True)
    if after[2]:
        print("No deadlock yet; " + str(left) + " thread" + plural(left) + " still to finish, and running the rest, lowest thread first, ends in deadlock.")
    else:
        print("No deadlock; " + str(left) + " thread" + plural(left) + " still to finish, and running the rest, lowest thread first, finishes every thread.")

def plural(n):
    if n == 1:
        return ""
    return "s"

def census(name, progs):
    if len(progs) > 2:
        print("Three threads of four steps have 34,650 interleavings, too many to run here. Every one was run in the cross-check:")
        if name == "ring":
            print("  14,400 of them end in deadlock; the 6 serial ones do not.")
        else:
            print("  none of them ends in deadlock: every thread takes its locks in the order A, B, C.")
        return
    c = locks.census(progs)
    print("Every interleaving of the " + name + " threads, each blocked thread trying again on its next turn and the rest run out at the end:")
    print("  " + str(c[0]) + " interleavings, " + str(c[1]) + " end in deadlock, " + str(c[0] - c[1]) + " with every lock free.")
    print("  Serial ones, one thread finishing before the other starts: " + str(c[2]) + ", of which " + str(c[3]) + " deadlock.")
    if c[1] > 0:
        print("A test that does not force the threads to overlap sees only the serial schedules, and they never deadlock.")

name = "opposite"
progs = locks.presets(name)
print("== Deadlock detector ==")
print("Threads taking and releasing locks by a schedule; a thread waits while its next lock is held, and a cycle of waiting is a deadlock.")
running = True
while running:
    print("")
    print("1) threads  2) run a schedule  3) every interleaving  4) other threads  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show(name, progs)
    elif choice == "2":
        run_one(progs)
    elif choice == "3":
        census(name, progs)
    elif choice == "4":
        n = plain(input("threads (opposite, ordered, ring or ring-ordered)> "))
        if n == "":
            print("Cancelled.")
        elif len(locks.presets(n)) == 0:
            print("Type opposite, ordered, ring or ring-ordered.")
        else:
            name = n
            progs = locks.presets(n)
            show(name, progs)
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

locks.eml

eml
# P116 deadlock detector - threads that take and release locks, run by a
# schedule that says whose turn it is. A thread whose next lock is held
# waits, and tries again on its next turn. A deadlock is a cycle in the
# wait-for graph: each thread waiting for a lock held by the next. As in
# the corpus case deadlock-by-lock-order, two threads that take the same
# two locks in opposite orders deadlock on most interleavings, and taking
# every lock in one global order removes it.
#
# A program is a list of steps [action, lock]: action "lock" or "unlock".

def presets(name):
    if name == "opposite":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "B"], ["lock", "A"], ["unlock", "A"], ["unlock", "B"]]]
    if name == "ordered":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]]]
    if name == "ring":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "B"], ["lock", "C"], ["unlock", "C"], ["unlock", "B"]], [["lock", "C"], ["lock", "A"], ["unlock", "A"], ["unlock", "C"]]]
    if name == "ring-ordered":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "B"], ["lock", "C"], ["unlock", "C"], ["unlock", "B"]], [["lock", "A"], ["lock", "C"], ["unlock", "C"], ["unlock", "A"]]]
    return []

def new_state(progs):
    # [pc per thread, holder per lock, lock waited for per thread ("" if none)]
    [] => pc
    [] => waiting
    for p in progs:
        pc + [0] => pc
        waiting + [""] => waiting
    return [pc, {}, waiting]

def step(progs, st, t):
    # thread t takes its turn: [what happened, thread that holds it or -1]
    st[0] => pc
    st[1] => holder
    if pc[t] >= len(progs[t]):
        return ["done", 0 - 1]
    progs[t][pc[t]] => s
    if s[0] == "lock":
        if s[1] in holder and holder[s[1]] != t:
            s[1] => st[2][t]
            return ["wait", holder[s[1]]]
        t => holder[s[1]]
        "" => st[2][t]
        pc[t] + 1 => pc[t]
        return ["take", 0 - 1]
    {} => rest
    for k in holder:
        if k != s[1]:
            holder[k] => rest[k]
    rest => st[1]
    pc[t] + 1 => pc[t]
    return ["release", 0 - 1]

def can_go(progs, st, t):
    if st[0][t] >= len(progs[t]):
        return False
    progs[t][st[0][t]] => s
    if s[0] == "unlock":
        return True
    return not (s[1] in st[1]) or st[1][s[1]] == t

def cycle(progs, st):
    # a cycle in the wait-for graph, as a list of threads, or []
    [] => edges
    for t in [0:len(progs) - 1]:
        0 - 1 => to
        if st[2][t] != "" and st[0][t] < len(progs[t]) and st[2][t] in st[1]:
            st[1][st[2][t]] => to
        edges + [to] => edges
    for s in [0:len(progs) - 1]:
        [s] => path
        edges[s] => x
        while x != 0 - 1 and len(path) <= len(progs):
            if x == s:
                return path
            path + [x] => path
            edges[x] => x
    return []

def run(progs, schedule, drain):
    # [log, state, deadlocked]: each turn of the schedule, then - with
    # drain - every thread that can still go, lowest number first, until
    # none can
    new_state(progs) => st
    [] => log
    for t in schedule:
        st[0][t] => at
        step(progs, st, t) => r
        log + [[t, at, r[0], r[1]]] => log
    if drain:
        True => moving
        while moving:
            False => moving
            for t in [0:len(progs) - 1]:
                if not moving and can_go(progs, st, t):
                    step(progs, st, t)
                    True => moving
    False => stuck
    for t in [0:len(progs) - 1]:
        if st[0][t] < len(progs[t]):
            True => stuck
    return [log, st, stuck]

def interleavings(lengths):
    # every order of the threads' turns, thread t appearing lengths[t] times
    [[[], lengths]] => frontier
    [] => out
    while len(frontier) > 0:
        frontier[len(frontier) - 1] => item
        frontier[0:len(frontier) - 1] => frontier
        item[0] => seq
        item[1] => left
        0 => total
        for n in left:
            total + n => total
        if total == 0:
            out + [seq] => out
        else:
            for t in [0:len(left) - 1]:
                if left[t] > 0:
                    [] => nl
                    for i in [0:len(left) - 1]:
                        if i == t:
                            nl + [left[i] - 1] => nl
                        else:
                            nl + [left[i]] => nl
                    frontier + [[seq + [t], nl]] => frontier
    return out

def census(progs):
    # over every interleaving: [interleavings, deadlocked, serial ones,
    # serial ones deadlocked]
    [] => lengths
    for p in progs:
        lengths + [len(p)] => lengths
    0 => n
    0 => dead
    0 => serial
    0 => serial_dead
    for seq in interleavings(lengths):
        n + 1 => n
        run(progs, seq, True)[2] => stuck
        if stuck:
            dead + 1 => dead
        0 => changes
        for i in [1:len(seq) - 1]:
            if seq[i] != seq[i - 1]:
                changes + 1 => changes
        if changes == len(progs) - 1:
            serial + 1 => serial
            if stuck:
                serial_dead + 1 => serial_dead
    return [n, dead, serial, serial_dead]
Python projection (locks.py)
def presets(name):
    if name == "opposite":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "B"], ["lock", "A"], ["unlock", "A"], ["unlock", "B"]]]
    if name == "ordered":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]]]
    if name == "ring":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "B"], ["lock", "C"], ["unlock", "C"], ["unlock", "B"]], [["lock", "C"], ["lock", "A"], ["unlock", "A"], ["unlock", "C"]]]
    if name == "ring-ordered":
        return [[["lock", "A"], ["lock", "B"], ["unlock", "B"], ["unlock", "A"]], [["lock", "B"], ["lock", "C"], ["unlock", "C"], ["unlock", "B"]], [["lock", "A"], ["lock", "C"], ["unlock", "C"], ["unlock", "A"]]]
    return []

def new_state(progs):
    pc = []
    waiting = []
    for p in progs:
        pc = pc + [0]
        waiting = waiting + [""]
    return [pc, {}, waiting]

def step(progs, st, t):
    pc = st[0]
    holder = st[1]
    if pc[t] >= len(progs[t]):
        return ["done", 0 - 1]
    s = progs[t][pc[t]]
    if s[0] == "lock":
        if s[1] in holder and holder[s[1]] != t:
            st[2][t] = s[1]
            return ["wait", holder[s[1]]]
        holder[s[1]] = t
        st[2][t] = ""
        pc[t] = pc[t] + 1
        return ["take", 0 - 1]
    rest = {}
    for k in holder:
        if k != s[1]:
            rest[k] = holder[k]
    st[1] = rest
    pc[t] = pc[t] + 1
    return ["release", 0 - 1]

def can_go(progs, st, t):
    if st[0][t] >= len(progs[t]):
        return False
    s = progs[t][st[0][t]]
    if s[0] == "unlock":
        return True
    return not s[1] in st[1] or st[1][s[1]] == t

def cycle(progs, st):
    edges = []
    for t in range(0, len(progs)):
        to = 0 - 1
        if st[2][t] != "" and st[0][t] < len(progs[t]) and st[2][t] in st[1]:
            to = st[1][st[2][t]]
        edges = edges + [to]
    for s in range(0, len(progs)):
        path = [s]
        x = edges[s]
        while x != 0 - 1 and len(path) <= len(progs):
            if x == s:
                return path
            path = path + [x]
            x = edges[x]
    return []

def run(progs, schedule, drain):
    st = new_state(progs)
    log = []
    for t in schedule:
        at = st[0][t]
        r = step(progs, st, t)
        log = log + [[t, at, r[0], r[1]]]
    if drain:
        moving = True
        while moving:
            moving = False
            for t in range(0, len(progs)):
                if not moving and can_go(progs, st, t):
                    step(progs, st, t)
                    moving = True
    stuck = False
    for t in range(0, len(progs)):
        if st[0][t] < len(progs[t]):
            stuck = True
    return [log, st, stuck]

def interleavings(lengths):
    frontier = [[[], lengths]]
    out = []
    while len(frontier) > 0:
        item = frontier[len(frontier) - 1]
        frontier = frontier[0:len(frontier) - 1]
        seq = item[0]
        left = item[1]
        total = 0
        for n in left:
            total = total + n
        if total == 0:
            out = out + [seq]
        else:
            for t in range(0, len(left)):
                if left[t] > 0:
                    nl = []
                    for i in range(0, len(left)):
                        if i == t:
                            nl = nl + [left[i] - 1]
                        else:
                            nl = nl + [left[i]]
                    frontier = frontier + [[seq + [t], nl]]
    return out

def census(progs):
    lengths = []
    for p in progs:
        lengths = lengths + [len(p)]
    n = 0
    dead = 0
    serial = 0
    serial_dead = 0
    for seq in interleavings(lengths):
        n = n + 1
        stuck = run(progs, seq, True)[2]
        if stuck:
            dead = dead + 1
        changes = 0
        for i in range(1, len(seq)):
            if seq[i] != seq[i - 1]:
                changes = changes + 1
        if changes == len(progs) - 1:
            serial = serial + 1
            if stuck:
                serial_dead = serial_dead + 1
    return [n, dead, serial, serial_dead]

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.