Strings

Building and taking apart text, accents and emoji included.

strings.py · output · proof · check · try it in the playground


The question

Programs handle text all the time: making a label, counting letters, looking up the code behind a symbol. Three small jobs:

The catch: é and 😀 are not plain English letters.

Does eyekind handle them correctly, and can the checker confirm it?


A word on Unicode

Computers store every character as a number. Unicode is the worldwide list that gives each character (Latin, Greek, Chinese, emoji) its own number, called a code point.

Some characters take several bytes to store, which is why naive programs sometimes count café as 5 letters instead of 4. eyekind counts code points: é is one character, and 😀 is one code, not the two 16-bit units of UTF-16.


What we tell eyekind

implied_by(label(Name, Label), atom_concat('hello_', Name, Label))
implied_by(
  characters(Text, Chars, Length),
  [atom_chars(Text, Chars), atom_length(Text, Length)],
)
implied_by(unicode_codes(Text, Codes), atom_codes(Text, Codes))
query(label('alice', Label))
query(characters('café', Chars, Length))
query(unicode_codes('😀', Codes))

What eyekind concludes

// Answers: each instance of a query() that holds.
label('alice', 'hello_alice')
characters('café', ['c', 'a', 'f', 'é'], 4)
unicode_codes('😀', [128512])

The comment line says what follows: one answer for each query.


The proof, line by line

eyekind --proof writes down how it reached each answer. This is the complete proof:

// Answers: each instance of a query() that holds.
label('alice', 'hello_alice')
characters('café', ['c', 'a', 'f', 'é'], 4)
unicode_codes('😀', [128512])

// Clauses: the program clauses the steps cite, by clause number.
clause(1, implied_by(label(Name, Label), atom_concat('hello_', Name, Label)))
clause(2, implied_by(characters(Text, Chars, Length), [atom_chars(Text, Chars), atom_length(Text, Length)]))
clause(3, implied_by(unicode_codes(Text, Codes), atom_codes(Text, Codes)))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(label('alice', 'hello_alice'), clause(1), {Name: 'alice', Label: 'hello_alice'}, [atom_concat('hello_', 'alice', 'hello_alice')])
step(atom_concat('hello_', 'alice', 'hello_alice'), 'builtin', {}, [])
step(characters('café', ['c', 'a', 'f', 'é'], 4), clause(2), {Text: 'café', Chars: ['c', 'a', 'f', 'é'], Length: 4}, [atom_chars('café', ['c', 'a', 'f', 'é']), atom_length('café', 4)])
step(atom_chars('café', ['c', 'a', 'f', 'é']), 'builtin', {}, [])
step(atom_length('café', 4), 'builtin', {}, [])
step(unicode_codes('😀', [128512]), clause(3), {Text: '😀', Codes: [128512]}, [atom_codes('😀', [128512])])
step(atom_codes('😀', [128512]), 'builtin', {}, [])

It has three parts:

Two kinds of justification occur:

Read the steps as sentences:

  1. The label of alice is hello_alice: by rule 1, with Name being alice and Label being hello_alice, because joining hello_ and alice gives hello_alice.
  2. Joining hello_ and alice gives hello_alice: a built-in calculation.
  3. café has the characters c, a, f, é and length 4: by rule 2, because of the next two steps.
  4. café splits into c, a, f, é: a built-in calculation.
  5. café has length 4: a built-in calculation.
  6. The codes of 😀 are [128512]: by rule 3, because of the next step.
  7. The code point of 😀 is 128512: a built-in calculation.

Checked: what the report says

A proof is only worth something if someone else can check it. eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Checked answers: these follow from the input program.
label('alice', 'hello_alice')
characters('café', ['c', 'a', 'f', 'é'], 4)
unicode_codes('😀', [128512])

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 3)
condition('C2', 'well_founded', 'ok', 7)
condition('C3', 'justification', 'ok', 7)
condition('C4', 'coverage', 'ok', 7)
condition('C5', 're_decision', 'ok', 4)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 10)
condition('C8', 'witness_integrity', 'ok', 0)

// Proof statistics: recorded steps and how they were checked.
steps(7)
verified(3)
recomputed(4)
composed(0)
trusted(0)
claims(3)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked')

Each condition line names a check, says 'ok', and counts what it looked at:

The statistics repeat this: 7 steps, 3 verified against the rules, 4 recomputed, none taken on trust. The verdict is checked: all three answers follow from the program. As the last comment says, this confirms the claims; it does not say the program has no other answers.


Try it

eyekind examples/strings.py            # the answers
eyekind --proof examples/strings.py    # answers with their proof

Or open it in the playground. Add query(characters('Zoë', Chars, Length)) and run again: you get characters('Zoë', ['Z', 'o', 'ë'], 3). Add query(label(Name, 'hello_bob')) to run rule 1 backward: atom_concat splits the label instead of joining, and the answer is label('bob', 'hello_bob').


Takeaway

Text is data like any other: it can be built, split and measured with plain rules — and the checker recounts the letters rather than taking the program’s word for it.