Project P109

Permissions

Roles as sets of actions in a partial order; folders that grant roles and deny actions, inherited by every document below them with the reason for each action; and documents moved without their stored grants being refreshed, audited for grants too many and too few.

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

Roles as sets of actions, folders that grant roles to users and can deny actions, and documents that inherit from every folder above them. The program also shows what happens when a document is moved and its stored grants are not refreshed.

  • main.eml - the menu, the roles and how they compare, the folder tree, one user on one document, moves, writes and the audit
  • perms.eml - the actions and roles, subset questions on lists, the order between roles, the folder chain, effective permissions, refresh and audit

How each part works:

  • Roles: viewer (read), editor (read, write), auditor (read, export) and owner (all five actions). As in the corpus case permission-set-algebra, one role is below another when every one of its actions is in the other. That is a partial order: editor and auditor each have an action the other lacks, so neither is below the other.
  • The program lists the roles directly above one another, and sorts the roles by "below" from two starting orders. Editor and auditor keep whichever order they came in, so the sort has no single answer.
  • Effective permissions: the union of every role granted to the user on the document's folder and on every folder above it, minus every action denied there. A deny wins over any grant. Each allowed action names the nearest folder that gives it.
  • Stored grants, as in the corpus case inherited-permission-after-move: each document keeps what its folder gave each user when it was last written. A move changes the folder, not the document, so nothing is refreshed. The audit lists every grant where the stored answer and the folder's answer differ, too much or too little. Writing a document refreshes its grants.

What is checked: menu choices 1 to 7; one of the five users; one of the documents; one of the four folders; a move to a different folder. An empty answer cancels.

Sessions: sessions/basic.in gives: - the roles, their order, and the two sorts; - the folder tree; - ben on contract.pdf (denied in legal), cy (auditor), dee on plan.txt, and guest on plan.txt (nothing); - the audit with nothing out of date; - contract.pdf moved to public and brochure.pdf to legal, the tree again, guest on both, and the audit: 3 grants too many and 3 too few; - contract.pdf written, and the audit with its entries gone.

sessions/bad-input.in gives: - menu choices 0 and x; - a cancel, an unknown user, a cancel and an unknown document; - moves: a cancel, an unknown document, a cancel, an unknown folder, and a move to the folder it is in; - writes: a cancel and an unknown document.

Built on the verified corpus cases permission-set-algebra (for sets, "less than" means subset, and the order is partial) and inherited-permission-after-move (the grants stored on a document are refreshed on a write, and a move is not a write).

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
== Permissions ==
Roles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 0
Pick a number from 1 to 7.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> x
Pick a number from 1 to 7.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> 
Cancelled.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> zed
Type ana, ben, cy, dee or guest.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> ana
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> 
Cancelled.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> ana
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> x.doc
Pick one of the documents listed.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 4
document> 
Cancelled.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 4
document> x.doc
Pick one of the documents listed.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 4
document> plan.txt
to folder (company, team, legal or public)> 
Cancelled.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 4
document> plan.txt
to folder (company, team, legal or public)> home
Type company, team, legal or public.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 4
document> plan.txt
to folder (company, team, legal or public)> team
plan.txt is already in team.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 5
document> 
Cancelled.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 5
document> x.doc
Pick one of the documents listed.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 7
Bye.
What was typed (30 lines)
0
x
3

3
zed
3
ana

3
ana
x.doc
4

4
x.doc
4
plan.txt

4
plan.txt
home
4
plan.txt
team
5

5
x.doc
7

basic

interpreter: byte-equal
== Permissions ==
Roles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 1
  viewer   read
  editor   read, write
  auditor  read, export
  owner    read, write, export, delete, share
  viewer < editor: every action of viewer is in editor
  viewer < auditor: every action of viewer is in auditor
  viewer < owner: every action of viewer is in owner
  editor and auditor: neither - each has an action the other lacks, so <, <=, > and >= are all false
  editor < owner: every action of editor is in owner
  auditor < owner: every action of auditor is in owner
