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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
# Chapter 22: A Hello, World!
English | [日本語](README.ja.md)
[Previous: Chapter 21](../chapter21/README.md) |
[Next: Chapter 23](../chapter23/README.md)
It's high time for strings and printing! In this chapter we look at what it
takes to make a basic string - not really a full fledged String class, that
will wait for another chapter.
```java
sys.io.p("Hello, World!");
```
Yeah! A 1-liner `Hello, World`! This example can be found in
`docs/examples/A_helloWorld.smp`.
Let's break it down.
- `sys`: is Just A Variable Name. Like other variables, it is looked up in the
current scope... which looks completely empty. Every Simple program now starts
with the `sys` variable in-scope. `sys` itself is a normal `struct`
and is defined in `src/main/smp/sys.smp`.
- `sys.io.p`: lookup the `io` field in `sys` struct, which yields the `sys.io`
struct; then a lookup of `p` (short for "print") in the `sys.io` struct.
This returns a function.
- `p("Hello, World!")`: call the function just loaded from the `p` field, with
`"Hello, World!"` as a string argument.
- `"Hello, World!"`: New syntax that yields a constant array of `u8` (chars).
The array is otherwise a normal range-checked array, with a leading `#`
length field. The array backing data eventually ends up in the *constant
pool*, and from there in an ELF file's RODATA section.
Chapter 11's lazy memory partitioning continues here: the parser carries one
`$mem`, and BulkMemPhi/MemPhi discover field aliases during optimization.
Allocations consume a partial MemMerge and produce `{ptr, $mem}`; calls carry
whole memory. The loop Load search follows every arm of a memory merge, so
BrainFuck's invariant program length folds out of its main loop.
You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter22) in a linear Git revision history on the [linear](https://github.com/SeaOfNodes/Simple/tree/linear) branch and [compare](https://github.com/SeaOfNodes/Simple/compare/linear-chapter21...linear-chapter22) it to the previous chapter.
Here is the [complete language grammar](docs/22-grammar.md) for this chapter.
## Nested Types and Static Fields
Simple now supports nested types - this is a name-space only change, so no new
semantics - just the ability to nest type definitions.
```java
struct Outer {
struct Inner {
int in;
flt pi = 3.14;
};
int out;
};
```
Any final field set in the declaration (as opposed to the constructor) is
automatically a "static" field - it is final for all instances, and does not
need to be stored in each instance. Instead one copy is kept in a global
space (not really a *class* object yet) and loaded from there.
In this example:
- Instances of the `Outer` struct have a single `int` field `out`.
- Instances of the `Inner` struct have a single `int` field `in`.
- There exists an `Outer.Inner.pi` field in a global space with value `3.14`.
The `sys` struct is another example, with nested `libc` and `io` structs at
least. All the fields in these structs are all final, hence static.
## FFI and I/O Calls
We can now call external / FFI functions, and they are declared like any other
variable except being assigned "C". Here is the binding for the libc `write`
call:
`{ i32 fd, i64 buf, u32 len -> u32 } write = "C";`
Any final variable assigned as "C" will be considered defined externally. The
syntax of this might change slightly to avoid ambiguity with assigning the same
"C" string.
There is no linker support for getting the correct signature or namespace; the
name `write` has to be unique when linking.
## Auto-Import and the "sys." namespace
The `sys` struct includes `libc` bindings, and any default library code
including e.g. printing support and collections. As of this chapter it
includes minimal libc bindings and an easy print. This excerpt shows the
parts used above:
```java
// top-level default import
struct sys {
// libc bindings
/** https://www.man7.org/linux/man-pages/man2/<libc call>.2.html */
struct libc {
// fd buf len -> len
{ i32 i64 u32 -> u32 } write = "C";
// addr len prot flags fd off -> void*
{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C";
// mmap flags
i32 PROT_EXEC = 4;
i32 PROT_READ = 1;
i32 PROT_WRITE= 2;
i32 PROT_NONE = 0;
i32 MAP_PRIVATE = 2;
i32 MAP_ANON = 32;
};
struct io {
val p = { u8[~] str ->
i64 ptr = str; // cast array base to i64
return sys.libc.write(1,ptr,str#);
};
};
};
```
The `sys` import itself is Just Another `struct` like any other struct; it has
fields and assignments and types declared internally. Note that these
assignments are all final and in the struct declaration, hence these are all
*static* fields.
## Casting a Pointer to an `i64`
You can now cast a pointer to an i64, although not the other way around.
`i64 ptr = str; // cast array base to i64`
For arrays, this cast is to the array *base* skipping the `#` length field, and
is suitable for passing to external "C" calls. For structs, this is just the
struct base. This is used to pass a length-checked Simple array to the libc
`write` call in the above `sys.io.p` function.
## Arrays of Constants and Constant Arrays
The Simple type system now supports the notion of a "constant array" - an array
of fixed constants, stored in the ELF file in the RODATA section. Currently we
only support strings at the parser level: `"Hello, World!"` makes a `u8[13]`
array containing 13 ASCII bytes; like all arrays it has a length and will be
range-checked at some point.
A constant version of a non-constant array can be assigned using the `u8[~]`
syntax.
```java
// Make a mutable array of ints
int N=4;
i32[] is = new i32[N];
// Mutate the array
for( int i=0; i<N; i++ )
is[i] = i*i;
// Function "sum" can read but not write the array
val sum = { i32[~] nums -> // Notice the [~] signature marking nums as immutable
int sum = 0;
for( int i=0; i<nums#; i++ )
sum += nums[i];
return sum;
};
return sum(is);
```
Note that this affects the deep contents of the array and not the array
variable itself.
## Quote Characters
The form ``` `0` ``` makes a `u8` value with the ASCII character `0`.
## Continuous Improvement
Executing larger programs exposes mistakes that graph-only tests can miss.
During the original development of this chapter, these included x86 instruction
encodings and flag clobbers, scheduling around loop backedges and memory stores,
and stale dominator depths during inlining. Corrections are being moved into the
earliest applicable chapters so readers can build on working compiler snapshots.
Native tests check both output and process exit status; emulator tests check the
returned values and memory as well as successful execution.
## RegAlloc improvements: better color bias
Chapter 21 introduced conservative copy coalescing. Here we improve the register
preferences used when related live ranges cannot coalesce. A copy chain can take
an already assigned register even at its terminal definition; a loop Phi prefers
its backedge's register, avoiding a move on each iteration when possible.
When no live range is trivially colorable, cheap values make better spill
candidates. We favor a cloneable definition with distant or multiple uses over
one already adjacent to its only use, then callee-save values whose long spans
are cheap to split. Register order breaks ties between callee saves. These
are small preferences, not a general cost model. Grouping popular values by
register class is left for Chapter 23, cold loop splits for 24, and area/cost
ranking for 25.
Rows are fixed test cohorts; columns are the compiler/allocator chapters. Each
cell is the sum of `_spillScaled`: retained split moves, including register
copies, weighted by loopDepth. Lower is better. **# tests counts
compilations** (program/CPU/ABI cases), including zero-spill cases, rather than
JUnit methods. The count and membership of each row stay fixed across columns.
<!-- spill-matrix:start -->
| Test cohort | # tests | Ch 20 | Ch 21 | Ch 22 |
|---|---:|---:|---:|---:|
| Ch 20 | 39 | 360 | 576 | 430 |
| Ch 21 | 52 | | 1,072 | 960 |
| Ch 22 | 26 | | | 63 |
<!-- spill-matrix:end -->
|