@fraczak/k 5.1.2 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex +0 -0
- package/Code-derivation-tests/blowup.mjs +3 -3
- package/Code-derivation-tests/bnat.mjs +2 -2
- package/Code-derivation-tests/codec-witness-pattern.mjs +50 -0
- package/Code-derivation-tests/convergence-strategy.mjs +24 -0
- package/Code-derivation-tests/dot.mjs +12 -9
- package/Code-derivation-tests/filter-syntax.mjs +21 -0
- package/Code-derivation-tests/loop.mjs +8 -2
- package/Code-derivation-tests/nat.mjs +1 -1
- package/Code-derivation-tests/not-not.mjs +3 -3
- package/Code-derivation-tests/pattern-envelope.mjs +68 -0
- package/Code-derivation-tests/rlz.mjs +2 -2
- package/{BIG_PICTURE.md → DOCS/BIG_PICTURE.md} +103 -50
- package/DOCS/REPL.md +191 -0
- package/DOCS/a.html +398 -0
- package/DOCS/book/01-introduction.md +51 -0
- package/DOCS/book/02-syntax-and-values.md +111 -0
- package/DOCS/book/03-types-as-finite-automata.md +156 -0
- package/DOCS/book/04-partial-functions-and-composition.md +146 -0
- package/DOCS/book/05-typing-filters-and-normalization.md +111 -0
- package/DOCS/book-combined.html +3798 -0
- package/DOCS/book-combined.md +3745 -0
- package/DOCS/book-combined.pdf +0 -0
- package/DOCS/book-combined.tex +5682 -0
- package/DOCS/book.html +65 -0
- package/DOCS/book.md +54 -0
- package/DOCS/book.pdf +0 -0
- package/DOCS/book.tex +150 -0
- package/NAT.k~ +452 -0
- package/README.md +204 -508
- package/TypeGraph.mjs +43 -0
- package/TypePatternGraph.mjs +251 -0
- package/Value.mjs +192 -0
- package/augmentation.mjs +198 -0
- package/codecs/BINARY_FORMAT.html +471 -0
- package/codecs/BINARY_FORMAT.md +53 -0
- package/codecs/POLYMORPHIC_BINARY_FORMAT.html +355 -0
- package/codecs/POLYMORPHIC_BINARY_FORMAT.md +303 -0
- package/codecs/README.md +215 -0
- package/codecs/example-pipeline.mjs +149 -0
- package/codecs/ieee.mjs +140 -0
- package/codecs/int.mjs +104 -0
- package/codecs/json-codec.mjs +215 -0
- package/codecs/json.mjs +40 -0
- package/codecs/k-parse.mjs +90 -0
- package/codecs/k-pattern.mjs +75 -0
- package/codecs/k-print.mjs +46 -0
- package/codecs/runtime/codec.mjs +768 -0
- package/codecs/runtime/ieee-pattern.mjs +30 -0
- package/codecs/runtime/pattern-json.mjs +96 -0
- package/codecs/runtime/prefix-codec.mjs +425 -0
- package/codecs/runtime/show-value.mjs +68 -0
- package/codecs/runtime/typeFromValue.mjs +34 -0
- package/codecs/runtime/unicode-string.mjs +220 -0
- package/codecs/show.mjs +41 -0
- package/codecs/string-codec.mjs +80 -0
- package/codecs/unit.mjs +51 -0
- package/codecs/utf16.mjs +66 -0
- package/codecs/utf8.mjs +37 -0
- package/codes.mjs +146 -44
- package/compiler.mjs +111 -0
- package/convergence.mjs +197 -0
- package/core.k +197 -0
- package/export.mjs +22 -32
- package/hash.mjs +29 -31
- package/ieee.klib +0 -0
- package/index.mjs +41 -15
- package/k-vscode/.vscode/launch.json +14 -0
- package/k-vscode/README.md +65 -0
- package/k-vscode/language-configuration.json +42 -0
- package/k-vscode/package-lock.json +4176 -0
- package/k-vscode/package.json +57 -0
- package/k-vscode/snippets/k.json +92 -0
- package/k-vscode/syntaxes/k.tmLanguage.json +243 -0
- package/k.mjs +56 -54
- package/object.mjs +517 -0
- package/objects/README.md +38 -0
- package/objects/compile-lib.mjs +42 -0
- package/objects/compile.mjs +42 -0
- package/objects/decompile.mjs +32 -0
- package/package.json +13 -6
- package/parser.jison +27 -46
- package/parser.mjs +68 -100
- package/pretty.mjs +21 -40
- package/repl2.mjs +662 -0
- package/run.mjs +264 -231
- package/symbol-table.mjs +3 -17
- package/test-fingerprint.mjs +7 -7
- package/test-hash-fuzz.mjs +107 -0
- package/test-hash-normalization.mjs +134 -0
- package/test-k-object.mjs +74 -0
- package/test-repl2.mjs +106 -0
- package/test.mjs +45 -70
- package/tests.sh +10 -1
- package/type_registry/registry.mjs~ +105 -0
- package/type_registry/typeParser.jison~ +132 -0
- package/typing.mjs +129 -676
- package/unification.mjs +157 -0
- package/valueIO.mjs +54 -0
- package/valueParser.jison +143 -0
- package/valueParser.mjs +713 -0
- package/Code-derivation-tests/built-in.mjs +0 -19
- package/Code-derivation-tests/vector.mjs +0 -19
- package/Filters/bug.k +0 -3
- package/Filters/composing.k +0 -15
- package/Filters/filter.k +0 -9
- package/TODO.md +0 -53
- package/TYPING.md +0 -365
- package/k-repl.cast +0 -1913
- package/k.cast +0 -618
- package/my-objects.json +0 -6
- package/patterns.mjs +0 -543
- package/repl.mjs +0 -213
- package/test-filters.mjs +0 -45
- package/typing-test.mjs +0 -40
|
@@ -0,0 +1,3745 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "k-language"
|
|
3
|
+
author: "W. Fraczak"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<div align="center">
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
{ } { }
|
|
10
|
+
/ \ {.lang name} |
|
|
11
|
+
lang by --> name
|
|
12
|
+
| | |
|
|
13
|
+
'k' 'wojtek' 'k'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
# Table of Contents
|
|
19
|
+
|
|
20
|
+
1. [Introduction](#chapter-1-introduction)
|
|
21
|
+
2. [Syntax and Values](#chapter-2-syntax-and-values)
|
|
22
|
+
3. [Types as Finite Automata](#chapter-3-types-as-finite-automata)
|
|
23
|
+
4. [Partial Functions and Composition](#chapter-4-partial-functions-and-composition)
|
|
24
|
+
5. [Typing, Filters, and Normalization](#chapter-5-typing-filters-and-normalization)
|
|
25
|
+
6. [Values in Memory](#chapter-6-values-in-memory)
|
|
26
|
+
7. [The Partial Function ABI](#chapter-7-the-partial-function-abi)
|
|
27
|
+
8. [Operational Semantics and Execution](#chapter-8-operational-semantics-and-execution)
|
|
28
|
+
9. [Compiler Architecture](#chapter-9-compiler-architecture)
|
|
29
|
+
10. [From AST to Intermediate Representation](#chapter-10-from-ast-to-intermediate-representation)
|
|
30
|
+
11. [LLVM Basics](#chapter-11-llvm-basics)
|
|
31
|
+
12. [Code Generation](#chapter-12-code-generation)
|
|
32
|
+
13. [Linking and Execution](#chapter-13-linking-and-execution)
|
|
33
|
+
14. [Canonical Serialization](#chapter-14-canonical-serialization)
|
|
34
|
+
15. [Optimization and Folding](#chapter-15-optimization-and-folding)
|
|
35
|
+
16. [Toward a Universal Schema Registry](#chapter-16-toward-a-universal-schema-registry)
|
|
36
|
+
|
|
37
|
+
## Appendices
|
|
38
|
+
|
|
39
|
+
- [Appendix A: Implementing a Prototype Compiler in Python](#appendix-a-implementing-a-prototype-compiler-in-python)
|
|
40
|
+
- [Appendix B: Incorporating External Predefined Types and Functions](#appendix-b-incorporating-external-predefined-types-and-functions)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Chapter 1 — Introduction
|
|
46
|
+
|
|
47
|
+
## **1.1 The purpose of this book**
|
|
48
|
+
|
|
49
|
+
This book describes a small programming language called **k**.
|
|
50
|
+
It is meant to show how data can be represented and transformed in a uniform, precise way.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## **1.2 Data as trees**
|
|
55
|
+
|
|
56
|
+
All information in `k` is treated as a **tree**.
|
|
57
|
+
A tree has:
|
|
58
|
+
|
|
59
|
+
* a single root,
|
|
60
|
+
* labeled edges (field names or tags),
|
|
61
|
+
* and possibly subtrees.
|
|
62
|
+
|
|
63
|
+
Both JSON objects and XML documents are trees in this sense.
|
|
64
|
+
Every `k` value is such a finite labeled tree.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## **1.3 Functions that may fail**
|
|
69
|
+
|
|
70
|
+
In ordinary mathematics a function always returns a result.
|
|
71
|
+
In `k`, a function may be **undefined** for some inputs.
|
|
72
|
+
For example, asking for the field `age` in a record that has no `age` is undefined.
|
|
73
|
+
|
|
74
|
+
We call such mappings **partial functions**.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## **1.4 Simplicity over features**
|
|
79
|
+
|
|
80
|
+
`k` avoids most language constructs: no *variables* and no *control statements*.
|
|
81
|
+
It has only:
|
|
82
|
+
|
|
83
|
+
1. **types** (which describe tree shapes), and
|
|
84
|
+
2. **partial functions** (which transform one tree into another).
|
|
85
|
+
|
|
86
|
+
These two ingredients are sufficient and powerful enough to express structured
|
|
87
|
+
computation in a concise and understandable way.
|
|
88
|
+
|
|
89
|
+
There is another concept in the language, called **filters**.
|
|
90
|
+
Filters play a role in the process of reasoning about types of partial functions.
|
|
91
|
+
Filters have been added to the syntax of the language as they allow explicit annotations
|
|
92
|
+
of "polymorphic" expressions, i.e., expressions which make sense in different contexts.
|
|
93
|
+
Filters are similar to *type classes* in other languages.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Chapter 2 — Syntax and Values
|
|
101
|
+
|
|
102
|
+
## **2.1 Language fragments**
|
|
103
|
+
|
|
104
|
+
The `k` language has only one kind of program element: a **definition**.
|
|
105
|
+
A program is a sequence of definitions of two kinds:
|
|
106
|
+
|
|
107
|
+
```k-lang
|
|
108
|
+
$ type_name = ...type_expression... ;
|
|
109
|
+
function_name = ...expression... ;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The definitions are followed in the file by the *main expression*; it represents the partial function defined by the program as a whole.
|
|
113
|
+
|
|
114
|
+
There is no syntax for values.
|
|
115
|
+
Every syntactic form in an expression describes a **function** -- possibly a constant function that always returns the same value.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## **2.2 Types**
|
|
120
|
+
|
|
121
|
+
A type describes the possible **tree shapes** of values.
|
|
122
|
+
Types are formed from two constructors:
|
|
123
|
+
|
|
124
|
+
1. **Product** – written `{ T₁ l₁, T₂ l₂, … }`
|
|
125
|
+
represents records with fixed labeled fields.
|
|
126
|
+
|
|
127
|
+
2. **Union** – written `< T₁ l₁, T₂ l₂, … >`
|
|
128
|
+
represents a choice between labeled alternatives.
|
|
129
|
+
|
|
130
|
+
Both are finite and fully explicit.
|
|
131
|
+
Every type denotes a finite tree automaton whose accepted trees are the possible values of that type.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## **2.3 Special and limiting cases**
|
|
136
|
+
|
|
137
|
+
1. **Empty product**
|
|
138
|
+
`{}` has no fields.
|
|
139
|
+
It represents the type that admits exactly one value, called *unit*.
|
|
140
|
+
There is nothing exceptional about the type and its value; it is simply the degenerate case of a product with zero fields.
|
|
141
|
+
|
|
142
|
+
2. **Empty union**
|
|
143
|
+
`<>` has no variants.
|
|
144
|
+
It represents a type with no possible values.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## **2.4 Projections**
|
|
149
|
+
|
|
150
|
+
**Dot** notation, e.g.,
|
|
151
|
+
|
|
152
|
+
```k
|
|
153
|
+
. field_name
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
is used for extracting the value of a field from a record (product type value).
|
|
157
|
+
**Div** notation, e.g.,
|
|
158
|
+
|
|
159
|
+
```k
|
|
160
|
+
/ variant_name
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
is used for asserting a particular variant of a union and extracting its value.
|
|
164
|
+
Product type `{ T₁ l₁, T₂ l₂, … , Tₙ lₙ }` naturally defines `n` partial functions: `.l₁, .l₂, … , .lₙ`,
|
|
165
|
+
and union type `< T₁ l₁, T₂ l₂, … , Tₙ lₙ >` naturally defines `n` partial functions `/l₁, /l₂, … , /lₙ`.
|
|
166
|
+
Those partial functions are called **projections** and they constitute the basis for the definition
|
|
167
|
+
of all other partial functions in the `k` language.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## **2.5 Functions**
|
|
172
|
+
|
|
173
|
+
Expressions combine partial functions using the following operators:
|
|
174
|
+
|
|
175
|
+
1. **Composition** `(f₁ f₂ …)` — sequential application.
|
|
176
|
+
2. **Union** `< f₁, f₂, … >` — try each in order, first defined wins.
|
|
177
|
+
3. **Product (parallel)** `{ f₁ l₁, f₂ l₂, … }` — apply all to the same input, succeed only if all succeed.
|
|
178
|
+
4. **Variant construction** `| tag` — lift the input as a variant `tag` of a union type.
|
|
179
|
+
|
|
180
|
+
Parentheses around a composition may be omitted except for the empty composition `()` — the identity function.
|
|
181
|
+
Empty product, `{ }`, defines a constant function (ignoring its input) returning *unit*.
|
|
182
|
+
|
|
183
|
+
It is important to notice that after the three markers: `dot (.)`, `div (/)`, or `pipe (|)`, there must be a constant label literal. For that reason, it is possible and advised not to leave any blanks after the marker, e.g.:
|
|
184
|
+
|
|
185
|
+
```k-lang
|
|
186
|
+
.field /'a-tag' |"strange and long tag name ✅"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## **2.6 Example**
|
|
192
|
+
|
|
193
|
+
```k-lang
|
|
194
|
+
$ bool = < {} true, {} false >;
|
|
195
|
+
true = {} |true $ bool;
|
|
196
|
+
false = {} |false $ bool;
|
|
197
|
+
neg = $ bool < /true false, /false true >;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
This defines a "two-variant" union type and three functions: two constants and one transformation exchanging the variants.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## **2.7 Summary**
|
|
205
|
+
|
|
206
|
+
* All types describe tree shapes.
|
|
207
|
+
* As type expression, `{}` is the empty product type (with one value, called *unit*).
|
|
208
|
+
* Functions, not values, are the only expressions in `k`.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Chapter 3 — Types as Finite Automata
|
|
216
|
+
|
|
217
|
+
## **3.1 Motivation**
|
|
218
|
+
|
|
219
|
+
A type in `k` describes a set of finite labeled trees, which can be recognized by a finite tree automaton (FTA). Equivalently, a type definition can be seen as a Context-Free Grammar (CFG), where the values of the type are the derivation trees of the grammar.
|
|
220
|
+
|
|
221
|
+
For example, the recursive type definition of a `list`:
|
|
222
|
+
|
|
223
|
+
```k-lang
|
|
224
|
+
$ bool = < {} true, {} false >;
|
|
225
|
+
$ list = < {} nil, { bool head, list tail } cons >;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
corresponds to the following 4-rule grammar:
|
|
229
|
+
|
|
230
|
+
```bnf
|
|
231
|
+
1: <bool> ::= {} |true
|
|
232
|
+
2: <bool> ::= {} |false
|
|
233
|
+
3: <list> ::= {} |nil
|
|
234
|
+
4: <list> ::= { <bool> head, <list> tail } |cons
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A value of type `list` is a derivation tree of this grammar. For instance, a list containing a single boolean value `true` can be represented by the following derivation:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
<list>
|
|
241
|
+
-- (rule 4) --> { <bool> head, <list> tail } |cons
|
|
242
|
+
| |
|
|
243
|
+
| -- (rule 3) --> {} |nil
|
|
244
|
+
|
|
|
245
|
+
-- (rule 1) --> {} |true
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This derivation tree corresponds to the value generated by constant function: `{ {} |true head, {} |nil tail } |cons`.
|
|
249
|
+
|
|
250
|
+
Representing types as automata (or CFGs) provides each type with a well-defined structure, independent of the names used in the source code. This allows for canonical normal forms and for comparing types for equality by structure alone.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## **3.2 States and Transitions**
|
|
255
|
+
|
|
256
|
+
Each type definition introduces one or more *states* in the automaton. A union or a product definition corresponds to a state, and the variants or fields correspond to transitions to other states.
|
|
257
|
+
|
|
258
|
+
For example, consider the `$bool` type:
|
|
259
|
+
|
|
260
|
+
```k-lang
|
|
261
|
+
$ bool = < {} true, {} false >;
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
This definition translates to an automaton with two states:
|
|
265
|
+
|
|
266
|
+
- A root state, let's call it `C0`, representing the `$bool` type itself (a union).
|
|
267
|
+
- A state `C1` representing the empty product `{}`.
|
|
268
|
+
|
|
269
|
+
From the root state `C0`, there are two transitions:
|
|
270
|
+
|
|
271
|
+
- A transition labeled `true` to state `C1`.
|
|
272
|
+
- A transition labeled `false` to state `C1`.
|
|
273
|
+
|
|
274
|
+
`C0` is the root state of the type, and `C1` is a leaf state, representing an empty value.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## **3.3 Canonical Form**
|
|
279
|
+
|
|
280
|
+
Different type expressions can describe the same automaton. For example:
|
|
281
|
+
|
|
282
|
+
```k-lang
|
|
283
|
+
$ bool = < {} false, {} true >;
|
|
284
|
+
$ pair = { bool x, bool y };
|
|
285
|
+
$ pair2 = { < {} true, {} false > x, bool y };
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The types `pair` and `pair2` are structurally equivalent because `$bool` is defined as `< {} true, {} false >`, making the definition of `x` in `pair2` identical to the standalone `$bool` type used in `pair`. Canonicalization is the process of removing these superficial differences by erasing local names and renumbering states to produce a single stable representation for each unique type structure.
|
|
289
|
+
|
|
290
|
+
The canonical form is generated by traversing the type's graph structure starting from its root state. The process ensures a unique representation by following a consistent ordering:
|
|
291
|
+
|
|
292
|
+
1. **Start at the Root**: Assign the number `C0` to the root state of the type.
|
|
293
|
+
2. **Explore Neighbors**: Visit all states reachable from `C0`. Sort the transitions alphabetically by their labels (field or variant names) and assign consecutive numbers (`C1`, `C2`, ...) to the destination states in that order.
|
|
294
|
+
3. **Iterate**: Move to the next state in the numbered sequence (`C1`, then `C2`, etc.) and repeat the process for any of its neighbors that have not yet been numbered.
|
|
295
|
+
4. **Complete**: Continue until all reachable states have been assigned a unique number.
|
|
296
|
+
|
|
297
|
+
This traversal produces a unique and deterministic numbering for any finite directed graph, ensuring that any two structurally identical types will have the exact same canonical representation.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## **3.4 Examples**
|
|
302
|
+
|
|
303
|
+
For a type representing binary natural numbers:
|
|
304
|
+
|
|
305
|
+
```k-lang
|
|
306
|
+
$ bnat = < bnat 0, bnat 1, {} _ >;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
the canonical form is:
|
|
310
|
+
|
|
311
|
+
```k-lang
|
|
312
|
+
$C0 = < C0 "0", C0 "1", C1 "_" >;
|
|
313
|
+
$C1 = {};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The automaton has two states. `C0` is a recursive state that recognizes sequences of "0"s and "1"s, while `C1` is the terminal state, marked by the "_" transition.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## **3.5 Normalization Process**
|
|
321
|
+
|
|
322
|
+
When compiling a program, the following steps are taken to normalize types:
|
|
323
|
+
|
|
324
|
+
1. **Collect**: Gather all type definitions from the program.
|
|
325
|
+
2. **Expand**: Recursively replace all named types with their underlying definitions until only products and unions remain.
|
|
326
|
+
3. **Deduplicate**: Eliminate duplicate type structures by comparing them structurally.
|
|
327
|
+
4. **Canonicalize**: Assign stable state numbers to the unique types to produce their canonical text representation.
|
|
328
|
+
|
|
329
|
+
This process is deterministic, ensuring that the same type structure always produces the exact same canonical form.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## **3.6 Hash-Based Naming**
|
|
334
|
+
|
|
335
|
+
To create a globally unique and stable identifier for each type, the compiler computes a hash of its canonical representation. This hash becomes the official, unambiguous name of the type.
|
|
336
|
+
|
|
337
|
+
For example:
|
|
338
|
+
|
|
339
|
+
```text
|
|
340
|
+
Canonical Form: $C0=<C0"0",C0"1",C1"_">;$C1={}
|
|
341
|
+
↓
|
|
342
|
+
Hash
|
|
343
|
+
↓
|
|
344
|
+
Identifier: @56b95dc611742a8e1cbb72d399660b5f18e4c3426dba0171f86dcdb2c41e9d91
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Program objects can then refer to types by their hash-based identifiers (prefixed with `@`) without ambiguity.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## **3.7 Interpretation**
|
|
352
|
+
|
|
353
|
+
The canonical representation of a type can be interpreted as:
|
|
354
|
+
|
|
355
|
+
- A finite set of states, `C0…Cn`.
|
|
356
|
+
- Each state is either a product (a record with named fields) or a union (a set of variants).
|
|
357
|
+
- Transitions between states are labeled with strings (field or variant names).
|
|
358
|
+
|
|
359
|
+
Every value of a given type is a finite tree that is accepted by this automaton.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## **3.8 Summary**
|
|
364
|
+
|
|
365
|
+
- Types in `k` denote sets of finite, labeled trees.
|
|
366
|
+
- Every type can be expressed as a finite tree automaton or a context-free grammar.
|
|
367
|
+
- The canonical form eliminates naming differences, providing a single representation for structurally identical types.
|
|
368
|
+
- Hash-based naming gives each unique type a stable and globally unique identifier.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Chapter 4 — Partial Functions and Composition
|
|
376
|
+
|
|
377
|
+
## **4.1 Partial functions**
|
|
378
|
+
|
|
379
|
+
A **partial function** is a mapping that may be undefined for some inputs.
|
|
380
|
+
In `k`, every expression denotes such a function.
|
|
381
|
+
If a function is undefined for a given input, the evaluation process halts at that point, producing no result.
|
|
382
|
+
|
|
383
|
+
There is no notion of error or exception.
|
|
384
|
+
Undefined means simply “no output”.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## **4.2 Composition**
|
|
389
|
+
|
|
390
|
+
Composition combines two or more partial functions sequentially.
|
|
391
|
+
If `f` and `g` are functions, `(f g)` means “apply `f`, then apply `g` to the result”.
|
|
392
|
+
|
|
393
|
+
A composition `(f g)` is defined on a value `x` if and only if:
|
|
394
|
+
|
|
395
|
+
* `f` is defined on `x`, and
|
|
396
|
+
* `g` is defined on the result of `f(x)`.
|
|
397
|
+
|
|
398
|
+
Otherwise, the composition is undefined.
|
|
399
|
+
|
|
400
|
+
Composition is **associative**:
|
|
401
|
+
|
|
402
|
+
```math
|
|
403
|
+
(f (g h)) ≡ ((f g) h)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Therefore, parentheses are unnecessary, except for the special case of the **empty composition**, written `()`, which is the identity function:
|
|
407
|
+
|
|
408
|
+
```math
|
|
409
|
+
() x = x () = x
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## **4.3 Product composition**
|
|
415
|
+
|
|
416
|
+
Product composition creates a function that applies several subfunctions in parallel to the same input and gathers their results into a product value.
|
|
417
|
+
|
|
418
|
+
Syntax:
|
|
419
|
+
|
|
420
|
+
```math
|
|
421
|
+
{ f₁ l₁, f₂ l₂, …, fₙ lₙ }
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
This expression is defined on an input value if all component functions are defined on that input.
|
|
425
|
+
Its result is a product with fields labeled `l₁ … lₙ`, each containing the result of the corresponding function.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## **4.4 Union composition**
|
|
430
|
+
|
|
431
|
+
Union composition represents concurrent evaluation with fallback:
|
|
432
|
+
|
|
433
|
+
```k-lang
|
|
434
|
+
< f₁, f₂, …, fₙ >
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The result is defined for an input `x` if at least one subfunction is defined.
|
|
438
|
+
Evaluation proceeds left to right; the first defined result is used.
|
|
439
|
+
If none are defined, the result is undefined.
|
|
440
|
+
|
|
441
|
+
Example:
|
|
442
|
+
|
|
443
|
+
```k-lang
|
|
444
|
+
< /x, /y >
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
extracts variant `x` if present; otherwise `y` if present.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## **4.5 Constants**
|
|
452
|
+
|
|
453
|
+
A constant function is one that always returns the same value, regardless of its input.
|
|
454
|
+
Since `k` lacks a direct syntax for literal values, constants are created using functions that produce them.
|
|
455
|
+
|
|
456
|
+
For example, we can define functions that return `true` and `false`:
|
|
457
|
+
|
|
458
|
+
```k-lang
|
|
459
|
+
$ bool = < {} true, {} false >;
|
|
460
|
+
true_bool = {} |true $bool ;
|
|
461
|
+
false_bool = {} |false $bool ;
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Here, `true_bool` and `false_bool` are constant functions. They are also **total functions**, meaning they are defined for all inputs. Each function ignores its input and produces a fixed boolean value.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## **4.6 Projection and Variant Value constructor**
|
|
469
|
+
|
|
470
|
+
Projection selects a field or a variant from a product or union.
|
|
471
|
+
It is written with a leading dot `.` or a leading slash `/`, respectively:
|
|
472
|
+
|
|
473
|
+
```k-lang
|
|
474
|
+
.x // Selects field 'x' from a product: { T x, ... }
|
|
475
|
+
/x // Extracts the value from a union variant 'x': < T x, ... >
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
If the input is a product, `.x` returns the value of field `x`. If the input is a union, `/x` returns the value of variant `x`. In all other cases, the projection is undefined.
|
|
479
|
+
|
|
480
|
+
Variant value constructors for a given union type `T = <T₁ tag₁, T₂ tag₂, … >` are written as `|tag₁`, `|tag₂`, ….
|
|
481
|
+
|
|
482
|
+
```k-lang
|
|
483
|
+
|tag₁ // lift a value of type T₁ into a value of type T
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Projections and variant constructors are the most basic partial functions.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## **4.7 Derived combinations**
|
|
491
|
+
|
|
492
|
+
Complex functions are built by nesting compositions.
|
|
493
|
+
For example:
|
|
494
|
+
|
|
495
|
+
```k-lang
|
|
496
|
+
{ .x field1, .y |tag field2 }
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## **4.8 Identity and emptiness**
|
|
502
|
+
|
|
503
|
+
The empty composition `()` is the identity function.
|
|
504
|
+
The empty union `<>` is the always-undefined function.
|
|
505
|
+
The empty product `{}` is the constant function returning the unit value.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## **4.9 Summary**
|
|
510
|
+
|
|
511
|
+
* In `k`, all expressions denote **partial functions**.
|
|
512
|
+
* An undefined result simply means “no output,” not an error.
|
|
513
|
+
* **Composition** `(f g)` applies functions sequentially and is associative, so parentheses are often optional.
|
|
514
|
+
* The empty composition `()` is the **identity function**.
|
|
515
|
+
* **Product composition** `{...}` runs functions in parallel and gathers their results.
|
|
516
|
+
* **Union composition** `<...>` tries functions in order, returning the first successful result.
|
|
517
|
+
* **Projections** (`.x`, `/x`) are fundamental building blocks for accessing data structures.
|
|
518
|
+
* **Variant constructor** (`|tag`) allows lifting a value into a variant type value.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# Chapter 5 — Typing, Filters, and Normalization
|
|
526
|
+
|
|
527
|
+
## **5.1 Types as functions**
|
|
528
|
+
|
|
529
|
+
A type expression in `k` can appear wherever a function is expected.
|
|
530
|
+
When used this way, it is prefixed by `$`.
|
|
531
|
+
It then behaves as a partial **identity** function that is defined only for values of that type.
|
|
532
|
+
For example, `$ bool` is a partial identity: it returns its argument unchanged when the argument is of type `bool`, and it is undefined otherwise.
|
|
533
|
+
|
|
534
|
+
This convention eliminates any special syntax for annotating sub-expressions with types.
|
|
535
|
+
An expression may be *restricted* to a type simply by composing it with the corresponding type expression.
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## **5.2 Filters**
|
|
540
|
+
|
|
541
|
+
A **filter** is a syntactic form that denotes a *class of types* — a set of types that share a common structure.
|
|
542
|
+
|
|
543
|
+
Filter expressions are introduced by `?` and can be:
|
|
544
|
+
|
|
545
|
+
- **Type expressions** — `? $bool`, `? $< {} x, bool y >`
|
|
546
|
+
- **Product filters** — `? { Filter1 field1, Filter2 field2 }`
|
|
547
|
+
- **Union filters** — `? < Filter1 tag1, Filter2 tag2 >`
|
|
548
|
+
- **Any-type filter** — `? ( ... )`
|
|
549
|
+
- **Filter variables** — `? X` (metavariables representing unknown types)
|
|
550
|
+
- **Filter bindings** — `? < X f, (...) = Y g, ... > = X`
|
|
551
|
+
|
|
552
|
+
Filters may contain `...` to indicate that additional fields or tags are allowed:
|
|
553
|
+
`? { Filter1 field1, ... }` matches any product with at least `field1`.
|
|
554
|
+
|
|
555
|
+
The filter `? (...)` matches any type.
|
|
556
|
+
The filter `? {...}` matches any product type.
|
|
557
|
+
The filter `? <...>` matches any union type.
|
|
558
|
+
There is no closed unknown filter: use `?{}` for the empty product or `?<>` for the empty union.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## **5.3 Examples of filters**
|
|
563
|
+
|
|
564
|
+
- `?(...)` — represents any type.
|
|
565
|
+
- `?< (...) f, (...) g >` — represents all union types having exactly two variants `f` and `g`.
|
|
566
|
+
- `?{ X f, X g }` — represents all product types with two fields `f` and `g`, both of the same type.
|
|
567
|
+
|
|
568
|
+
A filter constrains where a partial function is defined; it does not affect the operational behavior of the function once defined.
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## **5.4 Recursive filters**
|
|
573
|
+
|
|
574
|
+
Filters may be recursive.
|
|
575
|
+
They can describe families of recursive types by defining a metavariable in terms of a filter that references it.
|
|
576
|
+
|
|
577
|
+
Example (list definition):
|
|
578
|
+
|
|
579
|
+
```k-lang
|
|
580
|
+
?< {} nil, {X car, Y cdr} cons > = Y
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
This filter states that `Y` is a union type with two variants: `nil` and `cons`.
|
|
584
|
+
|
|
585
|
+
- The `nil` variant holds an empty product `{}`.
|
|
586
|
+
- The `cons` variant is a product with two fields: `car` of some type `X`, and `cdr` of type `Y` itself.
|
|
587
|
+
|
|
588
|
+
This recursive structure defines a linked list where each element has a `car` (the value) and a `cdr` (the rest of the list).
|
|
589
|
+
It thus denotes lists of elements of type `X`.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## **5.5 Type variables and scope**
|
|
594
|
+
|
|
595
|
+
A type variable (an identifier, typically starting with an uppercase letter) introduced in a filter is visible within the enclosing function definition.
|
|
596
|
+
For example:
|
|
597
|
+
|
|
598
|
+
```k-lang
|
|
599
|
+
car = ?< {} nil, {X car, Y cdr} cons > = Y /cons .car ?X;
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Here:
|
|
603
|
+
|
|
604
|
+
- `X` and `Y` are type variables.
|
|
605
|
+
- The filter `?<{}nil,{X car,Y cdr}cons>=Y` constrains the function `car` to be defined only on types that match the recursive list structure.
|
|
606
|
+
- The expression `/cons` selects the `cons` variant of the union.
|
|
607
|
+
- The expression `.car` accesses the `car` field of a `cons` variant.
|
|
608
|
+
- The final filter `?X` asserts that the result of the field access is of type `X`, the type of the list's elements.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## **5.6 Type inference and normalization**
|
|
613
|
+
|
|
614
|
+
Every `k` program can be analyzed to assign an input and an output filter to every sub-expression. This process is called type inference abd normalization.
|
|
615
|
+
|
|
616
|
+
Type inference and normalization proceeds as follows:
|
|
617
|
+
|
|
618
|
+
1. Build a graph of all type references appearing explicitly and implicitly in the program.
|
|
619
|
+
2. Annotate each expression node with a pair of (input filter, output filter).
|
|
620
|
+
3. Replace filters that match a single, concrete type with that type expression, adding any newly-discovered types to the graph.
|
|
621
|
+
4. Repeat until a fixed point is reached and no more changes occur.
|
|
622
|
+
5. Compute canonical forms for all newly introduced types.
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## **5.7 Summary**
|
|
627
|
+
|
|
628
|
+
- Type expressions act as identity functions defined on values of that type.
|
|
629
|
+
- Filters describe sets of types and can constrain where a function is defined.
|
|
630
|
+
- Filters may be products, unions, or the unconstrained any-type form, and can be recursive.
|
|
631
|
+
- Variables in filters have definition-level scope.
|
|
632
|
+
- Normalization computes explicit input/output filters for every expression.
|
|
633
|
+
- Filters are used in the normalization and type checking process. They are ignored at runtime.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# Chapter 6 — Values in Memory and Canonical Serialization
|
|
641
|
+
|
|
642
|
+
## **6.1 Unified value representation**
|
|
643
|
+
|
|
644
|
+
Every `k` value exists in one of three forms during execution:
|
|
645
|
+
|
|
646
|
+
1. **Serialized form** — a canonical bit sequence that can be stored, transmitted, or passed between processes.
|
|
647
|
+
2. **Lazy (or hybrid) form** — a partially deserialized structure that keeps most data in its serialized form until access is needed.
|
|
648
|
+
3. **Materialized form** — a fully deserialized tree of nodes in memory.
|
|
649
|
+
|
|
650
|
+
This unified approach enables programs to operate on serialized inputs without unnecessary deserialization, optimizing performance for identity operations and partial data access patterns.
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## **6.2 The KValue abstraction**
|
|
655
|
+
|
|
656
|
+
All values are represented by a single abstraction:
|
|
657
|
+
|
|
658
|
+
```c
|
|
659
|
+
struct KValue {
|
|
660
|
+
enum ValueForm {
|
|
661
|
+
SERIALIZED, // pure bitstream + type info
|
|
662
|
+
LAZY, // hybrid: some nodes materialized, others serialized
|
|
663
|
+
MATERIALIZED, // traditional tree structure
|
|
664
|
+
EXTERNAL // built-in/foreign type with custom handlers
|
|
665
|
+
} form;
|
|
666
|
+
|
|
667
|
+
union {
|
|
668
|
+
struct {
|
|
669
|
+
uint8_t* bits; // canonical bitstream
|
|
670
|
+
int type_id; // identifies canonical type
|
|
671
|
+
} serialized;
|
|
672
|
+
|
|
673
|
+
struct {
|
|
674
|
+
struct KNode* root; // root of materialized or partially materialized tree
|
|
675
|
+
} materialized;
|
|
676
|
+
|
|
677
|
+
struct {
|
|
678
|
+
void* data; // opaque external data
|
|
679
|
+
int type_id; // external type identifier
|
|
680
|
+
ExternalOps* ops; // function pointers for operations
|
|
681
|
+
} external;
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### **6.2.1 Self-delimiting streams**
|
|
687
|
+
|
|
688
|
+
The canonical encoding is **self-delimiting**, meaning each value can be parsed without knowing its length in advance:
|
|
689
|
+
|
|
690
|
+
- **Union nodes**: Fixed-length discriminators define clear boundaries
|
|
691
|
+
- **Product nodes**: Type structure determines field count and termination
|
|
692
|
+
- **External data**: Length-prefixed encoding (LEB128 + payload)
|
|
693
|
+
- **Unit values**: Zero bytes consumed
|
|
694
|
+
|
|
695
|
+
This enables **streaming evaluation** where programs can process data incrementally without buffering entire inputs.
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## **6.3 Lazy node structure**
|
|
700
|
+
|
|
701
|
+
Nodes in lazy values can be in different states of materialization:
|
|
702
|
+
|
|
703
|
+
```c
|
|
704
|
+
struct KNode {
|
|
705
|
+
int state; // canonical type state (C0, C1,…)
|
|
706
|
+
int tag; // variant index, −1 for product
|
|
707
|
+
int arity; // number of children
|
|
708
|
+
|
|
709
|
+
enum NodeState {
|
|
710
|
+
SERIALIZED_CHILD, // child exists as bitstream only
|
|
711
|
+
MATERIALIZED_CHILD, // child is fully materialized
|
|
712
|
+
EXTERNAL_CHILD // child is external/built-in type
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
union KChild {
|
|
716
|
+
struct {
|
|
717
|
+
enum NodeState state; // which kind of child this is
|
|
718
|
+
union {
|
|
719
|
+
struct KNode* node; // materialized child
|
|
720
|
+
struct {
|
|
721
|
+
uint8_t* bits; // serialized child data
|
|
722
|
+
int bit_offset; // position within parent's bitstream
|
|
723
|
+
} serialized;
|
|
724
|
+
struct KValue* external; // external child
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
} child[];
|
|
728
|
+
};
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## **6.4 Canonical serialization format**
|
|
734
|
+
|
|
735
|
+
The serialized form uses a deterministic canonical encoding that produces identical bit sequences for structurally identical values, extended to support lazy evaluation:
|
|
736
|
+
|
|
737
|
+
### **6.4.0 Canonical encoding principles**
|
|
738
|
+
|
|
739
|
+
The encoding is based on the canonical finite automaton representation of types (Chapter 3):
|
|
740
|
+
|
|
741
|
+
#### **Fixed-length union codes**
|
|
742
|
+
|
|
743
|
+
For each union state with *m* variants, we use `k = ceil(log2(m))` bits to encode the variant selection:
|
|
744
|
+
|
|
745
|
+
| Variants | Bits needed | Example codes |
|
|
746
|
+
|----------|-------------|---------------|
|
|
747
|
+
| 2 | 1 | `0`, `1` |
|
|
748
|
+
| 3 | 2 | `00`, `01`, `10` |
|
|
749
|
+
| 4 | 2 | `00`, `01`, `10`, `11` |
|
|
750
|
+
| 5-8 | 3 | `000` through `111` |
|
|
751
|
+
|
|
752
|
+
#### **Encoding by node type**
|
|
753
|
+
|
|
754
|
+
Each node in the value tree is encoded based on its automaton state:
|
|
755
|
+
|
|
756
|
+
- **Product nodes** — emit no bits, encode children in canonical field order (alphabetical)
|
|
757
|
+
- **Union nodes** — emit fixed-length variant code, then encode the selected child
|
|
758
|
+
- **Unit nodes** — emit nothing (arity = 0, no children)
|
|
759
|
+
- **External nodes** — emit type marker + delegate to external serializer
|
|
760
|
+
|
|
761
|
+
#### **Example: Natural numbers**
|
|
762
|
+
|
|
763
|
+
For type `bnat`, intuitively binary natural numbers defined by `$bnat = < bnat 0, bnat 1, {} _ >`, we get
|
|
764
|
+
|
|
765
|
+
```k-repl
|
|
766
|
+
--C bnat
|
|
767
|
+
$ @56b95dc611742a8e1cbb72d399660b5f18e4c3426dba0171f86dcdb2c41e9d91 = < @56b95dc611742a8e1cbb72d399660b5f18e4c3426dba0171f86dcdb2c41e9d91 0, @56b95dc611742a8e1cbb72d399660b5f18e4c3426dba0171f86dcdb2c41e9d91 1, @368d370568fa01bc70e837de57958e968bee510ba5e512e6a6b7b8c1c184a20e _ >; -- $C0=<C0"0",C0"1",C1"_"> $C1={};
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
The corresponding Context-Free grammar is:
|
|
771
|
+
|
|
772
|
+
```text
|
|
773
|
+
// 3 rules for C0, so we need 2 bits to encode the variant choice:
|
|
774
|
+
C0 -> C0 "|0" // encoded by '00'
|
|
775
|
+
C0 -> C0 "|1" // encoded by '01'
|
|
776
|
+
C0 -> C1 "|_" // encoded by '10'
|
|
777
|
+
// 1 rule for C1 so we need zero bits:
|
|
778
|
+
C1 -> '{}'; // encoded by ''
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
- 3 variants require `ceil(log2(3)) = 2` bits
|
|
782
|
+
- Codes: `00` (0-bit), `01` (1-bit), `10` (end marker)
|
|
783
|
+
|
|
784
|
+
The value `{}|_|0|1` or in JSON-like notation `{1:{0:{_:{}}}}` (binary 10₂ = decimal 2) encodes as:
|
|
785
|
+
|
|
786
|
+
```text
|
|
787
|
+
01 00 10
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
This represents the left-most derivation of `{}|_|0|1` from `C0`.
|
|
791
|
+
|
|
792
|
+
#### **Prefix-free property**
|
|
793
|
+
|
|
794
|
+
The encoding is **prefix-free**: no valid encoding is a prefix of another. This enables:
|
|
795
|
+
|
|
796
|
+
- Linear-time parsing without backtracking
|
|
797
|
+
- Efficient `skip_field()` operations for lazy evaluation
|
|
798
|
+
- Deterministic decoding from any valid position in the stream
|
|
799
|
+
|
|
800
|
+
### **6.4.1 DAG compression for repeated subtrees**
|
|
801
|
+
|
|
802
|
+
The canonical serialization can be compressed using **DAG (Directed Acyclic Graph) representation** to eliminate duplicate subtrees:
|
|
803
|
+
|
|
804
|
+
#### **Subtree identification**
|
|
805
|
+
|
|
806
|
+
Each unique subtree is assigned a **node ID** based on its canonical hash:
|
|
807
|
+
|
|
808
|
+
```text
|
|
809
|
+
hash(node) = hash(state, tag, arity, hash(child_0), hash(child_1), ...)
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Identical subtrees (same structure and content) get the same hash and share the same node ID.
|
|
813
|
+
|
|
814
|
+
#### **Two-pass encoding**
|
|
815
|
+
|
|
816
|
+
##### **Pass 1: Collection**
|
|
817
|
+
|
|
818
|
+
- Traverse the value tree and compute hashes for all subtrees
|
|
819
|
+
- Build a **node table** mapping `node_id → (first_occurrence_offset, reference_count)`
|
|
820
|
+
- Subtrees with `reference_count > 1` are candidates for sharing
|
|
821
|
+
|
|
822
|
+
##### **Pass 2: Emission**
|
|
823
|
+
|
|
824
|
+
- **First occurrence**: emit the full subtree encoding
|
|
825
|
+
- **Subsequent occurrences**: emit a **back-reference** to the node ID
|
|
826
|
+
|
|
827
|
+
#### **Back-reference encoding**
|
|
828
|
+
|
|
829
|
+
```text
|
|
830
|
+
| Reference Bit (1) | Node ID (variable length) |
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
- `0` bit indicates normal encoding follows
|
|
834
|
+
- `1` bit indicates back-reference follows
|
|
835
|
+
- Node ID uses variable-length encoding (e.g., LEB128)
|
|
836
|
+
|
|
837
|
+
#### **Example: Shared subtrees**
|
|
838
|
+
|
|
839
|
+
Consider a binary tree with repeated leaf patterns:
|
|
840
|
+
|
|
841
|
+
```k-lang
|
|
842
|
+
$tree = < { tree left, tree right } branch, int leaf >;
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
Value: A tree corresponding to the structure:
|
|
846
|
+
`branch(
|
|
847
|
+
leaf(42),
|
|
848
|
+
branch(
|
|
849
|
+
leaf(42),
|
|
850
|
+
leaf(17)
|
|
851
|
+
)
|
|
852
|
+
)`:
|
|
853
|
+
|
|
854
|
+
**Without DAG**: Each leaf `(42)` is encoded separately
|
|
855
|
+
**With DAG**: The `(42)` subtree is encoded once, then referenced
|
|
856
|
+
|
|
857
|
+
**Encoding sequence**:
|
|
858
|
+
|
|
859
|
+
```text
|
|
860
|
+
Node Table:
|
|
861
|
+
N0: 42 (appears 2 times)
|
|
862
|
+
N1: 17 (appears 1 time)
|
|
863
|
+
|
|
864
|
+
Bitstream:
|
|
865
|
+
00 // branch variant
|
|
866
|
+
0 <N0> // first occurrence of value: 42
|
|
867
|
+
00 // branch variant
|
|
868
|
+
1 N0 // back-reference to value: 42
|
|
869
|
+
0 <N1> // {value: 17}
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
### **6.4.2 Stream structure**
|
|
873
|
+
|
|
874
|
+
```text
|
|
875
|
+
| Header (8 bytes) | Node Count (4 bytes) | Node Table | Bitstream |
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
- **Header** — type hash identifying the canonical automaton
|
|
879
|
+
- **Node Count** — number of shared nodes (0 = no DAG compression)
|
|
880
|
+
- **Node Table** — `[node_id, offset_in_bitstream, size_in_bits]` for each shared node
|
|
881
|
+
- **Bitstream** — concatenated prefix-free codes with back-references
|
|
882
|
+
|
|
883
|
+
#### **Node table format**
|
|
884
|
+
|
|
885
|
+
When DAG compression is used (`Node Count > 0`):
|
|
886
|
+
|
|
887
|
+
```text
|
|
888
|
+
For each shared node:
|
|
889
|
+
| Node ID (4 bytes) | Offset (4 bytes) | Size in bits (4 bytes) |
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
This allows the decoder to:
|
|
893
|
+
|
|
894
|
+
1. **Random access**: Jump directly to any shared subtree definition
|
|
895
|
+
2. **Lazy loading**: Resolve back-references only when needed
|
|
896
|
+
3. **Validation**: Verify subtree boundaries during parsing
|
|
897
|
+
|
|
898
|
+
#### **Compression algorithms**
|
|
899
|
+
|
|
900
|
+
**Greedy sharing**: Share any subtree that appears more than once
|
|
901
|
+
|
|
902
|
+
- Simple to implement
|
|
903
|
+
- Good compression for naturally occurring patterns
|
|
904
|
+
|
|
905
|
+
**Size-based sharing**: Only share subtrees above a minimum size threshold
|
|
906
|
+
|
|
907
|
+
- Avoids overhead of referencing tiny subtrees
|
|
908
|
+
- Better compression ratio in practice
|
|
909
|
+
|
|
910
|
+
**Frequency-weighted sharing**: Prioritize subtrees by `size × frequency`
|
|
911
|
+
|
|
912
|
+
- Optimal compression for space-constrained scenarios
|
|
913
|
+
- Requires more sophisticated analysis
|
|
914
|
+
|
|
915
|
+
#### **Lazy back-reference resolution**
|
|
916
|
+
|
|
917
|
+
Back-references integrate seamlessly with lazy evaluation:
|
|
918
|
+
|
|
919
|
+
```c
|
|
920
|
+
DecoratedPtr resolve_reference(DecoratedPtr backref) {
|
|
921
|
+
node_id = read_variable_int(backref.bits, backref.bit_offset);
|
|
922
|
+
|
|
923
|
+
// Look up in node table
|
|
924
|
+
node_entry = node_table[node_id];
|
|
925
|
+
|
|
926
|
+
return (DecoratedPtr) {
|
|
927
|
+
.bits = backref.bits,
|
|
928
|
+
.bit_offset = node_entry.offset,
|
|
929
|
+
.type_state = node_entry.type_state,
|
|
930
|
+
.ref_count = 1
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
The decorated pointer model naturally handles back-references as lightweight pointer redirections.
|
|
936
|
+
|
|
937
|
+
#### **Memory vs. compression trade-offs**
|
|
938
|
+
|
|
939
|
+
| Strategy | Memory usage | Compression ratio | Decode speed |
|
|
940
|
+
|----------|--------------|-------------------|--------------|
|
|
941
|
+
| No DAG | Low | 1.0× (baseline) | Fastest |
|
|
942
|
+
| Greedy DAG | Medium | 2-10× | Fast |
|
|
943
|
+
| Optimal DAG | High | 5-50× | Medium |
|
|
944
|
+
|
|
945
|
+
For **streaming applications**: Prefer greedy DAG with small node tables
|
|
946
|
+
For **archival storage**: Use optimal DAG compression
|
|
947
|
+
For **real-time processing**: Skip DAG compression entirely
|
|
948
|
+
|
|
949
|
+
#### **Compression example: Repeated data structures**
|
|
950
|
+
|
|
951
|
+
Consider a list of similar records:
|
|
952
|
+
|
|
953
|
+
```k-lang
|
|
954
|
+
$record = { string name, int age, string department };
|
|
955
|
+
$company = < { record head, company tail } cons, {} nil >;
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
Value representing 1000 employees where 80% work in "Engineering":
|
|
959
|
+
|
|
960
|
+
```text
|
|
961
|
+
Without DAG: Each "Engineering" string encoded separately
|
|
962
|
+
- 1000 records × 30 bytes/record = 30KB
|
|
963
|
+
|
|
964
|
+
With DAG: "Engineering" string shared via back-references
|
|
965
|
+
- 1 × "Engineering" (11 bytes) + 800 × back-reference (3 bytes) = 2.4KB
|
|
966
|
+
- Compression ratio: ~12.5×
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
The DAG representation transforms the **tree** into a more efficient **directed acyclic graph**:
|
|
970
|
+
|
|
971
|
+
```text
|
|
972
|
+
Tree: Record1 → "Engineering"
|
|
973
|
+
Record2 → "Engineering"
|
|
974
|
+
Record3 → "Engineering"
|
|
975
|
+
...
|
|
976
|
+
|
|
977
|
+
DAG: Record1 ↘
|
|
978
|
+
Record2 → Shared("Engineering")
|
|
979
|
+
Record3 ↗
|
|
980
|
+
...
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### **6.4.3 Hardware-optimized encoding**
|
|
984
|
+
|
|
985
|
+
While bit-level precision is theoretically optimal, modern hardware operates on **byte-aligned data** using **word-sized registers**. A practical encoding must balance compression with decode performance.
|
|
986
|
+
|
|
987
|
+
#### **Register-friendly union encoding**
|
|
988
|
+
|
|
989
|
+
Instead of `ceil(log2(m))` bits, use **byte-aligned codes** optimized for common union sizes:
|
|
990
|
+
|
|
991
|
+
| Union variants | Theoretical bits | Hardware-optimized |
|
|
992
|
+
|----------------|------------------|-------------------|
|
|
993
|
+
| 2-3 variants | 1-2 bits | 1 byte (8 bits) |
|
|
994
|
+
| 4-15 variants | 2-4 bits | 1 byte (8 bits) |
|
|
995
|
+
| 16-255 variants| 4-8 bits | 1 byte (8 bits) |
|
|
996
|
+
| 256+ variants | 8+ bits | 2+ bytes (LEB128) |
|
|
997
|
+
|
|
998
|
+
**Rationale**: Single-byte discriminators enable efficient decoding with simple memory loads and bit masking.
|
|
999
|
+
|
|
1000
|
+
#### **SIMD-friendly data layout**
|
|
1001
|
+
|
|
1002
|
+
Modern CPUs can process multiple bytes simultaneously using **SIMD instructions**. Structure the encoding to leverage this:
|
|
1003
|
+
|
|
1004
|
+
```c
|
|
1005
|
+
// Bad: bit-packed discriminators
|
|
1006
|
+
struct BitPacked {
|
|
1007
|
+
uint64_t discriminators; // 8 × 8-bit = 64 discriminators packed
|
|
1008
|
+
// Requires expensive bit extraction
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
// Good: byte-aligned discriminators
|
|
1012
|
+
struct ByteAligned {
|
|
1013
|
+
uint8_t discriminators[8]; // 8 discriminators, SIMD-friendly
|
|
1014
|
+
// Can be processed with single AVX2 instruction
|
|
1015
|
+
};
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
#### **Cache-line aware field ordering**
|
|
1019
|
+
|
|
1020
|
+
Arrange product fields to minimize **cache misses**:
|
|
1021
|
+
|
|
1022
|
+
```c
|
|
1023
|
+
// Product field layout optimization
|
|
1024
|
+
struct ProductLayout {
|
|
1025
|
+
uint8_t discriminators[N]; // All discriminators together (hot data)
|
|
1026
|
+
uint8_t padding[64 - N % 64]; // Align to cache line boundary
|
|
1027
|
+
uint8_t field_data[]; // Field data follows (cold data)
|
|
1028
|
+
};
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
Fields are ordered by **access frequency** rather than alphabetical order:
|
|
1032
|
+
|
|
1033
|
+
- **Hot fields** (frequently accessed): placed first
|
|
1034
|
+
- **Cold fields** (rarely accessed): placed last
|
|
1035
|
+
- **Large fields** (arrays, strings): placed at end to avoid cache pollution
|
|
1036
|
+
|
|
1037
|
+
#### **Word-aligned pointer arithmetic**
|
|
1038
|
+
|
|
1039
|
+
Replace bit-level navigation with **word-aligned operations**:
|
|
1040
|
+
|
|
1041
|
+
```c
|
|
1042
|
+
// Bit-level navigation (slow)
|
|
1043
|
+
DecoratedPtr navigate_bitwise(DecoratedPtr ptr, int field_index) {
|
|
1044
|
+
size_t bit_offset = ptr.bit_offset;
|
|
1045
|
+
for (int i = 0; i < field_index; i++) {
|
|
1046
|
+
bit_offset += extract_field_bit_size(ptr.bits, bit_offset); // Expensive!
|
|
1047
|
+
}
|
|
1048
|
+
return make_ptr(ptr.bits, bit_offset);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Word-aligned navigation (fast)
|
|
1052
|
+
DecoratedPtr navigate_wordwise(DecoratedPtr ptr, int field_index) {
|
|
1053
|
+
uint8_t* byte_ptr = ptr.bits + (ptr.bit_offset >> 3); // Convert to byte offset
|
|
1054
|
+
|
|
1055
|
+
// Read field offset table (precomputed during encoding)
|
|
1056
|
+
uint32_t* offset_table = (uint32_t*)byte_ptr;
|
|
1057
|
+
uint32_t field_offset = offset_table[field_index]; // Single memory load
|
|
1058
|
+
|
|
1059
|
+
return make_ptr(ptr.bits, field_offset << 3); // Convert back to bits
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
#### **CPU instruction optimization**
|
|
1064
|
+
|
|
1065
|
+
**Branch prediction friendly encoding**:
|
|
1066
|
+
|
|
1067
|
+
```c
|
|
1068
|
+
// Bad: unpredictable branching
|
|
1069
|
+
uint8_t discriminator = *byte_ptr++;
|
|
1070
|
+
switch (discriminator) {
|
|
1071
|
+
case 0: handle_variant_0(); break;
|
|
1072
|
+
case 1: handle_variant_1(); break;
|
|
1073
|
+
case 2: handle_variant_2(); break;
|
|
1074
|
+
// ... many cases cause branch misprediction
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Good: function table dispatch
|
|
1078
|
+
typedef void (*VariantHandler)(uint8_t* data);
|
|
1079
|
+
static VariantHandler variant_handlers[256] = {
|
|
1080
|
+
handle_variant_0, handle_variant_1, handle_variant_2, /* ... */
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
uint8_t discriminator = *byte_ptr++;
|
|
1084
|
+
variant_handlers[discriminator](byte_ptr); // Single indirect call
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
**Prefetch-aware access patterns**:
|
|
1088
|
+
|
|
1089
|
+
```c
|
|
1090
|
+
// Sequential prefetching for product fields
|
|
1091
|
+
void prefetch_product_fields(DecoratedPtr ptr, int num_fields) {
|
|
1092
|
+
uint32_t* offset_table = (uint32_t*)(ptr.bits + (ptr.bit_offset >> 3));
|
|
1093
|
+
|
|
1094
|
+
// Prefetch offset table into L1 cache
|
|
1095
|
+
__builtin_prefetch(offset_table, 0, 3); // Read, high temporal locality
|
|
1096
|
+
|
|
1097
|
+
// Prefetch first few fields
|
|
1098
|
+
for (int i = 0; i < min(num_fields, 4); i++) {
|
|
1099
|
+
uint8_t* field_ptr = ptr.bits + offset_table[i];
|
|
1100
|
+
__builtin_prefetch(field_ptr, 0, 2); // Read, medium temporal locality
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
#### **Memory bandwidth optimization**
|
|
1106
|
+
|
|
1107
|
+
**Streaming-friendly encoding**:
|
|
1108
|
+
|
|
1109
|
+
```c
|
|
1110
|
+
// Structure data for optimal memory bandwidth
|
|
1111
|
+
struct StreamLayout {
|
|
1112
|
+
struct {
|
|
1113
|
+
uint32_t total_size; // 4 bytes
|
|
1114
|
+
uint16_t num_fields; // 2 bytes
|
|
1115
|
+
uint16_t flags; // 2 bytes (padding + metadata)
|
|
1116
|
+
} header; // 8 bytes total (fits in single cache line)
|
|
1117
|
+
|
|
1118
|
+
uint32_t field_offsets[]; // 4N bytes (word-aligned)
|
|
1119
|
+
uint8_t field_data[]; // Variable length data
|
|
1120
|
+
};
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
**Memory access patterns**:
|
|
1124
|
+
|
|
1125
|
+
- **Sequential reads**: Optimal for streaming workloads
|
|
1126
|
+
- **Random access**: Offset table enables O(1) field access
|
|
1127
|
+
- **Bulk operations**: SIMD can process multiple discriminators simultaneously
|
|
1128
|
+
|
|
1129
|
+
#### **Hardware-specific optimizations**
|
|
1130
|
+
|
|
1131
|
+
**x86-64 optimizations**:
|
|
1132
|
+
|
|
1133
|
+
```c
|
|
1134
|
+
// Exploit x86-64 addressing modes
|
|
1135
|
+
DecoratedPtr navigate_x86(DecoratedPtr ptr, int field_index) {
|
|
1136
|
+
uint32_t* offset_table = (uint32_t*)(ptr.bits + (ptr.bit_offset >> 3));
|
|
1137
|
+
|
|
1138
|
+
// Single instruction: LEA + memory operand
|
|
1139
|
+
uint8_t* field_ptr = ptr.bits + offset_table[field_index];
|
|
1140
|
+
|
|
1141
|
+
return make_ptr_from_bytes(field_ptr);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Utilize SIMD for bulk discriminator reading
|
|
1145
|
+
void decode_discriminators_avx2(uint8_t* input, uint8_t* output, size_t count) {
|
|
1146
|
+
__m256i* input_vec = (__m256i*)input;
|
|
1147
|
+
__m256i* output_vec = (__m256i*)output;
|
|
1148
|
+
|
|
1149
|
+
for (size_t i = 0; i < count / 32; i++) {
|
|
1150
|
+
__m256i data = _mm256_load_si256(&input_vec[i]);
|
|
1151
|
+
|
|
1152
|
+
// Process 32 discriminators in parallel
|
|
1153
|
+
__m256i decoded = _mm256_and_si256(data, _mm256_set1_epi8(0x0F));
|
|
1154
|
+
|
|
1155
|
+
_mm256_store_si256(&output_vec[i], decoded);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
**ARM64 optimizations**:
|
|
1161
|
+
|
|
1162
|
+
```c
|
|
1163
|
+
// Exploit ARM64 load-with-offset instructions
|
|
1164
|
+
DecoratedPtr navigate_arm64(DecoratedPtr ptr, int field_index) {
|
|
1165
|
+
uint32_t* offset_table = (uint32_t*)(ptr.bits + (ptr.bit_offset >> 3));
|
|
1166
|
+
|
|
1167
|
+
// ARM64: single LDR instruction with register offset
|
|
1168
|
+
uint32_t offset = offset_table[field_index];
|
|
1169
|
+
uint8_t* field_ptr = ptr.bits + offset;
|
|
1170
|
+
|
|
1171
|
+
return make_ptr_from_bytes(field_ptr);
|
|
1172
|
+
}
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
#### **Performance analysis: Hardware vs. theoretical encoding**
|
|
1176
|
+
|
|
1177
|
+
**Decoding performance comparison** (cycles per field access):
|
|
1178
|
+
|
|
1179
|
+
| Encoding strategy | Field access cost | Cache behavior | SIMD support |
|
|
1180
|
+
|-------------------|-------------------|----------------|--------------|
|
|
1181
|
+
| Bit-packed optimal | 50-100 cycles | Poor (bit manipulation) | No |
|
|
1182
|
+
| Byte-aligned | 5-15 cycles | Good (word loads) | Yes |
|
|
1183
|
+
| Word-aligned + offset table | 2-8 cycles | Excellent (prefetchable) | Yes |
|
|
1184
|
+
|
|
1185
|
+
**Space vs. time trade-offs**:
|
|
1186
|
+
|
|
1187
|
+
```
|
|
1188
|
+
Bit-packed: 100% space efficiency, 10× decode overhead
|
|
1189
|
+
Byte-aligned: 90% space efficiency, 2× decode overhead
|
|
1190
|
+
Word-aligned: 80% space efficiency, 1× decode overhead (baseline)
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
**Real-world measurements** (modern x86-64, 3.2GHz):
|
|
1194
|
+
|
|
1195
|
+
```c
|
|
1196
|
+
// Benchmark: Navigate to field in 1000-field product
|
|
1197
|
+
Bit-manipulation: 2,400 ns (7,680 cycles)
|
|
1198
|
+
Byte-aligned: 480 ns (1,536 cycles)
|
|
1199
|
+
Word-aligned table: 160 ns (512 cycles)
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
The **offset table approach** provides the best practical performance despite modest space overhead.
|
|
1203
|
+
|
|
1204
|
+
#### **Recommended encoding strategy**
|
|
1205
|
+
|
|
1206
|
+
**Hybrid approach** balancing theory and practice:
|
|
1207
|
+
|
|
1208
|
+
1. **Small products** (≤8 fields): Byte-aligned, no offset table
|
|
1209
|
+
2. **Medium products** (9-64 fields): Word-aligned with offset table
|
|
1210
|
+
3. **Large products** (65+ fields): Hierarchical offset tables + prefetching
|
|
1211
|
+
4. **Unions**: Single byte discriminator (sufficient for 99.9% of real types)
|
|
1212
|
+
|
|
1213
|
+
**Encoding format**:
|
|
1214
|
+
|
|
1215
|
+
```c
|
|
1216
|
+
struct OptimalEncoding {
|
|
1217
|
+
uint8_t type_tag; // 1 byte: product/union/external marker
|
|
1218
|
+
uint8_t arity; // 1 byte: number of children (0-255)
|
|
1219
|
+
uint16_t flags; // 2 bytes: compression flags, alignment info
|
|
1220
|
+
uint32_t total_size; // 4 bytes: total size for skipping
|
|
1221
|
+
|
|
1222
|
+
// For products with >8 fields:
|
|
1223
|
+
uint32_t field_offsets[arity]; // 4×N bytes: random access table
|
|
1224
|
+
|
|
1225
|
+
// Payload data follows
|
|
1226
|
+
uint8_t data[];
|
|
1227
|
+
};
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
**Benefits**:
|
|
1231
|
+
|
|
1232
|
+
- **8-byte aligned** structures for optimal memory access
|
|
1233
|
+
- **Single cache line** headers for small products
|
|
1234
|
+
- **SIMD-friendly** data layout
|
|
1235
|
+
- **Prefetch-optimized** for large structures
|
|
1236
|
+
- **Hardware-agnostic** but optimized for common architectures
|
|
1237
|
+
|
|
1238
|
+
### **6.4.4 External type integration**
|
|
1239
|
+
|
|
1240
|
+
```text
|
|
1241
|
+
| External Marker (2 bits) | Type ID | External Data Length | External Data |
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
This allows built-in types (strings, floats, etc.) to be efficiently serialized within `k` values while maintaining the canonical property.
|
|
1245
|
+
|
|
1246
|
+
---
|
|
1247
|
+
|
|
1248
|
+
## **6.5 Demand-driven deserialization**
|
|
1249
|
+
|
|
1250
|
+
Operations on values trigger selective deserialization:
|
|
1251
|
+
|
|
1252
|
+
### **6.5.1 Access patterns**
|
|
1253
|
+
|
|
1254
|
+
- **Identity operation `()`** — no deserialization, direct bitstream copy
|
|
1255
|
+
- **Field access `.field`** — deserialize only the path to the requested field
|
|
1256
|
+
- **Pattern matching** — deserialize only the discriminator, then the matched branch
|
|
1257
|
+
- **Full evaluation** — progressive deserialization as computation proceeds
|
|
1258
|
+
|
|
1259
|
+
### **6.5.2 Lazy evaluation strategy**
|
|
1260
|
+
|
|
1261
|
+
```c
|
|
1262
|
+
function access_field(value: KValue, field_path: String[]): KValue {
|
|
1263
|
+
if (value.form == SERIALIZED) {
|
|
1264
|
+
// Partially deserialize to reach the field
|
|
1265
|
+
root = deserialize_to_depth(value, field_path.length);
|
|
1266
|
+
value = make_lazy(root, value.serialized.bits);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (value.form == LAZY) {
|
|
1270
|
+
node = navigate_path(value.lazy.root, field_path);
|
|
1271
|
+
if (node.child[field_idx].state == SERIALIZED_CHILD) {
|
|
1272
|
+
// Deserialize this child on demand
|
|
1273
|
+
child_value = deserialize_child(node, field_idx);
|
|
1274
|
+
node.child[field_idx].state = MATERIALIZED_CHILD;
|
|
1275
|
+
node.child[field_idx].node = child_value;
|
|
1276
|
+
}
|
|
1277
|
+
return &node.child[field_idx];
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Traditional navigation for fully materialized values
|
|
1281
|
+
return navigate_materialized(value, field_path);
|
|
1282
|
+
}
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
---
|
|
1286
|
+
|
|
1287
|
+
## **6.6 External type system**
|
|
1288
|
+
|
|
1289
|
+
Built-in and foreign types integrate through a plugin interface:
|
|
1290
|
+
|
|
1291
|
+
### **6.6.1 External operations**
|
|
1292
|
+
|
|
1293
|
+
```c
|
|
1294
|
+
struct ExternalOps {
|
|
1295
|
+
KValue* (*serialize)(void* data);
|
|
1296
|
+
void* (*deserialize)(KValue* serialized);
|
|
1297
|
+
KValue* (*apply_function)(void* data, const char* func_name, KValue* args);
|
|
1298
|
+
bool (*equals)(void* a, void* b);
|
|
1299
|
+
uint64_t (*hash)(void* data);
|
|
1300
|
+
void (*destroy)(void* data);
|
|
1301
|
+
};
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
### **6.6.2 Built-in type examples**
|
|
1305
|
+
|
|
1306
|
+
- **Strings** — UTF-8 encoded with length prefix
|
|
1307
|
+
- **Integers** — variable-length encoding (LEB128)
|
|
1308
|
+
- **Floats** — IEEE 754 binary representation
|
|
1309
|
+
- **Blobs** — raw binary data with length prefix
|
|
1310
|
+
- **Handles** — opaque references to external resources
|
|
1311
|
+
|
|
1312
|
+
### **6.6.3 Type registration**
|
|
1313
|
+
|
|
1314
|
+
```c
|
|
1315
|
+
void register_external_type(int type_id, const char* name, ExternalOps* ops) {
|
|
1316
|
+
external_types[type_id] = {
|
|
1317
|
+
.name = name,
|
|
1318
|
+
.ops = ops,
|
|
1319
|
+
.canonical_hash = compute_type_hash(name, ops->signature)
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
---
|
|
1325
|
+
|
|
1326
|
+
## **6.7 Memory management and performance**
|
|
1327
|
+
|
|
1328
|
+
### **6.7.1 Arena allocation**
|
|
1329
|
+
|
|
1330
|
+
All materialized nodes use arena allocation for fast allocation and bulk deallocation:
|
|
1331
|
+
|
|
1332
|
+
```c
|
|
1333
|
+
struct Arena {
|
|
1334
|
+
uint8_t* memory;
|
|
1335
|
+
size_t capacity;
|
|
1336
|
+
size_t used;
|
|
1337
|
+
Arena* next; // linked list of arena blocks
|
|
1338
|
+
};
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
### **6.7.2 Sharing and deduplication**
|
|
1342
|
+
|
|
1343
|
+
- **Canonical folding** — identical subtrees share the same materialized nodes
|
|
1344
|
+
- **Bitstream sharing** — multiple lazy values can reference the same underlying bitstream
|
|
1345
|
+
- **DAG back-references** — shared subtrees in serialized form avoid duplication during lazy materialization
|
|
1346
|
+
- **External sharing** — external values use reference counting or garbage collection as appropriate
|
|
1347
|
+
|
|
1348
|
+
### **6.7.3 Optimization opportunities**
|
|
1349
|
+
|
|
1350
|
+
- **Zero-copy operations** — identity, field projection on serialized values
|
|
1351
|
+
- **Streaming evaluation** — process large datasets without full materialization
|
|
1352
|
+
- **Parallel deserialization** — independent subtrees can be materialized concurrently
|
|
1353
|
+
- **DAG-aware caching** — cache resolutions of frequently accessed back-references
|
|
1354
|
+
- **Memoization** — cache frequently accessed paths in lazy structures
|
|
1355
|
+
|
|
1356
|
+
---
|
|
1357
|
+
|
|
1358
|
+
## **6.8 Example: Lazy evaluation in practice**
|
|
1359
|
+
|
|
1360
|
+
Consider processing a large JSON-like structure where we only need one field:
|
|
1361
|
+
|
|
1362
|
+
```k-lang
|
|
1363
|
+
$record = { string name, list data, metadata meta };
|
|
1364
|
+
$list = < {nat value, list next} cons, {} nil >;
|
|
1365
|
+
$metadata = { timestamp created, string author };
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
For the input program `\x.x.name` (extract name field):
|
|
1369
|
+
|
|
1370
|
+
1. **Input** arrives as serialized bitstream (e.g., from network/disk)
|
|
1371
|
+
2. **Lazy parsing** deserializes only the discriminator of the root product
|
|
1372
|
+
3. **Field access** deserializes path to `name` field, leaving `data` and `meta` serialized
|
|
1373
|
+
4. **Output** the extracted string value (possibly in external string format)
|
|
1374
|
+
|
|
1375
|
+
The entire `data` list and `metadata` remain as untouched bitstreams, providing massive performance benefits for large, sparsely accessed data structures.
|
|
1376
|
+
|
|
1377
|
+
### **6.8.1 Serialized representation**
|
|
1378
|
+
|
|
1379
|
+
The same value can exist in different forms simultaneously:
|
|
1380
|
+
|
|
1381
|
+
```c
|
|
1382
|
+
// Original input: fully serialized
|
|
1383
|
+
KValue input = {
|
|
1384
|
+
.form = SERIALIZED,
|
|
1385
|
+
.serialized = {
|
|
1386
|
+
.bits = bitstream_buffer,
|
|
1387
|
+
.type_hash = hash("record")
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
// After partial access: hybrid lazy
|
|
1392
|
+
KValue hybrid = {
|
|
1393
|
+
.form = LAZY,
|
|
1394
|
+
.lazy.root = {
|
|
1395
|
+
.state = 0, .tag = -1, .arity = 3,
|
|
1396
|
+
.child = {
|
|
1397
|
+
[0] = { .state = MATERIALIZED_CHILD, .node = materialized_string_node }, // name: materialized
|
|
1398
|
+
[1] = { .state = SERIALIZED_CHILD, .serialized = {bits+offset, 64} }, // data: still serialized
|
|
1399
|
+
[2] = { .state = SERIALIZED_CHILD, .serialized = {bits+offset, 1088} } // meta: still serialized
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
### **6.8.2 External type integration example**
|
|
1406
|
+
|
|
1407
|
+
```c
|
|
1408
|
+
// Built-in string type
|
|
1409
|
+
KValue string_value = {
|
|
1410
|
+
.form = EXTERNAL,
|
|
1411
|
+
.external = {
|
|
1412
|
+
.data = utf8_string_data,
|
|
1413
|
+
.type_id = BUILTIN_STRING,
|
|
1414
|
+
.ops = &string_operations
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// The string_operations provide:
|
|
1419
|
+
ExternalOps string_operations = {
|
|
1420
|
+
.serialize = string_to_canonical_bits,
|
|
1421
|
+
.deserialize = canonical_bits_to_string,
|
|
1422
|
+
.apply_function = string_builtin_functions, // length, concat, etc.
|
|
1423
|
+
.equals = utf8_string_compare,
|
|
1424
|
+
.hash = utf8_string_hash,
|
|
1425
|
+
.destroy = free_string_data
|
|
1426
|
+
};
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
## **6.9 Summary**
|
|
1432
|
+
|
|
1433
|
+
This unified representation provides several key advantages:
|
|
1434
|
+
|
|
1435
|
+
- **Performance** — avoid unnecessary deserialization through lazy evaluation
|
|
1436
|
+
- **Memory efficiency** — share serialized data between multiple references
|
|
1437
|
+
- **Composability** — external types integrate seamlessly with k's type system
|
|
1438
|
+
- **Streaming** — process large data structures with bounded memory usage
|
|
1439
|
+
- **Determinism** — canonical serialization ensures reproducible computation
|
|
1440
|
+
|
|
1441
|
+
The design supports the full spectrum from zero-copy identity operations to fully materialized tree traversal, allowing the runtime to automatically choose the most efficient representation for each use case.
|
|
1442
|
+
|
|
1443
|
+
### **6.9.1 Key design principles**
|
|
1444
|
+
|
|
1445
|
+
- **Immutability** — all representations are immutable, enabling safe sharing
|
|
1446
|
+
- **Demand-driven** — materialization happens only when computation requires it
|
|
1447
|
+
- **Canonical** — serialized forms are deterministic and hashable
|
|
1448
|
+
- **Extensible** — external types extend the system without breaking canonical properties
|
|
1449
|
+
- **Unified** — single abstraction handles all value representations
|
|
1450
|
+
|
|
1451
|
+
---
|
|
1452
|
+
|
|
1453
|
+
## **6.10 Execution model with decorated pointers**
|
|
1454
|
+
|
|
1455
|
+
The formal execution model is based on decorated pointers, which represent values during execution:
|
|
1456
|
+
|
|
1457
|
+
### **6.10.1 Decorated pointers**
|
|
1458
|
+
|
|
1459
|
+
Every value during execution is represented by a **decorated pointer**:
|
|
1460
|
+
|
|
1461
|
+
```c
|
|
1462
|
+
struct DecoratedPtr {
|
|
1463
|
+
uint8_t* bits; // pointer into serialized input heap
|
|
1464
|
+
size_t bit_offset; // bit position within the stream
|
|
1465
|
+
TypeState type_state; // canonical automaton state (C0, C1, ...)
|
|
1466
|
+
int ref_count; // for sharing and caching
|
|
1467
|
+
struct {
|
|
1468
|
+
bool is_cached; // memoization flag
|
|
1469
|
+
uint64_t content_hash; // for cache lookup
|
|
1470
|
+
} cache;
|
|
1471
|
+
};
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
The key insight: **decorated pointers are lightweight and can be computed without memory allocation**.
|
|
1475
|
+
|
|
1476
|
+
### **6.10.2 Program execution pipeline**
|
|
1477
|
+
|
|
1478
|
+
A compiled k program is a composition of operations: `projection ∘ product ∘ union ∘ builtin_call ∘ ...`
|
|
1479
|
+
|
|
1480
|
+
Each operation transforms a decorated pointer:
|
|
1481
|
+
|
|
1482
|
+
```text
|
|
1483
|
+
DecoratedPtr → DecoratedPtr
|
|
1484
|
+
```
|
|
1485
|
+
|
|
1486
|
+
#### **Projection operation**
|
|
1487
|
+
|
|
1488
|
+
For field access `.field_name`:
|
|
1489
|
+
|
|
1490
|
+
```c
|
|
1491
|
+
DecoratedPtr project_field_optimized(DecoratedPtr input, int field_index) {
|
|
1492
|
+
// Hardware-optimized field navigation
|
|
1493
|
+
uint8_t* byte_ptr = input.bits + (input.bit_offset >> 3);
|
|
1494
|
+
|
|
1495
|
+
// Read encoding header
|
|
1496
|
+
struct OptimalEncoding* header = (struct OptimalEncoding*)byte_ptr;
|
|
1497
|
+
|
|
1498
|
+
if (header->arity <= 8) {
|
|
1499
|
+
// Small product: sequential scan (cache-friendly)
|
|
1500
|
+
uint8_t* field_ptr = byte_ptr + sizeof(struct OptimalEncoding);
|
|
1501
|
+
for (int i = 0; i < field_index; i++) {
|
|
1502
|
+
uint32_t field_size = *(uint32_t*)field_ptr; // First 4 bytes = size
|
|
1503
|
+
field_ptr += field_size;
|
|
1504
|
+
}
|
|
1505
|
+
return make_ptr_from_bytes(field_ptr);
|
|
1506
|
+
} else {
|
|
1507
|
+
// Large product: offset table lookup (O(1) access)
|
|
1508
|
+
uint32_t field_offset = header->field_offsets[field_index];
|
|
1509
|
+
return make_ptr_from_bytes(byte_ptr + field_offset);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
**Key property**: Hardware-optimized navigation using word-aligned loads and offset tables.
|
|
1515
|
+
|
|
1516
|
+
#### **Union operation**
|
|
1517
|
+
|
|
1518
|
+
For pattern matching `case { tag1 → ..., tag2 → ... }`:
|
|
1519
|
+
|
|
1520
|
+
```c
|
|
1521
|
+
DecoratedPtr match_union_optimized(DecoratedPtr input, int expected_tag) {
|
|
1522
|
+
// Read single-byte discriminator (hardware-friendly)
|
|
1523
|
+
uint8_t* byte_ptr = input.bits + (input.bit_offset >> 3);
|
|
1524
|
+
struct OptimalEncoding* header = (struct OptimalEncoding*)byte_ptr;
|
|
1525
|
+
|
|
1526
|
+
uint8_t actual_tag = header->data[0]; // First byte of data = discriminator
|
|
1527
|
+
|
|
1528
|
+
if (actual_tag != expected_tag) {
|
|
1529
|
+
return UNDEFINED_PTR; // Pattern match failure
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Skip past header + discriminator
|
|
1533
|
+
uint8_t* child_ptr = byte_ptr + sizeof(struct OptimalEncoding) + 1;
|
|
1534
|
+
|
|
1535
|
+
return make_ptr_from_bytes(child_ptr);
|
|
1536
|
+
}
|
|
1537
|
+
```
|
|
1538
|
+
|
|
1539
|
+
#### **Product operation**
|
|
1540
|
+
|
|
1541
|
+
For product construction `{ field1, field2, ... }`:
|
|
1542
|
+
|
|
1543
|
+
```c
|
|
1544
|
+
DecoratedPtr construct_product(DecoratedPtr* field_ptrs, int arity) {
|
|
1545
|
+
// This is the primary point where memory allocation is required.
|
|
1546
|
+
ProductNode* result = allocate_product_node(arity);
|
|
1547
|
+
|
|
1548
|
+
// Evaluate each field (possibly in parallel)
|
|
1549
|
+
for (int i = 0; i < arity; i++) {
|
|
1550
|
+
result->fields[i] = evaluate_expression(field_ptrs[i]);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return make_materialized_ptr(result);
|
|
1554
|
+
}
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
#### **Built-in call operation**
|
|
1558
|
+
|
|
1559
|
+
For external functions `string_length`, `add_int`, etc.:
|
|
1560
|
+
|
|
1561
|
+
```c
|
|
1562
|
+
DecoratedPtr builtin_call(DecoratedPtr input, BuiltinFunc func) {
|
|
1563
|
+
// Force materialization of the input
|
|
1564
|
+
ExternalValue* materialized = force_external(input);
|
|
1565
|
+
|
|
1566
|
+
// Delegate to external implementation
|
|
1567
|
+
ExternalValue* result = func->call(materialized);
|
|
1568
|
+
|
|
1569
|
+
return make_external_ptr(result);
|
|
1570
|
+
}
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
### **6.10.3 Execution strategy**
|
|
1574
|
+
|
|
1575
|
+
1. **Input setup**: Place serialized input in heap, create root decorated pointer
|
|
1576
|
+
2. **Lazy evaluation**: Transform decorated pointers without allocation when possible
|
|
1577
|
+
3. **Forced materialization**: Allocate memory only when constructing products or calling built-ins
|
|
1578
|
+
4. **Reference counting**: Share decorated pointers for common subexpressions
|
|
1579
|
+
5. **Output serialization**: Convert final decorated pointer back to canonical bits
|
|
1580
|
+
|
|
1581
|
+
### **6.10.4 Example execution trace**
|
|
1582
|
+
|
|
1583
|
+
For program `\x.(x.data.head.value, x.name)` on input `{ name: "alice", data: [42, 17] }`:
|
|
1584
|
+
|
|
1585
|
+
```text
|
|
1586
|
+
Step 1: input_ptr = {bits: heap_start, offset: 0, state: C0_record}
|
|
1587
|
+
|
|
1588
|
+
Step 2: data_ptr = project_field(input_ptr, 1) // .data
|
|
1589
|
+
= {bits: heap_start, offset: 120, state: C0_list}
|
|
1590
|
+
|
|
1591
|
+
Step 3: head_ptr = match_union(data_ptr, CONS_TAG) // pattern match
|
|
1592
|
+
= {bits: heap_start, offset: 122, state: C0_cons}
|
|
1593
|
+
|
|
1594
|
+
Step 4: value_ptr = project_field(head_ptr, 0) // .value
|
|
1595
|
+
= {bits: heap_start, offset: 122, state: C0_int}
|
|
1596
|
+
|
|
1597
|
+
Step 5: name_ptr = project_field(input_ptr, 0) // .name
|
|
1598
|
+
= {bits: heap_start, offset: 8, state: C0_string}
|
|
1599
|
+
|
|
1600
|
+
Step 6: result_ptr = construct_product([value_ptr, name_ptr])
|
|
1601
|
+
// Note: memory allocation for the result tuple occurs here.
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
### **6.10.5 Optimization opportunities**
|
|
1605
|
+
|
|
1606
|
+
- **Parallel field evaluation** — Independent fields can be computed concurrently
|
|
1607
|
+
- **Memoization** — Cache decorated pointers for repeated subexpressions
|
|
1608
|
+
- **Streaming** — Process parts of large inputs without loading everything
|
|
1609
|
+
- **Zero-copy identity** — `()` just returns the input decorated pointer
|
|
1610
|
+
- **Prefix-free parsing** — No backtracking needed, linear-time navigation
|
|
1611
|
+
|
|
1612
|
+
This model provides **optimal performance** for sparse data access while maintaining the mathematical precision of k's type system.
|
|
1613
|
+
|
|
1614
|
+
---
|
|
1615
|
+
|
|
1616
|
+
## **6.11 Implementation considerations**
|
|
1617
|
+
|
|
1618
|
+
### **6.11.1 Prefix-free code requirements**
|
|
1619
|
+
|
|
1620
|
+
The execution model depends critically on **prefix-free codes** in the serialization:
|
|
1621
|
+
|
|
1622
|
+
- **Union discriminators** — Fixed-length codes ensure constant-time tag reading
|
|
1623
|
+
- **External data** — Length-prefixed to enable skipping without parsing
|
|
1624
|
+
- **Product fields** — Implicit boundaries defined by type structure
|
|
1625
|
+
|
|
1626
|
+
This enables the `skip_field()` function to advance pointers without full deserialization.
|
|
1627
|
+
|
|
1628
|
+
### **6.11.2 Reference counting and caching**
|
|
1629
|
+
|
|
1630
|
+
Decorated pointers should be reference-counted for several reasons:
|
|
1631
|
+
|
|
1632
|
+
Benefits:
|
|
1633
|
+
|
|
1634
|
+
- **Sharing** — Multiple references to the same subexpression
|
|
1635
|
+
- **Caching** — Avoid recomputation of expensive operations
|
|
1636
|
+
- **Memory efficiency** — Single copy of large serialized inputs
|
|
1637
|
+
|
|
1638
|
+
### **6.11.3 Parallel evaluation**
|
|
1639
|
+
|
|
1640
|
+
Product construction offers natural parallelization:
|
|
1641
|
+
|
|
1642
|
+
```c
|
|
1643
|
+
DecoratedPtr construct_product_parallel(DecoratedPtr* field_ptrs, int arity) {
|
|
1644
|
+
ProductNode* result = allocate_product_node(arity);
|
|
1645
|
+
|
|
1646
|
+
#pragma omp parallel for
|
|
1647
|
+
for (int i = 0; i < arity; i++) {
|
|
1648
|
+
result->fields[i] = evaluate_expression(field_ptrs[i]);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return make_materialized_ptr(result);
|
|
1652
|
+
}
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
Each field evaluation is independent and can run on separate threads.
|
|
1656
|
+
|
|
1657
|
+
### **6.11.4 Memory layout optimization**
|
|
1658
|
+
|
|
1659
|
+
The input heap should be designed for efficient navigation:
|
|
1660
|
+
|
|
1661
|
+
```c
|
|
1662
|
+
struct InputHeap {
|
|
1663
|
+
uint8_t* serialized_data; // the original bitstream
|
|
1664
|
+
size_t total_bits; // total size
|
|
1665
|
+
|
|
1666
|
+
// Optional: precomputed navigation table for large inputs
|
|
1667
|
+
struct {
|
|
1668
|
+
size_t* field_offsets; // byte offsets for major fields
|
|
1669
|
+
int table_size; // number of cached offsets
|
|
1670
|
+
} navigation_cache;
|
|
1671
|
+
};
|
|
1672
|
+
```
|
|
1673
|
+
|
|
1674
|
+
For very large inputs, precomputing field offsets can eliminate repeated prefix-free parsing.
|
|
1675
|
+
|
|
1676
|
+
---
|
|
1677
|
+
|
|
1678
|
+
## **6.12 Final summary**
|
|
1679
|
+
|
|
1680
|
+
This unified design achieves several important goals:
|
|
1681
|
+
|
|
1682
|
+
1. **Zero-copy operations** — Identity and projection work directly on serialized data
|
|
1683
|
+
2. **Demand-driven materialization** — Memory allocation only when necessary
|
|
1684
|
+
3. **External type integration** — Built-ins fit seamlessly into the execution model
|
|
1685
|
+
4. **Parallel execution** — Natural parallelization opportunities
|
|
1686
|
+
5. **Streaming capability** — Process large inputs with bounded memory
|
|
1687
|
+
|
|
1688
|
+
The decorated pointer model provides a clean abstraction that bridges the gap between the mathematical elegance of k's type system and the performance requirements of real-world data processing.
|
|
1689
|
+
|
|
1690
|
+
**Key insight**: By treating serialized data as the primary representation and materialized structures as secondary optimization, we invert the traditional approach and achieve much better performance for sparse data access patterns.
|
|
1691
|
+
|
|
1692
|
+
### **6.12.1 Hardware-optimized encoding summary**
|
|
1693
|
+
|
|
1694
|
+
The final encoding strategy balances theoretical optimality with practical hardware constraints:
|
|
1695
|
+
|
|
1696
|
+
**Trade-offs achieved**:
|
|
1697
|
+
|
|
1698
|
+
- **Space efficiency**: 80-90% of theoretical optimum
|
|
1699
|
+
- **Decode performance**: 5-10× faster than bit-level encoding
|
|
1700
|
+
- **Cache behavior**: Excellent (word-aligned, sequential access)
|
|
1701
|
+
- **SIMD compatibility**: Yes (bulk discriminator processing)
|
|
1702
|
+
- **Architecture agnostic**: Optimized for x86-64 and ARM64
|
|
1703
|
+
|
|
1704
|
+
**Performance characteristics**:
|
|
1705
|
+
|
|
1706
|
+
```text
|
|
1707
|
+
Operation Hardware-optimized Bit-level optimal
|
|
1708
|
+
Field access 2-8 cycles 50-100 cycles
|
|
1709
|
+
Union matching 1-3 cycles 20-40 cycles
|
|
1710
|
+
Memory bandwidth 95% theoretical 60% theoretical
|
|
1711
|
+
Cache miss ratio <5% >25%
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
**Recommended for**:
|
|
1715
|
+
|
|
1716
|
+
- **Production systems** requiring high performance
|
|
1717
|
+
- **Streaming applications** with large data volumes
|
|
1718
|
+
- **Mobile/embedded** systems with limited CPU resources
|
|
1719
|
+
- **SIMD-heavy workloads** processing bulk data
|
|
1720
|
+
|
|
1721
|
+
The hardware-optimized approach provides the best practical balance for real-world k implementations.
|
|
1722
|
+
|
|
1723
|
+
---
|
|
1724
|
+
|
|
1725
|
+
---
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
# Chapter 7 — The Partial Function ABI
|
|
1729
|
+
|
|
1730
|
+
## **7.1 Purpose**
|
|
1731
|
+
|
|
1732
|
+
The **application binary interface (ABI)** defines how partial functions in `k` are represented and invoked at runtime.
|
|
1733
|
+
Its goal is to make all functions - whether user-defined or compiled - compatible with the same calling convention and data layout.
|
|
1734
|
+
|
|
1735
|
+
Every compiled `k` function receives a single argument (a value tree) and may either produce a result or remain undefined.
|
|
1736
|
+
The ABI provides a uniform way to express both outcomes.
|
|
1737
|
+
|
|
1738
|
+
---
|
|
1739
|
+
|
|
1740
|
+
## **7.2 Function result representation**
|
|
1741
|
+
|
|
1742
|
+
The result of every function call is a pair of fields:
|
|
1743
|
+
|
|
1744
|
+
```c
|
|
1745
|
+
struct KOpt {
|
|
1746
|
+
bool ok; // 1 if function is defined for the given input
|
|
1747
|
+
struct KNode* val; // valid only if ok == true
|
|
1748
|
+
};
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
If `ok == 1`, the function succeeded and `val` points to the root node of the resulting value.
|
|
1752
|
+
If `ok == 0`, the function is undefined for the input, and `val` is meaningless.
|
|
1753
|
+
|
|
1754
|
+
This structure carries both the success flag and the value pointer, allowing partiality to be expressed directly in generated code.
|
|
1755
|
+
|
|
1756
|
+
---
|
|
1757
|
+
|
|
1758
|
+
## **7.3 Function parameter representation**
|
|
1759
|
+
|
|
1760
|
+
Each function takes exactly one parameter:
|
|
1761
|
+
|
|
1762
|
+
```c
|
|
1763
|
+
struct KNode* input;
|
|
1764
|
+
```
|
|
1765
|
+
|
|
1766
|
+
The parameter points to the root of the input value.
|
|
1767
|
+
Since all values are immutable, the function must not modify this structure.
|
|
1768
|
+
|
|
1769
|
+
---
|
|
1770
|
+
|
|
1771
|
+
## **7.4 Calling convention**
|
|
1772
|
+
|
|
1773
|
+
Every `k` function compiled to machine code follows this C-style signature:
|
|
1774
|
+
|
|
1775
|
+
```c
|
|
1776
|
+
struct KOpt k_function(struct KNode* input);
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1779
|
+
The return value indicates success or failure.
|
|
1780
|
+
The convention is uniform for all functions, regardless of the specific types involved.
|
|
1781
|
+
|
|
1782
|
+
---
|
|
1783
|
+
|
|
1784
|
+
## **7.5 Core runtime operations**
|
|
1785
|
+
|
|
1786
|
+
The runtime provides a small set of primitive functions implementing the fundamental operations of `k`:
|
|
1787
|
+
|
|
1788
|
+
| Operation | Purpose | Result |
|
|
1789
|
+
| -------------------------------------- | ----------------------------- | ------------------------------------------------- |
|
|
1790
|
+
| `k_project(input, label_id)` | projection `.label` | defined if the field or variant `label_id` exists |
|
|
1791
|
+
| `k_make_product(state, children[], n)` | construct a product node | always defined |
|
|
1792
|
+
| `k_make_union(state, tag, child)` | construct a union node | always defined |
|
|
1793
|
+
| `k_fail()` | represent undefined result | returns `{ ok = 0 }` |
|
|
1794
|
+
| `k_unit()` | return the constant unit node | returns `{ ok = 1, val = &unit_node }` |
|
|
1795
|
+
|
|
1796
|
+
All user-defined functions can be compiled entirely in terms of these operations.
|
|
1797
|
+
|
|
1798
|
+
---
|
|
1799
|
+
|
|
1800
|
+
## **7.6 Metadata tables**
|
|
1801
|
+
|
|
1802
|
+
To interpret or construct nodes correctly, the runtime holds constant tables derived from canonical type definitions:
|
|
1803
|
+
|
|
1804
|
+
* `state_kind[state]` — `0` for product, `1` for union,
|
|
1805
|
+
* `state_arity[state]` — number of child fields for products (always 1 for unions),
|
|
1806
|
+
* `field_index[state][label_id]` — index of field in product (or `-1` if absent),
|
|
1807
|
+
* `variant_index[state][label_id]` — index of variant in union (or `-1` if absent).
|
|
1808
|
+
|
|
1809
|
+
These tables are read-only and known at compile time for each canonical type.
|
|
1810
|
+
|
|
1811
|
+
---
|
|
1812
|
+
|
|
1813
|
+
## **7.7 Example: projection**
|
|
1814
|
+
|
|
1815
|
+
To implement the projection `.x`:
|
|
1816
|
+
|
|
1817
|
+
```c
|
|
1818
|
+
struct KOpt k_project_x(struct KNode* input) {
|
|
1819
|
+
int state = input->state;
|
|
1820
|
+
int kind = state_kind[state];
|
|
1821
|
+
|
|
1822
|
+
if (kind == PRODUCT) {
|
|
1823
|
+
int idx = field_index[state]["x"];
|
|
1824
|
+
if (idx < 0 || idx >= input->arity)
|
|
1825
|
+
return (struct KOpt){0, NULL};
|
|
1826
|
+
return (struct KOpt){1, input->child[idx]};
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
if (kind == UNION) {
|
|
1830
|
+
int tag = variant_index[state]["x"];
|
|
1831
|
+
if (tag < 0 || input->tag != tag)
|
|
1832
|
+
return (struct KOpt){0, NULL};
|
|
1833
|
+
return (struct KOpt){1, input->child[0]};
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
return (struct KOpt){0, NULL};
|
|
1837
|
+
}
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
This code demonstrates the generic principle:
|
|
1841
|
+
the projection is defined if and only if the input has a field or variant named `x`.
|
|
1842
|
+
|
|
1843
|
+
---
|
|
1844
|
+
|
|
1845
|
+
## **7.8 Error propagation**
|
|
1846
|
+
|
|
1847
|
+
When one function calls another, the success flag `ok` propagates forward.
|
|
1848
|
+
If a subcall returns `ok == 0`, the caller must return `{0, NULL}` immediately.
|
|
1849
|
+
This makes undefinedness explicit and local—no exceptions, no global flags.
|
|
1850
|
+
|
|
1851
|
+
---
|
|
1852
|
+
|
|
1853
|
+
## **7.9 Summary**
|
|
1854
|
+
|
|
1855
|
+
* Every function has the form `KOpt f(KNode*)`.
|
|
1856
|
+
* Partiality is expressed by the `ok` flag.
|
|
1857
|
+
* Runtime operations handle projection and construction.
|
|
1858
|
+
* Metadata tables describe field positions and variant indices.
|
|
1859
|
+
* By inspecting `arity`, `state`, and `tag`, the runtime can interpret any value correctly.
|
|
1860
|
+
* This ABI ensures all generated and runtime functions can interoperate safely.
|
|
1861
|
+
|
|
1862
|
+
---
|
|
1863
|
+
|
|
1864
|
+
---
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
# Chapter 8 — Operational Semantics and Execution
|
|
1868
|
+
|
|
1869
|
+
## **8.1 Purpose**
|
|
1870
|
+
|
|
1871
|
+
The **operational semantics** of `k` describe how expressions are evaluated step by step on actual value trees.
|
|
1872
|
+
It defines when a function is *defined* for a particular input and what value it returns.
|
|
1873
|
+
All execution—interpretive or compiled—follows these rules.
|
|
1874
|
+
|
|
1875
|
+
---
|
|
1876
|
+
|
|
1877
|
+
## **8.2 Evaluation relation**
|
|
1878
|
+
|
|
1879
|
+
Evaluation is written as:
|
|
1880
|
+
|
|
1881
|
+
```text
|
|
1882
|
+
⟨ e , v ⟩ ⇓ r
|
|
1883
|
+
```
|
|
1884
|
+
|
|
1885
|
+
meaning that expression `e` applied to value `v` yields result `r`.
|
|
1886
|
+
If `e` is undefined for `v`, the relation does not hold.
|
|
1887
|
+
|
|
1888
|
+
`r` is always a tree (or node) in memory, represented as described in Chapter 6.
|
|
1889
|
+
Undefined results are expressed by the absence of any rule that produces `r`.
|
|
1890
|
+
|
|
1891
|
+
---
|
|
1892
|
+
|
|
1893
|
+
## **8.3 Rules for base expressions**
|
|
1894
|
+
|
|
1895
|
+
### **Identity**
|
|
1896
|
+
|
|
1897
|
+
```text
|
|
1898
|
+
⟨ () , v ⟩ ⇓ v
|
|
1899
|
+
```
|
|
1900
|
+
|
|
1901
|
+
The empty composition `()` returns its argument unchanged.
|
|
1902
|
+
|
|
1903
|
+
### **Projection**
|
|
1904
|
+
|
|
1905
|
+
Let `label` be a field or variant name.
|
|
1906
|
+
If `v` has a child under `label`,
|
|
1907
|
+
|
|
1908
|
+
```text
|
|
1909
|
+
⟨ .label , v ⟩ ⇓ v.label
|
|
1910
|
+
```
|
|
1911
|
+
|
|
1912
|
+
Otherwise the projection is undefined.
|
|
1913
|
+
|
|
1914
|
+
### **Type expression**
|
|
1915
|
+
|
|
1916
|
+
For a type `T`,
|
|
1917
|
+
|
|
1918
|
+
```text
|
|
1919
|
+
⟨ $T , v ⟩ ⇓ v if v ∈ T
|
|
1920
|
+
```
|
|
1921
|
+
|
|
1922
|
+
and undefined otherwise.
|
|
1923
|
+
Type expressions thus act as identity functions restricted to their type.
|
|
1924
|
+
|
|
1925
|
+
---
|
|
1926
|
+
|
|
1927
|
+
## **8.4 Rules for composition**
|
|
1928
|
+
|
|
1929
|
+
### **Sequential composition**
|
|
1930
|
+
|
|
1931
|
+
```text
|
|
1932
|
+
⟨ (f g) , v ⟩ ⇓ r
|
|
1933
|
+
```
|
|
1934
|
+
|
|
1935
|
+
if there exists `u` such that
|
|
1936
|
+
`⟨ f , v ⟩ ⇓ u` and `⟨ g , u ⟩ ⇓ r`.
|
|
1937
|
+
|
|
1938
|
+
If either step is undefined, the composition is undefined.
|
|
1939
|
+
Because composition is associative,
|
|
1940
|
+
|
|
1941
|
+
```text
|
|
1942
|
+
(f (g h)) ≡ ((f g) h) ≡ (f g h)
|
|
1943
|
+
```
|
|
1944
|
+
|
|
1945
|
+
Parentheses are needed only for `()`.
|
|
1946
|
+
|
|
1947
|
+
---
|
|
1948
|
+
|
|
1949
|
+
## **8.5 Rules for product composition**
|
|
1950
|
+
|
|
1951
|
+
```text
|
|
1952
|
+
⟨ { f₁ l₁ , f₂ l₂ , … , fₙ lₙ } , v ⟩ ⇓ { r₁ l₁ , r₂ l₂ , … , rₙ lₙ }
|
|
1953
|
+
```
|
|
1954
|
+
|
|
1955
|
+
if and only if all subfunctions `fᵢ` are defined on `v` and yield `rᵢ`.
|
|
1956
|
+
If any subfunction is undefined, the whole product composition is undefined.
|
|
1957
|
+
|
|
1958
|
+
A product composition constructs a new product node;
|
|
1959
|
+
each result `rᵢ` becomes one child in canonical field order.
|
|
1960
|
+
|
|
1961
|
+
---
|
|
1962
|
+
|
|
1963
|
+
## **8.6 Rules for union composition**
|
|
1964
|
+
|
|
1965
|
+
```text
|
|
1966
|
+
⟨ < f₁ , f₂ , … , fₙ > , v ⟩ ⇓ rⱼ
|
|
1967
|
+
```
|
|
1968
|
+
|
|
1969
|
+
if there exists the smallest index `j` such that
|
|
1970
|
+
`⟨ fⱼ , v ⟩ ⇓ rⱼ`.
|
|
1971
|
+
|
|
1972
|
+
If no subfunction is defined, the union composition is undefined.
|
|
1973
|
+
|
|
1974
|
+
---
|
|
1975
|
+
|
|
1976
|
+
## **8.7 Rules for filters**
|
|
1977
|
+
|
|
1978
|
+
If a filter `F` matches a single type `T`
|
|
1979
|
+
|
|
1980
|
+
```text
|
|
1981
|
+
⟨ ?F , v ⟩ ⇓ ⟨ $T , v ⟩
|
|
1982
|
+
```
|
|
1983
|
+
|
|
1984
|
+
otherwise it is an identity
|
|
1985
|
+
|
|
1986
|
+
```text
|
|
1987
|
+
⟨ ?F , v ⟩ ⇓ v
|
|
1988
|
+
```
|
|
1989
|
+
|
|
1990
|
+
Filters therefore act as compile time annotations for type checking only.
|
|
1991
|
+
They are ignored at run-time, unless they lead to fully typed expressions (i.e., can be replaced by a type).
|
|
1992
|
+
|
|
1993
|
+
---
|
|
1994
|
+
|
|
1995
|
+
## **8.8 Evaluation order**
|
|
1996
|
+
|
|
1997
|
+
Evaluation proceeds left to right.
|
|
1998
|
+
In products, all subfunctions receive the same input;
|
|
1999
|
+
in unions, later functions are evaluated only if earlier ones fail.
|
|
2000
|
+
|
|
2001
|
+
The semantics are deterministic:
|
|
2002
|
+
for any given input, at most one result tree can be produced.
|
|
2003
|
+
|
|
2004
|
+
---
|
|
2005
|
+
|
|
2006
|
+
## **8.9 Example**
|
|
2007
|
+
|
|
2008
|
+
Given:
|
|
2009
|
+
|
|
2010
|
+
```k-lang
|
|
2011
|
+
$bool = < {} true, {} false >;
|
|
2012
|
+
neg = $bool < .true {{ } false}, .false {{ } true} > $bool;
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
and input value `{ {} true }` of type `bool`,
|
|
2016
|
+
evaluation steps are:
|
|
2017
|
+
|
|
2018
|
+
1. `⟨ $bool , { {} true } ⟩ ⇓ { {} true }`
|
|
2019
|
+
2. `⟨ < .true {{ } false}, .false {{ } true} > , { {} true } ⟩ ⇓ {{ } false}`
|
|
2020
|
+
3. `⟨ $bool , {{ } false} ⟩ ⇓ {{ } false}`
|
|
2021
|
+
|
|
2022
|
+
Final result: `{{ } false}`.
|
|
2023
|
+
If the input were of another type, step 1 would be undefined.
|
|
2024
|
+
|
|
2025
|
+
---
|
|
2026
|
+
|
|
2027
|
+
## **8.10 Implementation correspondence**
|
|
2028
|
+
|
|
2029
|
+
The evaluation rules map directly onto the runtime ABI:
|
|
2030
|
+
|
|
2031
|
+
| Semantic rule | Runtime operation |
|
|
2032
|
+
| ------------------- | ------------------------------------- |
|
|
2033
|
+
| Projection | `k_project` |
|
|
2034
|
+
| Product composition | multiple subcalls + `k_make_product` |
|
|
2035
|
+
| Union composition | sequential subcalls with early return |
|
|
2036
|
+
| Type | runtime check of `state` |
|
|
2037
|
+
| Composition | function call chain |
|
|
2038
|
+
|
|
2039
|
+
In compiled form, the `ok` flag of `KOpt` represents whether a rule applies;
|
|
2040
|
+
the node pointer represents the result value.
|
|
2041
|
+
|
|
2042
|
+
---
|
|
2043
|
+
|
|
2044
|
+
## **8.11 Summary**
|
|
2045
|
+
|
|
2046
|
+
* Execution follows deterministic, left-to-right rules.
|
|
2047
|
+
* All expressions denote partial functions on value trees.
|
|
2048
|
+
* Type expressions act as restricted identities.
|
|
2049
|
+
* Composition is associative; undefined propagates automatically.
|
|
2050
|
+
* Runtime semantics match the formal evaluation relation exactly.
|
|
2051
|
+
|
|
2052
|
+
---
|
|
2053
|
+
|
|
2054
|
+
---
|
|
2055
|
+
|
|
2056
|
+
|
|
2057
|
+
# Chapter 9 — Compiler Architecture
|
|
2058
|
+
|
|
2059
|
+
## **9.1 What the compiler does**
|
|
2060
|
+
|
|
2061
|
+
A *compiler* for the `k` language is a program that reads another program (written in `k`) and produces a lower-level version of it.
|
|
2062
|
+
The lower-level version can then be executed efficiently by a computer.
|
|
2063
|
+
|
|
2064
|
+
For `k`, the compiler’s task is simple in concept:
|
|
2065
|
+
|
|
2066
|
+
1. **Read** the definitions of types and functions.
|
|
2067
|
+
2. **Understand** their structure and the relationships between them.
|
|
2068
|
+
3. **Translate** them into equivalent instructions that follow the runtime conventions described in Chapters 6–8.
|
|
2069
|
+
|
|
2070
|
+
The compiler must ensure that every function in the source program becomes a machine-level function that behaves in exactly the same way:
|
|
2071
|
+
it takes a value as input, may or may not be defined for it, and, if defined, produces another value.
|
|
2072
|
+
|
|
2073
|
+
---
|
|
2074
|
+
|
|
2075
|
+
## **9.2 The main stages**
|
|
2076
|
+
|
|
2077
|
+
A `k` compiler is organized as a sequence of well-defined stages.
|
|
2078
|
+
Each stage transforms one representation of the program into another, simpler one.
|
|
2079
|
+
|
|
2080
|
+
1. **Reading and parsing** — The compiler converts the source text into a tree of syntactic objects.
|
|
2081
|
+
This tree is called the *abstract syntax tree* (AST).
|
|
2082
|
+
It records the structure of expressions such as `{ .x a, .y b }` or `< .x, .y >`.
|
|
2083
|
+
|
|
2084
|
+
2. **Type analysis and normalization** —
|
|
2085
|
+
The compiler expands type names, checks their correctness, and replaces all filters and meta-variables by concrete type information, following the procedure from Chapter 5.
|
|
2086
|
+
The result is a fully typed AST.
|
|
2087
|
+
|
|
2088
|
+
3. **Preparation for execution** —
|
|
2089
|
+
The compiler builds a *type table* for all canonical type states (C0, C1, …).
|
|
2090
|
+
For each type, it records whether it is a product or a union and the number and names of its fields or variants.
|
|
2091
|
+
These tables become the metadata used at runtime.
|
|
2092
|
+
|
|
2093
|
+
4. **Code generation** —
|
|
2094
|
+
Finally, the compiler walks through the typed AST and produces an equivalent description in a lower-level form that follows the runtime conventions: functions returning a success flag and a value pointer.
|
|
2095
|
+
This form can be expressed directly in C or in the *LLVM intermediate representation*, which is a platform-independent format understood by many compilers.
|
|
2096
|
+
|
|
2097
|
+
---
|
|
2098
|
+
|
|
2099
|
+
## **9.3 Internal structure**
|
|
2100
|
+
|
|
2101
|
+
A minimal compiler can be viewed as three connected modules:
|
|
2102
|
+
|
|
2103
|
+
* **Front end** — reads source code, builds the AST, performs type analysis.
|
|
2104
|
+
* **Middle** — resolves all references, normalizes types, and simplifies expressions.
|
|
2105
|
+
* **Back end** — produces the executable form.
|
|
2106
|
+
|
|
2107
|
+
Although these terms are traditional, the distinction is simple:
|
|
2108
|
+
the front end *understands* the program, the back end *emits* it, and the middle part connects the two.
|
|
2109
|
+
|
|
2110
|
+
---
|
|
2111
|
+
|
|
2112
|
+
## **9.4 Data kept by the compiler**
|
|
2113
|
+
|
|
2114
|
+
During translation the compiler maintains several tables:
|
|
2115
|
+
|
|
2116
|
+
| Name | Purpose |
|
|
2117
|
+
| ------------------ | ----------------------------------------------------------------- |
|
|
2118
|
+
| **Type table** | Maps type names to canonical forms and assigns numeric state IDs. |
|
|
2119
|
+
| **Field table** | For each product type, records the order and index of its fields. |
|
|
2120
|
+
| **Variant table** | For each union type, records the tags and their numeric indices. |
|
|
2121
|
+
| **Function table** | Associates function names with their input and output types. |
|
|
2122
|
+
|
|
2123
|
+
These tables ensure that generated code can find fields and variants by number rather than by name, which simplifies execution.
|
|
2124
|
+
|
|
2125
|
+
---
|
|
2126
|
+
|
|
2127
|
+
## **9.5 Example of transformation**
|
|
2128
|
+
|
|
2129
|
+
Consider a simple k program:
|
|
2130
|
+
|
|
2131
|
+
```k-lang
|
|
2132
|
+
$bool = < {} true, {} false >;
|
|
2133
|
+
neg = $bool < /true | false, /false | true > $bool;
|
|
2134
|
+
```
|
|
2135
|
+
|
|
2136
|
+
1. **Parsing:**
|
|
2137
|
+
The compiler recognizes a type definition `$bool` and a function `neg`.
|
|
2138
|
+
|
|
2139
|
+
2. **Type analysis:**
|
|
2140
|
+
Both the input and output of `neg` are of type `bool`.
|
|
2141
|
+
|
|
2142
|
+
3. **Internal representation:**
|
|
2143
|
+
The compiler constructs an internal tree describing that
|
|
2144
|
+
`< /true f₁, /false f₂ >` means:
|
|
2145
|
+
try the projection `/true`, then `/false`, each producing a constant value.
|
|
2146
|
+
|
|
2147
|
+
4. **Generated form:**
|
|
2148
|
+
In the target language, `neg` becomes a small function that:
|
|
2149
|
+
|
|
2150
|
+
* calls `k_project` to check which variant the input has,
|
|
2151
|
+
* creates a new node with the opposite variant using `k_make_union`, and
|
|
2152
|
+
* returns the result with `ok = 1`.
|
|
2153
|
+
|
|
2154
|
+
This sequence of steps is fully automatic and follows directly from the semantic rules in Chapter 8.
|
|
2155
|
+
|
|
2156
|
+
---
|
|
2157
|
+
|
|
2158
|
+
## **9.6 Intermediate representation**
|
|
2159
|
+
|
|
2160
|
+
Before producing final code, many compilers use an *intermediate representation* (IR).
|
|
2161
|
+
It is a language designed to be easy to generate and easy to translate further into real machine code.
|
|
2162
|
+
For `k`, the IR mirrors the structure of partial functions:
|
|
2163
|
+
|
|
2164
|
+
| Concept in k | IR operation |
|
|
2165
|
+
| --------------------- | -------------------------------- |
|
|
2166
|
+
| Projection `.x` '/x' | `PROJECT label_id` |
|
|
2167
|
+
| Composition `(f g)` | `CALL f; CALL g` |
|
|
2168
|
+
| Union `<f,g>` | `TRY f; IF undefined THEN TRY g` |
|
|
2169
|
+
| Product `{.x f, .y g}` | `PRODUCT_CALL (f, g); COMBINE` |
|
|
2170
|
+
| Type restriction `$T` | `CHECK_TYPE state_id` |
|
|
2171
|
+
|
|
2172
|
+
The compiler converts each AST node into one or more IR instructions.
|
|
2173
|
+
The `PRODUCT_CALL(f, g)` instruction is a higher-level concept that implies applying both `f` and `g` to the same input value.
|
|
2174
|
+
The low-level implementation of this would use stack operations to duplicate the input, but the IR itself remains more abstract.
|
|
2175
|
+
Later, these instructions are translated into LLVM or C code using the runtime ABI.
|
|
2176
|
+
|
|
2177
|
+
---
|
|
2178
|
+
|
|
2179
|
+
## **9.7 Linking with the runtime**
|
|
2180
|
+
|
|
2181
|
+
The generated code does not contain memory allocation or type tables itself.
|
|
2182
|
+
Instead, it relies on the runtime library (`krt`), which provides:
|
|
2183
|
+
|
|
2184
|
+
* functions such as `k_project` and `k_make_union`,
|
|
2185
|
+
* metadata tables for each canonical type, and
|
|
2186
|
+
* the unit value `{}` shared by all programs.
|
|
2187
|
+
|
|
2188
|
+
When the compiler finishes, it outputs a text file in the target language plus references to this runtime library.
|
|
2189
|
+
The two together form an executable program.
|
|
2190
|
+
|
|
2191
|
+
---
|
|
2192
|
+
|
|
2193
|
+
## **9.8 Simplicity of the model**
|
|
2194
|
+
|
|
2195
|
+
Because every expression in `k` takes exactly one input and may return one output or be undefined,
|
|
2196
|
+
the compiler never has to deal with variable environments, loops, or side effects.
|
|
2197
|
+
This makes the structure of the generated program very regular:
|
|
2198
|
+
|
|
2199
|
+
```text
|
|
2200
|
+
input → [series of calls and checks] → output or undefined
|
|
2201
|
+
```
|
|
2202
|
+
|
|
2203
|
+
Each compiled function is independent and stateless.
|
|
2204
|
+
This regularity is what allows `k` to map cleanly to low-level code.
|
|
2205
|
+
|
|
2206
|
+
---
|
|
2207
|
+
|
|
2208
|
+
## **9.9 Summary**
|
|
2209
|
+
|
|
2210
|
+
* The compiler transforms `k` programs into executable form through a sequence of simple, deterministic steps.
|
|
2211
|
+
* Type information and structure are resolved before code generation.
|
|
2212
|
+
* All generated functions follow the same calling convention (`ok`, `val`).
|
|
2213
|
+
* The runtime library provides the shared operations needed by every program.
|
|
2214
|
+
* The entire process mirrors the formal semantics defined earlier but expresses it as concrete machine operations.
|
|
2215
|
+
|
|
2216
|
+
---
|
|
2217
|
+
|
|
2218
|
+
---
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
# Chapter 10 — From AST to Intermediate Representation
|
|
2222
|
+
|
|
2223
|
+
## **10.1 The purpose of an intermediate form**
|
|
2224
|
+
|
|
2225
|
+
The abstract syntax tree (AST) of a `k` program directly reflects how the source code was written.
|
|
2226
|
+
It is easy to read but not convenient for generating machine code.
|
|
2227
|
+
Before code can be produced, the compiler rewrites this tree into a **simpler, regular structure** called the *intermediate representation* (IR).
|
|
2228
|
+
|
|
2229
|
+
The IR is close to what the machine will execute, but it still looks like a structured program rather than raw binary instructions.
|
|
2230
|
+
Each node in the IR corresponds to a single, well-defined operation such as *project a field*, *apply a function*, or *combine results*.
|
|
2231
|
+
|
|
2232
|
+
Using an intermediate form helps in three ways:
|
|
2233
|
+
|
|
2234
|
+
1. It separates understanding of the language from machine details.
|
|
2235
|
+
2. It allows the compiler to verify that every function is well formed.
|
|
2236
|
+
3. It provides a uniform surface for optimization and for later translation into C or LLVM.
|
|
2237
|
+
|
|
2238
|
+
---
|
|
2239
|
+
|
|
2240
|
+
## **10.2 Shape of the intermediate representation**
|
|
2241
|
+
|
|
2242
|
+
Each IR instruction corresponds to one semantic rule from Chapter 8.
|
|
2243
|
+
The IR does not need to represent filters or meta-variables, since those are resolved earlier.
|
|
2244
|
+
Only concrete actions remain.
|
|
2245
|
+
|
|
2246
|
+
| Operation | Meaning |
|
|
2247
|
+
| ----------------------------- | ---------------------------------------------------------------- |
|
|
2248
|
+
| **PROJECT label_id** | Apply `.label` to the input value. |
|
|
2249
|
+
| **TYPECHECK state_id** | Accept the input only if its root has the given canonical state. |
|
|
2250
|
+
| **CALL function_id** | Invoke another function following the ABI. |
|
|
2251
|
+
| **UNION [f₁, f₂, …]** | Try several functions in order until one succeeds. |
|
|
2252
|
+
| **PRODUCT [l₁=f₁, l₂=f₂, …]** | Apply each function to the same input and combine results. |
|
|
2253
|
+
| **FAIL** | Always undefined. |
|
|
2254
|
+
| **SEQ [f₁, f₂, …]** | Apply functions in sequence (composition). |
|
|
2255
|
+
|
|
2256
|
+
Each function in the program becomes a small list or tree of such IR instructions.
|
|
2257
|
+
|
|
2258
|
+
---
|
|
2259
|
+
|
|
2260
|
+
## **10.3 Example**
|
|
2261
|
+
|
|
2262
|
+
Source program:
|
|
2263
|
+
|
|
2264
|
+
```k-lang
|
|
2265
|
+
$bool = < {} true, {} false >;
|
|
2266
|
+
neg = $bool < .true {{ } false}, .false {{ } true} > $bool;
|
|
2267
|
+
```
|
|
2268
|
+
|
|
2269
|
+
The IR for `neg` can be written informally as:
|
|
2270
|
+
|
|
2271
|
+
```text
|
|
2272
|
+
neg:
|
|
2273
|
+
TYPECHECK bool
|
|
2274
|
+
UNION [
|
|
2275
|
+
SEQ [ PROJECT true, CONST false ],
|
|
2276
|
+
SEQ [ PROJECT false, CONST true ]
|
|
2277
|
+
]
|
|
2278
|
+
TYPECHECK bool
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
This IR expresses the same logic as the source:
|
|
2282
|
+
check the type, then try the first projection `.true`, otherwise the second, and finally ensure the result is again of type `$bool`.
|
|
2283
|
+
|
|
2284
|
+
---
|
|
2285
|
+
|
|
2286
|
+
## **10.4 Relation to evaluation**
|
|
2287
|
+
|
|
2288
|
+
Every IR operation corresponds to a runtime action:
|
|
2289
|
+
|
|
2290
|
+
| IR operation | Runtime function or behavior |
|
|
2291
|
+
| ------------ | --------------------------------------------------- |
|
|
2292
|
+
| PROJECT | `k_project(input, label_id)` |
|
|
2293
|
+
| TYPECHECK | verify `input->state == state_id` |
|
|
2294
|
+
| CALL | call another generated function |
|
|
2295
|
+
| UNION | sequential `if`–`else` test on success flags |
|
|
2296
|
+
| PRODUCT | multiple subcalls + `k_make_product` |
|
|
2297
|
+
| FAIL | return `{ ok = 0 }` immediately |
|
|
2298
|
+
| SEQ | connect the output of one step as input to the next |
|
|
2299
|
+
|
|
2300
|
+
By describing computation in these terms, the compiler can generate executable code simply by replacing each IR instruction with its runtime equivalent.
|
|
2301
|
+
|
|
2302
|
+
---
|
|
2303
|
+
|
|
2304
|
+
## **10.5 Building the IR**
|
|
2305
|
+
|
|
2306
|
+
The process of converting the AST to IR follows a recursive pattern:
|
|
2307
|
+
|
|
2308
|
+
1. **Projections** become single instructions `PROJECT`.
|
|
2309
|
+
2. **Compositions** `(f g h)` become a `SEQ` list of their components.
|
|
2310
|
+
3. **Unions** `<f,g>` become a `UNION` list.
|
|
2311
|
+
4. **Products** `{f l₁, g l₂}` become a `PRODUCT` list with labeled entries.
|
|
2312
|
+
5. **Type expressions** `$T` become `TYPECHECK` instructions.
|
|
2313
|
+
6. **Filters** disappear; their meaning is already captured by typechecks.
|
|
2314
|
+
|
|
2315
|
+
Each transformation step produces IR nodes with fixed behavior and explicit order.
|
|
2316
|
+
|
|
2317
|
+
---
|
|
2318
|
+
|
|
2319
|
+
## **10.6 Verifying the IR**
|
|
2320
|
+
|
|
2321
|
+
Because k programs are defined by simple composition, a small set of checks ensures correctness:
|
|
2322
|
+
|
|
2323
|
+
1. Each `UNION` list must have at least one element.
|
|
2324
|
+
2. Every `PROJECT` must have a valid label.
|
|
2325
|
+
3. Input and output states of consecutive steps must match.
|
|
2326
|
+
4. A function must end in an operation that produces a result (`PROJECT`, `CALL`, or combination).
|
|
2327
|
+
|
|
2328
|
+
After verification, the IR is guaranteed to represent a valid partial function according to the semantics in Chapter 8.
|
|
2329
|
+
|
|
2330
|
+
---
|
|
2331
|
+
|
|
2332
|
+
## **10.7 Intermediate form as a small language**
|
|
2333
|
+
|
|
2334
|
+
The IR can be viewed as a minimal language on its own.
|
|
2335
|
+
It has:
|
|
2336
|
+
|
|
2337
|
+
* one data type (`KNode*`),
|
|
2338
|
+
* one universal value format (trees),
|
|
2339
|
+
* and a fixed set of operators.
|
|
2340
|
+
|
|
2341
|
+
Its execution model is the same as the runtime ABI.
|
|
2342
|
+
This means an interpreter for the IR can serve as a reference implementation of the k semantics before any machine code generation is added.
|
|
2343
|
+
|
|
2344
|
+
---
|
|
2345
|
+
|
|
2346
|
+
## **10.8 Example: product composition**
|
|
2347
|
+
|
|
2348
|
+
For input function `{ .x a, .y b }`,
|
|
2349
|
+
the IR is:
|
|
2350
|
+
|
|
2351
|
+
```text
|
|
2352
|
+
PRODUCT [
|
|
2353
|
+
a = PROJECT x,
|
|
2354
|
+
b = PROJECT y
|
|
2355
|
+
]
|
|
2356
|
+
```
|
|
2357
|
+
|
|
2358
|
+
The runtime sequence is:
|
|
2359
|
+
|
|
2360
|
+
1. Apply each projection.
|
|
2361
|
+
2. If both succeed, build a new node with two children in order `(a,b)`.
|
|
2362
|
+
|
|
2363
|
+
This pattern covers all record-building operations in the language.
|
|
2364
|
+
|
|
2365
|
+
---
|
|
2366
|
+
|
|
2367
|
+
## **10.9 Why this representation is useful**
|
|
2368
|
+
|
|
2369
|
+
This intermediate form provides three practical benefits:
|
|
2370
|
+
|
|
2371
|
+
1. **Clarity** – every step is explicit and local.
|
|
2372
|
+
2. **Verifiability** – type consistency and definedness can be checked mechanically.
|
|
2373
|
+
3. **Flexibility** – the same IR can be translated to many targets (LLVM IR, C, or even interpreted directly).
|
|
2374
|
+
|
|
2375
|
+
Because of these advantages, most compilers for declarative languages use an intermediate form of this kind.
|
|
2376
|
+
|
|
2377
|
+
---
|
|
2378
|
+
|
|
2379
|
+
## **10.10 Summary**
|
|
2380
|
+
|
|
2381
|
+
* The abstract syntax tree is rewritten into a simpler, machine-oriented form.
|
|
2382
|
+
* Each IR instruction represents one operation defined by the `k` semantics.
|
|
2383
|
+
* Type and filter information appear as explicit checks.
|
|
2384
|
+
* The IR is easy to verify and to translate into executable code.
|
|
2385
|
+
* The IR can also serve as a precise description of how `k` functions behave step by step.
|
|
2386
|
+
|
|
2387
|
+
---
|
|
2388
|
+
|
|
2389
|
+
---
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
# Chapter 11 — LLVM Basics
|
|
2393
|
+
|
|
2394
|
+
## **11.1 Purpose**
|
|
2395
|
+
|
|
2396
|
+
The previous chapters described how a `k` program can be reduced to a small set of intermediate operations.
|
|
2397
|
+
To execute these operations efficiently, the compiler must translate them into an actual machine language.
|
|
2398
|
+
Instead of generating raw machine code directly, we use a common intermediate format called **LLVM IR** (LLVM *Intermediate Representation*).
|
|
2399
|
+
|
|
2400
|
+
LLVM IR is a low-level, strongly typed language that resembles a simplified form of assembly.
|
|
2401
|
+
It can be read and written as text, analyzed, optimized, and compiled to machine code for almost any processor.
|
|
2402
|
+
For the compiler writer, it provides a bridge between high-level language semantics and executable programs.
|
|
2403
|
+
|
|
2404
|
+
---
|
|
2405
|
+
|
|
2406
|
+
## **11.2 Structure of an LLVM program**
|
|
2407
|
+
|
|
2408
|
+
An LLVM program consists of **global definitions** and **functions**.
|
|
2409
|
+
|
|
2410
|
+
* *Global definitions* describe data structures shared between functions.
|
|
2411
|
+
In the `k` compiler, this includes the runtime metadata tables and constant nodes such as `{}`.
|
|
2412
|
+
|
|
2413
|
+
* *Functions* contain sequences of instructions grouped into **basic blocks**.
|
|
2414
|
+
A basic block is a straight-line segment of code with no branches except at the end.
|
|
2415
|
+
Branches connect blocks, forming a control-flow graph.
|
|
2416
|
+
|
|
2417
|
+
Each instruction produces a new value; values are immutable once created.
|
|
2418
|
+
This property is called *single static assignment* (SSA).
|
|
2419
|
+
|
|
2420
|
+
---
|
|
2421
|
+
|
|
2422
|
+
## **11.3 Data types**
|
|
2423
|
+
|
|
2424
|
+
LLVM provides several primitive types.
|
|
2425
|
+
Only a few are required for the `k` compiler:
|
|
2426
|
+
|
|
2427
|
+
| LLVM type | Meaning |
|
|
2428
|
+
| --------------- | -------------------------------------------------- |
|
|
2429
|
+
| `i1` | one-bit boolean (used for success flag) |
|
|
2430
|
+
| `i32` | 32-bit integer (used for state IDs, indices, tags) |
|
|
2431
|
+
| `ptr` or `%T*` | pointer to data in memory |
|
|
2432
|
+
| `%struct {...}` | user-defined record (used for `KNode` and `KOpt`) |
|
|
2433
|
+
|
|
2434
|
+
The main structures used by generated code are:
|
|
2435
|
+
|
|
2436
|
+
```llvm
|
|
2437
|
+
%KNode = type { i32, i32, i32, [0 x %KNode*] }
|
|
2438
|
+
%KVal = type { %KNode* }
|
|
2439
|
+
%KOpt = type { i1, %KVal }
|
|
2440
|
+
```
|
|
2441
|
+
|
|
2442
|
+
`%KNode` represents one tree node in memory;
|
|
2443
|
+
`%KOpt` represents the result of a partial function, with an `ok` flag and a value pointer.
|
|
2444
|
+
|
|
2445
|
+
---
|
|
2446
|
+
|
|
2447
|
+
## **11.4 Function signatures**
|
|
2448
|
+
|
|
2449
|
+
Every compiled `k` function is translated into one LLVM function with the following signature:
|
|
2450
|
+
|
|
2451
|
+
```llvm
|
|
2452
|
+
define %KOpt @f_name(%KVal %in) { ... }
|
|
2453
|
+
```
|
|
2454
|
+
|
|
2455
|
+
The function takes a single argument `%in`, which holds the pointer to the input value.
|
|
2456
|
+
It returns a `%KOpt` structure containing the success flag and the resulting value.
|
|
2457
|
+
|
|
2458
|
+
This mirrors the runtime ABI introduced in Chapter 7.
|
|
2459
|
+
By using the same layout, all functions can call each other without conversions.
|
|
2460
|
+
|
|
2461
|
+
---
|
|
2462
|
+
|
|
2463
|
+
## **11.5 Control flow**
|
|
2464
|
+
|
|
2465
|
+
LLVM IR uses **conditional branches** to express choices and **phi-nodes** to merge results from different paths.
|
|
2466
|
+
|
|
2467
|
+
A typical pattern for union composition `<f,g>` is:
|
|
2468
|
+
|
|
2469
|
+
```llvm
|
|
2470
|
+
%r1 = call %KOpt @f(%in)
|
|
2471
|
+
%ok1 = extractvalue %KOpt %r1, 0
|
|
2472
|
+
br i1 %ok1, label %success, label %try_next
|
|
2473
|
+
|
|
2474
|
+
try_next:
|
|
2475
|
+
%r2 = call %KOpt @g(%in)
|
|
2476
|
+
br label %merge
|
|
2477
|
+
|
|
2478
|
+
success:
|
|
2479
|
+
br label %merge
|
|
2480
|
+
|
|
2481
|
+
merge:
|
|
2482
|
+
%res = phi %KOpt [ %r1, %success ], [ %r2, %try_next ]
|
|
2483
|
+
ret %res
|
|
2484
|
+
```
|
|
2485
|
+
|
|
2486
|
+
This code means: try `f`; if it succeeds, use that result; otherwise, try `g`.
|
|
2487
|
+
The `phi` instruction chooses between values coming from different blocks.
|
|
2488
|
+
|
|
2489
|
+
---
|
|
2490
|
+
|
|
2491
|
+
## **11.6 Memory operations**
|
|
2492
|
+
|
|
2493
|
+
Most functions in k do not modify existing nodes; they create new ones.
|
|
2494
|
+
Node creation is implemented as calls to runtime functions:
|
|
2495
|
+
|
|
2496
|
+
```llvm
|
|
2497
|
+
declare %KVal @k_make_product(i32 %state, i32 %n, %KVal* %children)
|
|
2498
|
+
declare %KVal @k_make_union(i32 %state, i32 %tag, %KVal %child)
|
|
2499
|
+
```
|
|
2500
|
+
|
|
2501
|
+
These calls allocate new immutable nodes.
|
|
2502
|
+
The compiler provides the correct `state`, `tag`, and child references according to the canonical type.
|
|
2503
|
+
|
|
2504
|
+
---
|
|
2505
|
+
|
|
2506
|
+
## **11.7 Constants**
|
|
2507
|
+
|
|
2508
|
+
Constant values such as `{{} true}` are represented as global variables in LLVM:
|
|
2509
|
+
|
|
2510
|
+
```llvm
|
|
2511
|
+
@unit_node = constant %KNode { 1, -1, 0, [] }
|
|
2512
|
+
@true_node = constant %KNode { 0, 0, 1, [@unit_node] }
|
|
2513
|
+
@false_node = constant %KNode { 0, 1, 1, [@unit_node] }
|
|
2514
|
+
```
|
|
2515
|
+
|
|
2516
|
+
Each constant is a fully formed node with fixed fields.
|
|
2517
|
+
When a constant function is called, the compiler returns a pointer to the corresponding global node.
|
|
2518
|
+
|
|
2519
|
+
---
|
|
2520
|
+
|
|
2521
|
+
## **11.8 From IR to LLVM**
|
|
2522
|
+
|
|
2523
|
+
Translating the compiler’s intermediate form (Chapter 10) into LLVM is mechanical:
|
|
2524
|
+
|
|
2525
|
+
| IR operation | LLVM translation |
|
|
2526
|
+
| ------------ | ------------------------------------------------------ |
|
|
2527
|
+
| `PROJECT` | call `@k_project` |
|
|
2528
|
+
| `TYPECHECK` | compare `input->state` with constant |
|
|
2529
|
+
| `CONST` | return constant node |
|
|
2530
|
+
| `PRODUCT` | evaluate subcalls, check flags, call `@k_make_product` |
|
|
2531
|
+
| `UNION` | generate branching structure as above |
|
|
2532
|
+
| `SEQ` | chain calls: feed output to next input |
|
|
2533
|
+
| `FAIL` | return `%KOpt { 0, undef }` |
|
|
2534
|
+
|
|
2535
|
+
These mappings produce correct, low-level code while remaining faithful to the language semantics.
|
|
2536
|
+
|
|
2537
|
+
---
|
|
2538
|
+
|
|
2539
|
+
## **11.9 Example**
|
|
2540
|
+
|
|
2541
|
+
For the function `neg` from earlier:
|
|
2542
|
+
|
|
2543
|
+
```k-lang
|
|
2544
|
+
$bool = < {} true, {} false >;
|
|
2545
|
+
neg = $bool < .true {{ } false}, .false {{ } true} > $bool;
|
|
2546
|
+
```
|
|
2547
|
+
|
|
2548
|
+
The simplified LLVM version may look as follows (comments added):
|
|
2549
|
+
|
|
2550
|
+
```llvm
|
|
2551
|
+
define %KOpt @neg(%KVal %in) {
|
|
2552
|
+
entry:
|
|
2553
|
+
; Try the .true branch
|
|
2554
|
+
%r1 = call %KOpt @project_true(%in)
|
|
2555
|
+
%ok1 = extractvalue %KOpt %r1, 0
|
|
2556
|
+
br i1 %ok1, label %case_true, label %case_false
|
|
2557
|
+
|
|
2558
|
+
case_true:
|
|
2559
|
+
%res_true = insertvalue %KOpt undef, i1 1, 0
|
|
2560
|
+
%res_true2 = insertvalue %KOpt %res_true, %KVal @false_node, 1
|
|
2561
|
+
ret %KOpt %res_true2
|
|
2562
|
+
|
|
2563
|
+
case_false:
|
|
2564
|
+
%r2 = call %KOpt @project_false(%in)
|
|
2565
|
+
%ok2 = extractvalue %KOpt %r2, 0
|
|
2566
|
+
br i1 %ok2, label %case_false_valid, label %undefined
|
|
2567
|
+
|
|
2568
|
+
case_false_valid:
|
|
2569
|
+
%res_false = insertvalue %KOpt undef, i1 1, 0
|
|
2570
|
+
%res_false2 = insertvalue %KOpt %res_false, %KVal @true_node, 1
|
|
2571
|
+
ret %KOpt %res_false2
|
|
2572
|
+
|
|
2573
|
+
undefined:
|
|
2574
|
+
ret %KOpt { i1 0, %KVal undef }
|
|
2575
|
+
}
|
|
2576
|
+
```
|
|
2577
|
+
|
|
2578
|
+
While verbose, this code mirrors the evaluation rules exactly and can be optimized by standard LLVM tools.
|
|
2579
|
+
|
|
2580
|
+
---
|
|
2581
|
+
|
|
2582
|
+
## **11.10 Optimization and verification**
|
|
2583
|
+
|
|
2584
|
+
LLVM provides built-in passes that simplify generated code:
|
|
2585
|
+
|
|
2586
|
+
* **mem2reg** — removes unnecessary memory operations,
|
|
2587
|
+
* **instcombine** — merges simple operations,
|
|
2588
|
+
* **gvn** — removes redundant calculations,
|
|
2589
|
+
* **simplifycfg** — cleans up control flow.
|
|
2590
|
+
|
|
2591
|
+
The compiler can run these passes automatically to produce compact and efficient output.
|
|
2592
|
+
LLVM also verifies that all types and values are consistent before emitting machine code.
|
|
2593
|
+
|
|
2594
|
+
---
|
|
2595
|
+
|
|
2596
|
+
## **11.11 Summary**
|
|
2597
|
+
|
|
2598
|
+
* LLVM IR is a portable, low-level language ideal for code generation.
|
|
2599
|
+
* Every `k` function becomes an LLVM function returning the pair `{ok, value}`.
|
|
2600
|
+
* All constants, type checks, and runtime calls are represented explicitly.
|
|
2601
|
+
* The translation from `k`’s intermediate form to LLVM is direct and systematic.
|
|
2602
|
+
* Standard LLVM tools can then optimize and compile the result into executable machine code.
|
|
2603
|
+
|
|
2604
|
+
---
|
|
2605
|
+
|
|
2606
|
+
---
|
|
2607
|
+
|
|
2608
|
+
|
|
2609
|
+
# Chapter 12 — Code Generation
|
|
2610
|
+
|
|
2611
|
+
## **12.1 Purpose**
|
|
2612
|
+
|
|
2613
|
+
The **code generation** phase translates the intermediate representation (IR) of each k function into executable code using the LLVM format described in Chapter 11.
|
|
2614
|
+
This translation is systematic: every IR operation corresponds to a specific sequence of LLVM instructions or calls to the runtime library.
|
|
2615
|
+
|
|
2616
|
+
The compiler’s objective is to produce code that behaves exactly like the formal semantics of k—no more and no less.
|
|
2617
|
+
Each step of the translation can therefore be viewed as a mechanical implementation of one of the rules introduced in Chapter 8.
|
|
2618
|
+
|
|
2619
|
+
---
|
|
2620
|
+
|
|
2621
|
+
## **12.2 Structure of generated functions**
|
|
2622
|
+
|
|
2623
|
+
Each k function becomes a stand-alone LLVM function with the signature:
|
|
2624
|
+
|
|
2625
|
+
```llvm
|
|
2626
|
+
define %KOpt @f_name(%KVal %in)
|
|
2627
|
+
```
|
|
2628
|
+
|
|
2629
|
+
Internally, the function has one or more *basic blocks*.
|
|
2630
|
+
Each block performs a small action: a projection, a check, a call, or a node construction.
|
|
2631
|
+
Blocks are connected by explicit branches that depend on the `ok` flag of intermediate results.
|
|
2632
|
+
|
|
2633
|
+
At the end of the function, exactly one `%KOpt` value is returned.
|
|
2634
|
+
|
|
2635
|
+
---
|
|
2636
|
+
|
|
2637
|
+
## **12.3 Translating core operations**
|
|
2638
|
+
|
|
2639
|
+
The compiler maintains a mapping from IR operations (Chapter 10) to LLVM code patterns.
|
|
2640
|
+
|
|
2641
|
+
| IR Operation | Generated Code Pattern |
|
|
2642
|
+
| ---------------------- | ---------------------------------------------------------------------------------- |
|
|
2643
|
+
| **PROJECT label** | `call %KOpt @k_project(%KVal %in, i32 label_id)` |
|
|
2644
|
+
| **TYPECHECK state** | Compare `input->state` with constant `state`; if unequal, return `{0, undef}` |
|
|
2645
|
+
| **CONST node** | Return `{1, node_pointer}` |
|
|
2646
|
+
| **SEQ [f₁,f₂,…]** | Emit each function call in order, test `ok` after each |
|
|
2647
|
+
| **UNION [f₁,f₂,…]** | Emit chained `if` blocks; return first successful result |
|
|
2648
|
+
| **PRODUCT [l₁=f₁, …]** | Call all subfunctions; if all succeed, collect children and call `@k_make_product` |
|
|
2649
|
+
| **FAIL** | Return `{0, undef}` immediately |
|
|
2650
|
+
|
|
2651
|
+
These templates cover the entire language.
|
|
2652
|
+
|
|
2653
|
+
---
|
|
2654
|
+
|
|
2655
|
+
## **12.4 Example: sequential composition**
|
|
2656
|
+
|
|
2657
|
+
For IR:
|
|
2658
|
+
|
|
2659
|
+
```
|
|
2660
|
+
SEQ [ PROJECT x, CALL f, CALL g ]
|
|
2661
|
+
```
|
|
2662
|
+
|
|
2663
|
+
the compiler emits:
|
|
2664
|
+
|
|
2665
|
+
```llvm
|
|
2666
|
+
%r1 = call %KOpt @k_project_x(%in)
|
|
2667
|
+
%ok1 = extractvalue %KOpt %r1, 0
|
|
2668
|
+
br i1 %ok1, label %cont1, label %fail
|
|
2669
|
+
|
|
2670
|
+
cont1:
|
|
2671
|
+
%r2 = call %KOpt @f(%r1.val)
|
|
2672
|
+
%ok2 = extractvalue %KOpt %r2, 0
|
|
2673
|
+
br i1 %ok2, label %cont2, label %fail
|
|
2674
|
+
|
|
2675
|
+
cont2:
|
|
2676
|
+
%r3 = call %KOpt @g(%r2.val)
|
|
2677
|
+
ret %KOpt %r3
|
|
2678
|
+
|
|
2679
|
+
fail:
|
|
2680
|
+
ret %KOpt { i1 0, %KVal undef }
|
|
2681
|
+
```
|
|
2682
|
+
|
|
2683
|
+
Each block corresponds to one stage in the composition.
|
|
2684
|
+
The use of conditional branches ensures that undefinedness propagates correctly.
|
|
2685
|
+
|
|
2686
|
+
---
|
|
2687
|
+
|
|
2688
|
+
## **12.5 Example: product composition**
|
|
2689
|
+
|
|
2690
|
+
For IR:
|
|
2691
|
+
|
|
2692
|
+
```
|
|
2693
|
+
PRODUCT [ a = PROJECT x, b = PROJECT y ]
|
|
2694
|
+
```
|
|
2695
|
+
|
|
2696
|
+
the compiler generates code equivalent to:
|
|
2697
|
+
|
|
2698
|
+
```llvm
|
|
2699
|
+
%ra = call %KOpt @k_project_x(%in)
|
|
2700
|
+
%ok_a = extractvalue %KOpt %ra, 0
|
|
2701
|
+
br i1 %ok_a, label %try_b, label %fail
|
|
2702
|
+
|
|
2703
|
+
try_b:
|
|
2704
|
+
%rb = call %KOpt @k_project_y(%in)
|
|
2705
|
+
%ok_b = extractvalue %KOpt %rb, 0
|
|
2706
|
+
%ok_all = and i1 %ok_a, %ok_b
|
|
2707
|
+
br i1 %ok_all, label %build, label %fail
|
|
2708
|
+
|
|
2709
|
+
build:
|
|
2710
|
+
%children = alloca [%KVal, 2]
|
|
2711
|
+
%p0 = getelementptr [%KVal,2], [%KVal,2]* %children, i32 0, i32 0
|
|
2712
|
+
store %KVal (extractvalue %KOpt %ra, 1), %KVal* %p0
|
|
2713
|
+
%p1 = getelementptr [%KVal,2], [%KVal,2]* %children, i32 0, i32 1
|
|
2714
|
+
store %KVal (extractvalue %KOpt %rb, 1), %KVal* %p1
|
|
2715
|
+
%res = call %KVal @k_make_product(i32 state_id, i32 2, %KVal* %children)
|
|
2716
|
+
ret %KOpt { i1 1, %KVal %res }
|
|
2717
|
+
|
|
2718
|
+
fail:
|
|
2719
|
+
ret %KOpt { i1 0, %KVal undef }
|
|
2720
|
+
```
|
|
2721
|
+
|
|
2722
|
+
The resulting LLVM code faithfully follows the semantic rule: the product is defined only if all components are defined.
|
|
2723
|
+
|
|
2724
|
+
---
|
|
2725
|
+
|
|
2726
|
+
## **12.6 Example: union composition**
|
|
2727
|
+
|
|
2728
|
+
For IR:
|
|
2729
|
+
|
|
2730
|
+
```
|
|
2731
|
+
UNION [ .x, .y ]
|
|
2732
|
+
```
|
|
2733
|
+
|
|
2734
|
+
the compiler produces a branching structure:
|
|
2735
|
+
|
|
2736
|
+
```llvm
|
|
2737
|
+
%r1 = call %KOpt @k_project_x(%in)
|
|
2738
|
+
%ok1 = extractvalue %KOpt %r1, 0
|
|
2739
|
+
br i1 %ok1, label %merge, label %try_next
|
|
2740
|
+
|
|
2741
|
+
try_next:
|
|
2742
|
+
%r2 = call %KOpt @k_project_y(%in)
|
|
2743
|
+
br label %merge
|
|
2744
|
+
|
|
2745
|
+
merge:
|
|
2746
|
+
%res = phi %KOpt [ %r1, %ok1 ], [ %r2, %try_next ]
|
|
2747
|
+
ret %KOpt %res
|
|
2748
|
+
```
|
|
2749
|
+
|
|
2750
|
+
This pattern ensures that the first defined branch wins, as specified by the semantics.
|
|
2751
|
+
|
|
2752
|
+
---
|
|
2753
|
+
|
|
2754
|
+
## **12.7 Managing constants and types**
|
|
2755
|
+
|
|
2756
|
+
Whenever an expression references a constant value or a type restriction:
|
|
2757
|
+
|
|
2758
|
+
* **Constant values** are replaced by pointers to global constant nodes.
|
|
2759
|
+
* **Type expressions `$T`** are compiled to a `TYPECHECK` instruction:
|
|
2760
|
+
if `input->state != T_state`, the function returns undefined.
|
|
2761
|
+
|
|
2762
|
+
This approach removes all high-level notation before reaching machine level.
|
|
2763
|
+
|
|
2764
|
+
---
|
|
2765
|
+
|
|
2766
|
+
## **12.8 Optimizing during generation**
|
|
2767
|
+
|
|
2768
|
+
Because the structure of k is very regular, many optimizations can be applied while generating code:
|
|
2769
|
+
|
|
2770
|
+
* **Eliminate redundant checks** — consecutive type checks on the same state can be merged.
|
|
2771
|
+
* **Inline small functions** — short constant or projection functions can be inserted directly into their callers.
|
|
2772
|
+
* **Remove unreachable branches** — if a type restriction guarantees that one union branch can never apply, it is omitted.
|
|
2773
|
+
|
|
2774
|
+
Such simplifications can be done by the compiler itself or delegated to LLVM’s optimization passes.
|
|
2775
|
+
|
|
2776
|
+
---
|
|
2777
|
+
|
|
2778
|
+
## **12.9 Interaction with the runtime**
|
|
2779
|
+
|
|
2780
|
+
All allocation and field access are performed through runtime calls.
|
|
2781
|
+
The compiler never manipulates pointers directly beyond passing them to these helpers.
|
|
2782
|
+
This design ensures that compiled code is independent of the internal memory layout.
|
|
2783
|
+
|
|
2784
|
+
The generated functions thus resemble small graphs of calls and conditionals built entirely on top of the fixed ABI.
|
|
2785
|
+
|
|
2786
|
+
---
|
|
2787
|
+
|
|
2788
|
+
## **12.10 Linking and verification**
|
|
2789
|
+
|
|
2790
|
+
After all functions are emitted, the compiler writes a single LLVM module containing:
|
|
2791
|
+
|
|
2792
|
+
* all generated functions,
|
|
2793
|
+
* declarations of runtime primitives (`k_project`, `k_make_product`, etc.),
|
|
2794
|
+
* and constant nodes representing literal values.
|
|
2795
|
+
|
|
2796
|
+
The LLVM verifier then checks that:
|
|
2797
|
+
|
|
2798
|
+
* every function returns a correctly typed `%KOpt`,
|
|
2799
|
+
* all control-flow paths end with a `ret`,
|
|
2800
|
+
* and all global references are defined.
|
|
2801
|
+
|
|
2802
|
+
Only verified code is passed to the optimizer or the machine-code generator.
|
|
2803
|
+
|
|
2804
|
+
---
|
|
2805
|
+
|
|
2806
|
+
## **12.11 Summary**
|
|
2807
|
+
|
|
2808
|
+
* Code generation turns IR into real executable instructions.
|
|
2809
|
+
* Each k function maps to an LLVM function returning `{ok, value}`.
|
|
2810
|
+
* Projection, composition, union, and product translate into small, fixed code patterns.
|
|
2811
|
+
* Type restrictions become simple integer comparisons.
|
|
2812
|
+
* The compiler can apply immediate simplifications during translation.
|
|
2813
|
+
* The final output is a verified LLVM module ready for optimization and linking.
|
|
2814
|
+
|
|
2815
|
+
---
|
|
2816
|
+
---
|
|
2817
|
+
|
|
2818
|
+
|
|
2819
|
+
# Chapter 13 — Linking and Execution
|
|
2820
|
+
|
|
2821
|
+
## **13.1 Purpose**
|
|
2822
|
+
|
|
2823
|
+
The last stage of compilation connects the generated code with the runtime library and produces an executable program.
|
|
2824
|
+
This process is called **linking**.
|
|
2825
|
+
Once linked, the program can be run like any other compiled program: it reads input values, executes the compiled k functions, and returns a result.
|
|
2826
|
+
|
|
2827
|
+
The linking and execution stages ensure that the compiled code, the runtime library, and the data it manipulates all agree on structure and conventions.
|
|
2828
|
+
|
|
2829
|
+
---
|
|
2830
|
+
|
|
2831
|
+
## **13.2 The runtime library**
|
|
2832
|
+
|
|
2833
|
+
All compiled k programs depend on a small runtime component, often distributed as a file named `krt.c` or `krt.o`.
|
|
2834
|
+
This library provides:
|
|
2835
|
+
|
|
2836
|
+
| Function | Purpose |
|
|
2837
|
+
| ------------------------------------ | -------------------------------------------------- |
|
|
2838
|
+
| `k_make_product(state, n, children)` | Allocate a product node with `n` fields. |
|
|
2839
|
+
| `k_make_union(state, tag, child)` | Allocate a union node with a single variant. |
|
|
2840
|
+
| `k_project(input, label_id)` | Perform projection `.label`. |
|
|
2841
|
+
| `k_fail()` | Return a predefined undefined result `{ ok = 0 }`. |
|
|
2842
|
+
| `k_unit()` | Return the constant unit value `{}`. |
|
|
2843
|
+
|
|
2844
|
+
These functions implement the same concepts as the semantic rules described earlier, but at the level of actual memory operations.
|
|
2845
|
+
They are compiled once in a low-level language such as C and reused by all generated programs.
|
|
2846
|
+
|
|
2847
|
+
---
|
|
2848
|
+
|
|
2849
|
+
## **13.3 Metadata tables**
|
|
2850
|
+
|
|
2851
|
+
Along with the runtime functions, the compiler produces **metadata** describing each canonical type used in the program:
|
|
2852
|
+
|
|
2853
|
+
* `state_kind[state]` — `0` for product, `1` for union
|
|
2854
|
+
* `state_arity[state]` — number of children
|
|
2855
|
+
* `field_index[state][label_id]` — position of field `label_id` within product
|
|
2856
|
+
* `variant_index[state][label_id]` — tag number for variant `label_id` within union
|
|
2857
|
+
|
|
2858
|
+
These tables are stored as constant arrays in the compiled module.
|
|
2859
|
+
Runtime functions use them to interpret and construct nodes correctly without needing any dynamic reflection.
|
|
2860
|
+
|
|
2861
|
+
---
|
|
2862
|
+
|
|
2863
|
+
## **13.4 Linking**
|
|
2864
|
+
|
|
2865
|
+
The generated LLVM file is compiled and linked with the runtime library in two simple steps:
|
|
2866
|
+
|
|
2867
|
+
```bash
|
|
2868
|
+
clang -O2 -c krt.c -o krt.o
|
|
2869
|
+
clang -O2 program.ll krt.o -o program
|
|
2870
|
+
```
|
|
2871
|
+
|
|
2872
|
+
The first command compiles the runtime once.
|
|
2873
|
+
The second command combines the runtime with the program’s own functions and constant definitions.
|
|
2874
|
+
|
|
2875
|
+
The result is a native executable file named `program`.
|
|
2876
|
+
When run, it behaves exactly like the original k program.
|
|
2877
|
+
|
|
2878
|
+
---
|
|
2879
|
+
|
|
2880
|
+
## **13.5 Initialization**
|
|
2881
|
+
|
|
2882
|
+
When the executable starts, it performs a few basic steps:
|
|
2883
|
+
|
|
2884
|
+
1. Allocate an **arena** for storing newly created nodes.
|
|
2885
|
+
2. Initialize constant nodes such as the unit `{}` and any user-defined constants.
|
|
2886
|
+
3. Optionally, prepare a table of function pointers for fast invocation.
|
|
2887
|
+
|
|
2888
|
+
After initialization, the main compiled function can be called directly.
|
|
2889
|
+
|
|
2890
|
+
---
|
|
2891
|
+
|
|
2892
|
+
## **13.6 Executing a compiled function**
|
|
2893
|
+
|
|
2894
|
+
Every compiled function has the signature:
|
|
2895
|
+
|
|
2896
|
+
```c
|
|
2897
|
+
struct KOpt function(struct KNode* input);
|
|
2898
|
+
```
|
|
2899
|
+
|
|
2900
|
+
To run a program, a caller must supply an input value tree in the expected canonical form.
|
|
2901
|
+
The value is typically created by another function or loaded from serialized data.
|
|
2902
|
+
|
|
2903
|
+
For example, to execute a function `neg` expecting a `$bool`:
|
|
2904
|
+
|
|
2905
|
+
```c
|
|
2906
|
+
struct KOpt result = neg(&true_node);
|
|
2907
|
+
if (result.ok)
|
|
2908
|
+
print_value(result.val);
|
|
2909
|
+
else
|
|
2910
|
+
printf("undefined\n");
|
|
2911
|
+
```
|
|
2912
|
+
|
|
2913
|
+
If the input is `{ {} true }`, the output will be `{ {} false }`.
|
|
2914
|
+
|
|
2915
|
+
---
|
|
2916
|
+
|
|
2917
|
+
## **13.7 Input and output conventions**
|
|
2918
|
+
|
|
2919
|
+
At the machine level, a “value” is always represented by a pointer to a `KNode`.
|
|
2920
|
+
Each compiled function expects exactly one such pointer.
|
|
2921
|
+
When a function returns, its output pointer may refer to an existing constant node, a new node allocated in the arena, or nothing at all if undefined.
|
|
2922
|
+
|
|
2923
|
+
There is no special I/O system in k itself; printing or reading values is the responsibility of the host environment.
|
|
2924
|
+
In practice, programs are tested by calling compiled functions from C or Python and inspecting their returned structures.
|
|
2925
|
+
|
|
2926
|
+
---
|
|
2927
|
+
|
|
2928
|
+
## **13.8 Debugging and inspection**
|
|
2929
|
+
|
|
2930
|
+
Because both the generated code and the runtime library are deterministic and memory-safe, debugging usually consists of examining node contents.
|
|
2931
|
+
|
|
2932
|
+
Two simple runtime utilities are useful:
|
|
2933
|
+
|
|
2934
|
+
* `print_value(node)` — prints a tree in human-readable form.
|
|
2935
|
+
* `print_type(state)` — prints the canonical description of a type state.
|
|
2936
|
+
|
|
2937
|
+
They allow the user to confirm that compiled functions construct the correct tree shapes.
|
|
2938
|
+
|
|
2939
|
+
---
|
|
2940
|
+
|
|
2941
|
+
## **13.9 Verification**
|
|
2942
|
+
|
|
2943
|
+
After linking, the compiler can run a *self-verification pass*:
|
|
2944
|
+
|
|
2945
|
+
1. Construct small sample inputs for each exported function.
|
|
2946
|
+
2. Execute the compiled function.
|
|
2947
|
+
3. Compare results with the interpreter’s result using structural equality.
|
|
2948
|
+
|
|
2949
|
+
If all match, the compiler’s translation is confirmed correct for those test cases.
|
|
2950
|
+
This method is especially valuable during compiler development and textbook exercises.
|
|
2951
|
+
|
|
2952
|
+
---
|
|
2953
|
+
|
|
2954
|
+
## **13.10 Example: complete run**
|
|
2955
|
+
|
|
2956
|
+
Example program:
|
|
2957
|
+
|
|
2958
|
+
```
|
|
2959
|
+
$bool = < {} true, {} false >;
|
|
2960
|
+
neg = $bool < .true {{ } false}, .false {{ } true} > $bool;
|
|
2961
|
+
```
|
|
2962
|
+
|
|
2963
|
+
Steps:
|
|
2964
|
+
|
|
2965
|
+
1. Compile to `neg.ll`.
|
|
2966
|
+
2. Link with `krt.o`.
|
|
2967
|
+
3. Run a small C driver that calls `neg`.
|
|
2968
|
+
|
|
2969
|
+
Output:
|
|
2970
|
+
|
|
2971
|
+
```
|
|
2972
|
+
input = {{ } true}
|
|
2973
|
+
result = {{ } false}
|
|
2974
|
+
```
|
|
2975
|
+
|
|
2976
|
+
If `input` is `{}` or any non-`$bool` value, the function is undefined and prints “undefined”.
|
|
2977
|
+
|
|
2978
|
+
---
|
|
2979
|
+
|
|
2980
|
+
## **13.11 Summary**
|
|
2981
|
+
|
|
2982
|
+
* Linking combines generated LLVM code with the shared runtime library.
|
|
2983
|
+
* The runtime provides allocation, projection, and constant handling.
|
|
2984
|
+
* Metadata tables describe type structure to the runtime.
|
|
2985
|
+
* Every compiled function follows the uniform calling convention `KOpt f(KNode*)`.
|
|
2986
|
+
* Execution produces immutable tree values exactly matching the semantics of k.
|
|
2987
|
+
* Verification against the interpreter ensures correctness of the translation.
|
|
2988
|
+
|
|
2989
|
+
---
|
|
2990
|
+
---
|
|
2991
|
+
|
|
2992
|
+
|
|
2993
|
+
# Chapter 14 — Canonical Serialization
|
|
2994
|
+
|
|
2995
|
+
## **14.1 Purpose**
|
|
2996
|
+
|
|
2997
|
+
Serialization is the process of converting a value in memory into a compact sequence of bits or bytes that can be stored or transmitted.
|
|
2998
|
+
For the k language, serialization serves an additional goal: it defines a **canonical representation** of each value that is unique and independent of the machine on which it was produced.
|
|
2999
|
+
|
|
3000
|
+
A canonical encoding allows values to be compared, hashed, or stored in a registry in a reproducible way.
|
|
3001
|
+
Two values that are structurally identical will always produce the same bit sequence.
|
|
3002
|
+
|
|
3003
|
+
---
|
|
3004
|
+
|
|
3005
|
+
## **14.2 Canonical type information**
|
|
3006
|
+
|
|
3007
|
+
Serialization always depends on the **canonical form of the type**.
|
|
3008
|
+
A type in canonical form is a finite tree automaton (Chapter 3) that describes the structure of all its values.
|
|
3009
|
+
Because this form is unique, every possible value of the type can be encoded deterministically.
|
|
3010
|
+
|
|
3011
|
+
Before any value is serialized, the compiler or runtime must know:
|
|
3012
|
+
|
|
3013
|
+
* the list of states (C0, C1, …),
|
|
3014
|
+
* for each state, whether it is a product or a union,
|
|
3015
|
+
* the number of possible transitions, and
|
|
3016
|
+
* for unions, the order of their variants.
|
|
3017
|
+
|
|
3018
|
+
This information acts as the grammar from which all bit sequences are derived.
|
|
3019
|
+
|
|
3020
|
+
---
|
|
3021
|
+
|
|
3022
|
+
## **14.3 Encoding principle**
|
|
3023
|
+
|
|
3024
|
+
Each node in a value tree corresponds to one of the type’s states.
|
|
3025
|
+
|
|
3026
|
+
* **Product node** – emits no bits; the encoder simply serializes each child in canonical field order.
|
|
3027
|
+
* **Union node** – emits a small binary code that identifies which variant is used, then serializes its single child.
|
|
3028
|
+
* **Unit node** (empty product) – emits nothing; it has no children.
|
|
3029
|
+
* **Empty union** – cannot be encoded because it has no possible values.
|
|
3030
|
+
|
|
3031
|
+
Thus, the sequence of bits records exactly which union branches were taken while descending the tree.
|
|
3032
|
+
|
|
3033
|
+
---
|
|
3034
|
+
|
|
3035
|
+
## **14.4 Fixed-length codes per state**
|
|
3036
|
+
|
|
3037
|
+
For simplicity, each union state uses fixed-length binary codes of equal size.
|
|
3038
|
+
|
|
3039
|
+
If a union has *m* variants, then
|
|
3040
|
+
|
|
3041
|
+
```
|
|
3042
|
+
k = ceil(log2(m))
|
|
3043
|
+
```
|
|
3044
|
+
|
|
3045
|
+
bits are needed.
|
|
3046
|
+
Each variant is numbered in canonical order from 0 to m − 1 and represented by the binary form of its index.
|
|
3047
|
+
|
|
3048
|
+
Products and units use k = 0 and emit no bits.
|
|
3049
|
+
|
|
3050
|
+
This rule allows the decoder to know, from the type alone, how many bits to read at each step.
|
|
3051
|
+
|
|
3052
|
+
---
|
|
3053
|
+
|
|
3054
|
+
## **14.5 Example: natural numbers**
|
|
3055
|
+
|
|
3056
|
+
For the type
|
|
3057
|
+
|
|
3058
|
+
```
|
|
3059
|
+
$bnat = < bnat 0, bnat 1, {} _ >;
|
|
3060
|
+
```
|
|
3061
|
+
|
|
3062
|
+
the canonical form has three transitions from state C0.
|
|
3063
|
+
Therefore, `k = ceil(log2(3)) = 2` bits are required per union node.
|
|
3064
|
+
|
|
3065
|
+
Assigning codes in order:
|
|
3066
|
+
|
|
3067
|
+
| Rule | Code |
|
|
3068
|
+
| -------------- | ---- |
|
|
3069
|
+
| C0 → {C0 "0"} | `00` |
|
|
3070
|
+
| C0 → {C0 "1"} | `01` |
|
|
3071
|
+
| C0 → {C1 "_" } | `10` |
|
|
3072
|
+
|
|
3073
|
+
The value `{ {{ {} _ } 1 } 0 }` is encoded by concatenating the codes of the transitions chosen during a leftmost derivation:
|
|
3074
|
+
|
|
3075
|
+
```
|
|
3076
|
+
00 → first C0
|
|
3077
|
+
01 → next C0
|
|
3078
|
+
10 → final C0
|
|
3079
|
+
```
|
|
3080
|
+
|
|
3081
|
+
Resulting bit sequence: `000110`.
|
|
3082
|
+
|
|
3083
|
+
---
|
|
3084
|
+
|
|
3085
|
+
## **14.6 Decoding**
|
|
3086
|
+
|
|
3087
|
+
Decoding is deterministic and mirrors the encoding process:
|
|
3088
|
+
|
|
3089
|
+
1. Start at the root state (C0).
|
|
3090
|
+
2. If the current state is a union, read k bits to choose a rule.
|
|
3091
|
+
3. Create the corresponding node, then recursively decode its child or children.
|
|
3092
|
+
4. For products, decode all children in order; for units, stop.
|
|
3093
|
+
|
|
3094
|
+
The decoder stops when the entire tree has been reconstructed and all bits have been consumed.
|
|
3095
|
+
|
|
3096
|
+
Because every union code is of fixed length, the decoding process requires no backtracking and can be implemented as a simple loop.
|
|
3097
|
+
|
|
3098
|
+
---
|
|
3099
|
+
|
|
3100
|
+
## **14.7 Folding repeated subtrees**
|
|
3101
|
+
|
|
3102
|
+
Many values contain repeated substructures.
|
|
3103
|
+
To avoid serializing the same tree several times, identical subtrees can be *folded* into a **directed acyclic graph (DAG)** before encoding.
|
|
3104
|
+
|
|
3105
|
+
Each unique node is assigned an identifier.
|
|
3106
|
+
When the encoder encounters a repeated node, it emits a reference to its identifier instead of encoding it again.
|
|
3107
|
+
|
|
3108
|
+
During decoding, the identifier is resolved to the corresponding subtree, reconstructing the shared structure.
|
|
3109
|
+
|
|
3110
|
+
Folding is optional; it saves space without changing the logical value.
|
|
3111
|
+
|
|
3112
|
+
---
|
|
3113
|
+
|
|
3114
|
+
## **14.8 Stream format**
|
|
3115
|
+
|
|
3116
|
+
A minimal canonical bitstream consists of:
|
|
3117
|
+
|
|
3118
|
+
1. **Header** – type hash (e.g., 8 bytes) identifying the canonical type.
|
|
3119
|
+
2. **Node count** – integer *N* if DAG encoding is used.
|
|
3120
|
+
3. **Encoded data** – concatenation of all fixed-length union codes and, if folded, node references.
|
|
3121
|
+
|
|
3122
|
+
If two systems share the same canonical type table, the bitstream alone is enough to reconstruct the original value.
|
|
3123
|
+
|
|
3124
|
+
---
|
|
3125
|
+
|
|
3126
|
+
## **14.9 Implementation sketch**
|
|
3127
|
+
|
|
3128
|
+
A compact encoder can be written as follows:
|
|
3129
|
+
|
|
3130
|
+
```
|
|
3131
|
+
procedure encode(node):
|
|
3132
|
+
if node.arity == 0:
|
|
3133
|
+
return
|
|
3134
|
+
if node.arity == 1:
|
|
3135
|
+
write_bits(code_for(node.state, node.tag))
|
|
3136
|
+
encode(node.child[0])
|
|
3137
|
+
else:
|
|
3138
|
+
for each child in node.children:
|
|
3139
|
+
encode(child)
|
|
3140
|
+
```
|
|
3141
|
+
|
|
3142
|
+
The corresponding decoder reverses the process.
|
|
3143
|
+
Both procedures require only the canonical type tables and simple bit operations.
|
|
3144
|
+
|
|
3145
|
+
---
|
|
3146
|
+
|
|
3147
|
+
## **14.10 Determinism and equality**
|
|
3148
|
+
|
|
3149
|
+
Because encoding is fully determined by the canonical type and field order, two equal values always produce identical bit sequences.
|
|
3150
|
+
Conversely, decoding the same bit sequence always yields the same tree.
|
|
3151
|
+
|
|
3152
|
+
This property allows equality comparison by direct byte comparison of serialized forms, without traversing the trees in memory.
|
|
3153
|
+
|
|
3154
|
+
---
|
|
3155
|
+
|
|
3156
|
+
## **14.11 Summary**
|
|
3157
|
+
|
|
3158
|
+
* Every type’s canonical automaton defines a unique bit-level grammar for its values.
|
|
3159
|
+
* Products emit no bits; unions emit fixed-length variant codes.
|
|
3160
|
+
* The number of bits required for each state depends only on the number of variants in that state.
|
|
3161
|
+
* Optional DAG folding eliminates repeated subtrees.
|
|
3162
|
+
* The resulting bitstream is compact, deterministic, and machine-independent.
|
|
3163
|
+
|
|
3164
|
+
---
|
|
3165
|
+
---
|
|
3166
|
+
|
|
3167
|
+
|
|
3168
|
+
# Chapter 15 — Optimization and Folding
|
|
3169
|
+
|
|
3170
|
+
## **15.1 Purpose**
|
|
3171
|
+
|
|
3172
|
+
Optimization in the k compiler is the process of simplifying functions without changing their meaning.
|
|
3173
|
+
The goal is to make the generated code smaller, faster, and easier to verify.
|
|
3174
|
+
Because k is purely functional and has no side effects, optimizations are safe whenever the simplified function is equivalent to the original one.
|
|
3175
|
+
|
|
3176
|
+
This chapter describes a few basic optimizations that follow directly from the semantics introduced earlier.
|
|
3177
|
+
|
|
3178
|
+
---
|
|
3179
|
+
|
|
3180
|
+
## **15.2 Constant folding**
|
|
3181
|
+
|
|
3182
|
+
A **constant** is a function that ignores its input and always produces the same value.
|
|
3183
|
+
If a composition of functions can be evaluated entirely at compile time, it can be replaced by a single constant function.
|
|
3184
|
+
|
|
3185
|
+
Example:
|
|
3186
|
+
|
|
3187
|
+
```
|
|
3188
|
+
true_bool = {{ } true} $bool;
|
|
3189
|
+
not_true = (true_bool neg);
|
|
3190
|
+
```
|
|
3191
|
+
|
|
3192
|
+
The compiler observes that `(true_bool neg)` always yields `false_bool`, so it replaces it with a constant:
|
|
3193
|
+
|
|
3194
|
+
```
|
|
3195
|
+
not_true = {{ } false} $bool;
|
|
3196
|
+
```
|
|
3197
|
+
|
|
3198
|
+
Constant folding removes unnecessary runtime computation.
|
|
3199
|
+
|
|
3200
|
+
---
|
|
3201
|
+
|
|
3202
|
+
## **15.3 Type-based simplifications**
|
|
3203
|
+
|
|
3204
|
+
Since type expressions act as identity functions defined only on specific values, they can be eliminated when redundant:
|
|
3205
|
+
|
|
3206
|
+
```
|
|
3207
|
+
$bool $bool f → $bool f
|
|
3208
|
+
```
|
|
3209
|
+
|
|
3210
|
+
If a function’s input or output type is already known from context, repeated checks can be omitted.
|
|
3211
|
+
The compiler ensures that at least one valid check remains to preserve correctness.
|
|
3212
|
+
|
|
3213
|
+
---
|
|
3214
|
+
|
|
3215
|
+
## **15.4 Inlining**
|
|
3216
|
+
|
|
3217
|
+
Inlining replaces a call to a small function by its body.
|
|
3218
|
+
It avoids the overhead of a function call and often exposes further simplifications.
|
|
3219
|
+
|
|
3220
|
+
Example:
|
|
3221
|
+
|
|
3222
|
+
```
|
|
3223
|
+
id = ();
|
|
3224
|
+
f = (id g);
|
|
3225
|
+
```
|
|
3226
|
+
|
|
3227
|
+
Because `id` is the identity function, `(id g)` is equivalent to `g`.
|
|
3228
|
+
Inlining removes the useless call.
|
|
3229
|
+
|
|
3230
|
+
For larger functions, inlining is applied selectively—only when it shortens the resulting code or eliminates intermediate values.
|
|
3231
|
+
|
|
3232
|
+
---
|
|
3233
|
+
|
|
3234
|
+
## **15.5 Dead-branch elimination**
|
|
3235
|
+
|
|
3236
|
+
In a union composition `<f, g>`, if type analysis shows that the input type can only satisfy the first branch, the second branch can never be defined and is removed.
|
|
3237
|
+
|
|
3238
|
+
Example:
|
|
3239
|
+
|
|
3240
|
+
```
|
|
3241
|
+
$bool = < {} true, {} false >;
|
|
3242
|
+
f = $bool < .true {{ } false} >;
|
|
3243
|
+
```
|
|
3244
|
+
|
|
3245
|
+
Here, since `$bool` restricts the input to only the two variants `true` and `false`, and the second variant `.false` is not handled, the compiler infers that `f` is undefined for `.false` and simplifies the code accordingly.
|
|
3246
|
+
|
|
3247
|
+
Dead-branch elimination keeps the generated control flow minimal.
|
|
3248
|
+
|
|
3249
|
+
---
|
|
3250
|
+
|
|
3251
|
+
## **15.6 Common subexpression elimination**
|
|
3252
|
+
|
|
3253
|
+
If the same partial function is applied multiple times to the same input, the result will always be the same.
|
|
3254
|
+
The compiler can compute it once and reuse the result.
|
|
3255
|
+
|
|
3256
|
+
Example:
|
|
3257
|
+
|
|
3258
|
+
```
|
|
3259
|
+
{ .x .y, .x .z }
|
|
3260
|
+
```
|
|
3261
|
+
|
|
3262
|
+
Both fields start with the projection `.x`.
|
|
3263
|
+
The compiler computes `.x` once, stores the result, and reuses it for `.y` and `.z`.
|
|
3264
|
+
This optimization reduces repeated traversal of the same input structure.
|
|
3265
|
+
|
|
3266
|
+
---
|
|
3267
|
+
|
|
3268
|
+
## **15.7 Canonical folding**
|
|
3269
|
+
|
|
3270
|
+
During evaluation, many values share identical subtrees.
|
|
3271
|
+
A **folding pass** replaces identical subtrees by shared nodes, forming a minimal DAG (directed acyclic graph).
|
|
3272
|
+
|
|
3273
|
+
At compile time, folding may also apply to constant expressions.
|
|
3274
|
+
If two constant subtrees are identical, they are merged and stored as a single global node.
|
|
3275
|
+
|
|
3276
|
+
This process ensures that structural equality corresponds to pointer equality: two values are identical if they share the same root node.
|
|
3277
|
+
|
|
3278
|
+
---
|
|
3279
|
+
|
|
3280
|
+
## **15.8 Function canonicalization**
|
|
3281
|
+
|
|
3282
|
+
Every function can be normalized by expanding all type aliases, inlining trivial definitions, and folding identical subfunctions.
|
|
3283
|
+
The normalized form of a function depends only on its semantics, not on the way it was written.
|
|
3284
|
+
|
|
3285
|
+
A stable hash computed from this canonical representation serves as a permanent identifier for the function, just as type hashes identify canonical types.
|
|
3286
|
+
|
|
3287
|
+
This property is essential for the schema registry described in the next chapter.
|
|
3288
|
+
|
|
3289
|
+
---
|
|
3290
|
+
|
|
3291
|
+
## **15.9 Runtime simplifications**
|
|
3292
|
+
|
|
3293
|
+
At runtime, additional micro-optimizations are possible:
|
|
3294
|
+
|
|
3295
|
+
* **Arena reuse:** allocate nodes in a memory region that is cleared after each function call.
|
|
3296
|
+
* **Hash-consing:** maintain a small table of recently created nodes to reuse identical ones automatically.
|
|
3297
|
+
* **Shortcut constants:** return pointers to global constants instead of allocating new copies for unit or boolean values.
|
|
3298
|
+
|
|
3299
|
+
Such optimizations reduce memory usage without changing program behavior.
|
|
3300
|
+
|
|
3301
|
+
---
|
|
3302
|
+
|
|
3303
|
+
## **15.10 Equivalence and correctness**
|
|
3304
|
+
|
|
3305
|
+
Each optimization must preserve *semantic equivalence*: for every input, the optimized function must be defined for exactly the same values and must return the same result when defined.
|
|
3306
|
+
|
|
3307
|
+
Because k’s semantics are purely functional, equivalence can be tested mechanically by comparing evaluation results on all finite inputs of a small type, or by structural reasoning when types are infinite.
|
|
3308
|
+
|
|
3309
|
+
---
|
|
3310
|
+
|
|
3311
|
+
## **15.11 Summary**
|
|
3312
|
+
|
|
3313
|
+
* Optimization in k relies on algebraic properties of pure functions.
|
|
3314
|
+
* Common transformations include constant folding, inlining, dead-branch elimination, and subexpression reuse.
|
|
3315
|
+
* Folding shared subtrees yields compact DAG representations of values.
|
|
3316
|
+
* Canonical function forms allow stable hashing and reproducible compilation results.
|
|
3317
|
+
* All optimizations preserve exact semantic behavior.
|
|
3318
|
+
|
|
3319
|
+
---
|
|
3320
|
+
---
|
|
3321
|
+
|
|
3322
|
+
|
|
3323
|
+
# Chapter 16 — Toward a Universal Schema Registry
|
|
3324
|
+
|
|
3325
|
+
## **16.1 Motivation**
|
|
3326
|
+
|
|
3327
|
+
Every canonical type and function in k has a unique structural form and a stable hash.
|
|
3328
|
+
These hashes can serve as global identifiers.
|
|
3329
|
+
A registry that maps such identifiers to definitions allows programs and systems to exchange schemas and functions safely and reproducibly.
|
|
3330
|
+
|
|
3331
|
+
---
|
|
3332
|
+
|
|
3333
|
+
## **16.2 Basic structure**
|
|
3334
|
+
|
|
3335
|
+
A schema registry stores entries of two kinds:
|
|
3336
|
+
|
|
3337
|
+
| Kind | Example content |
|
|
3338
|
+
| ------------ | ------------------------------------------------------------- |
|
|
3339
|
+
| **Type** | canonical automaton text for a type hash |
|
|
3340
|
+
| **Function** | canonical IR or serialized representation for a function hash |
|
|
3341
|
+
|
|
3342
|
+
Each entry is immutable.
|
|
3343
|
+
New versions are added as new hashes.
|
|
3344
|
+
|
|
3345
|
+
---
|
|
3346
|
+
|
|
3347
|
+
## **16.3 Key operations**
|
|
3348
|
+
|
|
3349
|
+
1. **Lookup by hash** — return the canonical definition.
|
|
3350
|
+
2. **Lookup by shape** — search for functions with given input/output type hashes.
|
|
3351
|
+
3. **Verification** — ensure that two parties share the same canonical form before exchanging values.
|
|
3352
|
+
|
|
3353
|
+
These operations can be implemented as simple key–value queries.
|
|
3354
|
+
|
|
3355
|
+
---
|
|
3356
|
+
|
|
3357
|
+
## **16.4 Example format**
|
|
3358
|
+
|
|
3359
|
+
A minimal JSON entry:
|
|
3360
|
+
|
|
3361
|
+
```json
|
|
3362
|
+
{
|
|
3363
|
+
"hash": "C0ABCDEF",
|
|
3364
|
+
"kind": "type",
|
|
3365
|
+
"definition": "$C0=<C0\"0\",C0\"1\",C1\"_\">;$C1={};"
|
|
3366
|
+
}
|
|
3367
|
+
```
|
|
3368
|
+
|
|
3369
|
+
or for a function:
|
|
3370
|
+
|
|
3371
|
+
```json
|
|
3372
|
+
{
|
|
3373
|
+
"hash": "F7B1A2",
|
|
3374
|
+
"kind": "function",
|
|
3375
|
+
"input": "C0ABCDEF",
|
|
3376
|
+
"output": "C9FFF1",
|
|
3377
|
+
"ir": "SEQ [PROJECT x, CONST true]"
|
|
3378
|
+
}
|
|
3379
|
+
```
|
|
3380
|
+
|
|
3381
|
+
---
|
|
3382
|
+
|
|
3383
|
+
## **16.5 Uses**
|
|
3384
|
+
|
|
3385
|
+
* Preventing schema mismatches in distributed systems.
|
|
3386
|
+
* Deduplicating identical definitions across projects.
|
|
3387
|
+
* Enabling reproducible builds where all types and functions are referenced by hash.
|
|
3388
|
+
|
|
3389
|
+
---
|
|
3390
|
+
|
|
3391
|
+
## **16.6 Summary**
|
|
3392
|
+
|
|
3393
|
+
* Canonical types and functions can be globally identified by stable hashes.
|
|
3394
|
+
* A simple registry provides lookup and verification of these definitions.
|
|
3395
|
+
* Such registries make k programs portable, self-describing, and safe to share between systems.
|
|
3396
|
+
|
|
3397
|
+
---
|
|
3398
|
+
---
|
|
3399
|
+
|
|
3400
|
+
|
|
3401
|
+
# Appendix A — Implementing a Prototype Compiler in Python
|
|
3402
|
+
## **A.1 Purpose**
|
|
3403
|
+
|
|
3404
|
+
This appendix outlines how to implement a simple, working prototype of the k compiler in Python.
|
|
3405
|
+
The goal is to let the reader experiment with parsing, type normalization, and LLVM code generation using only standard tools and a few small libraries.
|
|
3406
|
+
The prototype does not need to support optimization or a full runtime—only the basic translation pipeline.
|
|
3407
|
+
|
|
3408
|
+
---
|
|
3409
|
+
|
|
3410
|
+
## **A.2 Required environment**
|
|
3411
|
+
|
|
3412
|
+
Install:
|
|
3413
|
+
|
|
3414
|
+
```bash
|
|
3415
|
+
python3 -m venv kenv
|
|
3416
|
+
source kenv/bin/activate
|
|
3417
|
+
pip install llvmlite
|
|
3418
|
+
```
|
|
3419
|
+
|
|
3420
|
+
`llvmlite` provides an interface to LLVM for code generation.
|
|
3421
|
+
No additional dependencies are required.
|
|
3422
|
+
|
|
3423
|
+
---
|
|
3424
|
+
|
|
3425
|
+
## **A.3 Recommended directory structure**
|
|
3426
|
+
|
|
3427
|
+
```
|
|
3428
|
+
k-compiler/
|
|
3429
|
+
├── main.py # command-line driver
|
|
3430
|
+
├── parser.py # converts source text to AST
|
|
3431
|
+
├── types.py # canonical type construction
|
|
3432
|
+
├── normalize.py # filter resolution and typing
|
|
3433
|
+
├── ir.py # intermediate representation
|
|
3434
|
+
├── codegen.py # translation from IR to LLVM
|
|
3435
|
+
├── runtime.ll # minimal runtime in LLVM
|
|
3436
|
+
└── examples/ # sample k programs
|
|
3437
|
+
```
|
|
3438
|
+
|
|
3439
|
+
Each module is small and focused on one task.
|
|
3440
|
+
|
|
3441
|
+
---
|
|
3442
|
+
|
|
3443
|
+
## **A.4 The AST**
|
|
3444
|
+
|
|
3445
|
+
Represent the language syntax with Python classes:
|
|
3446
|
+
|
|
3447
|
+
```python
|
|
3448
|
+
class Expr: pass
|
|
3449
|
+
|
|
3450
|
+
class Composition(Expr):
|
|
3451
|
+
def __init__(self, parts): self.parts = parts
|
|
3452
|
+
|
|
3453
|
+
class Product(Expr):
|
|
3454
|
+
def __init__(self, fields): self.fields = fields # [(label, expr), ...]
|
|
3455
|
+
|
|
3456
|
+
class Union(Expr):
|
|
3457
|
+
def __init__(self, options): self.options = options # [expr, ...]
|
|
3458
|
+
|
|
3459
|
+
class Projection(Expr):
|
|
3460
|
+
def __init__(self, label): self.label = label
|
|
3461
|
+
|
|
3462
|
+
class Constant(Expr):
|
|
3463
|
+
def __init__(self, name): self.name = name
|
|
3464
|
+
```
|
|
3465
|
+
|
|
3466
|
+
The parser builds these objects from source text.
|
|
3467
|
+
|
|
3468
|
+
---
|
|
3469
|
+
|
|
3470
|
+
## **A.5 Type normalization**
|
|
3471
|
+
|
|
3472
|
+
Maintain canonical type states as small Python objects:
|
|
3473
|
+
|
|
3474
|
+
```python
|
|
3475
|
+
class TypeState:
|
|
3476
|
+
def __init__(self, kind, transitions):
|
|
3477
|
+
self.kind = kind # 'product' or 'union'
|
|
3478
|
+
self.transitions = transitions # [(label, next_state)]
|
|
3479
|
+
```
|
|
3480
|
+
|
|
3481
|
+
Normalization expands type aliases and assigns numeric state IDs (`C0`, `C1`, …).
|
|
3482
|
+
For a prototype, a simple structural hash (using `json.dumps`) can serve as the canonical identifier.
|
|
3483
|
+
|
|
3484
|
+
---
|
|
3485
|
+
|
|
3486
|
+
## **A.6 Intermediate representation**
|
|
3487
|
+
|
|
3488
|
+
Represent IR instructions as simple tuples or small classes:
|
|
3489
|
+
|
|
3490
|
+
```python
|
|
3491
|
+
class Project: def __init__(self, label): self.label = label
|
|
3492
|
+
class Const: def __init__(self, node): self.node = node
|
|
3493
|
+
class Seq: def __init__(self, parts): self.parts = parts
|
|
3494
|
+
class UnionIR: def __init__(self, parts): self.parts = parts
|
|
3495
|
+
class ProductIR:def __init__(self, fields):self.fields = fields
|
|
3496
|
+
```
|
|
3497
|
+
|
|
3498
|
+
A recursive function converts an AST expression into IR following Chapter 10.
|
|
3499
|
+
|
|
3500
|
+
---
|
|
3501
|
+
|
|
3502
|
+
## **A.7 Generating LLVM code**
|
|
3503
|
+
|
|
3504
|
+
Using `llvmlite.ir`, create one LLVM function per compiled function:
|
|
3505
|
+
|
|
3506
|
+
```python
|
|
3507
|
+
from llvmlite import ir
|
|
3508
|
+
|
|
3509
|
+
mod = ir.Module(name="k")
|
|
3510
|
+
|
|
3511
|
+
# LLVM type definitions
|
|
3512
|
+
KNodePtr = ir.PointerType(ir.IntType(8))
|
|
3513
|
+
KVal = ir.LiteralStructType([KNodePtr])
|
|
3514
|
+
KOpt = ir.LiteralStructType([ir.IntType(1), KVal])
|
|
3515
|
+
|
|
3516
|
+
def emit_function(name, ir_expr):
|
|
3517
|
+
fn_type = ir.FunctionType(KOpt, [KVal])
|
|
3518
|
+
fn = ir.Function(mod, fn_type, name=name)
|
|
3519
|
+
block = fn.append_basic_block('entry')
|
|
3520
|
+
builder = ir.IRBuilder(block)
|
|
3521
|
+
# translate ir_expr recursively into builder calls
|
|
3522
|
+
builder.ret(ir.Constant(KOpt, (ir.Constant(ir.IntType(1), 0),
|
|
3523
|
+
ir.Constant(KVal, ir.Constant(KNodePtr, None)))))
|
|
3524
|
+
```
|
|
3525
|
+
|
|
3526
|
+
The translation patterns follow those described in Chapter 12.
|
|
3527
|
+
Each IR node is emitted as a call or conditional block using `builder.call`, `builder.if_then`, and so on.
|
|
3528
|
+
|
|
3529
|
+
---
|
|
3530
|
+
|
|
3531
|
+
## **A.8 Minimal runtime**
|
|
3532
|
+
|
|
3533
|
+
Write a minimal `runtime.ll` that defines stubs for:
|
|
3534
|
+
|
|
3535
|
+
```llvm
|
|
3536
|
+
declare %KOpt @k_project(%KVal %v, i32 %label)
|
|
3537
|
+
declare %KVal @k_make_product(i32 %state, i32 %n, %KVal* %children)
|
|
3538
|
+
declare %KVal @k_make_union(i32 %state, i32 %tag, %KVal %child)
|
|
3539
|
+
```
|
|
3540
|
+
|
|
3541
|
+
These can simply return placeholder constants for testing.
|
|
3542
|
+
Later, they can be replaced with the real runtime implemented in C.
|
|
3543
|
+
|
|
3544
|
+
---
|
|
3545
|
+
|
|
3546
|
+
## **A.9 Putting it together**
|
|
3547
|
+
|
|
3548
|
+
A small driver script (`main.py`) can compile a file and emit LLVM text:
|
|
3549
|
+
|
|
3550
|
+
```python
|
|
3551
|
+
import parser, normalize, ir, codegen
|
|
3552
|
+
|
|
3553
|
+
def compile_file(path):
|
|
3554
|
+
src = open(path).read()
|
|
3555
|
+
ast = parser.parse(src)
|
|
3556
|
+
typed = normalize.process(ast)
|
|
3557
|
+
ir_tree = ir.from_ast(typed)
|
|
3558
|
+
llvm_module = codegen.emit(ir_tree)
|
|
3559
|
+
print(str(llvm_module))
|
|
3560
|
+
```
|
|
3561
|
+
|
|
3562
|
+
Run:
|
|
3563
|
+
|
|
3564
|
+
```bash
|
|
3565
|
+
python main.py examples/neg.k > neg.ll
|
|
3566
|
+
clang -O2 neg.ll runtime.ll -o neg
|
|
3567
|
+
```
|
|
3568
|
+
|
|
3569
|
+
---
|
|
3570
|
+
|
|
3571
|
+
## **A.10 Suggested extensions**
|
|
3572
|
+
|
|
3573
|
+
1. Implement the runtime functions in Python for quick testing.
|
|
3574
|
+
2. Add a textual REPL that reads a k expression and prints its IR.
|
|
3575
|
+
3. Integrate a simple interpreter to compare interpreter and compiled results.
|
|
3576
|
+
4. Use hashing of canonical forms to name generated LLVM functions.
|
|
3577
|
+
|
|
3578
|
+
---
|
|
3579
|
+
|
|
3580
|
+
## **A.11 Summary**
|
|
3581
|
+
|
|
3582
|
+
A prototype compiler can be realized in fewer than a thousand lines of Python.
|
|
3583
|
+
It can parse k definitions, construct canonical types, translate expressions into IR, and generate valid LLVM code.
|
|
3584
|
+
Such a prototype is enough to experiment with all ideas presented in this textbook and to verify the semantics of the k language in practice.
|
|
3585
|
+
|
|
3586
|
+
---
|
|
3587
|
+
|
|
3588
|
+
|
|
3589
|
+
# Appendix B — Incorporating External Predefined Types and Functions
|
|
3590
|
+
## **B.1 Purpose**
|
|
3591
|
+
|
|
3592
|
+
The k language is intentionally minimal.
|
|
3593
|
+
Nevertheless, real systems often need to interact with data types and operations defined outside of k—for example, numeric values, text, or platform-specific constants.
|
|
3594
|
+
This appendix explains how external predefined types and functions can be introduced into a k compiler without changing the language itself.
|
|
3595
|
+
|
|
3596
|
+
---
|
|
3597
|
+
|
|
3598
|
+
## **B.2 External types**
|
|
3599
|
+
|
|
3600
|
+
An **external type** is a canonical type that is *not* expressed through k's own product-and-union syntax but is known to the compiler through registration.
|
|
3601
|
+
|
|
3602
|
+
Each external type has:
|
|
3603
|
+
|
|
3604
|
+
| Field | Description |
|
|
3605
|
+
| --------------------- | ---------------------------------------------------------------------------- |
|
|
3606
|
+
| **Name** | symbolic name (e.g., `$int`, `$string`) |
|
|
3607
|
+
| **Hash** | unique stable identifier, just like canonical types |
|
|
3608
|
+
| **Runtime kind** | how the value is stored in memory (pointer, immediate integer, etc.) |
|
|
3609
|
+
| **Adapter functions** | conversion between the external representation and the standard `KNode` tree |
|
|
3610
|
+
|
|
3611
|
+
Example registry entry:
|
|
3612
|
+
|
|
3613
|
+
```json
|
|
3614
|
+
{
|
|
3615
|
+
"hash": "E001",
|
|
3616
|
+
"kind": "type",
|
|
3617
|
+
"external": true,
|
|
3618
|
+
"name": "$int",
|
|
3619
|
+
"runtime_kind": "primitive_i64"
|
|
3620
|
+
}
|
|
3621
|
+
```
|
|
3622
|
+
|
|
3623
|
+
The compiler treats such a type as an atomic node, represented internally as a single `KNode` with `arity = 0` but tagged as *external*.
|
|
3624
|
+
|
|
3625
|
+
---
|
|
3626
|
+
|
|
3627
|
+
## **B.3 External constructors**
|
|
3628
|
+
|
|
3629
|
+
External types may provide special constructors or constants.
|
|
3630
|
+
For example, `$int` might include predefined constant functions:
|
|
3631
|
+
|
|
3632
|
+
```
|
|
3633
|
+
zero = {} $int;
|
|
3634
|
+
one = {} $int;
|
|
3635
|
+
```
|
|
3636
|
+
|
|
3637
|
+
At compile time these appear as constant functions returning fixed external nodes provided by the runtime.
|
|
3638
|
+
|
|
3639
|
+
---
|
|
3640
|
+
|
|
3641
|
+
## **B.4 External functions**
|
|
3642
|
+
|
|
3643
|
+
External functions are partial functions implemented outside of k but declared inside a k program.
|
|
3644
|
+
They have known input and output types, possibly external.
|
|
3645
|
+
|
|
3646
|
+
Declaration syntax:
|
|
3647
|
+
|
|
3648
|
+
```
|
|
3649
|
+
extern add : $int $int → $int;
|
|
3650
|
+
extern less : $int $int → $bool;
|
|
3651
|
+
```
|
|
3652
|
+
|
|
3653
|
+
At compile time the compiler records the signature and associates it with a runtime symbol name such as `k_ext_add`.
|
|
3654
|
+
|
|
3655
|
+
When generating code, each external call becomes a standard function call following the ABI:
|
|
3656
|
+
|
|
3657
|
+
```llvm
|
|
3658
|
+
declare %KOpt @k_ext_add(%KVal %a, %KVal %b)
|
|
3659
|
+
```
|
|
3660
|
+
|
|
3661
|
+
The runtime library provides these implementations in a native language (C, C++, Rust, etc.).
|
|
3662
|
+
|
|
3663
|
+
---
|
|
3664
|
+
|
|
3665
|
+
## **B.5 Integration with canonical types**
|
|
3666
|
+
|
|
3667
|
+
External types are integrated into the canonical system as *leaf states*.
|
|
3668
|
+
They can appear as components in products or unions just like built-in ones.
|
|
3669
|
+
|
|
3670
|
+
Example:
|
|
3671
|
+
|
|
3672
|
+
```
|
|
3673
|
+
$point = { $int x, $int y };
|
|
3674
|
+
```
|
|
3675
|
+
|
|
3676
|
+
Canonical form:
|
|
3677
|
+
|
|
3678
|
+
```
|
|
3679
|
+
$C0 = { C1 "x", C1 "y" };
|
|
3680
|
+
$C1 = external $int;
|
|
3681
|
+
```
|
|
3682
|
+
|
|
3683
|
+
This allows normal serialization and type comparison; the external leaf is treated as opaque but identified by its hash.
|
|
3684
|
+
|
|
3685
|
+
---
|
|
3686
|
+
|
|
3687
|
+
## **B.6 Serialization and external values**
|
|
3688
|
+
|
|
3689
|
+
During serialization, an external node is encoded by:
|
|
3690
|
+
|
|
3691
|
+
1. Its type hash (to indicate which external kind it represents).
|
|
3692
|
+
2. A binary blob produced by a runtime-supplied encoder for that type.
|
|
3693
|
+
|
|
3694
|
+
Deserialization reverses the process using the corresponding decoder.
|
|
3695
|
+
All external codecs must be deterministic and version-stable to preserve canonical equality.
|
|
3696
|
+
|
|
3697
|
+
---
|
|
3698
|
+
|
|
3699
|
+
## **B.7 Example**
|
|
3700
|
+
|
|
3701
|
+
Suppose we introduce `$int` and external function `add`:
|
|
3702
|
+
|
|
3703
|
+
```
|
|
3704
|
+
$pair_int = { $int a, $int b };
|
|
3705
|
+
sum = { .a x, .b y } (add x y);
|
|
3706
|
+
```
|
|
3707
|
+
|
|
3708
|
+
The compiler generates code that:
|
|
3709
|
+
|
|
3710
|
+
* projects fields `.a` and `.b`,
|
|
3711
|
+
* passes them as `KVal` pointers to `@k_ext_add`,
|
|
3712
|
+
* and returns the result as a new `$int` node.
|
|
3713
|
+
|
|
3714
|
+
All logic outside arithmetic remains standard k code.
|
|
3715
|
+
|
|
3716
|
+
---
|
|
3717
|
+
|
|
3718
|
+
## **B.8 Practical implementation**
|
|
3719
|
+
|
|
3720
|
+
In a Python prototype:
|
|
3721
|
+
|
|
3722
|
+
```python
|
|
3723
|
+
EXTERNAL_TYPES = {
|
|
3724
|
+
"$int": {"hash": "E001", "kind": "external", "runtime_kind": "primitive_i64"}
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
EXTERNAL_FUNCS = {
|
|
3728
|
+
"add": {"inputs": ["$int", "$int"], "output": "$int", "symbol": "k_ext_add"},
|
|
3729
|
+
"less": {"inputs": ["$int", "$int"], "output": "$bool", "symbol": "k_ext_less"}
|
|
3730
|
+
}
|
|
3731
|
+
```
|
|
3732
|
+
|
|
3733
|
+
During code generation, when a call refers to an external function, the compiler emits a call to the symbol given in the table.
|
|
3734
|
+
|
|
3735
|
+
---
|
|
3736
|
+
|
|
3737
|
+
## **B.9 Summary**
|
|
3738
|
+
|
|
3739
|
+
* External predefined types extend k without altering its core semantics.
|
|
3740
|
+
* They are registered with hashes and treated as atomic nodes.
|
|
3741
|
+
* External functions follow the same calling convention as normal ones.
|
|
3742
|
+
* Serialization of external values uses type-specific codecs.
|
|
3743
|
+
* This mechanism allows integration of native computations (numbers, strings, system data) while preserving the purity and determinism of the k language.
|
|
3744
|
+
|
|
3745
|
+
---
|