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
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
|
- Short-term tasks:
- Test suite
- Rename
- Plan for initialization checks:
- Implement a tree based data flow analysis
Processing:
statements and expressions have after sets
Boolean expressions also have assigned-if-true assigned-if-false
sets.
Almost all statements and expressions are called with
a before set. The call is supposed to set.
The "after" set applies after an expression/statement has
been fully executed, including all its children. Note:
children means children. goto only has the label
expression as a child.
Ast nodes with multiple predecessors are treated
specially. They need to compute their before sets
themselves. When we reach them in the tree, they will
look at the after set of all their other predecessors and
combine those with the passed before set.
It's important that labels don't rely on their immediate
predecessor being stored in their list of predecessors
because in wacky cases like this:
while (<expr>)
@lll:
it may not be apparent which node is the predecessor. Or,
if you try to compute the control flow graph, you will
find that it's sucessor is some sub-expression of <expr>,
which then means it doesn't know what to do about if-true
and if-false assignments.
Nodes that transfer control (break, continue, goto,
return) must store an after set that is just a copy of
the before set, but then return a set that
is "everything". This is because the code that follows
immediateloy after such a node can safely dereference
variables that are not initialized on the path coming
from the transfer node. Because they will never be
reached from that node.
However, functions should ignore the before set because
their tree-based predecessor doesn't actually transfer
control.
Processing terminates once none of the after sets are
changing.
Code-wise, we can adopt the convention that a NULL set is
a full set - ie., intersect will just ignore it. Maybe
there should even be explicit support for such sets in
util.c
So a processing function will
- take a "before" set as a parameter
- store an "after" set that contains variables
that are initialized on all exits from the
node.
- expressions also store "initialized-if-true"
and "initialized-if-false" sets.
- return a set that contains initialized
variables on the path that falls through.
- For the vast majority of nodes, the "after" set
is also the one that should be returned, but
those that transfer control will generally
return a "full" while storing something else.
We will need to compute 'begin' and 'end' labels for
if/for/while/do/switch. This may mean we can get rid of
those for the nodes since a label will have its own label
node.
We are going to need a similar static analysis if we do the thing
where all values can be marked as errors. Also if we add
non-nullable pointers.
Note:
if ((value :+ obj.value) == null)
return;
These will need similar code, so some of it may be sharable.
- Generate code with explicit initialization
- Optimize dead stores away.
- The optimizer can't be too aggressive; it must not optimize so
much that the VM can't prove that variables really are
initialized.
At the moment we simply rely on the optimizer to turn the code
into something the VM can understand, but this is not viable long
term.
- Initialization check should probably be done after constant
propagation. Otherwise, things like
if (i == 0 && (k = 100) != 0)
{
print k;
}
will not be allowed. Also:
if (true)
k = 100;
print k;
That one might be a little more iffy. Or what about this one:
if (true && k = 10)
...;
Possibly && should do something special if the first expression is
constant and true. Similarly, || should do something if the first
is constant false.
Do we need to follow ECMA 334 here? Or can we get away with
maintaining the state of each variable at each node:
Assigned if stackpos=k is true
Assigned if stackpos=k is false
Assigned
Unknown
Doing this analysis on the graph gets too complicated because we
need to keep track of the truth values of the whole stack. A way
to do it may be to have two passes,
init_check_expressions()
init_checK_graph()
where init_check_expressions() is a tree walk that computes the
initialization state of variables for expressions. The graph pass then
issues INIT nodes, and the graph check does the data flow analysis.
A similar issue will come up with the non-null check. We need to
allow things like
x = null;
if (bah && (x = new Bah()))
{
/* dereference x */
}
Is there a way to generalize this "has property after true/false"?
Maybe expressions (e) could contain a list of subexpressions that
are evaluated if e is true/false. Then analysis for an if
statement could look like
analyze_true (expr)
{
only walk sub-expressions that are guaranteed to be
evaluated if expr is true.
}
analyze_false (expr)
{
only walk sub-expressions that are guaranteed to be
evaluated if expr is false
}
A possibly better thing to do might be to first generate the graph
without generating code for evaluating expressions; instead just
add them as separte "expression" nodes. [How about statement
expressions though? - in fact what exactly happens if a statement
expression has a goto in it?]
y = k + [[ goto label ]] x;
@label:
print y
Not very clear ... maybe it needs to go away and be replaced with
definition expressions.
There is actually a big problem here because expression *can* have
gotos in them:
B() -> int32 { goto blah; return 100; }
print 7 + B();
which will cause problems because the "7" is left on the stack even
as control goes elsewhere.
- At the moment we directly change the nodes in optimize.c, this should
be fixed with helper functions in graph.c (which should probably be
split out in node.c). Things we need
- delete nodes from the graph
- ability to insert nodes after a node
- ability to change nodes into something else
(this can probably be accomplished by
combining the two others)
What is it again that we need the ast's for? For nodes where we
need the types, we should just have the types. The ast's will make it
harder to copy nodes.
- Find out if we can avoid having to do ast_type_spec_resolve() all
over the place.
- Move much of the stuff in CALL into a new PROLOG node that every
function has. (There is a PROLOG node now).
* OBJECT TYPE CHECKING
- Proper type checking for object assignemnt
- Probably need a distinction between class types and object
types. The thing you put after "new" should have class type, but
instances would have object type.
- Also need to make a decision about non-nullable pointers.
- Binary ops that work on objects need to be updated in the interpreter.
- Generally, there is some thinking that needs to be done about
objects and type checking.
- Liveness:
We generate a bunch of temporaries when doing assignments. Those
should be eliminated since they will very often be dead.
- peep:
do
{
Propagation
- Copy propagation
- Constant propagation
Peephole optimization
liveness analysis
} while (changed);
where peephole optimization will replace dead stores with pop's
(among other things).
We'll probably just have to fix up the offsets for dead
variables. The graph stores pointers to definitions, so if we just
change the offsets in the ast, the graph will automatically pick
them up.
Basically, just run offsets() once more. Actually just run the
offsets pass after all the optimizations.
Of course, as an intra-procedural optimization we can't remove
variables that have wider scope than the function we are
optimizing. So variables need to know their enclosing definition,
and if that isn't the function we are optimizing, then we can't
declare them dead.
Peephole optimization:
node_t *peep_hole (node_t *node)
{
do {
node = do_peep (node, &changed);
while (changed);
if (node->next)
return peep_hole (node->next)
else
return node;
}
will take a node and optimize it. Labels should be ref counted,
and maintain a list of all their sources so that two adjacent
labels can be collapsed.
- For stuff like this:
a.x += <expr>
where <expr> doesn't change a, we will generate
load a t = a
store tmp t2 = t
load tmp t3 = t2
load_ind x t4 = t3.x
<expr> t5 = expr
plus
load tmp t6 = t2
store_ind x t6.x = t5
to eliminate tmp we will have to get the "load tmp" changed to
"load a" at which point we can prove that tmp is dead and
eliminate a lot of code. It should be reasonably easy to find out
that tmp is guaranteed to be equal to a, but how do we determine
which one to eliminate? Basically this is copy propagation. Ie.,
how can we be sure that s/load tmp/load a/ is actually making
progress? Because there is a termination function: the total
distance between loads and stores becomes smaller
- Variables that don't escape an activation record should be
allocated on the stack. Need to figure out just how the stack
is exposed (ie., is it 32 bits wide, can you get the address of it
what about 64 bit values? What about 128 bit values?)
- Move the reachability code into return-check.
Consider consolidating init-check and return check.
- Make ++ and -- syntactic sugar for +=. To make this work for
postfix inc, we will need a statement_expression which is a
statement followed by an expression. Ie.,
i++ => t = i; i = i + 1; t
and
++i => i = i + 1;
A problem with this is typechecking. The desugared version may be
accepted by the typechecker where the sugared version would
not. Is this really worth the trouble? They already work ...
(Adding temporary variables is a bit nasty since they are quite
expensive; we should at some point make sure that variables that
don't escape are allocated on the stack. This way we can actually
allocate entire activation records on the stack - note that the
outer pointer only escapes if an inner function is accessing
variables from outer->outer)
- This has interactions with the design of the VM since we want the
code executed by the interpreter to be close to what the VM would
run.
The VM should ideally be untyped with type annotations added on
top in their own segment. This is a lot more flexible for
high-level compilers, and means the garbage collector can be
written in bytecode.
However, some issues that a statically typed bytecode format takes
care of are now left to the high-level compiler. The most
important is "what is the size of pointers". Is it 32 bits or 64
bits? The HLL needs to know so it can compute layouts of
structures etc.
Making pointers 64 bit is more future-proof, but will also cause
bloat on both 32 and 64 bit architectures. And 4G should really be
enough for many people for a couple of years.
32 bit pointers will work well on 32 bit architectures such as
x86-32 and ARM. Even on 64 bit architectures, the overhead from
converting a 32 bit value to a 64 pointer is not too bad,
particularly if we can get away with mapping the heap in the
lowest 4G of the address space. If we can get the heap 4G aligned,
then converting is just a matter of or'ing in a fixed set of bits
on top of the 32 bits. The stack may be a bit of a problem since
unaligned access is so expensive. So what will happen if you push
a 64 bit integer on a 32 bit stack? Maybe just suck it up and do
two reads. Hot code will be jit-compiled anyway and we should be
able to allocate 64 bit integers in registers there.
Or we could say that the stack is 64 bits while keeping pointers
32. Or we could say that everything is 64 bits.
Alternative approaches:
- Statically typed bytecode.
- Adding a ptrsize bytecode. The idea here would be
to dynamically compute the pointer size every time
it was needed. This is less crazy than it sounds
since
(a) functions that return the size of
particular structure can be generated, so
we don't get too much code bloat.
(b) Such functions would be inlined and
reduced to constants by the jit compiler.
Of course it is also necessary to decide what size floats
are. Probably only 64 bits - single precision can be added
Explicitly allowing 32 bit aligned 64 bit access is probably the
best.
The VM will need to know what types it should use. Do we add the
floating point operations or what? If the VM can't statically
check anything, then yes, we do need typed ops.
- Maybe split out the graph stuff into node.c
- node.c has utilities
- graph.c builds the graph
- flow-check.c checks initialization and return
(and later null use).
- typedefs
- types can be arbitrary identifiers
- arbitrary identifiers can be shorthands for other types.
type int_func = fn int32 -> int32;
- Expressions with goto and the spaghetti stack is now done. However,
if we ever add support for statement expressions, we may need to
make it so that goto always sets the stack to NULL.
- Calling convention:
/* On top of the stack is the closure, followed by all
* the arguments
*
* What we need to do:
*
* - find out what function we should be running
* - allocate an environment
* - copy stuff from the stack to the environment
* - push the current environment
* - push the 'return address'
*
* Should 'current env' and 'return address' be stored in
* the new environment? If the new environment becomes a
* closure, then it will be wasted space; on the other
* hand it avoids annoying stack manipulation with the
* return value.
*
* Of course, the return value could also be stored in
* a (64 bit) register. For struct returns, the convention
* could be that the return register would be initialized to
* a pointer to the location before the call.
*
* The call sequence would then look like this (for a something
* that returns a struct):
*
* push size of retval
* push RV
* rv = sp // stack grows down [1]
* push args
* push function
* call
* pop args
* pop rv
* <return value is now top of stack>
*
*
* [1] We may not want to make the assumption that the stack is
* addressable. If we do, we may not want to assume that the
* stack grows down. Although we will almost certainly need the
* ability to take the address of variables on the stack anyway, eg.
* for implementing C.
*
* It is important that bytecode behaves identically on different
* types of machines. (Ie., if it breaks on obscure architectures,
* then it should also break on x86, so that we don't get architecture
* specific bugs). This probably requires that the stack direction
* is fixed in one direction. Can this be implemented efficiently
* on upwards machines?
*
*/
- Unreachable code. What happens is that the "goto done"
after the else-part of an if statement can be unreachable if the
else part ends with a goto. There are some issues though. In this
code:
for (i = 0; i < n; ++i)
{
break;
}
++i is unreachable. Should we report this? gcc does actually
report it. What about when it's empty:
for (i = 0; i < n ; )
{
break;
}
internally we generate a "true" expression there, which would be
reported as unreachable. In this case:
if (true)
{
}
else
{
return;
}
The "goto done" statement can never be executed since it follows
the return. How do we prevent that from triggering reports?
Similar issues may arise basically anywhere we generate code that
follows a user-provided goto.
Can we separate the nodes into 'user generated' and 'compiler
generated' and only report the user-generated ones? We probably
can, but we also need to mark compiler generated expressions as
such so that the corresponding nodes can get marked correctly.
A possibility may be to mark some nodes as "should be reached",
then if they are not reached, complain. Basically, the first node
of each statement and expression would me marked this way.
Unreachable nodes should probably be complained about only if they
are preceded by a reachable node. That should prevent most
cascading reports. Or, when a node has been reported unreachable,
run the reachability algorithm on it, then continue to the next
node.
There is no "unreachable" field in program anymore. If/when we
reinstate this check, just use ast_enclosing() and store a list of
all nodes, reachable or not, then do a pass and complain about the
unreachable ones.
- Explicit tail calls.
In some cases it is useful to rely on tail call optimizations for
correctness (ie., it won't overflow the stack). Maybe having the ability
to say
goto some_function (x, y, z);
that would jump to that function and return immediately afterward. It
would be semantically equivalent to
return some_function (x, y, z);
but the language would guarantee that there is no overhead
associated with the call. It could make threaded interpreters
possible if some_function could be a function variable.
Could this be generalized to allow coroutines?
- We probably should have local type inference. C# 3.0 has some good stuff,
and it definitely would make lambda expressions nicer to look at.
- As far as I can tell, in C# 3.0 the only thing you can do to a
lambda expressions is to convert it to a delegate type; ie.
you can't directly apply it. This simplifies type inference
since there is always a list of parameter types. Ie., you
don't need to infer the type of the body of the expression.
- Do we also want a "let" statement? Probably not, it's
equivalent to
{
x: var = new Whatever();
}
Though:
let (cr <- drawable.get_cairo())
{
cr.move_to();
cr.rectangle();
cr....
}
has some appeal.
Also, if we have non-nullable types, accessing fields requires
something like it:
if ((cr := drawable.get_cairo()) != null)
{
...;
}
- Maybe "var" should just be empty, like
x: = new Whatever();
Which could also be written as
x := new Whatever();
In for loops:
for (i := 0; i < 100; ++i)
{
}
Maybe a bit too cute though.
In foreach loops:
foreach (e: in employees)
{
}
"<-" is also a possibility as "type inferred declaration".
for (i <- 0; i < 100; ++i)
{
}
- Implementation of type inference may be as simple as simply
passing a "inference target type" to each type checker function.
Then whenever a type needs to be inferred, we know what type
to shoot for. If the type is NULL, then type inference can't
work at that particular point.
For example a call expression would not be able to generate
a target type, which means inferred lambda expressions can't
be called directly.
Assignments and coercions would generally generate an
inferable context.
- It is desirable to have inferred types even without
initialization. Ie., sometimes it's just not very convenient
to have to come up with an initializer, especially if the type
is not nullable.
Maybe all assignments to that variable could be looked at, and
the best possible type selected. Example:
x: ;
if (something())
{
x = new Fish();
}
else
{
x = null;
}
would ideally cause x to get the inferred type Fish?.
So, for assignment expressions:
- if left hand doesn't have a type, set it to
the type of the right hand, but marked with "inferred".
- if left hand has an inferred type T, and the type of the
right hand is NullType, then infer the type "T?"
The problem with this approach is that sometimes this will
cause us to infer a too specific type
x = new Fish();
print x; // ok since x is non-nullable
x = null; // ok, since the type of x is infered
print x; // not ok since x is now nullable
On the other hand this is how the flow analysis is supposed
to work anyway. Basically, nullable and non-nullable types
should be indistinguishable wrt. to the type checker.
So inferring should just always produce a nullable type; the
flow analyzer can sort things out later.
Maybe nullability should only be a question for interface
types. Ie. an interface can declare that some pointer can't
be null, but local variables can always be null. Field
variables and arrays are probably always nullable as a flow
analysis is going to have a hard time proving them non-null
across unrelated method calls. (But then, when are you
allowed to dereference them?)
Possibly nullable fields should never be inferred as
non-null. Instead the user can just use a local variable. If
we allow them in if statements it's not that horrible. Ie.,
do
if (tmp <- obj.a)
bah (tmp);
instead of
if (obj.a)
bah (obj.a);
Keep in mind that the code between checking and calling the
function really *could* change obj.a, so we are potentially
preventing real bugs.
We'd also want
if (t1 <- obj.a && t2 <- obj.b && t3 <- obj.c)
bah (t1, t2, t3);
In some cases we'd probably want to also check that obj is
non-null:
if (o <- obj && t <- o.a)
bah (t);
of course if obj is a local, then this is fine:
if (obj && t <- obj.a)
bah (t);
When checking a long chain: o1.o2.o3.o4.o5.a:
if (o1 && o1.o2 && o1.o2.o3 && o1.o2.o3.o4 && o1.o2.o3.o4.o5 && o1.o2.o3.o4.o5.a)
{
bah (o1.o2.o3.o4.o5.a);
}
it was pretty stupid in the first place, but it highlights
the need for non-null field variables, with the associated
issue of initialization. We will probably have to prevent
constructors from calling virtual methods in "this".
With the locals, it looks like this:
if (o <- o1 && o <- o.o2 && o <- o.o3 && o <- o.o4 && o <- o.o5 && a <- o.a)
{
bah (a);
}
If we allow the same variable to be declared multiple times.
Maybe there should be syntaxtic sugar for checking a chain
of nullable fields:
if (o <- o1&.o2&.o3&.o4&.o5 && a <- o.a)
bah (a);
In foreach:
get_files (...) -> array[string!]!
{
}
foreach (s <- get_files())
{
bah (s);
}
- Dependent types.
Integers that are confined to the size of an array? Arrays can
never be dereferenced by an integer that is not guaranteed to fit
within it?
- Can we get to the point where runtime exceptions *never* occur?
- Division by zero?
- Out of memory?
- Integers
Maybe it's worthwhile to have bigints built into the language (as
opposed to the library). That way type promotion for integers
becomes really simple: Just promote to the closest type that
contains both.
- Precedence:
At the moment Oort inherits the broken precedence from C where ==
and >= have higher precedence than &, | and ^.
This means that
a == b & c
will be parsed as (a == b) & c, which can never be legal since
(a == b) is a bool. Hence changing the precedence would
not cause any confusion.
It is probably a bad idea to make &, | and ^ have a higher
precedence than >> and << since a << b & c is more usefully parsed
as (a << b) & c.
- Test suite
- Sane error, warning, and debug output.
- Maybe s/type/kind/g whenever type means "type of ast node", to
prevent confusion with type_specs. Maybe also rename kind ->
st_kind/ex_kind
- in the type checker, consider inserting cast nodes for expressions
where widening is going to take place. This should reduce the amount
of runtime type checking in the interpreter.
- Consider adding new binary ops PLUS_DOUBLE, PLUS_INT etc. That will
make the interpreter and code generator simpler. Then again maybe
it's not worth it.
- How to deal with errors in the parser. One possibility is to mark
tokens that have successfully been parsed, then if parsing fails,
report the first token without such a mark.
- for-, while- and if-statements should allow declarations, at least
in the first expression
Note that
for (a; b; c) d;
is *not* equivalent to
a; while (b) { d; c; }
since if d contains a "continue" (which we will want to support at
some point), c will not be executed in the while statement, but it
will in the for statement.
However, I don't see any reason "a" could not be treated taken out
and the for expression reduced to two expressions. Ie.,
for (i: int32; i < 20; ++i)
;
would become
{
i: int32;
for (; i < 20; ++i)
{
;
}
}
- A less stupid name?
- Zinc? already used for some other software (see zinc.com)
- Boron? sounds like an insult, which is good. boron.com is
available. People could take other insults and replace the
first letter with b: bool, bucktard, bimbecile, bimbo, bretin.
- "is" and "as"
Lifted from C#.
- Much casting happens as a result of parsing binary streams. Can we
add language support to help with that?
uint32 x = b1 + b2 * 256 + b3 * 256 * 256 + b4.
uint32 y;
int i;
bparser
{
x: uint32;
p: enum { X, Y, Z };
align 32;
y: array[struct bah] (x);
switch (p)
{
case Y:
uint32: pp;
uint32: xx;
uint16: x;
uint16: z;
{ little_endian;
}
bparser LE
{
for (i = 0; i < x; ++i)
{
uint y;
if (x > 100)
{
uint32 y;
}
else
{
int32 z;
}
}
DONE:
* Expressions with goto in them
A simple example is
F() { goto blah; }
print 7 + F();
@blah:;
where we jump to blah, leaving 7 on the stack.
There is another case, captured in popbug3.nl, where we keep
returning from the same function. Each time the result is added to
something popped from the stack. This results in a stack underflow.
Part of the solution will have to be to keep a reference to the
expression stack. This of course implies that popping the stack can't
actually remove the stack entries.
A different problem is if the stack references a variable. Should
that variable be evaluated every time? My intuition says no. Consider
ret: label = null;
x = 0;
F() { ret = back; @back: return 10; }
print x + F();
x++;
if (x < 100)
goto ret;
if you do this, F() will return 100 times, but the evaluation of
the 'x' in 'x + F()' will be skipped every time.
It seems the expression stack will have to become a tree with heap
allocated entries. An environment will then contain a pointer to a
node of this tree.
What does this mean for the bytecode? For example, an ADD operation
should now take the two values on top of the stack and create a new
value whose 'next' pointer is the same as the next pointer of the
second value on the stack.
Should this be done at the bytecode level? It may be that there
should be separate 'dynamic' versions of all the stack ops. These
might operate on a leaf pointer on the stack.
In the case where we can detect that an expression doesn't escape,
the array based stack could then be used. Is detection as simple as
there being no function calls in the expression? And no
statement-expressions, presumably. Not sure. This will have to be
considered an optimization.
With these changes, will it be possible to move the return address
back onto the 'stack'? If so, the contents of an environment will
look like this:
env_t *outer;
env_t *prev;
stack_t *stack;
[variables]
A label consists of a code pointer, an environment, and a stack
pointer. Jumping to a label consists of activating the
environment, resetting the stack pointer, and then going to the
code pointer.
Is is in fact going to be the case that a label will *always* have
a stack height of zero? Then jumping to a label just needs to set
the stack pointer to NULL. Jumping into the middle of an expression
that hasn't been activated seems ill-defined, which would indicate
that the (expression) stack height is indeed NULL. (This probably
then precludes having the return pointer stored on the stack).
Preliminary conclusions:
- expression stack must become a heap allocated tree
- there is a global current_stack
- environments need to contain
- outer_env [pointer to containing environment]
- prev_env [the environment to restore after return]
- return address [where to jump when returning]
- return stack [stack to restore when returning]
= Expressions:
- Operate on current stack
= Call:
- Allocate new env
- Store current env as prev_env
- Store closure env as outer_env
- Store current stack as prev_stack
- Set stack pointer to NULL
- Set return address to current address
- Jump to function
= Returning:
- push the return value on top of prev_stack
- restore outer environment
- set stack to prev_stack
- jump to return address
= Generating a label:
- push env and code pointer
= Goto:
- restore env
- set expression stack pointer to NULL
- goto code pointer
- consolidate while and for into a new "loop" that takes two expressions,
a test, and an increment. then
for (e1; e2; e3) s; => e1; loop (e2; e3) s;
while (e1) s; => loop (e1; true) s;
break exits the loop, and continue evaluates the increment
before testing.
Unfortunately do/while doesn't fit here. Probably still worth
doing though. hmm
do s while e; => s while (e) s =>
goto l; while (e) l: s => goto l; loop (e, true) l: s;
requires a jump into a loop though.
Can we use do/while for everything?
while (e) s; => do { goto l; s; l: } while (e);
Or a new construction:
loop (e, inc) s;
which executes s, then tests e, then if e is true, incs, and restarts.
do s while (e) => loop (e, true) s;
for (e1;e2;e3) s => e1; goto l; loop (e2; e3) { s; l:; }
while (e) s; => goto l; loop (e; true) { s; l:; }
* Expression variables
- Make sure variables defined in an expression survive
- Add scope expression whose only purpose is to limit scope of
variables, but still allow values to survive:
struct scope_expression
{
symbol_table *table;
expression_t *expr;
}
-
- see if the offsets pass can't be done with a generic tree walk
- possibly, but there are some subtle considerations. Not
worth it.
- We produce false reports of uninitialized variables at the
moment. This is because the list_predecessors() doesn't list
predecessors for the callee nodes. The solution here may be to
have separate prolog and fun-ref nodes.
Or maybe better, just somehow special-case it in the
init-check. Do we need the feature elsewhere?
No, the separate nodes are better. We can delete them in the
optimize path so they don't interfere with the peephole patterns.
- Remove nop is crack at the moment
Removing nodes is horribly complicated because the successor and
predecessor arrays have to be updated. Think it through.
One possibility is to completely ignore those arrays, and
keep a separate ref count for labels. That's pretty simple.
There is also the "unreachable" node in program which is
annoying as it keeps things alive. Also the recursive
remove_nops() is too complicated. Just do a simple for-loop and
with a saved next pointer.
Maybe succ/pred could be confined to init-check? We need
it for breadth-first though.
It's annoying that we lose program->exit/program->unreachable,
even though it's not actually harmful.
optimizing should actually work reasonably now.
- allow constant expressions in case statements.
- Should be done by splitting the expression evaluator out
from the interpreter. This will also be useful for the
peephole optimizer
- Find out exactly when identifier types should get resolved. Note
that they are needed as part of argument lists. Note defs.nl
crashes. (Yes, they are needed as part of argument lists, but in
the gathering phase (pass1) they don't have to be resolved. So we
can do it as a pass between pass1 and pass2).
- A simpler solution might be to just have a prepare_lvalue(expr) that
would generate the necessary temporary variables and return an expr
making use of it. Eg.
birnan().i += 200
For a compound assignment like this we first make a statement:
ast_statement_t *statement;
ast_expression_t *safe_expr;
prepare_lvalue (expr, &statement, &expr);
return ast_expression_new_statement
(statement,
expression_new_assign (
safe_expr,
ast_new_binop (PLUS, safe_expr, right)));
One potential problem is that the temporary variable would have to
have its type inferred as its not available at parse time. So
local type inference may be a prerequisite. Or we could add a "get
your type from this expression" type. (That would be sufficient
for this problem, but possibly not for local type inference where
the expression to get the type from is not necessarily known at
parse time).
However,
liveness analysis is easiest to do on the graph, but eliminating
variables will affect the offsets which are calculated before the
code is generated. (Actually, we could split offsets into 'levels'
and 'offsets') The only thing we copy into the nodes are the 'levels'.
Or instead of storing levels, we could store expressions ... (done)
- Find out how to generate code for dot expressions as lvalues.
There is the beginning of this now. Remaining to do still:
- Generate the correct types for temporaries (currently
hardcoded to Link).
This will require a new "Take the type from this
expression" type. Or maybe the expression in question
should have a pointer to the variable to whom it is
supposed to donate its type.
- Add a new store_ind bytecode similar to the load_ind, and
make the various lvalue expressions use it.
Issues:
To leave a copy of the value on the stack, we will have to duplicate it somehow.
Ie.,
tmp.x = <expr2>
<expr2>
dup
tmp
store_ind
we can do this safely since we know the evaulation of "tmp" will not have any side
effects, nor will the value of tmp be affected by <expr2>.
Maybe add a new ast_lvalue_t node, then have the parser or the
prepare pass convert expressions into lvalues as required.
An lvalue could contain a statement to be executed first. Ie.,
<complicated expression>.id
would be converted to
statement_t s;
union { dot, variable }
generating code for an assignment would then be
lvalue.code (code for s);
lvalue.load (just treat the dot/variable normally)
lvalue.store
birnan().i += j => lvalue (t = birnan(), t.i) =
- Add null value (and probably null type too).
- Insert init nodes for class variables (just don't complain about fields)
- Implementing methods as closures. The important observation is that for a method,
the 'this' pointer is actually just the outer pointer. So for this case:
blah.foo(a, b, c);
we need to generate a closure with "blah" as the environment and "foo" as the
function. So object layout has to be with "outer" as the first pointer, and
vtable as the second.one.
(Of course for objects without an outer, we can omit it, with a
bit extra complexity in other places).
- Make the interpreter graph (and stack-) based
- should be faster
- can support gotos
- closer to be able to generate machine code.
Nodes should probably get types:
- IF
- BINOP
- UNOP
- LABEL
- GOTO
- LOAD_ID
- etc.
with statically determined successors. LOAD_ID would have a successor
called 'entry', that would be used in the init-check. It may also
be worthwhile to say that LABEL nodes are the only ones that can
have more than one sucessor. And IF nodes are the only ones that can
have more than one successor.
- Consider making the compound-assignment operators real ast nodes. We
would then only have to compute the left-hand once. The current
sugar scheme may even be incorrect according to the java spec.
- Find out why reachability broke (unreach2.nl)
- Make a new ast_is (common_type, specific_type) so we can write
if (ast_is (AST_EXPRESSION, AST_IDENTIFIER_EXPRESSION))
...;
- add a child list to ast_common.
Should make it possible to delete a ton of tree-walking code.
- Make sure the function pass walks all of the tree
- Make graph.c use "next" nodes
- 'next' nodes are automatically added as successors, except for
return and goto nodes.
- Check that the graph passes (return and init-check) work
- Fix assign compounds. Currently the lvalue is evaluated twice.
Eg.
blah().x += 100
will cause blah() to be called twice.
The solution is probably to just have a new assign_compound_expression_t
with a binary operator.
- Return check is completely broken at the moment, since
- it doesn't recurse into expressions, so lambda functions
do not get checked
- it ignores gotos and labels.
- it should probably be graph based
- "If exit has a predecessor which is reachable and
not a return, then complain" should be right.
- Need list of functions. Maybe just add a new pass that
gathers various misc stuff?
- Is there any good reason that labels have a 'child' statment?
- don't think so.
- Add initialization for variables
x: int32 = 30; can be syntactic sugar for x: int32; x = 30;
x, y: int32 = 40, 50; can be syntactic sugar for
x: int32; x = 40; y: int32; y = 50;
which will also prevent weird stuff such as x, y: int32 = y, x;
- Split unary expressions into their own ast_unary_expression_t type
- allow function valued variables
- will require full closure support. A closure is a pointer to
an ast_function_t and a pointer to an environment. When the
closure is invoked, a new environment for the function is
created and the 'outer' pointer is set to saved environment.
Then the function body is interpreted.
- "bool" or "boolean"?
"bool" is fine.
- get rid of crackrock ast_statement_definition_t node
- allow nested functions
- should hopefully be fairly simple with the environment
level code above.
- allow mutual recursion
- symbol checker needs to be two-pass:
- first gather all functions names
- then look at the function bodies
- type checker needs to be two-pass
- first generate the types of all functions
- then type check the function bodies
- allow function definitions to be mixed with
global variables
To do this a new ast node that can contain a function or a statement
will be needed. And classes, when those are added. When things
can be nested, this thing will be allowed everywhere a statement
is allowed. Maybe call it a definition, and have
definition -> declaration
-> function
-> class
[->statement]
statement [-> definition]
-> ...;
which would also allow us to get rid of the symbol_t type and just store a
pointer to a definition.
- requires two-level environments, so the offsets pass should
compute the env level. The interpreter should chase the 'outer'
pointers as necessary.
Declarations will store the level on which they were declared,
and either
- the interpreter can keep track of the current level
or
- each identifier can know the level on which it is used.
The second one preferable. While it uses more memory, it is the
correct thing to do in a compiler.
Based on this information the interpreter can track env pointers
until it is at the right level.
- Return checking is broken at the moment
- it doesn't detect if part1 of a compound statement has
the required return in it.
Should split flow.c into several files.
- First class labels
Is this actually that complicated to do? A first class label would
be a code pointer and an environment. When you goto such a label,
the environment is restored and control is transferred to the code
pointer.
This subsumes coroutines and continuations.
- Flow check
- Required initialization:
- Zeroth, compute a list of all function definitions in the program -
maybe as part of the parser.
- First, for every function body compute a list of
uninitialized outer variables. That is, variables that are
- defined outside the function
- used in the function without being initialized by the
function body (disregarding any further nested functions)
- Second, for every function compute a list of other functions that it
references. Essentially walk the identifiers used in its body, and
list those that refer to functions.
- Third, walk the tree and do normal initialization checks. At every
point in the program maintain a table of uninitialized variables.
Every time a function name is referenced
- Walk the graph of referenced functions from that function
- For every referenced function, if any variables are in both
the list of uninitialized variables in the function and in
the maintained table of uninitialized, report an error.
Every time a variable name is referenced
- Just check that it is not in the table of uninitialized variables.
Figure out how required local variable initialization interacts with
nested function. For example:
foo -> int32
{
x: int32;
bar();
x = 100;
bar -> int32
{
return x; /* use of initialized variable
* that we don't catch
*/
}
}
With closures this could get more complicated. Can we require that a
function is not used for _anything_ until it is known that all
variables it references are initialized? Or do we need to bite the
bullet and initialize locals to 0?
The conservative check should be fairly easy to do:
- Maintain table of initialized variables as we do now
- Whenever the name of a function is used (for anything) check
the function body for references to uninitialized variables.
won't work due to recursion. Maybe this:
- For every function body generate a list of variables
referenced
- Do this transitively, so if a function references other
functions, those variables are also included. Note that
functions defined in the function itself should not be
included.
- Then, maintain table of initialized variables. When a function
name is used check the table against the list of variables.
Actually the function may initialize the variable
itself. If it does that, then it's always okay for it to use
it. So the list of variables should be those that it uses
uninitialized.
- Rethink scope_table_t and what it can be used for.
We are sort of abusing it in init-check.c, and it was always a bit
iffy the way scope_table_insert() just assumes symbol table semantics.
Maybe the init table should just be its own data structure and
scope_table folded back into symbol.c
- Simplify graph.c to walk the parent chains instead of
passing a ton of nodes around. unreachable should be
stored in program_t.
- There is a bug in the optimizer. callcc.nl produces different results
depending on whether it's run with optimization turned on.
- Fix crasher.nl (actually this was just a bug in the program,
generating NULL pointer exceptions is really needed)
|