Directly above one another: viewer -> editor, viewer -> auditor, editor -> owner, auditor -> owner.
Sorted by <, from two starting orders: viewer, editor, auditor, owner / viewer, auditor, editor, owner. Roles that are neither keep whatever order they came in.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 2
  company    ana owner, ben viewer, cy viewer
    team     ben editor, dee editor
      documents: notes.txt, plan.txt
    legal    cy auditor; deny ben read
      documents: contract.pdf
    public   guest viewer
      documents: brochure.pdf

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> ben
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> contract.pdf
ben on contract.pdf (in legal): nothing.
  read   denied

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> cy
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> contract.pdf
cy on contract.pdf (in legal): read, export.
  read   auditor on legal
  export auditor on legal

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> dee
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> plan.txt
dee on plan.txt (in team): read, write.
  read   editor on team
  write  editor on team

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> guest
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> plan.txt
guest on plan.txt (in team): nothing.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 6
Every stored grant matches what the folders give now.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 4
document> contract.pdf
to folder (company, team, legal or public)> public
contract.pdf moved from legal to public. Its stored grants were not refreshed: a move changes the folder, not the document.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 4
document> brochure.pdf
to folder (company, team, legal or public)> legal
brochure.pdf moved from public to legal. Its stored grants were not refreshed: a move changes the folder, not the document.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 2
  company    ana owner, ben viewer, cy viewer
    team     ben editor, dee editor
      documents: notes.txt, plan.txt
    legal    cy auditor; deny ben read
      documents: brochure.pdf
    public   guest viewer
      documents: contract.pdf

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> guest
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> contract.pdf
guest on contract.pdf (in public): read.
  read   viewer on public
  The grants stored on the document still say: nothing.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 3
user (ana, ben, cy, dee or guest)> guest
document (brochure.pdf, contract.pdf, notes.txt, plan.txt)> brochure.pdf
guest on brochure.pdf (in legal): nothing.
  The grants stored on the document still say: read.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 6
  brochure.pdf ben   read   stored yes, folder no - more than it should
  brochure.pdf cy    export stored no, folder yes - less than it should
  brochure.pdf guest read   stored yes, folder no - more than it should
  contract.pdf ben   read   stored no, folder yes - less than it should
  contract.pdf cy    export stored yes, folder no - more than it should
  contract.pdf guest read   stored no, folder yes - less than it should
6 stored grants are out of date: 3 give more than the folder does, 3 less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 5
document> contract.pdf
contract.pdf written: its stored grants were refreshed from public.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 6
  brochure.pdf ben   read   stored yes, folder no - more than it should
  brochure.pdf cy    export stored no, folder yes - less than it should
  brochure.pdf guest read   stored yes, folder no - more than it should
3 stored grants are out of date: 2 give more than the folder does, 1 less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.

1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit
choice> 7
Bye.
What was typed (33 lines)
1
2
3
ben
contract.pdf
3
cy
contract.pdf
3
dee
plan.txt
3
guest
plan.txt
6
4
contract.pdf
public
4
brochure.pdf
legal
2
3
guest
contract.pdf
3
guest
brochure.pdf
6
5
contract.pdf
6
7

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
# P109 permissions: roles compared as sets, folders that grant and deny,
# what a user may do with a document and why, and documents moved without
# their stored grants being refreshed.
import perms

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 "nothing"
    "" => out
    for x in xs:
        if out != "":
            out + ", " => out
        out + x => out
    return out

def show_roles():
    perms.roles() => r
    for n in perms.role_names():
        ("  " + pad(n, 9) + listed(r[n])) ^0
    perms.role_names() => names
    for i in [0:len(names) - 1]:
        for j in [i + 1:len(names) - 1]:
            perms.compare(names[i], names[j]) => c
            if c == "below":
                ("  " + names[i] + " < " + names[j] + ": every action of " + names[i] + " is in " + names[j]) ^0
            elif c == "above":
                ("  " + names[i] + " > " + names[j]) ^0
            elif c == "neither":
                ("  " + names[i] + " and " + names[j] + ": neither - each has an action the other lacks, so <, <=, > and >= are all false") ^0
    "" => line
    for c in perms.covers():
        if line != "":
            line + ", " => line
        line + c[0] + " -> " + c[1] => line
    ("Directly above one another: " + line + ".") ^0
    perms.sort_by_less(["editor", "auditor", "viewer", "owner"]) => a
    perms.sort_by_less(["auditor", "editor", "viewer", "owner"]) => b
    ("Sorted by <, from two starting orders: " + listed(a) + " / " + listed(b) + ". Roles that are neither keep whatever order they came in.") ^0

