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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
|
# Type Analysis via Constant Propagation
## Prelude
While I was working on the C2 compiler in HotSpot, I noticed something
interesting: although Java generics are *erased*, C2 is able to nearly recover
them completely via simple [constant propagation](https://en.wikipedia.org/wiki/Sparse_conditional_constant_propagation).
Afterwards I developed two languages which are directly designed to do all
their type-checking via constant propagation:
[Simple](https://github.com/SeaOfNodes/Simple) and [AA](https://github.com/cliffclick/aa). Both languages are statically typed
and type-safe; AA uses a blend of extended Hindley-Milner and constant propagation.
[Simple](https://github.com/SeaOfNodes/Simple) uses constant propagation
exclusively to do type analysis (and the generate any resulting semantic
errors), and has a working implementation so you can view it in action!
In both languages, after the first round of *pessimistic* constant propagation
(and extensive peephole optimizations) completes, every program point (node in
the IR) has a type - and if there are no error types, then the program is
type-safe. No separate typing pass is needed, and this is typically done
partially as the program parses, finishing shortly thereafter. Optionally, an
*optimistic* constant propagation
([SCCP](https://en.wikipedia.org/wiki/Sparse_conditional_constant_propagation))
can be run to type more programs than the *pessimistic* approach alone.
This discussion relies on the program being in
[SSA](https://en.wikipedia.org/wiki/Static_single-assignment_form)
form, which I assume the reader is familiar with, and is commonly used in
constant propagation because it allows the algorithm to be *sparse* without
effort. Also, all three compilers use the Sea-of-Nodes representation which is
only relevant here because the same SCCP algorithm is now also *conditional* without
any other changes; SCCP does not require the Sea-of-Nodes, although the
presentation is simpler.
More jargon: I use *program points* interchangeably with *nodes in an SSA-based
IR graph* or sometimes simply *nodes*.
## Table of Contents
* [The Basics](#the-basics)
* [More Lattices](#more-lattices)
* [A Sample Typing Error](#a-sample-typing-error)
* [Tuples](#tuples)
* [Structs](#structs)
* [Pointers](#pointers)
* [Functions](#functions)
* [Type Sugar And All The Rest](#type-sugar-and-all-the-rest)
* [Methods](#methods)
* [Subclassing](#Subclassing)
* [Structural Typing](#structural-typing)
* [Nomative Typing and Traits and Interfaces](#nomative-typing-and-traits-and-interfaces)
* [Conditional Conformance](#conditional-conformance)
* [Parametric Polymorphism](#parametric-polymorphism)
* Call Graph Discovery and Separate compilation
* Performance Concerns
* A Self Type
* Overloading
* Typing other simple properties: mutable-or-not, [initialized/destructed]-or-not
* Arrays
## The Basics
Variations of constant propagation have a long history in compilers, and here I
am referring to the well understood
[Monotone Analysis Framework](http://janvitek.org/events/NEU/7580/papers/more-papers/1977-acta-kam-monotone.pdf);
a quick google search finds plethora of courseworks on the topic. I assume the
reader is familiar and I only give a very brief overview here. Constant
propagation has long been used to discover interesting facts about program
points, generally to further optimize a program. We are going to use it to
*type* our programs.
At the core of the problem is a
[lattice](https://en.wikipedia.org/wiki/Lattice_(order)) used to track what values a program point can take on. Here is a common example for tracking integer constants:
[Lattice1](./docs/lattice_i.svg) <img src="./docs/lattice_i.svg"/>
The lattices being used here are:
* Symmetric - every lattice element has a *dual*
* Complete - the *meet* of any two elements is another element
* Bounded (ranked) - the lattice has finite height, which also means there are maximum and minimum elements
and of course the *meet* is idempotent, commutative and associative.
The lattice *join* is defined from the *meet* and *dual* in
the normal way: `~(~x meet ~y)`
Also *isa* is defined as `(x meet y)==y`, e.g. `17.isa(Int)` expands to `(17
meet Int)==Int` which becomes `(Int)==Int` which is `true`.
### The Constant Propagation Algorithm
All nodes are initialized to either *top* ⊤ (SCCP) or *bottom* ⊥ (during
Parsing), and also put on a worklist. We pull work off the worklist until it
runs dry, evaluating the node's *transfer function* to partially evaluate
w.r.t. the lattice. If the evaluation produces a new lattice element, we put
dependent nodes' back on the worklist (using e.g. use-def edges).
```java
while( (node = work.pop()) != null )
if( node.if_changed(node.transfer_fcn()) )
work.addAll( node.uses )
```
The beauty of this algorithm is that it hits a *least fixed point*, a "best
answer" in time proportional to the program size and lattice depth. That
single "best answer" will also become our *typing*, our mapping from program
points' to their *type*.
### Pessmistic vs Optimistic
In general these lattices can be used in either a *pessimistic* or *optimistic*
direction.
The *pessimistic* direction starts with all program points at the least
element, here ⊥, and applies the program points' *transfer function* to
partially evaluate wrt the lattice, potientially discovering new facts or
constants. Another way to think about *bottom* ⊥ is that it represents that
the compiler does not know anything about this value, and that it must emit
code to compute the result the hard way - no shortcuts, no optimizations.
Trivial Example#1:
```python
var x # declare a variable 'x', untyped
x = 2 # assign a 2
print(x) # print x, which is 2
```
Here we see `x=2` at the `print(x)` and this program can be simplified to
`print(2)`, dropping the `x`.
Example#2:
```python
x0 = 1 # Note the SSA renaming
while( rand() ): # Loop an unknown number of times
x1 = φ(x0,x2)
x2 = 2 - x1 # can we discover x2 = 1 here?
print(x1)
```
While evaluating this program from the start, we can see that `x0 = 1` when we
reach the loop entry, but what happens to `x` around the backedge? Since we
start at `⊥` (know nothing), the `φ(x0,x2)` becomes `φ(1,⊥)` which does a
*meet* to yield `⊥` (because the we know nothing about what comes around the
backedge). Then in the loop body `x2 = 2 - x1` becomes `x2 = 2 - ⊥` which is
`⊥` which is what we started with. We have then hit a fixed point with no
values further changing: `x0 = 1; x1 = ⊥; x2 = ⊥`.
Let's repeat Example#2 using the *optimistic* starting point: all values at
`⊤`. An intuition for *top* ⊤ might be "the compiler can pick any convenient
value (as long as the ⊤ is removed before trying to emit code)". Another
intuition might be "top is all possible constants, all at once - an impossible
situation - so later we'll have to pick one (and only one)".
The example is structured a little differently; it interleaves the current
known values for the `x` variables between the lines of code, and follows down
the worklist, and by the loop end shows what we know so-far:
Example#3:
```python
# x0=⊤, x1=⊤, x2=⊤
x0 = 1 # Note the SSA renaming
while( rand() ): # Loop an unknown number of times
# x0=1, x1=⊤, x2=⊤ # x2 is ⊤ around the backedge, instead of ⊥
x1 = φ(x0,x2)
# x0=1, x1=1, x2=⊤
x2 = 2 - x1 # can we discover x2 = 1 here?
# x0=1, x1=1, x2=1
```
Since x2 changed from `⊤` to `1`, we need to repeat uses of x2:
```python
# x0=1, x1=1, x2=1
x1 = φ(x0,x2)
# x0=1, x1=1, x2=1
```
With no change from before and after the `φ(x0,x2)` we hit our fixed point -
and discover that `print(x)` is really `print(1)` and all the `x` math (and the
loop) can be dropped.
The general rule here is that the *optimistic* approach may find more
constants, but never less; while the *pessimistic* approach starts from the
lowest possible types and can be stopped any time yielding a correct analysis
(but a possibly weaker final result). Also the *pessimistic* approach can
be interleaved with other optimizations as long as those optimizations do not lose
any type information - and indeed this is the mode the C2 compiler has been
running in since 1997.
First appears in [Simple chapter02](https://github.com/SeaOfNodes/Simple/tree/main/chapter02):
the pessimistic treatment during parsing with the simple integer lattice.
## More Lattices
We enhance this integer lattice to have a "uber-top" and "uber-bottom",
and rename the existing integer `⊤` and `⊥` to `⊤:int` and `⊥:int`.
We further extend the integers to cover integer ranges; this covers common
language types like `i32`, `i64`, `u16`, `byte`, `bool` and so forth.
[Lattice2](./docs/lattice_i2.svg) <img src = ./docs/lattice_i2.svg>
First appears in [Simple chapter14](https://github.com/SeaOfNodes/Simple/tree/main/chapter14).
Parallel to the integer lattice we add a IEEE 754 floating point lattice, which
allows us to track FP constants, e.g. `0.` or `pi`. We track both 32b and 64b
values, and can observe that the 32b ones are a strict subset of the 64b ones.
[Lattice3](./docs/lattice_if.svg) <img src = ./docs/lattice_if.svg>
First appears in [Simple chapter12](https://github.com/SeaOfNodes/Simple/tree/main/chapter12).
Now we add more "inner" lattices, the key being that inner lattices do not
themselves contain another lattice element. For example a hypothetical
sub-word FP format:
[Lattice4](./docs/lattice_iff.svg) <img src = ./docs/lattice_iff.svg>
A *control* type, used to signify that a Node is reachable or dead. These
show up in the *conditional* part of SCCP when analyzing e.g.
```python
test = False;
if test:
dead_code
```
[Lattice5](./docs/lattice_iffc.svg) <img src = ./docs/lattice_iffc.svg>
First appears in [Simple chapter04](https://github.com/SeaOfNodes/Simple/tree/main/chapter04).
Hopefully it is clear how to add another sub-lattice that itself is symmetric,
complete and ranked.
## A Sample Typing Error
```python
var x = 1
x = 3.14
```
Here `x` is given a type, which the language parser records with the expression
defining `x`. i.e., `x` is currently typed as a `1` (also has the *value* `1`,
which is not the same thing). Generally this is too restrictive a type to be
desirable, so commonly the parser widens the type `1` to e.g. `int64` or `Int`.
After the initial type inference for `x`, further updates are guarded by a
*type cast*, an IR Node which *meets* any assigned value with the inferred type
for `x`. If the *meet* hits `⊥`, this signifies a type error.
This is in general how we handle type errors: at user added type ascriptions,
declarations on structs or classes, or any place a type is known, we insert a
*type cast* in the IR. The *type cast* should eventually become redundant and
fold away. If the incoming type *isa* the internal type, the cast is a no-op.
If it does not, it is a type error.
## Tuples
So far our lattice is composed of a series of sub-lattices joined by an
"uber-top" and "uber-bottom", and each sub-lattice in turn is composed of
various type base elements (e.g. 'int' or '3.14').
Now we introduce a collection of lattice elements, as itself a lattice.
Examples might be `[Ctrl, 17]` or `[3.14, ⊤:int, 0:FP8]`, or `[3.14, 17,
⊥:FP8]`.
*Meet* on these tuples is defined element wise; looking at the last two examples
the meet is `[3.14 meet 3.14, ⊤:int meet 17, 0:FP8 meet ⊥:FP8]` which folds to
`[3.14, 17, ⊥:FP8]`.
We also need to handle when the number of elements does not match, such as
`[Ctrl, 17]` meet `[3.14, 17, ⊥:FP8]`. Meeting such mismatches is required to
keep our lattice *complete*. In general there are two obvious ways forward:
fall hard to ⊥, or try to preserve some knowledge. Falling hard to ⊥ basically
declares a type error and is probably correct in some situations. However
preserving knowledge is really helpful during type inference and is absolutely
required in a few cases, so we use the other plan: every tuple has some prefix
of interesting values, and an infinite number of trailing ⊥ (or ⊤) fields, not
printed for brevity.
`[Ctrl, 17]` is really: `[Ctrl, 17, ⊥, ⊥, ⊥, ....]`.
`[3.14, 17, ⊥:FP8]` is really `[3.14, 17, ⊥:FP8, ⊥, ⊥, ⊥, ....]`.
Their meet is then:
`[⊥, 17, ⊥, ⊥, ⊥, ....]` (which prints as: `[⊥, 17]`)
and our lattice is once again complete.
Tuples by themselves are not terribly interesting (or common in programming)
but we will shortly use them in building structs and functions.
[Lattice6](./docs/lattice_iffct.svg) <img src = ./docs/lattice_iffct.svg>
First appears in [Simple chapter10](https://github.com/SeaOfNodes/Simple/tree/main/chapter10)
along with structs and pointers.
## Structs
We implement *struct types* as a tuple with named fields:
`[myPi: 3.14, result: 17, someFP8: ⊥:FP8]`
The field ordering is not important (and indeed in many languages the compiler
is free to re-order fields to achieve a better packing density). For the
trailing infinitely many ⊥ fields, we can find all possible field names in the
infinite set.
Actually, to cut down the number of cases we need to handle, we define tuples
as *structs* with numbers for field names: `[0,1,2,3...]`
The actual implementation of structs might use hash tables to map unordered
field strings to types, or might use linear scan for small structs.
[Lattice7](./docs/lattice_iffcs.svg) <img src = ./docs/lattice_iffcs.svg>
Struct fields can have more properties than a name; easy examples include
`initialized` or not, `mutable` or not, `destructed` not. Adding some boolean
properties here does not fundamentally change how a field works; the field
*meet* operations will either AND or OR these properties according to the
desired semantics, as well as the normal *meet* on the field type.
I am not covering here how to initialize struct members, or the syntax for
declaring structs or traits - just the resulting types.
First appears in [Simple chapter10](https://github.com/SeaOfNodes/Simple/tree/main/chapter10)
along with tuples and pointers.
## Pointers
We implement typed pointers as a wrapper over a struct, with extra bits
for carrying more information depending on the level of precision desired.
[Lattice8](./docs/lattice_iffcsp.svg) <img src = ./docs/lattice_iffcsp.svg>
Tracking null pointers is easy this way, with a simple lattice `{ ⊤, ptr, ptr?, ⊥}` where the `ptr?`
indicates a pointer which may be null. We can make dereferencing a may-be-null pointer
safe with a runtime check and an upcast:
```
// Inferred type: *?[name:"Whiskers", makeSound:#1{}]
// Pronounced: pointer-to-a-nullable-Cat
val ptr = rand() ? null : Cat("Whiskers");
// null-check
if( ptr )
// hidden upcast to drop the null
ptr.makeSound() // Legal, because ptr cannot be null
```
The same code with a "possibly null pointer" error:
```
// Same as above:
// Inferred type: *?[name:"Whiskers", makeSound:#1{}]
// Pronounced: pointer-to-a-nullable-Cat
val ptr = rand() ? null : Cat("Whiskers");
// Illegal, because ptr might be null
ptr.makeSound() // TYPE ERROR!
```
Here the load to fetch the `makeSound` method fails during error reporting; in
Simple this is just a `LoadNode` which has an error check against its pointer
input being possibly `null`.
First appears in [Simple chapter10](https://github.com/SeaOfNodes/Simple/tree/main/chapter10),
along with structs and tuples.
### Equivalence Class Aliasing
The Sea-of-Nodes IR design uses the *equivalence class* model of aliasing,
where memory is broken up into *equivalence classes*. Pointers point into one
or another class; pointers in unrelated classes **never** alias, those in the
same class **always** alias.
This model works really well for strongly typed languages such as Java and
Simple. I believe it will work well for other strongly typed languages as
well, although I have not tried it. Obviously other models work as well. In
this case it is easy to add aliasing support in the type system: all fields
have an *alias number*, a unique indicator of which *equivalence class* they
belong too. Set of aliases are possible, often implemented with a bitvector.
In the above example with a struct `[myPi: 3.14, result: 17, someFP8: ⊥:FP8]`
memory is broken up into classes for the `myPi` fields, `result` fields and
`someFP8` fields, leading to at least 3 alias classes. Pointers to the `myPi`
fields will never alias with `result` fields because the field names are
different (also, the core types are different).
Pointer types may also carry information like `raw` or `unsafe`, or pointers
into e.g. GPU memory (probably as another alias class), or pointers to
uninitialized memory, or destructed memory or singletons.
## Functions
Functions have argument types and some return types (often just one return
type). We'll model them as an argument struct and a return struct (or type if
the language only allows 1 return).
Because of co-variance/contra-variance, the two structs are treated slightly
differently: the arguments will *joined* and the returns will be *meet*.
Example:
```java
var fcn = rand()
? fcn_widen_ascii_char:{u8 -> u16}
: fcn_narrow_wide_char:{u16 -> u8};
var some_ary = map(get_u8_ary(), fcn);
```
What is the type of the `fcn` variable? It is a function which takes either a
`u8` or `u16`... which means the best `map` can hope for is that `fcn` can only
be passed `u8`s, and not something in the `u16` range. Similarly for the
result, `map` might see `u16`s if `fcn_widen_ascii_char` is being called.
`fcn`'s type is the meet of `fcn_widen_ascii_char` and `fcn_narrow_wide_char`
which is
`fcn: {u8 -> u16}.meet( {u16 -> u8 } )`
`fcn: { u8.join(u16) -> u16.meet(u8) }`
`fcn: { u8 -> u16 }`
### Function Indices
There are a finite number of functions in any fixed program; it is useful to
name them (even if anonymous!) so the type system can reason about them.
The type for `fcn` might also include the functions it is built from,
so e.g. `fcn: [fcn_widen_ascii_char,fcn_narrow_wide_char]{u8 -> u16}`.
Collections of possible functions are easy to track as e.g. a simple bitvector,
with one bit for each function in the program. Other choices also work, such
as a sparse bitvector when only a few functions are possible from a large
program with many 1000's of functions. The most common function type is that
of a single function, i.e., just calling a function by name.
A *function index* is simply a unique small integer that refers to a function
(perhaps via array lookup), and can be efficiently tracked. We print them as
`[#fidxs]{signature -> return}` The above `fcn` is typed `[#2,3]{u8 -> u16}`,
indicating both function #2 and function #3 are included here.
The type system will track function collections *in general*, so the general
case is that the function type has a set of allowed functions, and might
include an indication of the infinite number of functions that might be found
in some external compilation unit.
The reason to track functions this way is that the type system will allow us to
build a reasonably accurate Call Graph: the set of function types arriving at a call
site dictates which functions might be called here. More on this in the [Call
Graph](#call-graph) section.
First appears in [Simple chapter18](https://github.com/SeaOfNodes/Simple/tree/main/chapter18).
## Type Sugar And All The Rest
From here on out, we will mostly be adding a small amount of *type sugar*
to the existing types to implement all the cool type features we want.
Methods, SubClassing, Structural-Typing, and Traits will all be handled via *type
sugar*. I *believe* (not directly implemented as of today), Conditional
Conformances will fall out as a case of structural-typing, which *is* handled here.
## Methods
In many languages, structs have *methods*, functions which take a `self` or
`this` extra argument from the struct itself.
```java
struct String {
String toUpperCase(/*hidden this argument */) { ... };
}
```
In this typing formulation, methods will be **fields** that happen to be
function-typed, or basically as type sugar over the existing types already
mentioned above.
The `toUpperCase` field here is **finally** assigned a function constant - and,
as with all final constant fields, the field value is the same for all
instances of `String` so does not need to be implemented as actually in the
`String` instances. As an implementation detail, these final field constants
can be moved over to some collection of such constant fields, e.g. a `class String`
instance.
From the typing systems point-of-view, the field is a full-fledged
member of the `String` type. E.g. some pseudo-definition of `String`:
```java
String = : { // String is a struct type...
toUpperCase // with a field called toUpperCase...
: [#1]{ String -> String }, // typed as a function from String to String
... // And probably has many more fields, elided here
};
```
Here is a method call `"abc".toUpperCase()` from the type systems'
point-of-view: There is a field lookup `toUpperCase`, which needs to be found
in the struct type for `"abc"`; that needs to return a no-arg (plus `self`)
function which will be called.
```
Code Type
"abc" : {toUpperCase:[#1]{String->String}, ...other fields}
.toUpperCase : [#1]{String->String} // Just the field contents, which is a function
("abc") String // Call fcn#1 with self
```
When is a special String type e.g. UTF8String *isa* String? Exactly when the *isa* call
returns `True`; when UTF8String has all the same fields as a `String` (and
probably has more).
This is the general way *methods* are handled: they are reduced to function-
typed fields in a struct, and then the existing *meet* and *join* operations
are well defined.
First appears in [Simple chapter23](https://github.com/SeaOfNodes/Simple/tree/main/chapter23).
## Subclassing
As alluded to in the prior section, sub-classing is a modest amount of *type
sugar* over the existing struct types. Here is a generic example with totally
made-up syntax:
```java
// An abstract Pet class
Pet = : {
name: String
makeSound: { Self -> String } = 0;// function pointer is null; this class is *abstract*
};
// Cat is a sub-class of Pet, and concretely defines `makeSound`
Cat = : Pet : {
makeSound = { self -> "meow" };
};
cat = Cat("Whiskers");
// Another sub-class of Pet
Dog = : Pet : {
makeSound = { self -> "bark" };
};
dog = Dog("Spot");
```
`Pet` is a name for a type `[name:String, makeSound:[#0]{Pet->String}]`. The
`makeSound` field is typed with a null function pointer, shown as `[#0]`.
`Cat` is a name for a type `[name:String, makeSound:[#1]{Pet->String}]`,
i.e. it is a `Pet` except the `makeSound` field is now typed as a concrete
function `[#1]`. Similarly, for `Dog` and concrete function `[#2]`
The instance variable `cat` is typed as `[name:"Whiskers",
makeSound:[#1]{Pet->String}]` which *isa* `Cat`, and similar for `dog` and
`Dog`.
`cat.isa(Cat)` and `Cat.isa(Pet)` and
`dog.isa(Dog)` and `Dog.isa(Pet)`.
Subclassing generally means supporting *virtual* calls, so here I will show a
typical virtual call execution strategy; other strategies are possible
(e.g. *inline caches* in some VMs). In this case, the optimized version of
methods means moving from the instance to the class, then doing the field
lookup to get a function... then calling it. This is the same sequence as done
by both C++ and Java.
`(rand() ? cat : dog).makeSound()`
An implementation in psuedo-asm:
```
rPet = ... # pick one of cat or dog
rClass = ld.ptr (rPet+#classOffset)
rSound = ld.ptr (rClass+#fieldOffsetMakeSound)
call (rSound)
```
Another issue is handling shadowing of fields; when a subclass adds a new field
with the same name as a prior one. The parser will need a "shadow-smart"
lookup mechanism, not discussed here. From the type systems point-of-view, we
would like to define a subclass as simply the normal (super-class) struct with
some extra fields. We can use name mangling to keep the fields apart as one
simple solution; there are other solutions such as stacked successive lookups
when checking on fields.
## Structural Typing
We already have structural typing (e.g. static duck-typing) with no further
ado. Here we make a struct type with a single field `quack`, and an infinite
number of `⊤` fields, meaning we don't care what the other fields are. This
type is more-or-less an *interface* or *trait* without a name.
```
{quack : { self -> String }, ⊤, ⊤, ⊤,,, }
```
The `quackathon` function checks that the caller passes in a struct with the
`quack` field:
```
quackathon = { arg: {quack : { self -> String }, ⊤, ⊤, ⊤,,, } ->
arg.quack();
}
```
Any argument type that matches that signature is allowed; the *type cast* used
to check the types just does a *meet* with the incoming argument; if the
incoming argument lacks a `quack` field, it will take the default `⊥` from the
argument, and will lack a `quack` field... which will type error at the field load.
```
Duck : Pet : {
makeSound = { self -> "quack" }
quack = makeSound();
};
StealthCow : Pet : {
makeSound = { self -> "moo" }
quack = { self -> "quack" }
};
```
Both of these types have a `quack` function-typed field, so both are legal
here. The requirement is strictly that the passed-in argument has the needed
signature (needs a function-typed `quack` field).
However if we try to call `dog.quack()`, we will attempt to lookup the `quack`
field in the `Dog` type. `Dog` does not explicity have such a field, and so we
get the default field type: `quack:⊥` which we then try to treat as a
function... and since `⊥` is not a function type we get a type error.
## Nominative Typing and Traits and Interfaces
Nominative typing is basically structural typing, plus a constraint that we
*also* match the type name. If you do not structurally type, you'll also fail
nominative typing, but not vice-versa. We add a little type sugar to force a
failure if the type names do not match, a hidden field with a funny name:
```java
Duck : Pet : {
$classDuck : 0; // hidden field with mangled class name
makeSound = { self -> "quack" }
quack = makeSound();
};
StealthCow : Pet : {
$classStealthCow : 0; // hidden field with mangled class name
makeSound = { self -> "moo" }
quack = { self -> "quack" }
};
```
And now we need to constrain our `quackathon` to only allow `Duck`s:
```
quackathon = { arg: Duck ->
*type cast check Duck*
arg.quack();
}
```
### Nominative Typing Error Example
This code works as expected (Donald enters the quackathon):
`quackathon(Duck("Donald"))`
and this fails:
`quackathon(StealthCow("Betsy"))`
because although `StealthCow` quacks like a duck, it does not have the required
`$classDuck` field; this field goes to `⊥` and the *type cast* will fail.
Let's expand on this fail to make it more explicit:
There is a *type cast* that the arguments are correct; it does a *isa* check
with the input and expected type, and if the the argument is not *isa*, will
flag a type error at the time of *type analysis* (i.e. constant propagation).
Unlike *structural typing*, where we require a `quack` field from the argument
via directly loading a `quack` field, here we require the `$classDuck` field.
The argument type to the second `quackathon` call is a `StealthCow` which
expands to:
```java
{
$classPet: 0; // StealthCow is also a Pet
$classDuck: ⊥; // All other fields are ⊥
$classStealthCow: 0;
name: "Betsy"; // StealthCows are Pets, which have a name
makeSound = #3{ self -> "moo" }; // function index #3
quack = #4{ self -> "quack" };
// all other fields are ⊥
...
}
```
The check is against a generic `Duck` which expands to:
```java
{
$classPet: 0; // Duck is also a Pet
$classDuck: 0; //
$classStealthCow: ⊥; // All other fields are ⊥
name: String; // Ducks are Pets which have a name
makeSound = #5{ self -> "quack" }; // function index #3
quack = #5{ self -> "quack" };
// all other fields are ⊥
...
}
```
The *isa* check is `(StealthCow meet Duck) == Duck`. The *meet* of
StealthCow and Duck is done field by field and results in:
```java
{
$classPet: 0; // Since both are Pets, this field remains
$classDuck: ⊥; // Not really a Duck, the meet lacks $classDuck
$classStealthCow: ⊥; // Not really a StealthCow, the meet lacks $classStealthCow
name: String; // Generic name from the generic Pet
makeSound = #[3,5]{ Pet -> String }; // function indices #3,5
quack = #[4,5]{ Pet -> "quack" }; // function indices #4,5
}
```
And since this is not equal to a `Duck` (the `$classDuck` field is
missing/default and not `0`) , the `isa` returns false, the *type cast* will
report an error.
### A Trait Example
Let's make a concrete working example with a trait `Equals`; here is the
type for `Equals`:
```java
Equals = : { // Equals is a struct (trait) with...
eq = { Self Self -> bool }; // With a field 'eq' typed as a function taking 2 selfs
⊤, ⊤, ⊤,,, // And the infinite unknown fields are ⊤ ... allowing anything
};
```
From the above example with `Duck`s and `Cow`s, we can see they are not `Equals` (will fail
the *isa* test) because they lack an `eq` function. No two Ducks are the same.
Lets make our `String`s equatable:
```java
String = : {
eq = { String String -> bool };
... // plus all the other String stuff
};
```
And Strings can be compared with in code which declares String:
```java
isHappy = { str : String -> str.eq("Happy") }
```
Although code using Pets fails here:
```java
isHappy = { pet : Pet -> pet.eq("Happy") }
ERROR: No field `eq` on type Pet
```
And if we reverse the calling arguments:
```java
isHappy = { pet : Pet -> "Happy".eq(pet) }
ERROR: function String.eq argument#1 must be a String and not a Pet
```
## Parametric Polymorphism
**TODO**...
This looks to me to be solved with **specialization** (cloning the analysis in
any work should work, function inlining or function specialization are both
ways to clone the analysis).
Type var with the intent it becomes concrete - treat as bot/top, maybe not
even need specialized name ?
Trigger inline/specialize based on having concrete args to a function w/ type
var (Parm with a default BOT type). After specialize, Parm with lift to the
concrete and problem solved...
Specialization will split FIDXs, will need to deal with that (keep precise
Call Graph)
## Conditional Conformance
"Conditional Conformance" is a fancy name for a fairly simple idea: if an
Element has a property, then a Collection of such Elements also has that
property, and if not then not. An example might be something that is
`Stringable`, then a collection of them is also `Stringable`, perhaps as
printing as `[elem0,elem1,...]`. Same for e.g. `Equatable` or `Hashable` or `Comparable`.
The Collection defines how to extend the base notion. So e.g. a collection of
`Comparable`s is itself comparable, and that comparison might either return an
element-wise collection of `Bool`s, or might compare lexigraphically and return a
single `Bool` instead of a collection.
However, the collection can also work with elements that are not `Comparable`,
in which case the entire collection is not `Comparable`... but it still
functions as a collection.
So, conditional on the element having a trait `T`, the entire collection also has trait `T`.
So how does that work here?
**TODO**...
Expand to remove polymorphism (above)
Structual typing gives the answer.
Can refine further.
|