examples/todo/
Una lista di cose da fare salvata in un file.
- 334 righe
- 2 moduli
- riga di comando
- legge un file
- scrive un file
Il programma separa la gestione della lista dalla lettura e scrittura del file. Le funzioni che decidono cosa salvare lavorano solo sui dati ricevuti e restituiscono un risultato. I test della lista possono quindi essere eseguiti senza accedere al disco.
Eseguilo
1. [x] write the corpus harness
2. [x] find a compiler defect with it
3. [ ] make the CI run on both platforms
4. [ ] re-measure heroes mutate over the enlarged corpus
2 of 4 left
exit 0 · 5 righe
Il programma
# A todo list kept in a file.
#
# heroes run examples/todo/main.hero -- examples/todo/sample.todo
# heroes run examples/todo/main.hero -- /tmp/mine.todo add buy milk
# heroes run examples/todo/main.hero -- /tmp/mine.todo done 2
#
# main.hero the program: arguments, the file, the one write
# list.hero the list, the format, and every change to it
#
# **The corpus's first program that writes.** `read_file` has been exercised
# since M-ffi-ladder and `write_file` had never been asked to do anything a
# reader could check; here the round trip is the program — add a task, write it,
# read it back, and the file on disk is the only place the state lives.
#
# The one write is in `save`, and everything that decides *what* to write is pure
# next door. That is what lets `list.hero`'s twenty assertions run without
# touching the disk, and it is what value semantics makes free: `complete`
# returns a new list, because there is nothing else it could return.
use list
## Error codes
#
# One `constant` per code, so a typo is `error[unknown_name]` here rather
# than a comparison that is quietly false forever (panel 034 R4).
constant ERR_NO_NUMBER: str
"no_number"
function main()
given = args()
if len(given) == 0
print("usage: todo <file> [add TEXT… | done N]")
exit(2)
match read_file(given[0])
.err _ => stop("cannot read " + given[0])
.ok text => run(text, given)
function stop(message: str)
print(message)
exit(1)
function run(text: str, given: [str])
match list.read(text)
.err e => stop(e.code + ": " + e.msg)
.ok tasks => act(tasks, given)
function act(tasks: [list.Task], given: [str])
if len(given) == 1
show(tasks)
return
command = given[1]
if command == "add"
match list.add(tasks, rest_of(given, 2))
.err e => stop(e.code + ": " + e.msg)
.ok grown => save(grown, given[0])
else if command == "done"
match number_of(rest_of(given, 2))
.err e => stop(e.code + ": " + e.msg)
.ok n => finish(tasks, n, given[0])
else
stop("no command `" + command + "` — say `add TEXT…` or `done N`")
function finish(tasks: [list.Task], n: i64, path: str)
match list.complete(tasks, n)
.err e => stop(e.code + ": " + e.msg)
.ok after => save(after, path)
# The only line in this program that writes anything.
function save(tasks: [list.Task], path: str)
match write_file(path: path, text: list.render(tasks))
.err _ => stop("cannot write " + path)
.ok _ => show(tasks)
function show(tasks: [list.Task])
for line in list.numbered(tasks)
print(line)
print(list.left_to_do(tasks), " of ", tasks.len(), " left")
# Everything from `at` on, joined by a space — so `add buy milk` is one task and
# not two arguments the shell happened to split.
function rest_of(given: [str], at: i64) -> str
words: [str] @= []
for i in range(from: at, to: len(given))
words @ words.push(given[i])
return join(words, " ")
function number_of(word: str) -> i64?
if word.len() == 0
return fail(code: ERR_NO_NUMBER, msg: "`done` needs the number of a task")
v: i64 @= 0
for i in range(from: 0, to: word.len())
if word[i] < '0' || word[i] > '9'
return fail(code: ERR_NO_NUMBER, msg: "`" + word + "` is not a number")
v @ v * 10 + to_i64(word[i] - '0').must()
return ok(v)
test "the sample lists the way the file reads"
tasks = list.read(read_file("examples/todo/sample.todo").must()).must()
assert tasks.len() == 4
assert list.left_to_do(tasks) == 2
assert list.numbered(tasks)[0] == "1. [x] write the corpus harness"
test "a task added is a task on disk, read back by a second program"
path = "build/heroes-corpus-todo.txt"
write_file(path: path, text: "[ ] one\n").must()
first = list.read(read_file(path).must()).must()
grown = list.add(first, "two").must()
write_file(path: path, text: list.render(grown)).must()
again = list.read(read_file(path).must()).must()
assert again.len() == 2
assert again[1].text == "two"
assert again[1].done == false
test "completing a task survives the round trip through the file"
path = "build/heroes-corpus-todo-done.txt"
write_file(path: path, text: "[ ] one\n[ ] two\n").must()
tasks = list.read(read_file(path).must()).must()
write_file(path: path, text: list.render(list.complete(tasks, 1).must())).must()
after = list.read(read_file(path).must()).must()
assert after[0].done == true
assert after[1].done == false
assert list.left_to_do(after) == 1
test "arguments after the command become one task"
assert rest_of(["f", "add", "buy", "milk"], 2) == "buy milk"
assert rest_of(["f", "add"], 2) == ""
assert number_of("12").must() == 12
assert number_of("x").is_err()
assert number_of("").is_err()
list.hero · 196 righe
# The list, and the file it lives in.
#
# [ ] buy milk
# [x] write the maze solver
#
# A format a person can edit in any editor, which is the only reason to store a
# list as text at all — so it is read strictly enough to catch a typo and
# loosely enough to survive one: the marker must be `[ ]` or `[x]`, and
# everything after it is the task, spaces and brackets included.
#
# **Nothing here writes anything.** Reading, rendering and changing are all pure
# — a `[Task]` in, a `[Task]` out — and the one call to `write_file` is in
# `main.hero`. That split is what lets every test below run without touching the
# disk, and it is the same split value semantics makes free: `complete` returns a
# new list rather than editing one, because there is no other thing it could do.
## Error codes
#
# One `constant` per code, so a typo is `error[unknown_name]` here rather
# than a comparison that is quietly false forever (panel 034 R4).
constant ERR_BAD_LINE: str
"bad_line"
constant ERR_BAD_MARKER: str
"bad_marker"
constant ERR_EMPTY_TASK: str
"empty_task"
constant ERR_NO_SUCH_TASK: str
"no_such_task"
record Task
done: bool
text: str
function is_space(c: u8) -> bool
return c == ' ' || c == '\t' || c == 13
function trimmed(line: str) -> str
from: i64 @= 0
while from < line.len() && is_space(line[from])
from @ from + 1
to: i64 @= line.len()
while to > from && is_space(line[to - 1])
to @ to - 1
return line.slice(from: from, to: to)
function split_lines(text: str) -> [str]
out: [str] @= []
start: i64 @= 0
i: i64 @= 0
while i < text.len()
if text[i] == '\n'
out @ out.push(text.slice(from: start, to: i))
start @ i + 1
i @ i + 1
if start < text.len()
out @ out.push(text.slice(from: start, to: text.len()))
return out
## Reading and writing
function read(text: str) -> [Task]?
out: [Task] @= []
number: i64 @= 0
for line in split_lines(text)
number @ number + 1
head = trimmed(line)
if head == ""
continue
if head.len() < 4 || head[0] != '[' || head[2] != ']' || head[3] != ' '
return fail(code: ERR_BAD_LINE, msg: "line " + number.to_str() + ": a task is `[ ] text` or `[x] text`")
marker = head[1]
if marker != ' ' && marker != 'x'
return fail(code: ERR_BAD_MARKER, msg: "line " + number.to_str() + ": the marker is a space or an `x`")
out @ out.push(Task(done: marker == 'x', text: trimmed(head.slice(from: 4, to: head.len()))))
return ok(out)
function render(tasks: [Task]) -> str
if tasks.len() == 0
return ""
lines: [str] @= []
for task in tasks
lines @ lines.push(marker_of(task) + " " + task.text)
return join(lines, "\n") + "\n"
function marker_of(task: Task) -> str
if task.done
return "[x]"
return "[ ]"
## Changing
function add(tasks: [Task], text: str) -> [Task]?
if trimmed(text) == ""
return fail(code: ERR_EMPTY_TASK, msg: "a task needs some text")
return ok(tasks.push(Task(done: false, text: trimmed(text))))
# `n` counts from 1, because the list a person reads is numbered from 1 and the
# number they type is the one they read. Converting once, here, is cheaper than
# remembering which end of the program is which.
function complete(tasks: [Task], n: i64) -> [Task]?
if n < 1 || n > tasks.len()
return fail(
code: ERR_NO_SUCH_TASK,
msg: "there is no task " + n.to_str() + " — the list has " + tasks.len().to_str()
)
out: [Task] @= []
for i in range(from: 0, to: tasks.len())
if i == n - 1
out @ out.push(Task(done: true, text: tasks[i].text))
else
out @ out.push(tasks[i])
return ok(out)
function left_to_do(tasks: [Task]) -> i64
open: i64 @= 0
for task in tasks
if !task.done
open @ open + 1
return open
# `1. [ ] buy milk`, the view a person reads and types a number back at.
function numbered(tasks: [Task]) -> [str]
out: [str] @= []
for i in range(from: 0, to: tasks.len())
out @ out.push((i + 1).to_str() + ". " + marker_of(tasks[i]) + " " + tasks[i].text)
return out
test "a file round-trips through reading and rendering"
text = "[ ] one\n[x] two\n"
assert render(read(text).must()) == text
assert read("").must() == []
assert render([]) == ""
test "blank lines are skipped and spacing is forgiven"
tasks = read("\n [ ] one \n\n[x] two\n").must()
assert tasks.len() == 2
assert tasks[0].text == "one"
assert tasks[0].done == false
assert tasks[1].done == true
test "a line that is not a task is refused by name"
assert code_of("one\n") == ERR_BAD_LINE
assert code_of("[ ]one\n") == ERR_BAD_LINE
assert code_of("[?] one\n") == ERR_BAD_MARKER
test "adding appends, and an empty task is not a task"
tasks = read("[ ] one\n").must()
grown = add(tasks, " two ").must()
assert grown.len() == 2
assert grown[1].text == "two"
assert grown[1].done == false
assert add(tasks, " ").is_err()
assert tasks.len() == 1
test "completing counts from one, and leaves every other task alone"
tasks = read("[ ] one\n[ ] two\n[ ] three\n").must()
after = complete(tasks, 2).must()
assert after[0].done == false
assert after[1].done == true
assert after[2].done == false
assert left_to_do(after) == 2
assert complete(tasks, 0).is_err()
assert complete(tasks, 4).is_err()
assert tasks[1].done == false
test "the numbered view is what a person types a number back at"
tasks = read("[ ] one\n[x] two\n").must()
assert numbered(tasks) == ["1. [ ] one", "2. [x] two"]
function code_of(text: str) -> str
return match read(text)
.ok _ => "no_error"
.err e => e.code
Input
[x] write the corpus harness
[x] find a compiler defect with it
[ ] make the CI run on both platforms
[ ] re-measure heroes mutate over the enlarged corpus
impronta sha256 f12dd6874055 · 5 file · index.json
Successivo: logs. Tutti gli esempi. ← Precedente: shapes
Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.