def show_tree(folders, docs):
    for f in ["company", "team", "legal", "public"]:
        folders[f] => fo
        "" => g
        for u in ["ana", "ben", "cy", "dee", "guest"]:
            if u in fo[1]:
                if g != "":
                    g + ", " => g
                g + u + " " + fo[1][u] => g
        for u in ["ana", "ben", "cy", "dee", "guest"]:
            if u in fo[2]:
                if g != "":
                    g + "; " => g
                g + "deny " + u + " " + listed(fo[2][u]) => g
        "" => indent
        if fo[0] != "":
            "  " => indent
        ("  " + pad(indent + f, 11) + g) ^0
        "" => here
        for d in perms.sorted_docs(docs):
            if docs[d][0] == f:
                if here != "":
                    here + ", " => here
                here + d => here
        if here != "":
            ("  " + indent + "  documents: " + here) ^0

def check(folders, docs):
    plain(input("user (ana, ben, cy, dee or guest)> ")) => u
    if u == "":
        "Cancelled." ^0
        return
    if not perms.has(["ana", "ben", "cy", "dee", "guest"], u):
        "Type ana, ben, cy, dee or guest." ^0
        return
    plain(input("document (" + listed(perms.sorted_docs(docs)) + ")> ")) => d
    if d == "":
        "Cancelled." ^0
        return
    if not (d in docs):
        "Pick one of the documents listed." ^0
        return
    perms.effective(folders, docs[d][0], u) => e
    (u + " on " + d + " (in " + docs[d][0] + "): " + listed(e[0]) + ".") ^0
    for r in e[1]:
        ("  " + pad(r[0], 7) + r[1]) ^0
    docs[d][1][u] => cached
    if not perms.subset(cached, e[0]) or not perms.subset(e[0], cached):
        ("  The grants stored on the document still say: " + listed(perms.ordered(cached)) + ".") ^0

def move(folders, docs):
    plain(input("document> ")) => d
    if d == "":
        "Cancelled." ^0
        return
    if not (d in docs):
        "Pick one of the documents listed." ^0
        return
    plain(input("to folder (company, team, legal or public)> ")) => f
    if f == "":
        "Cancelled." ^0
        return
    if not (f in folders):
        "Type company, team, legal or public." ^0
        return
    if docs[d][0] == f:
        (d + " is already in " + f + ".") ^0
        return
    docs[d][0] => old
    f => docs[d][0]
    (d + " moved from " + old + " to " + f + ". Its stored grants were not refreshed: a move changes the folder, not the document.") ^0

def edit(folders, docs, users):
    plain(input("document> ")) => d
    if d == "":
        "Cancelled." ^0
        return
    if not (d in docs):
        "Pick one of the documents listed." ^0
        return
    perms.refresh(folders, docs, d, users)
    (d + " written: its stored grants were refreshed from " + docs[d][0] + ".") ^0

def show_audit(folders, docs, users):
    perms.audit(folders, docs, users) => a
    if len(a) == 0:
        "Every stored grant matches what the folders give now." ^0
        return
    0 => over
    0 => under
    for x in a:
        if x[3] == "over":
            over + 1 => over
            ("  " + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + "stored yes, folder no - more than it should") ^0
        else:
            under + 1 => under
            ("  " + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + "stored no, folder yes - less than it should") ^0
    (str(len(a)) + " stored grants are out of date: " + str(over) + " give more than the folder does, " + str(under) + " less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.") ^0

{"company": ["", {"ana": "owner", "ben": "viewer", "cy": "viewer"}, {}], "team": ["company", {"ben": "editor", "dee": "editor"}, {}], "legal": ["company", {"cy": "auditor"}, {"ben": ["read"]}], "public": ["company", {"guest": "viewer"}, {}]} => folders
{"plan.txt": ["team", {}], "notes.txt": ["team", {}], "contract.pdf": ["legal", {}], "brochure.pdf": ["public", {}]} => docs
["ana", "ben", "cy", "dee", "guest"] => users
for d in perms.sorted_docs(docs):
    perms.refresh(folders, docs, d, users)

"== Permissions ==" ^0
"Roles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them." ^0
True => running
while running:
    "" ^0
    "1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_roles()
    elif choice == "2":
        show_tree(folders, docs)
    elif choice == "3":
        check(folders, docs)
    elif choice == "4":
        move(folders, docs)
    elif choice == "5":
        edit(folders, docs, users)
    elif choice == "6":
        show_audit(folders, docs, users)
    elif choice == "7":
        False => running
    else:
        "Pick a number from 1 to 7." ^0
