summaryrefslogtreecommitdiff
path: root/docs/chapter29/README.md
blob: b4e8b539f489014733ea0f8b017b471923938666 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# Chapter 29: Tech Debt Cleanup

English | [日本語](README.ja.md)

[Previous: Chapter 28b — Separate Compilation](../chapter28b/README.md)

This chapter makes the compiler easier to read and change.  It starts by
breaking the parser into grammar-sized pieces.  Constructors now check field
initialization with ordinary memory types, and can pass an incomplete `self` to
other calls in a type-safe way.

There is lots more cleanup here; earlier chapters are left alone instead of
backporting the cleanups, in part to show the normal progression of large
project development.


## Following the grammar

The old `Parser.java` had grown beyond 3,000 lines, mixing recursive descent,
name resolution, scope management, and IR construction. The
[`parser/` package](../../src/main/java/com/seaofnodes/simple/parser/README.md) separates
those responsibilities without introducing an AST: parsing still builds
Sea-of-Nodes IR directly.

`Parser` owns the source cursor, active scope, type registry, and common token
and IR helpers.  Each component shares that state through `Parser P`.
`Statements`, `Expressions`, `Declarations`, `Types`, `Functions`, and `Structs`
follow the corresponding grammar productions.  `References` handles name and
field resolution; `Allocations`, `Conversions`, and `ControlFlow` hold shared
graph-building operations.  `Lexer` owns tokenization and source locations, and
`Var` records lexical bindings.

The [grammar and code map](29-grammar.md) connects productions to methods.
Start with `Statements.parseStatement` or `Expressions.parseAssignment` and
follow the calls alongside it.  Deferred parsing remains explicit: `for` updates
are parsed after their bodies, and declared constructors after field discovery.

The [source layout](../../src/main/java/com/seaofnodes/simple/README.md) describes the
other packages. `IterPeeps` lives in `codegen/` beside the compilation passes.


## Checking constructor completion

How constructors are handled has gone back and forth over the chapters; this
testament to how implementing a language is a journey of exploration more than
a rote rewriting of the grammer.

Instance fields now use ordinary Loads and Stores throughout initialization.
Instead of keeping field values in locals until construction ends, the
constructor works on a real `self` object with private memory.  This lets it
lend `self` to another constructor or function while construction is underway.

Fresh fields start at `TypeScalar.BOT`, which is the uninitialized indicator.
An initialized integer with an unknown value has `TypeInteger.BOT`, distinct
from the uninitialized `TypeScalar.BOT`, and similarly for other `BOT` types.
Stores replace `TypeScalar.BOT` with that field's type in private memory.
`TypeScalar.BOT` value survives merging at Phis and other control flow, and we
use it to make sure a field is initialized on all paths.

The completion form of `ConstructNode` checks that every required field is
initialized on every constructor exit.  Once the types prove completion and the
node adds no pointer restriction, it folds away during optimization.
Otherwise, `TypeCheck` asks it for an error.  This uses the existing type flow
and error walk; there is no extra construction pass.

Loads check for loading `TypeScalar.BOT` (i.e., loading from uninitialized
memory), and report an error.

Constructors keep their private-memory and precise aliasing, only losing it
after the constructor completes.


## Incomplete Types

A constructor can pass its partially constructed `self` to other constructors
and functions.  This permits doubly linked lists and parent/child tree nodes to
cross-link during construction without reading uninitialized fields.

To pass an incomplete type the receiving parameter must declare the partial
qualifier `^`, for example a`val foo = { Point ^p -> ... }`.  The function may
forward `p`, retain it in another object's private construction memory, and
read fields proven initialized by every caller.  It cannot write through `p`,
even to non-final fields, or store it into an ordinary object or global.  The
original constructor retains its write permission, including writes to final
fields until construction finishes.


```simple
struct _Point {
    int~ x;  // final int x
    int~ y;  // final int y
    new _Point = { int n -> x=n; y=_read(self); };
};
val _read = { _Point ^p -> return p.x; };
return new _Point(arg).y;
```

Here `x` is readable during `_read`, while `y` is still uninitialized.  The
private names keep the callers within this compilation: unknown callers of a
public `^` interface promise no initialized fields.  If any live caller passes
a partial without `x` set, the merged parameter loses that guarantee and the
Load reports an error.

A `new` object is *incomplete* when it starts life and if any of its actual
arguments are incomplete.  With only complete arguments, including null, the
object is complete after construction.  Otherwise it completes with the
original object; the whole collection completes at once.  This allows
construction to assemble a cycle: after the root construction completes, the
reachable objects can be used normally.  Each constructor must still initialize
its own fields before returning.

`TypeMemPtr` carries the *partial* qualifier through ordinary type flow.  An
`InitViewNode` combines `self` with its current private memory to describe which
fields are readable at that point: declared types for initialized fields,
`TypeScalar.BOT` for the rest.  Actual values still come from memory.  Calls
borrowing `self` carry its current Stores on their normal memory edge.

Redundant views fold during optimization.  Other views preserve the partial
restriction or mask unreadable fields until `TypeCheck` checks their consumers
and removes them.  A surviving view is not itself an error: `_read` above needs
only `x`, not a fully initialized object.

These guarantees remain conservative.  Reading a set object-reference field
does not prove that its referent's fields are initialized.  Arrays are already
initialized and cannot contain partial references.  Borrowed reads use aliasable
memory, so allocating another object of the same class is allowed.  Reading an
initialized final field during construction does not freeze its value: the
owning constructor may still change it afterward.

Separate compilation of `^` interfaces remains unsupported.  Inferred field
requirements and nested initialization guarantees remain on the roadmap.

The comment in
[`Construction`](../../src/main/java/com/seaofnodes/simple/parser/Construction.java)
and the [construction notes](construction-groups.md) explain the type flow,
memory edges, and remaining restrictions.


## Build and test

From the repository root:

```sh
make lib tests release CHAPTERS=chapter29 CTAGS=
```

The suite includes the inherited compiler, evaluator, native execution,
serialization, and separate-compilation tests, plus parser regression tests for
deferred fragments. Historical test class names retain their original identities.
The [Chapter 28b library/client example](../chapter28b/README.md) also runs from
this chapter directory.

IntelliJ has a `chapter29` module and a **chapter29 tests** run configuration.
Build the native runtime and `sys` library with Make before debugging tests
that need them; see [the IntelliJ setup](../intellij.md).