Project P108

Feature flags

Three environments compared as sets of flags - who is ahead, and what a promotion would turn on and off - and flags rolled out to a share of users by a bucket each user keeps, so a larger share only adds users; allow and deny lists; and the bucket against a new draw at every check.

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

Feature flags in three environments, compared as sets. Flags can also be rolled out to a share of users, each user keeping one bucket per flag.

  • main.eml - the menu, the environments table, one user's flags, a change of share, bucket against a new draw, and the allow and deny lists
  • flags.eml - the known flags, set questions on lists, the bucket hash, the decision for one user, and counting over many users

How each part works:

  • Environments, as in the corpus case feature-flag-rollout: whether one environment already has every flag of another is a subset question, and what promoting it would change is a difference. Two environments can each have a flag the other lacks, and then neither is ahead. Flags are always listed in one fixed order.
  • A user's bucket for a flag is 0 to 9999. It comes from a hash of the flag's name, a colon and the user's id: each character is folded in and the result cubed, modulo the prime 4294967291, and the last step is modulo 10000. Cubing is one-to-one modulo that prime, and it is not linear. With a plain polynomial hash, one user's buckets for two flags were correlated (0.32), so a user early in one rollout tended to be early in the next.
  • The decision: the deny list first, then the allow list, then the user is in when the bucket is under the share (in hundredths of a percent). Because the bucket never changes, raising the share only adds users and lowering it only removes them.
  • Against a new draw at every check, the share comes out about right too, but users flip from one check to the next.

What is checked: menu choices 1 to 6; user ids of up to 20 letters, digits and - _ . @; a flag that is rolled out; a share from 0 to 100% with at most two decimals; allow or deny. An empty answer cancels.

Sessions: sessions/basic.in gives: - the environments: dev is ahead of staging, while staging and prod, and dev and prod, each have a flag the other lacks; - the flags of ana (on by the allow list, off by the deny list), user42 and ops-bot; - new-checkout raised from 10% to 20% and 50% with nobody leaving, then lowered to 5% with nobody joining; - search-v2 by bucket against a new draw: 0 users flip against 411; - user42 added to search-v2's allow list, and their flags again.

sessions/bad-input.in gives: - menu choices 0 and x; - a cancel, a user id too long, and one with a !; - shares: a cancel, a flag that is not rolled out, a cancel, 101, abc and 12.345; - a cancel for bucket or new draw; - lists: neither allow nor deny, two cancels, and a user who is already in the list.

Built on the verified corpus case feature-flag-rollout (being ready to promote is a subset question, and what would change is a difference).

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
== Feature flags ==
Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 0
Pick a number from 1 to 6.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> x
Pick a number from 1 to 6.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 2
user id (letters, digits, - _ . @)> 
Cancelled.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 2
user id (letters, digits, - _ . @)> a very long user identifier
Keep a user id to 20 characters.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 2
user id (letters, digits, - _ . @)> bad id!
A user id has letters, digits and - _ . @ only.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> 
Cancelled.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> fancy
Pick a flag that is rolled out.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)> 
Cancelled.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)> 101
Type a share from 0 to 100, at most two decimals.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)> abc
Type a share from 0 to 100, at most two decimals.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)> 12.345
Type a share from 0 to 100, at most two decimals.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 4
flag (new-checkout, dark-mode, search-v2)> 
Cancelled.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> dark-mode
allow or deny> maybe
Type allow or deny.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> dark-mode
allow or deny> 
Cancelled.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> dark-mode
allow or deny> deny
user id (letters, digits, - _ . @)> 
Cancelled.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> new-checkout
allow or deny> allow
user id (letters, digits, - _ . @)> ana
ana is already in the allow list of new-checkout.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 6
Bye.
What was typed (41 lines)
0
x
2

2
a very long user identifier
2
bad id!
3

3
fancy
3
search-v2

3
search-v2
101
3
search-v2
abc
3
search-v2
12.345
4

5
dark-mode
maybe
5
dark-mode

5
dark-mode
deny

5
new-checkout
allow
ana
6

basic

interpreter: byte-equal
== Feature flags ==
Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 1
  environment  flags
  dev          new-checkout, dark-mode, search-v2, beta-banner
  staging      new-checkout, dark-mode
  prod         dark-mode, legacy-export
Promoting dev to staging would turn on search-v2, beta-banner and turn off (none).
  dev has every flag of staging and more.
Promoting staging to prod would turn on new-checkout and turn off legacy-export.
  Neither has every flag of the other, so neither is ahead: there is only the difference.