"Bye." ^0
Python projection (main.py)
import perms

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 "nothing"
    out = ""
    for x in xs:
        if out != "":
            out = out + ", "
        out = out + x
    return out

def show_roles():
    r = perms.roles()
    for n in perms.role_names():
        print("  " + pad(n, 9) + listed(r[n]))
    names = perms.role_names()
    for i in range(0, len(names)):
        for j in range(i + 1, len(names)):
            c = perms.compare(names[i], names[j])
            if c == "below":
                print("  " + names[i] + " < " + names[j] + ": every action of " + names[i] + " is in " + names[j])
            elif c == "above":
                print("  " + names[i] + " > " + names[j])
            elif c == "neither":
                print("  " + names[i] + " and " + names[j] + ": neither - each has an action the other lacks, so <, <=, > and >= are all false")
    line = ""
    for c in perms.covers():
        if line != "":
            line = line + ", "
        line = line + c[0] + " -> " + c[1]
    print("Directly above one another: " + line + ".")
    a = perms.sort_by_less(["editor", "auditor", "viewer", "owner"])
    b = perms.sort_by_less(["auditor", "editor", "viewer", "owner"])
    print("Sorted by <, from two starting orders: " + listed(a) + " / " + listed(b) + ". Roles that are neither keep whatever order they came in.")

def show_tree(folders, docs):
    for f in ["company", "team", "legal", "public"]:
        fo = folders[f]
        g = ""
        for u in ["ana", "ben", "cy", "dee", "guest"]:
            if u in fo[1]:
                if g != "":
                    g = g + ", "
                g = g + u + " " + fo[1][u]
        for u in ["ana", "ben", "cy", "dee", "guest"]:
            if u in fo[2]:
                if g != "":
                    g = g + "; "
                g = g + "deny " + u + " " + listed(fo[2][u])
        indent = ""
        if fo[0] != "":
            indent = "  "
        print("  " + pad(indent + f, 11) + g)
        here = ""
        for d in perms.sorted_docs(docs):
            if docs[d][0] == f:
                if here != "":
                    here = here + ", "
                here = here + d
        if here != "":
            print("  " + indent + "  documents: " + here)

def check(folders, docs):
    u = plain(input("user (ana, ben, cy, dee or guest)> "))
    if u == "":
        print("Cancelled.")
        return
    if not perms.has(["ana", "ben", "cy", "dee", "guest"], u):
        print("Type ana, ben, cy, dee or guest.")
        return
    d = plain(input("document (" + listed(perms.sorted_docs(docs)) + ")> "))
    if d == "":
        print("Cancelled.")
        return
    if not d in docs:
        print("Pick one of the documents listed.")
        return
    e = perms.effective(folders, docs[d][0], u)
    print(u + " on " + d + " (in " + docs[d][0] + "): " + listed(e[0]) + ".")
    for r in e[1]:
        print("  " + pad(r[0], 7) + r[1])
    cached = docs[d][1][u]
    if not perms.subset(cached, e[0]) or not perms.subset(e[0], cached):
        print("  The grants stored on the document still say: " + listed(perms.ordered(cached)) + ".")

def move(folders, docs):
    d = plain(input("document> "))
    if d == "":
        print("Cancelled.")
        return
    if not d in docs:
        print("Pick one of the documents listed.")
        return
    f = plain(input("to folder (company, team, legal or public)> "))
    if f == "":
        print("Cancelled.")
        return
    if not f in folders:
        print("Type company, team, legal or public.")
        return
    if docs[d][0] == f:
        print(d + " is already in " + f + ".")
        return
    old = docs[d][0]
    docs[d][0] = f
    print(d + " moved from " + old + " to " + f + ". Its stored grants were not refreshed: a move changes the folder, not the document.")

def edit(folders, docs, users):
    d = plain(input("document> "))
    if d == "":
        print("Cancelled.")
        return
    if not d in docs:
        print("Pick one of the documents listed.")
        return
    perms.refresh(folders, docs, d, users)
    print(d + " written: its stored grants were refreshed from " + docs[d][0] + ".")

