Event log
An account replayed from its events beside a stored balance column one write path forgets: a check that compares the column with itself, a replay that finds where they part, and how many orders of the events end where the log does.
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
An account kept two ways: the balance replayed from its events, and a stored balance column that each write path updates.
main.eml- the menu, the log with both balances, the two reconciliations, every order of the events, and adding or removing oneevents.eml- an event's effect, a replay, the ledger with the stored column, every order, and which pairs of events commute
How each part works:
- An event is a deposit, a withdrawal (refused if more than the balance), a fee of a percentage of the balance (rounded down), or an adjustment of either sign.
- As in the corpus case
denormalized-total-drifts, the adjustment's write path forgets the stored column. The column is always the replayed balance less the adjustments: after adjust +20 it is 20 short, and a later withdrawal of 120 takes it to -7, while the replayed balance is 13. - The check the system has compares the column with a copy kept by the same write paths, so it compares a number with itself and never finds a difference. Replaying the events finds it, and where they part.
- As in the corpus case
state-from-events-vs-stored, replaying is right only in the order the events happened: the 720 orders of the 6 events end at 40 different balances, and only 3 of the 15 pairs give the same balance in either order from every balance from 0 to 300.
What is checked: menu choices 1 to 6; an event as deposit, withdraw, fee or adjust and a whole number up to 9999 (a fee from 1 to 100%, only an adjustment negative), at most 6 events. An empty answer cancels.
Sessions: sessions/basic.in gives: - the log, the reconciliations and every order; - the last event removed, and the same again; - fee 5% added at the end, and every order again.
sessions/bad-input.in gives: - menu choices 0 and x; - one event removed; then events: a cancel, pay 10, deposit alone, x, 0 and 10000, fee 101 and withdraw -5, then adjust -15, and a seventh; - the log emptied one event at a time, with every order, the log and the reconciliations on the way and at the end.
Built on the verified corpus cases denormalized-total-drifts (a stored total kept by every write path, and a reconciliation that compares it with itself) and state-from-events-vs-stored (replaying events is right only for some orders).
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== Event log ==
An account kept two ways: the balance replayed from its events, and a stored balance column that each write path updates.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 0
Pick a number from 1 to 6.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> x
Pick a number from 1 to 6.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 5 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event>
Cancelled.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> pay 10
Type deposit, withdraw, fee or adjust, then a number.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> deposit
Type a whole number up to 9999 (a fee from 1 to 100).
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> deposit x
Type a whole number up to 9999 (a fee from 1 to 100).
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> deposit 0
Type a whole number up to 9999 (a fee from 1 to 100).
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> deposit 10000
Type a whole number up to 9999 (a fee from 1 to 100).
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> fee 101
Type a whole number up to 9999 (a fee from 1 to 100).
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> withdraw -5
Type a whole number up to 9999 (a fee from 1 to 100).
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> adjust -15
Added: adjust -15.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
6 events is the most.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 5 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 4 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 3 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 2 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 1 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 3
Two events or more are needed.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 0 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 1
The log is empty.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 2
The log is empty.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 3
Two events or more are needed.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
The log is empty.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 6
Bye.
What was typed (34 lines)
0
x
5
4
4
pay 10
4
deposit
4
deposit x
4
deposit 0
4
deposit 10000
4
fee 101
4
withdraw -5
4
adjust -15
4
5
5
5
5
5
3
5
1
2
3
5
6
basic
interpreter: byte-equal== Event log ==
An account kept two ways: the balance replayed from its events, and a stored balance column that each write path updates.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 1
# event replayed stored
1 deposit 100 100 100
2 withdraw 30 70 70
3 fee 10% = 7 63 63
4 deposit 50 113 113
5 adjust +20 133 113
6 withdraw 120 13 -7
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 2
The check the system has: the stored column, -7, against the copy the same write paths keep, -7: no difference.
Replaying the events: 13, and stored says -7: off by -20.
They part at event 5, adjust +20: that write path never touches the stored column.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 3
Every order of these 6 events, replayed: 720 orders, 40 different final balances; 8 end as the log does.
Of the 15 pairs of events, 3 give the same balance in either order from every starting balance from 0 to 300.
Replaying gives the truth only in the order the events happened: the log's order is part of what it records.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 5
Removed the last event; 5 left.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 1
# event replayed stored
1 deposit 100 100 100
2 withdraw 30 70 70
3 fee 10% = 7 63 63
4 deposit 50 113 113
5 adjust +20 133 113
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 2
The check the system has: the stored column, 113, against the copy the same write paths keep, 113: no difference.
Replaying the events: 133, and stored says 113: off by -20.
They part at event 5, adjust +20: that write path never touches the stored column.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 3
Every order of these 5 events, replayed: 120 orders, 19 different final balances; 6 end as the log does.
Of the 10 pairs of events, 3 give the same balance in either order from every starting balance from 0 to 300.
Replaying gives the truth only in the order the events happened: the log's order is part of what it records.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 4
An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.
event> fee 5
Added: fee 5%.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 3
Every order of these 6 events, replayed: 720 orders, 47 different final balances; 17 end as the log does.
Of the 15 pairs of events, 3 give the same balance in either order from every starting balance from 0 to 300.
Replaying gives the truth only in the order the events happened: the log's order is part of what it records.
1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit
choice> 6
Bye.
What was typed (11 lines)
1
2
3
5
1
2
3
4
fee 5
3
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# P131 event log: the log with both balances, two reconciliations, every
# order of the events, and adding or removing events.
import events
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):
s => out
while len(out) < n:
out + " " => out
return out
def lpad(s, n):
s => out
while len(out) < n:
" " + out => out
return out
def show(log):
if len(log) == 0:
"The log is empty." ^0
return
events.ledger(log) => rows
" # event replayed stored" ^0
for k in [0:len(log) - 1]:
rows[k] => r
(" " + str(k + 1) + " " + pad(r[0], 33) + lpad(str(r[1]), 10) + lpad(str(r[2]), 8)) ^0
def reconcile(log):
if len(log) == 0:
"The log is empty." ^0
return
events.ledger(log) => rows
rows[len(rows) - 1] => last
if last[2] == last[3]:
("The check the system has: the stored column, " + str(last[2]) + ", against the copy the same write paths keep, " + str(last[3]) + ": no difference.") ^0
else:
("The check the system has finds a difference: " + str(last[2]) + " against " + str(last[3]) + ".") ^0
if last[1] == last[2]:
("Replaying the events: " + str(last[1]) + ", the same as stored.") ^0
else:
("Replaying the events: " + str(last[1]) + ", and stored says " + str(last[2]) + ": off by " + events.signed(last[2] - last[1]) + ".") ^0
0 => first
for k in [0:len(rows) - 1]:
if first == 0 and rows[k][1] != rows[k][2]:
k + 1 => first
("They part at event " + str(first) + ", " + rows[first - 1][0] + ": that write path never touches the stored column.") ^0
def orders(log):
if len(log) < 2:
"Two events or more are needed." ^0
return
events.orders(log) => o
events.pairs(log, 300) => p
("Every order of these " + str(len(log)) + " events, replayed: " + str(o[0]) + " orders, " + str(o[1]) + " different final balances; " + str(o[2]) + " end as the log does.") ^0
("Of the " + str(p[0]) + " pairs of events, " + str(p[1]) + " give the same balance in either order from every starting balance from 0 to 300.") ^0
if o[1] > 1:
"Replaying gives the truth only in the order the events happened: the log's order is part of what it records." ^0
def add(log):
if len(log) >= 6:
"6 events is the most." ^0
return log
"An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25." ^0
plain(input("event> ")) => t
if t == "":
"Cancelled." ^0
return log
"" => kind
"" => num
False => space
for c in t:
if c == " " and not space:
True => space
elif space:
num + c => num
else:
kind + c => kind
if kind != "deposit" and kind != "withdraw" and kind != "fee" and kind != "adjust":
"Type deposit, withdraw, fee or adjust, then a number." ^0
return log
plain(num) => num
1 => sign
if len(num) > 0 and num[0] == "-" and kind == "adjust":
0 - 1 => sign
num[1:len(num)] => num
if len(num) == 0 or len(num) > 4:
"Type a whole number up to 9999 (a fee from 1 to 100)." ^0
return log
for c in num:
if c < "0" or c > "9":
"Type a whole number up to 9999 (a fee from 1 to 100)." ^0
return log
int(num) => n
if n == 0 or (kind == "fee" and n > 100):
"Type a whole number up to 9999 (a fee from 1 to 100)." ^0
return log
log + [[kind, sign * n]] => out
if kind == "fee":
("Added: fee " + str(n) + "%.") ^0
elif kind == "adjust":
("Added: adjust " + events.signed(sign * n) + ".") ^0
else:
("Added: " + kind + " " + str(n) + ".") ^0
return out
events.default_log() => log
"== Event log ==" ^0
"An account kept two ways: the balance replayed from its events, and a stored balance column that each write path updates." ^0
True => running
while running:
"" ^0
"1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show(log)
elif choice == "2":
reconcile(log)
elif choice == "3":
orders(log)
elif choice == "4":
add(log) => log
elif choice == "5":
if len(log) == 0:
"The log is empty." ^0
else:
log[0:len(log) - 1] => log
("Removed the last event; " + str(len(log)) + " left.") ^0
elif choice == "6":
False => running
else:
"Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import events
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):
out = s
while len(out) < n:
out = out + " "
return out
def lpad(s, n):
out = s
while len(out) < n:
out = " " + out
return out
def show(log):
if len(log) == 0:
print("The log is empty.")
return
rows = events.ledger(log)
print(" # event replayed stored")
for k in range(0, len(log)):
r = rows[k]
print(" " + str(k + 1) + " " + pad(r[0], 33) + lpad(str(r[1]), 10) + lpad(str(r[2]), 8))
def reconcile(log):
if len(log) == 0:
print("The log is empty.")
return
rows = events.ledger(log)
last = rows[len(rows) - 1]
if last[2] == last[3]:
print("The check the system has: the stored column, " + str(last[2]) + ", against the copy the same write paths keep, " + str(last[3]) + ": no difference.")
else:
print("The check the system has finds a difference: " + str(last[2]) + " against " + str(last[3]) + ".")
if last[1] == last[2]:
print("Replaying the events: " + str(last[1]) + ", the same as stored.")
else:
print("Replaying the events: " + str(last[1]) + ", and stored says " + str(last[2]) + ": off by " + events.signed(last[2] - last[1]) + ".")
first = 0
for k in range(0, len(rows)):
if first == 0 and rows[k][1] != rows[k][2]:
first = k + 1
print("They part at event " + str(first) + ", " + rows[first - 1][0] + ": that write path never touches the stored column.")
def orders(log):
if len(log) < 2:
print("Two events or more are needed.")
return
o = events.orders(log)
p = events.pairs(log, 300)
print("Every order of these " + str(len(log)) + " events, replayed: " + str(o[0]) + " orders, " + str(o[1]) + " different final balances; " + str(o[2]) + " end as the log does.")
print("Of the " + str(p[0]) + " pairs of events, " + str(p[1]) + " give the same balance in either order from every starting balance from 0 to 300.")
if o[1] > 1:
print("Replaying gives the truth only in the order the events happened: the log's order is part of what it records.")
def add(log):
if len(log) >= 6:
print("6 events is the most.")
return log
print("An event: deposit n, withdraw n, fee p (percent), or adjust n (either sign), such as withdraw 25.")
t = plain(input("event> "))
if t == "":
print("Cancelled.")
return log
kind = ""
num = ""
space = False
for c in t:
if c == " " and not space:
space = True
elif space:
num = num + c
else:
kind = kind + c
if kind != "deposit" and kind != "withdraw" and kind != "fee" and kind != "adjust":
print("Type deposit, withdraw, fee or adjust, then a number.")
return log
num = plain(num)
sign = 1
if len(num) > 0 and num[0] == "-" and kind == "adjust":
sign = 0 - 1
num = num[1:len(num)]
if len(num) == 0 or len(num) > 4:
print("Type a whole number up to 9999 (a fee from 1 to 100).")
return log
for c in num:
if c < "0" or c > "9":
print("Type a whole number up to 9999 (a fee from 1 to 100).")
return log
n = int(num)
if n == 0 or kind == "fee" and n > 100:
print("Type a whole number up to 9999 (a fee from 1 to 100).")
return log
out = log + [[kind, sign * n]]
if kind == "fee":
print("Added: fee " + str(n) + "%.")
elif kind == "adjust":
print("Added: adjust " + events.signed(sign * n) + ".")
else:
print("Added: " + kind + " " + str(n) + ".")
return out
log = events.default_log()
print("== Event log ==")
print("An account kept two ways: the balance replayed from its events, and a stored balance column that each write path updates.")
running = True
while running:
print("")
print("1) the log 2) reconcile 3) every order 4) add an event 5) remove the last 6) quit")
choice = plain(input("choice> "))
if choice == "1":
show(log)
elif choice == "2":
reconcile(log)
elif choice == "3":
orders(log)
elif choice == "4":
log = add(log)
elif choice == "5":
if len(log) == 0:
print("The log is empty.")
else:
log = log[0:len(log) - 1]
print("Removed the last event; " + str(len(log)) + " left.")
elif choice == "6":
running = False
else:
print("Pick a number from 1 to 6.")
print("Bye.")
events.eml
eml# P131 event log - an account kept two ways: the balance replayed from its
# events, and a stored balance column that each write path updates. As in
# the corpus case denormalized-total-drifts, one path (an adjustment)
# forgets the stored column, and a check that compares the column with a
# copy kept by the same paths compares a number with itself. As in
# state-from-events-vs-stored, replaying is right only for the order the
# events happened in: a fee and a refused withdrawal do not commute with
# the rest.
#
# An event is [kind, amount]: deposit n, withdraw n (refused if more than
# the balance), fee p (p percent of the balance, rounded down), adjust n
# (a correction, either sign).
def default_log():
return [["deposit", 100], ["withdraw", 30], ["fee", 10], ["deposit", 50], ["adjust", 20], ["withdraw", 120]]
def div(p, q):
int(p / q) => f
while f * q > p:
f - 1 => f
while (f + 1) * q <= p:
f + 1 => f
return f
def apply(balance, ev):
# [balance after, what happened]
if ev[0] == "deposit":
return [balance + ev[1], "deposit " + str(ev[1])]
if ev[0] == "withdraw":
if ev[1] > balance:
return [balance, "withdraw " + str(ev[1]) + " refused: only " + str(balance)]
return [balance - ev[1], "withdraw " + str(ev[1])]
if ev[0] == "fee":
div(balance * ev[1], 100) => f
return [balance - f, "fee " + str(ev[1]) + "% = " + str(f)]
return [balance + ev[1], "adjust " + signed(ev[1])]
def signed(n):
if n < 0:
return str(n)
return "+" + str(n)
def replay(log):
0 => b
for ev in log:
apply(b, ev)[0] => b
return b
def ledger(log):
# per event [what happened, replayed balance, stored column, the copy
# the check reads]; every path but adjust writes the column and the copy
0 => b
0 => stored
0 => copy
[] => out
for ev in log:
b => before
apply(b, ev) => r
r[0] => b
if ev[0] != "adjust":
stored + b - before => stored
stored => copy
out + [[r[1], b, stored, copy]] => out
return out
def permutations(xs):
if len(xs) <= 1:
return [xs]
[] => out
for i in [0:len(xs) - 1]:
[] => rest
for j in [0:len(xs) - 1]:
if j != i:
rest + [xs[j]] => rest
for p in permutations(rest):
out + [[xs[i]] + p] => out
return out
def orders(log):
# [orders, distinct final balances, orders ending as the log does]
replay(log) => right
{} => seen
0 => same
permutations(log) => ps
for p in ps:
replay(p) => b
1 => seen[b]
if b == right:
same + 1 => same
return [len(ps), len(seen), same]
def commutes(a, b, top):
# a then b equals b then a from every balance 0 to top
for x in [0:top]:
if apply(apply(x, a)[0], b)[0] != apply(apply(x, b)[0], a)[0]:
return False
return True
def pairs(log, top):
# [pairs, pairs that commute]
0 => n
0 => c
for i in [0:len(log) - 2]:
for j in [i + 1:len(log) - 1]:
n + 1 => n
if commutes(log[i], log[j], top):
c + 1 => c
return [n, c]
Python projection (events.py)
def default_log():
return [["deposit", 100], ["withdraw", 30], ["fee", 10], ["deposit", 50], ["adjust", 20], ["withdraw", 120]]
def div(p, q):
f = int(p / q)
while f * q > p:
f = f - 1
while (f + 1) * q <= p:
f = f + 1
return f
def apply(balance, ev):
if ev[0] == "deposit":
return [balance + ev[1], "deposit " + str(ev[1])]
if ev[0] == "withdraw":
if ev[1] > balance:
return [balance, "withdraw " + str(ev[1]) + " refused: only " + str(balance)]
return [balance - ev[1], "withdraw " + str(ev[1])]
if ev[0] == "fee":
f = div(balance * ev[1], 100)
return [balance - f, "fee " + str(ev[1]) + "% = " + str(f)]
return [balance + ev[1], "adjust " + signed(ev[1])]
def signed(n):
if n < 0:
return str(n)
return "+" + str(n)
def replay(log):
b = 0
for ev in log:
b = apply(b, ev)[0]
return b
def ledger(log):
b = 0
stored = 0
copy = 0
out = []
for ev in log:
before = b
r = apply(b, ev)
b = r[0]
if ev[0] != "adjust":
stored = stored + b - before
copy = stored
out = out + [[r[1], b, stored, copy]]
return out
def permutations(xs):
if len(xs) <= 1:
return [xs]
out = []
for i in range(0, len(xs)):
rest = []
for j in range(0, len(xs)):
if j != i:
rest = rest + [xs[j]]
for p in permutations(rest):
out = out + [[xs[i]] + p]
return out
def orders(log):
right = replay(log)
seen = {}
same = 0
ps = permutations(log)
for p in ps:
b = replay(p)
seen[b] = 1
if b == right:
same = same + 1
return [len(ps), len(seen), same]
def commutes(a, b, top):
for x in range(0, top+1):
if apply(apply(x, a)[0], b)[0] != apply(apply(x, b)[0], a)[0]:
return False
return True
def pairs(log, top):
n = 0
c = 0
for i in range(0, len(log) - 2+1):
for j in range(i + 1, len(log)):
n = n + 1
if commutes(log[i], log[j], top):
c = c + 1
return [n, c]