Promoting dev to prod would turn on new-checkout, search-v2, beta-banner and turn off legacy-export.
  Neither has every flag of the other, so neither is ahead: there is only the difference.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 2
user id (letters, digits, - _ . @)> ana
  new-checkout  on   in the allow list (10% rolled out)
  dark-mode     on   bucket 2435 is under 10000 (100% rolled out)
  search-v2     off  in the deny list (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 2
user id (letters, digits, - _ . @)> user42
  new-checkout  off  bucket 5396 is not under 1000 (10% rolled out)
  dark-mode     on   bucket 5395 is under 10000 (100% rolled out)
  search-v2     off  bucket 7645 is not under 2500 (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 2
user id (letters, digits, - _ . @)> ops-bot
  new-checkout  off  in the deny list (10% rolled out)
  dark-mode     on   bucket 7177 is under 10000 (100% rolled out)
  search-v2     off  bucket 8997 is not under 2500 (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> new-checkout
new share in percent (0 to 100, two decimals at most)> 20
new-checkout from 10% to 20%, over 1000 users:
  on before 76, on after 178: 76 stayed on, 102 joined, 0 left.
Raising the share only adds users: everyone whose bucket was under the old share is under the new one.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> new-checkout
new share in percent (0 to 100, two decimals at most)> 50
new-checkout from 20% to 50%, over 1000 users:
  on before 178, on after 499: 178 stayed on, 321 joined, 0 left.
Raising the share only adds users: everyone whose bucket was under the old share is under the new one.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> new-checkout
new share in percent (0 to 100, two decimals at most)> 5
new-checkout from 50% to 5%, over 1000 users:
  on before 499, on after 38: 38 stayed on, 0 joined, 461 left.
Lowering the share only removes users, those whose buckets lie between the new share and the old.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 4
flag (new-checkout, dark-mode, search-v2)> search-v2
search-v2 at 25%, each of 1000 users checked twice:
  by bucket:             218 on at the first check, 218 at the second, 0 users flipped
  a new draw each time:  257 on at the first check, 268 at the second, 411 users flipped
The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> search-v2
allow or deny> allow
user id (letters, digits, - _ . @)> user42
user42 added to the allow list of search-v2: user42.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 2
user id (letters, digits, - _ . @)> user42
  new-checkout  off  bucket 5396 is not under 500 (5% rolled out)
  dark-mode     on   bucket 5395 is under 10000 (100% rolled out)
  search-v2     on   in the allow list (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.

1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit
choice> 6
Bye.
What was typed (25 lines)
1
2
ana
2
user42
2
ops-bot
3
new-checkout
20
3
new-checkout
50
3
new-checkout
5
4
search-v2
5
search-v2
allow
user42
2
user42
6

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
# P108 feature flags: environments compared as sets of flags, one user's
# flags and why, a rollout raised or lowered, and stable buckets against a
# new draw at every check.
import flags

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 pad(s, n):
    while len(s) < n:
        s + " " => s
    return s

def listed(xs):
    if len(xs) == 0:
        return "(none)"
    "" => out
    for x in xs:
        if out != "":
            out + ", " => out
        out + x => out
    return out

def percent(bp):
    # hundredths of a percent as a percent
    bp % 100 => f
    str(int((bp - f) / 100)) => s
    if f == 0:
        return s + "%"
    if f < 10:
        return s + ".0" + str(f) + "%"
    return s + "." + str(f) + "%"

def ask_user():
    plain(input("user id (letters, digits, - _ . @)> ")) => u
    if u == "":
        return ""
    if len(u) > 20:
        "Keep a user id to 20 characters." ^0
        return "!"
    for c in u:
        if not ((c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9") or c == "-" or c == "_" or c == "." or c == "@"):
            "A user id has letters, digits and - _ . @ only." ^0
            return "!"
    return u

def ask_flag(rules):
    plain(input("flag (" + listed(rule_names(rules)) + ")> ")) => f
    if f == "":
        return ""
    if not (f in rules):
        "Pick a flag that is rolled out." ^0
        return "!"
    return f

def rule_names(rules):
    [] => out
    for f in flags.known():
        if f in rules:
            out + [f] => out
    return out

def environments(envs):
    "  environment  flags" ^0
    for e in ["dev", "staging", "prod"]:
        ("  " + pad(e, 13) + listed(flags.in_order(envs[e]))) ^0
    for pair in [["dev", "staging"], ["staging", "prod"], ["dev", "prod"]]:
        envs[pair[0]] => a
        envs[pair[1]] => b
        ("Promoting " + pair[0] + " to " + pair[1] + " would turn on " + listed(flags.minus(a, b)) + " and turn off " + listed(flags.minus(b, a)) + ".") ^0
        if flags.subset(a, b) and flags.subset(b, a):
            ("  The two have the same flags.") ^0
        elif flags.subset(a, b):
            ("  " + pair[1] + " already has every flag of " + pair[0] + ".") ^0
        elif flags.subset(b, a):
            ("  " + pair[0] + " has every flag of " + pair[1] + " and more.") ^0
        else:
            ("  Neither has every flag of the other, so neither is ahead: there is only the difference.") ^0

def one_user(rules):
    ask_user() => u
    if u == "":
        "Cancelled." ^0
        return
    if u == "!":
        return
    for f in rule_names(rules):
        flags.decide(rules[f], f, u) => d
        "off" => state
        if d[0]:
            "on " => state
        ("  " + pad(f, 14) + state + "  " + d[1] + " (" + percent(rules[f][0]) + " rolled out)") ^0
    "The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets." ^0

def roll(rules, people):
    ask_flag(rules) => f
    if f == "":
        "Cancelled." ^0
        return rules
    if f == "!":
        return rules
    plain(input("new share in percent (0 to 100, two decimals at most)> ")) => t
    if t == "":
        "Cancelled." ^0
        return rules
    -1 => bp
    "" => whole
    "" => frac
    False => dot
    True => ok
    for c in t:
        if c == "." and not dot:
            True => dot
        elif c >= "0" and c <= "9":
            if dot:
                frac + c => frac
            else:
                whole + c => whole
        else:
            False => ok
    if ok and whole != "" and len(whole) <= 3 and len(frac) <= 2 and not (dot and frac == ""):
        while len(frac) < 2:
            frac + "0" => frac
        int(whole) * 100 + int(frac) => bp
    if bp < 0 or bp > 10000:
        "Type a share from 0 to 100, at most two decimals." ^0
        return rules
    rules[f] => old
    [bp, old[1], old[2]] => new
    flags.change(old, new, f, people) => c
    (f + " from " + percent(old[0]) + " to " + percent(bp) + ", over " + str(len(people)) + " users:") ^0
    ("  on before " + str(c[0]) + ", on after " + str(c[1]) + ": " + str(c[2]) + " stayed on, " + str(c[3]) + " joined, " + str(c[4]) + " left.") ^0
    if bp >= old[0]:
        "Raising the share only adds users: everyone whose bucket was under the old share is under the new one." ^0
    else:
        "Lowering the share only removes users, those whose buckets lie between the new share and the old." ^0
    new => rules[f]
    return rules

def stable_vs_drawn(rules, people):
    ask_flag(rules) => f
    if f == "":
        "Cancelled." ^0
        return
    if f == "!":
        return
    rules[f] => r
    flags.count_on(r, f, people) => n
    flags.rerolled(r[0], people, 2026) => d
    (f + " at " + percent(r[0]) + ", each of " + str(len(people)) + " users checked twice:") ^0
    ("  by bucket:             " + str(n) + " on at the first check, " + str(n) + " at the second, 0 users flipped") ^0
    ("  a new draw each time:  " + str(d[0]) + " on at the first check, " + str(d[1]) + " at the second, " + str(d[2]) + " users flipped") ^0
    "The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next." ^0

def lists(rules):
    ask_flag(rules) => f
    if f == "":
        "Cancelled." ^0
        return rules
    if f == "!":
        return rules
    plain(input("allow or deny> ")) => k
    if k != "allow" and k != "deny":
        if k == "":
            "Cancelled." ^0
        else:
            "Type allow or deny." ^0
        return rules
    ask_user() => u
    if u == "":
        "Cancelled." ^0
        return rules
    if u == "!":
        return rules
    rules[f] => r
    1 => slot
    if k == "deny":
        2 => slot
    if flags.has(r[slot], u):
        (u + " is already in the " + k + " list of " + f + ".") ^0
        return rules
    r[slot] + [u] => r[slot]
    r => rules[f]
    (u + " added to the " + k + " list of " + f + ": " + listed(r[slot]) + ".") ^0
    return rules

{"dev": ["new-checkout", "dark-mode", "search-v2", "beta-banner"], "staging": ["dark-mode", "new-checkout"], "prod": ["dark-mode", "legacy-export"]} => envs
{"new-checkout": [1000, ["ana"], ["ops-bot"]], "dark-mode": [10000, [], []], "search-v2": [2500, [], ["ana"]]} => rules
flags.users(1000) => people

"== Feature flags ==" ^0
"Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps." ^0
True => running
while running:
    "" ^0
    "1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        environments(envs)
    elif choice == "2":
        one_user(rules)
    elif choice == "3":
        roll(rules, people) => rules
    elif choice == "4":
        stable_vs_drawn(rules, people)
    elif choice == "5":
        lists(rules) => rules
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import flags

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 pad(s, n):
    while len(s) < n:
        s = s + " "
    return s

def listed(xs):
    if len(xs) == 0:
        return "(none)"
    out = ""
    for x in xs:
        if out != "":
            out = out + ", "
        out = out + x
    return out

def percent(bp):
    f = bp % 100
    s = str(int((bp - f) / 100))
    if f == 0:
        return s + "%"
    if f < 10:
        return s + ".0" + str(f) + "%"
    return s + "." + str(f) + "%"

def ask_user():
    u = plain(input("user id (letters, digits, - _ . @)> "))
    if u == "":
        return ""
    if len(u) > 20:
        print("Keep a user id to 20 characters.")
        return "!"
    for c in u:
        if not (c >= "a" and c <= "z" or c >= "A" and c <= "Z" or c >= "0" and c <= "9" or c == "-" or c == "_" or c == "." or c == "@"):
            print("A user id has letters, digits and - _ . @ only.")
            return "!"
    return u

def ask_flag(rules):
    f = plain(input("flag (" + listed(rule_names(rules)) + ")> "))
    if f == "":
        return ""
    if not f in rules:
        print("Pick a flag that is rolled out.")
        return "!"
    return f

def rule_names(rules):
    out = []
    for f in flags.known():
        if f in rules:
            out = out + [f]
    return out

def environments(envs):
    print("  environment  flags")
    for e in ["dev", "staging", "prod"]:
        print("  " + pad(e, 13) + listed(flags.in_order(envs[e])))
    for pair in [["dev", "staging"], ["staging", "prod"], ["dev", "prod"]]:
        a = envs[pair[0]]
        b = envs[pair[1]]
        print("Promoting " + pair[0] + " to " + pair[1] + " would turn on " + listed(flags.minus(a, b)) + " and turn off " + listed(flags.minus(b, a)) + ".")
        if flags.subset(a, b) and flags.subset(b, a):
            print("  The two have the same flags.")
        elif flags.subset(a, b):
            print("  " + pair[1] + " already has every flag of " + pair[0] + ".")
        elif flags.subset(b, a):
            print("  " + pair[0] + " has every flag of " + pair[1] + " and more.")
        else:
            print("  Neither has every flag of the other, so neither is ahead: there is only the difference.")

def one_user(rules):
    u = ask_user()
    if u == "":
        print("Cancelled.")
        return
    if u == "!":
        return
    for f in rule_names(rules):
        d = flags.decide(rules[f], f, u)
        state = "off"
        if d[0]:
            state = "on "
        print("  " + pad(f, 14) + state + "  " + d[1] + " (" + percent(rules[f][0]) + " rolled out)")
    print("The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.")

def roll(rules, people):
    f = ask_flag(rules)
    if f == "":
        print("Cancelled.")
        return rules
    if f == "!":
        return rules
    t = plain(input("new share in percent (0 to 100, two decimals at most)> "))
    if t == "":
        print("Cancelled.")
        return rules
    bp = -1
    whole = ""
    frac = ""
    dot = False
    ok = True
    for c in t:
        if c == "." and not dot:
            dot = True
        elif c >= "0" and c <= "9":
            if dot:
                frac = frac + c
            else:
                whole = whole + c
        else:
            ok = False
    if ok and whole != "" and len(whole) <= 3 and len(frac) <= 2 and not (dot and frac == ""):
        while len(frac) < 2:
            frac = frac + "0"
        bp = int(whole) * 100 + int(frac)
    if bp < 0 or bp > 10000:
        print("Type a share from 0 to 100, at most two decimals.")
        return rules
    old = rules[f]
    new = [bp, old[1], old[2]]
    c = flags.change(old, new, f, people)
    print(f + " from " + percent(old[0]) + " to " + percent(bp) + ", over " + str(len(people)) + " users:")
    print("  on before " + str(c[0]) + ", on after " + str(c[1]) + ": " + str(c[2]) + " stayed on, " + str(c[3]) + " joined, " + str(c[4]) + " left.")
    if bp >= old[0]:
        print("Raising the share only adds users: everyone whose bucket was under the old share is under the new one.")
    else:
        print("Lowering the share only removes users, those whose buckets lie between the new share and the old.")
    rules[f] = new
    return rules

def stable_vs_drawn(rules, people):
    f = ask_flag(rules)
    if f == "":
        print("Cancelled.")
        return
    if f == "!":
        return
    r = rules[f]
    n = flags.count_on(r, f, people)
    d = flags.rerolled(r[0], people, 2026)
    print(f + " at " + percent(r[0]) + ", each of " + str(len(people)) + " users checked twice:")
    print("  by bucket:             " + str(n) + " on at the first check, " + str(n) + " at the second, 0 users flipped")
    print("  a new draw each time:  " + str(d[0]) + " on at the first check, " + str(d[1]) + " at the second, " + str(d[2]) + " users flipped")
    print("The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next.")

def lists(rules):
    f = ask_flag(rules)
    if f == "":
        print("Cancelled.")
        return rules
    if f == "!":
        return rules
    k = plain(input("allow or deny> "))
    if k != "allow" and k != "deny":
        if k == "":
            print("Cancelled.")
        else:
            print("Type allow or deny.")
        return rules
    u = ask_user()
    if u == "":
        print("Cancelled.")
        return rules
    if u == "!":
        return rules
    r = rules[f]
    slot = 1
    if k == "deny":
        slot = 2
    if flags.has(r[slot], u):
        print(u + " is already in the " + k + " list of " + f + ".")
        return rules
    r[slot] = r[slot] + [u]
    rules[f] = r
    print(u + " added to the " + k + " list of " + f + ": " + listed(r[slot]) + ".")
    return rules

envs = {"dev": ["new-checkout", "dark-mode", "search-v2", "beta-banner"], "staging": ["dark-mode", "new-checkout"], "prod": ["dark-mode", "legacy-export"]}
rules = {"new-checkout": [1000, ["ana"], ["ops-bot"]], "dark-mode": [10000, [], []], "search-v2": [2500, [], ["ana"]]}
people = flags.users(1000)
print("== Feature flags ==")
print("Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.")
running = True
while running:
    print("")
    print("1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        environments(envs)
    elif choice == "2":
        one_user(rules)
    elif choice == "3":
        rules = roll(rules, people)
    elif choice == "4":
        stable_vs_drawn(rules, people)
    elif choice == "5":
        rules = lists(rules)
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")

flags.eml

eml
# P108 feature flags - as in the corpus case feature-flag-rollout, whether
# one environment already has every flag of another is a subset question,
# and what promoting it would change is a difference. A flag can also be
# rolled out to a share of users: each user falls in a bucket from 0 to
# 9999 by a hash of the flag's name and the user's id, the same bucket
# every time, so raising the share only ever adds users. An allow list
# turns the flag on for a user and a deny list turns it off, the deny list
# first.

def known():
    # every flag, in the order reports list them
    return ["new-checkout", "dark-mode", "search-v2", "beta-banner", "legacy-export"]

def has(xs, x):
    for y in xs:
        if y == x:
            return True
    return False

def subset(a, b):
    # every flag of a is in b
    for x in a:
        if not has(b, x):
            return False
    return True

def minus(a, b):
    # the flags of a not in b, in the known order
    [] => out
    for x in known():
        if has(a, x) and not has(b, x):
            out + [x] => out
    return out

def in_order(a):
    [] => out
    for x in known():
        if has(a, x):
            out + [x] => out
    return out

def codes():
    # every character a flag name or a user id can hold, numbered from 1
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@:" => chars
    {} => out
    for i in [0:len(chars) - 1]:
        i + 1 => out[chars[i]]
    return out

codes() => code_table

def bucket(flag, user):
    # 0 to 9999: each character of flag:user is folded in and the result
    # cubed, modulo the prime P = 4294967291, then taken modulo 10000.
    # Cubing is a one-to-one map modulo P (3 does not divide P - 1) and is
    # not linear: a plain polynomial hash gives one user's buckets for two
    # flags a correlation of 0.32, so a user early in one rollout would
    # tend to be early in the next.
    flag + ":" + user => s
    0 => h
    for c in s:
        (h * 257 + code_table[c]) % 4294967291 => t
        t * t % 4294967291 * t % 4294967291 => h
    return h % 10000

def decide(rule, flag, user):
    # rule is [share in hundredths of a percent, allow list, deny list];
    # [on, why]
    if has(rule[2], user):
        return [False, "in the deny list"]
    if has(rule[1], user):
        return [True, "in the allow list"]
    bucket(flag, user) => b
    if b < rule[0]:
        return [True, "bucket " + str(b) + " is under " + str(rule[0])]
    return [False, "bucket " + str(b) + " is not under " + str(rule[0])]

def users(n):
    [] => out
    for i in [1:n]:
        out + ["user" + str(i)] => out
    return out

def count_on(rule, flag, people):
    0 => n
    for u in people:
        if decide(rule, flag, u)[0]:
            n + 1 => n
    return n

def change(old, new, flag, people):
    # [on before, on after, stayed on, joined, left]
    0 => before
    0 => after
    0 => stayed
    0 => joined
    0 => left
    for u in people:
        decide(old, flag, u)[0] => a
        decide(new, flag, u)[0] => b
        if a:
            before + 1 => before
        if b:
            after + 1 => after
        if a and b:
            stayed + 1 => stayed
        if b and not a:
            joined + 1 => joined
        if a and not b:
            left + 1 => left
    return [before, after, stayed, joined, left]

def rerolled(share, people, seed):
    # every check draws a new number from 0 to 9999 instead of the bucket:
    # [on at the first check, on at the second, users who flipped]
    [seed] => x
    0 => first
    0 => second
    0 => flipped
    for u in people:
        (1664525 * x[0] + 1013904223) % 4294967296 => x[0]
        (int((x[0] - x[0] % 65536) / 65536) % 10000 < share) => a
        (1664525 * x[0] + 1013904223) % 4294967296 => x[0]
        (int((x[0] - x[0] % 65536) / 65536) % 10000 < share) => b
        if a:
            first + 1 => first
        if b:
            second + 1 => second
        if a != b:
            flipped + 1 => flipped
    return [first, second, flipped]
Python projection (flags.py)
def known():
    return ["new-checkout", "dark-mode", "search-v2", "beta-banner", "legacy-export"]

def has(xs, x):
    for y in xs:
        if y == x:
            return True
    return False

def subset(a, b):
    for x in a:
        if not has(b, x):
            return False
    return True

def minus(a, b):
    out = []
    for x in known():
        if has(a, x) and not has(b, x):
            out = out + [x]
    return out

def in_order(a):
    out = []
    for x in known():
        if has(a, x):
            out = out + [x]
    return out

def codes():
    chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@:"
    out = {}
    for i in range(0, len(chars)):
        out[chars[i]] = i + 1
    return out

code_table = codes()

def bucket(flag, user):
    s = flag + ":" + user
    h = 0
    for c in s:
        t = (h * 257 + code_table[c]) % 4294967291
        h = t * t % 4294967291 * t % 4294967291
    return h % 10000

def decide(rule, flag, user):
    if has(rule[2], user):
        return [False, "in the deny list"]
    if has(rule[1], user):
        return [True, "in the allow list"]
    b = bucket(flag, user)
    if b < rule[0]:
        return [True, "bucket " + str(b) + " is under " + str(rule[0])]
    return [False, "bucket " + str(b) + " is not under " + str(rule[0])]

def users(n):
    out = []
    for i in range(1, n+1):
        out = out + ["user" + str(i)]
    return out

def count_on(rule, flag, people):
    n = 0
    for u in people:
        if decide(rule, flag, u)[0]:
            n = n + 1
    return n

def change(old, new, flag, people):
    before = 0
    after = 0
    stayed = 0
    joined = 0
    left = 0
    for u in people:
        a = decide(old, flag, u)[0]
        b = decide(new, flag, u)[0]
        if a:
            before = before + 1
        if b:
            after = after + 1
        if a and b:
            stayed = stayed + 1
        if b and not a:
            joined = joined + 1
        if a and not b:
            left = left + 1
    return [before, after, stayed, joined, left]

def rerolled(share, people, seed):
    x = [seed]
    first = 0
    second = 0
    flipped = 0
    for u in people:
        x[0] = (1664525 * x[0] + 1013904223) % 4294967296
        a = int((x[0] - x[0] % 65536) / 65536) % 10000 < share
        x[0] = (1664525 * x[0] + 1013904223) % 4294967296
        b = int((x[0] - x[0] % 65536) / 65536) % 10000 < share
        if a:
            first = first + 1
        if b:
            second = second + 1
        if a != b:
            flipped = flipped + 1
    return [first, second, flipped]

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.