def show_audit(folders, docs, users):
    a = perms.audit(folders, docs, users)
    if len(a) == 0:
        print("Every stored grant matches what the folders give now.")
        return
    over = 0
    under = 0
    for x in a:
        if x[3] == "over":
            over = over + 1
            print("  " + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + "stored yes, folder no - more than it should")
        else:
            under = under + 1
            print("  " + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + "stored no, folder yes - less than it should")
    print(str(len(a)) + " stored grants are out of date: " + str(over) + " give more than the folder does, " + str(under) + " less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.")

folders = {"company": ["", {"ana": "owner", "ben": "viewer", "cy": "viewer"}, {}], "team": ["company", {"ben": "editor", "dee": "editor"}, {}], "legal": ["company", {"cy": "auditor"}, {"ben": ["read"]}], "public": ["company", {"guest": "viewer"}, {}]}
docs = {"plan.txt": ["team", {}], "notes.txt": ["team", {}], "contract.pdf": ["legal", {}], "brochure.pdf": ["public", {}]}
users = ["ana", "ben", "cy", "dee", "guest"]
for d in perms.sorted_docs(docs):
    perms.refresh(folders, docs, d, users)
print("== Permissions ==")
print("Roles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them.")
running = True
while running:
    print("")
    print("1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_roles()
    elif choice == "2":
        show_tree(folders, docs)
    elif choice == "3":
        check(folders, docs)
    elif choice == "4":
        move(folders, docs)
    elif choice == "5":
        edit(folders, docs, users)
    elif choice == "6":
        show_audit(folders, docs, users)
    elif choice == "7":
        running = False
    else:
        print("Pick a number from 1 to 7.")
print("Bye.")

perms.eml

eml
# P109 permissions - roles as sets of actions, folders that grant roles to
# users and can deny actions, and documents that inherit from every folder
# above them. As in the corpus case permission-set-algebra, comparing two
# roles is a subset question and the answer can be "neither": a partial
# order. As in the corpus case inherited-permission-after-move, a
# document keeps its resolved grants and refreshes them on a write, and a
# move is not a write.
#
# Lists stand in for sets, always reported in a fixed order.

def actions():
    return ["read", "write", "export", "delete", "share"]

def role_names():
    return ["viewer", "editor", "auditor", "owner"]

def roles():
    return {"viewer": ["read"], "editor": ["read", "write"], "auditor": ["read", "export"], "owner": ["read", "write", "export", "delete", "share"]}

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 ordered(xs):
    # actions in the fixed order
    [] => out
    for a in actions():
        if has(xs, a):
            out + [a] => out
    return out

def compare(a, b):
    # how role a stands to role b: "same", "below", "above" or "neither"
    roles() => r
    subset(r[a], r[b]) => ab
    subset(r[b], r[a]) => ba
    if ab and ba:
        return "same"
    if ab:
        return "below"
    if ba:
        return "above"
    return "neither"

def covers():
    # [lower, upper]: upper holds lower and more, with no role in between
    [] => out
    role_names() => names
    for a in names:
        for b in names:
            if compare(a, b) == "below":
                True => direct
                for c in names:
                    if compare(a, c) == "below" and compare(c, b) == "below":
                        False => direct
                if direct:
                    out + [[a, b]] => out
    return out

def sort_by_less(xs):
    # an insertion sort that trusts "a is below b": with roles that are
    # neither, the result follows the order they came in
    [] => out
    for x in xs:
        out + [x] => out
    for i in [1:len(out) - 1]:
        i => j
        while j > 0 and compare(out[j], out[j - 1]) == "below":
            out[j] => t
            out[j - 1] => out[j]
            t => out[j - 1]
            j - 1 => j
    return out

def chain(folders, name):
    # the folder and every folder above it, nearest first
    [] => out
    name => f
    while f != "":
        out + [f] => out
        folders[f][0] => f
    return out

def effective(folders, folder, user):
    # [actions allowed, [[action, why], ...]]: every role granted to the
    # user on this folder or above, minus every action denied there; a deny
    # wins over any grant
    roles() => r
    [] => allowed
    [] => denied
    [] => why
    for f in chain(folders, folder):
        folders[f][1] => grants
        if user in grants:
            for a in r[grants[user]]:
                if not has(allowed, a):
                    allowed + [a] => allowed
                    why + [[a, grants[user] + " on " + f]] => why
        folders[f][2] => denies
        if user in denies:
            for a in denies[user]:
                if not has(denied, a):
                    denied + [a] => denied
    [] => out
    [] => reasons
    for a in actions():
        if has(denied, a):
            reasons + [[a, "denied"]] => reasons
        elif has(allowed, a):
            out + [a] => out
            for w in why:
                if w[0] == a:
                    reasons + [[a, w[1]]] => reasons
    return [out, reasons]

def refresh(folders, docs, name, users):
    # store what the document's folder gives each user now
    {} => cache
    for u in users:
        effective(folders, docs[name][0], u)[0] => cache[u]
    cache => docs[name][1]

def audit(folders, docs, users):
    # [[doc, user, action, "over" or "under"], ...] where the cached grants
    # differ from what the folder gives now: "over" when the cache allows
    # and the folder does not
    [] => out
    for d in sorted_docs(docs):
        for u in users:
            docs[d][1][u] => cached
            effective(folders, docs[d][0], u)[0] => now
            for a in actions():
                if has(cached, a) and not has(now, a):
                    out + [[d, u, a, "over"]] => out
                elif has(now, a) and not has(cached, a):
                    out + [[d, u, a, "under"]] => out
    return out

def sorted_docs(docs):
    [] => out
    for d in docs:
        out + [d] => out
    for i in [1:len(out) - 1]:
        i => j
        while j > 0 and out[j] < out[j - 1]:
            out[j] => t
            out[j - 1] => out[j]
            t => out[j - 1]
            j - 1 => j
    return out
Python projection (perms.py)
def actions():
    return ["read", "write", "export", "delete", "share"]

def role_names():
    return ["viewer", "editor", "auditor", "owner"]

def roles():
    return {"viewer": ["read"], "editor": ["read", "write"], "auditor": ["read", "export"], "owner": ["read", "write", "export", "delete", "share"]}

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 ordered(xs):
    out = []
    for a in actions():
        if has(xs, a):
            out = out + [a]
    return out

def compare(a, b):
    r = roles()
    ab = subset(r[a], r[b])
    ba = subset(r[b], r[a])
    if ab and ba:
        return "same"
    if ab:
        return "below"
    if ba:
        return "above"
    return "neither"

def covers():
    out = []
    names = role_names()
    for a in names:
        for b in names:
            if compare(a, b) == "below":
                direct = True
                for c in names:
                    if compare(a, c) == "below" and compare(c, b) == "below":
                        direct = False
                if direct:
                    out = out + [[a, b]]
    return out

def sort_by_less(xs):
    out = []
    for x in xs:
        out = out + [x]
    for i in range(1, len(out)):
        j = i
        while j > 0 and compare(out[j], out[j - 1]) == "below":
            t = out[j]
            out[j] = out[j - 1]
            out[j - 1] = t
            j = j - 1
    return out

def chain(folders, name):
    out = []
    f = name
    while f != "":
        out = out + [f]
        f = folders[f][0]
    return out

def effective(folders, folder, user):
    r = roles()
    allowed = []
    denied = []
    why = []
    for f in chain(folders, folder):
        grants = folders[f][1]
        if user in grants:
            for a in r[grants[user]]:
                if not has(allowed, a):
                    allowed = allowed + [a]
                    why = why + [[a, grants[user] + " on " + f]]
        denies = folders[f][2]
        if user in denies:
            for a in denies[user]:
                if not has(denied, a):
                    denied = denied + [a]
    out = []
    reasons = []
    for a in actions():
        if has(denied, a):
            reasons = reasons + [[a, "denied"]]
        elif has(allowed, a):
            out = out + [a]
            for w in why:
                if w[0] == a:
                    reasons = reasons + [[a, w[1]]]
    return [out, reasons]

def refresh(folders, docs, name, users):
    cache = {}
    for u in users:
        cache[u] = effective(folders, docs[name][0], u)[0]
    docs[name][1] = cache

def audit(folders, docs, users):
    out = []
    for d in sorted_docs(docs):
        for u in users:
            cached = docs[d][1][u]
            now = effective(folders, docs[d][0], u)[0]
            for a in actions():
                if has(cached, a) and not has(now, a):
                    out = out + [[d, u, a, "over"]]
                elif has(now, a) and not has(cached, a):
                    out = out + [[d, u, a, "under"]]
    return out

def sorted_docs(docs):
    out = []
    for d in docs:
        out = out + [d]
    for i in range(1, len(out)):
        j = i
        while j > 0 and out[j] < out[j - 1]:
            t = out[j]
            out[j] = out[j - 1]
            out[j - 1] = t
            j = j - 1
    return out

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.