Skip to content
← Back

src/syntax/process/generate-il/generate_il_statements.ghul

1
namespace Syntax.Process is
2
use System.Reflection.Metadata.ILOpCode
3
use IO.Std
4
5
use System.Text.StringBuilder
6
7
use Logging
8
use Trees
9
use Source
10
11
use IR
12
use IR.Values
13
14
use Ghul.Pipes
15
16
17
// Statement walking: statement and block lists, assertions, pragmas, labelled loops, break and continue.
18
partial GENERATE_IL is
19
pre(statement: Expressions.STATEMENT) -> bool is
20
super.pre(statement)
21
22
return false
23
si
24
25
visit(statement: Expressions.STATEMENT) is
26
si
27
28
// `val ... lav` emits as the IL contained in block.value
29
// (the BLOCK Value set by compile-expressions). The shape
30
// mirrors how `if`/`case`-in-expression emit:
31
//
32
// <body statements' IL>
33
// <tail value's IL> ; pushes value (or empty for void)
34
// <return-targeted return inside body>: push value; br end
35
// end: ; value (or empty) on the stack
36
//
37
// The natural fall-through emits the tail's value at the end
38
// of the body and falls into the label. Return-targeted
39
// returns push their value and `br` to the same label. No
40
// CLR local or frame field carries the value across the
41
// join — so an async/yield suspend point inside the body
42
// composes with the existing state-machine rewriting
43
// without bespoke handling.
44
pre(block: Trees.Expressions.VAL_BLOCK) -> bool is
45
super.pre(block)
46
47
if !block.value? \/ !isa Values.BLOCK(block.value) then
48
// No usable value from compile-expressions (a
49
// poisoned block, or a void-tolerant block whose
50
// BLOCK has been set with void type — both still
51
// need their body's IL to run for side effects).
52
// Fall back to a plain walk; returns inside will
53
// dispatch via val_block_target either way.
54
return false
55
fi
56
57
let value_block = cast Values.BLOCK(block.value)
58
59
// Capture-vs-spill decision: in a state machine whose
60
// body contains a suspend, the val-block's result
61
// routes through a frame field instead of being
62
// captured inline in value_block. Body IL (including
63
// suspend emission and val-targeted-return stores)
64
// flows to outer current_block; value_block ends up
65
// holding just a `ldfld` so consumers see a clean load.
66
let spiller = COMPOSITE_VALUE_SPILLER(
67
self,
68
block,
69
block.value,
70
_current_state_machine_frame()
71
)
72
73
spiller.enter()
74
75
try
76
let end_label = LABEL()
77
block.end_label = end_label
78
79
let result_type = value_block.type
80
let is_void = result_type.matches(_innate_symbol_lookup.get_void_type())
81
82
let frame_il = VAL_BLOCK_IL(
83
block,
84
end_label,
85
result_type,
86
is_void,
87
_loops.open_try_count,
88
value_block
89
)
90
frame_il.spill_field = spiller.spill_field
91
92
// Eagerly allocate the capture-mode cross-try TEMP
93
// so its declaration is encoded at the head of
94
// value_block rather than somewhere inside the
95
// body: lazy allocation mid-body would leave the
96
// post-body load of it reaching a slot that has
97
// not been declared yet. The TEMP is
98
// unused (and the join block un-emitted) when no
99
// val-targeted return crosses an inner try — set
100
// `cross_try_used` then drives the post-body
101
// emission. Spill mode doesn't need either: the
102
// frame field handles cross-try directly.
103
if !is_void /\ !spiller.is_spilling then
104
frame_il.cross_try_temp = IR.TEMP(value_block, "val_cross_try", value_block.type)
105
frame_il.cross_try_join_label = LABEL()
106
fi
107
108
_val_block_il_stack.add(frame_il)
109
110
try
111
for s in block.body.statements do
112
self.enter_node(s)
113
try
114
s.walk(self)
115
finally
116
self.leave_node(s)
117
yrt
118
od
119
120
// Natural fall-through. The tail statement is
121
// the last EXPRESSION in the body; in want_value
122
// mode visit(Statements.EXPRESSION) suppresses
123
// its own emission so the consuming context can
124
// emit the tail value. Val-block bypasses LIST,
125
// so we emit it here — the spiller picks spill-
126
// to-field or push-on-stack based on the mode
127
// chosen above. Val-targeted returns use the
128
// same emit_value path (see _gen_val_block_
129
// targeted_return), so all paths converge at
130
// end_label with the value on the stack (or in
131
// the spill field) consistently.
132
if let block.body.last?, last.value? then
133
// Coerce the fall-through value to the block's
134
// settled type so a value-type branch is boxed or
135
// wrapped (T → T?) and a genuine-null tail lowers
136
// to the optional's default — the same join the
137
// if / case paths apply to every arm value before
138
// spiller.emit_value.
139
let coerced =
140
if !is_void /\ !result_type.is_error then
141
_boxer.box_if_needed(value, result_type)
142
else
143
value
144
fi
145
146
spiller.emit_value(coerced)
147
fi
148
finally
149
assert _val_block_il_stack.count > 0 else "val_block_il_stack underflow"
150
_val_block_il_stack.remove_at(_val_block_il_stack.count - 1)
151
yrt
152
153
// Capture-mode cross-try paths stashed their value
154
// into cross_try_temp and `leave`d to cross_try_
155
// join_label. Skip over the join block on the
156
// natural path (tail value already on the stack)
157
// and emit it before end_label so the join paths
158
// ldloc the value back on and fall through. Only
159
// emit when the join was actually targeted —
160
// otherwise the unconditional ldloc would push an
161
// uninitialised value onto end_label's stack on
162
// divergent natural paths.
163
if frame_il.cross_try_used then
164
get_brancher_for_block().branch(end_label)
165
get_brancher_for_block().label(frame_il.cross_try_join_label!)
166
add(frame_il.cross_try_temp!.load())
167
fi
168
169
get_brancher_for_block().label(end_label)
170
finally
171
spiller.leave()
172
yrt
173
174
return true
175
si
176
177
visit(block: Trees.Expressions.VAL_BLOCK) is
178
si
179
180
// Emit the conditional-throw IL shared between statement-form
181
// `assert` and the expression-form `assert ... in expr`. Condition
182
// value is loaded by `brancher.branch`; on the failure path the
183
// message (or default condition-printout) is loaded, optionally
184
// wrapped in an AssertFailedException, and thrown. Falls through
185
// at the success label.
186
emit_assertion(
187
location: Source.LOCATION,
188
condition_value: IR.Values.Value,
189
message: Trees.Expressions.Expression?,
190
default_message_source: object
191
) is
192
let brancher = get_brancher_for_block()
193
194
let end = LABEL()
195
196
brancher.branch(BRANCH.NZ, condition_value, end)
197
198
let need_exception_wrapper mut = true
199
200
if message? then
201
let message_value = message.value
202
if !_innate_symbol_lookup.get_exception_type().is_assignable_from(message_value!.type!) then
203
add(
204
Literal.STRING(
205
"{location}: ",
206
_innate_symbol_lookup.get_string_type()
207
)
208
)
209
210
add(message_value!)
211
212
add(Values.CALL_STRING_CONCAT())
213
else
214
add(message_value!)
215
need_exception_wrapper = false
216
fi
217
else
218
add(
219
Literal.STRING(
220
"{location}: {default_message_source}",
221
_innate_symbol_lookup.get_string_type()
222
)
223
)
224
fi
225
226
if need_exception_wrapper then
227
add(Values.NEW_ASSERT_FAILED_EXCEPTION())
228
fi
229
230
add(Values.INSTRUCTION(ILOpCode.THROW))
231
232
brancher.label(end)
233
si
234
235
visit(`assert: Statements.ASSERT) is
236
super.visit(`assert)
237
238
emit_assertion(
239
`assert.location,
240
`assert.expression.value!,
241
`assert.message,
242
`assert.expression
243
)
244
si
245
246
pre(assert_in: Expressions.ASSERT_IN) -> bool is
247
super.pre(assert_in)
248
249
assert_in.condition.walk(self)
250
251
if assert_in.message? then
252
assert_in.message.walk(self)
253
fi
254
255
emit_assertion(
256
assert_in.location,
257
assert_in.condition.value!,
258
assert_in.message,
259
assert_in.condition
260
)
261
262
assert_in.expression.walk(self)
263
264
return true
265
si
266
267
visit(assert_in: Expressions.ASSERT_IN) is
268
assert_in.compile_expressions_state.value = assert_in.expression.value
269
si
270
271
pre(list: Statements.LIST) -> bool is
272
super.pre(list)
273
return true
274
si
275
276
visit(list: Statements.LIST) is
277
// BLOCK-with-suspend spill. When the LIST is in a value-
278
// required position (list.value non-null) AND we're in
279
// a state machine AND any statement (outside nested
280
// function literals) contains an `await` or `yield`, the
281
// captured-IL pattern would trap the suspend's `leave` /
282
// `ret` inside list.value's BLOCK; when the consumer
283
// later gens that BLOCK, the suspend replays wrapped in
284
// the consumer's stack setup (receiver on stack at leave
285
// is invalid IL; state lost across yield's MoveNext re-
286
// entry breaks the next stfld). Spill the BLOCK's value
287
// to a frame field: body IL (including the suspend)
288
// flows to outer current_block in the same flat MoveNext-
289
// body stream as the dispatcher; the BLOCK is rewritten
290
// to a load of the spill field. The recursive case (val-
291
// block inside `if` in expression position, etc.) works
292
// because each composite's spill produces a clean load
293
// for its enclosing composite.
294
//
295
// The `want_dispose` (RAII) path keeps the existing .try
296
// wrapping — its body must live inside the protected
297
// region.
298
if _try_spill_block_with_suspend(list) then
299
return
300
fi
301
302
if list.value? then
303
enter_block(cast IR.Values.BLOCK?(list.value)!)
304
fi
305
306
let return_type = current_function!.return_type
307
308
let return_needed: TEMP? mut
309
let return_value: TEMP? mut = _
310
let label: LOOP_LABELS? mut = _
311
312
let outer_try: LOOP_LABELS? mut = _
313
314
// A statement list leaves its tail value on the stack for
315
// whatever consumes the list - the function epilogue's
316
// `ret`, or an enclosing expression. A disposal region is
317
// left with `leave`, which empties the stack, so a tail
318
// inside one is stored here and loaded again once the
319
// region is behind it.
320
let tail_value: TEMP? mut = _
321
322
let brancher = get_brancher_for_block()
323
324
// A `.try` inside a state-machine body needs its own
325
// dispatch holder (so awaits/yields inside register here,
326
// keeping the `beq cold_resume_N` and the target label
327
// in the same protected region) plus a state-guarded
328
// finally body (skip dispose / user-cleanup on suspend
329
// `leave`). Both works the same way for let-use and
330
// user-written try; the helpers below no-op outside a
331
// state machine.
332
let dispatch_holder: ASYNC_DISPATCH_HOLDER? mut = null
333
334
// The disposal region's own extent. A disposed local is
335
// declared outside it, so that `finally` can still reach
336
// the local after an exception has left the block.
337
let dispose_extent = Values.TRY_EXTENT()
338
let dispose_region = Values.PROTECTED_REGION(dispose_extent, null)
339
340
if list.want_dispose then
341
(outer_try, return_needed, return_value) = get_exception_handler_temps()
342
343
for v in list.variables_to_dispose do
344
_declare_local(v)
345
od
346
347
label = _loops.enter_try(return_needed, return_value)
348
349
add(Values.TRY_START(dispose_extent))
350
351
dispatch_holder = _maybe_push_dispatch_holder()
352
fi
353
354
for s in list.statements do
355
enter_node(s)
356
try
357
s.walk(self)
358
finally
359
leave_node(s)
360
yrt
361
od
362
363
if list.value? then
364
if let list.last?, last.value? then
365
if list.want_dispose then
366
// The type the tail presents rather than the
367
// value's own: a tail of `null` has the null
368
// type, which no local can be declared with,
369
// and a body whose every contribution is null
370
// takes its type from what the function
371
// returns.
372
let block = cast IR.Values.BLOCK?(list.value)!
373
374
let tail_type =
375
if block.type.is_null /\ return_type? then
376
return_type
377
else
378
block.type
379
fi
380
381
tail_value = TEMP(current_block, "tail", tail_type)
382
383
tail_value.store(value)
384
else
385
add(value)
386
fi
387
fi
388
fi
389
390
if list.want_dispose then
391
let try_label = label!
392
393
_maybe_pop_dispatch_holder(dispatch_holder)
394
395
ensure_runtime_symbols_are_materialized()
396
397
try_label.is_in_finally = true
398
399
brancher.leave(try_label.start)
400
401
add(Values.TRY_END(dispose_extent))
402
add(Values.HANDLER_START(dispose_region))
403
404
let skip_dispose_label = _open_finally_state_guard()
405
406
let to_dispose = list.variables_to_dispose
407
408
let i mut = to_dispose.count - 1
409
410
while i >= 0 do
411
let v = to_dispose[i]
412
413
let skip_label: LABEL? mut = null
414
415
if !v.type!.is_value_type then
416
skip_label = LABEL()
417
418
// v.load rather than load_local_variable: a top-level
419
// `let use` promotes the variable to a field on the
420
// globals container, whose owner is not the current
421
// function, and load_local_variable would route it
422
// through the captured-value path. Both loads carry
423
// the variable's own location rather than the list's:
424
// the list starts before the declaration, which a
425
// top-level variable reads as use-before-declaration.
426
brancher.branch(BRANCH.Z, v.load(v.location, null, _symbol_loader), skip_label)
427
fi
428
429
let variable_value = v.load(v.location, null, _symbol_loader)
430
let dispose_call = _dispose.call(list.location, variable_value, System.Array.empty`[Value](), null, _function_caller)
431
432
add(dispose_call)
433
434
if skip_label? then
435
brancher.label(skip_label)
436
fi
437
438
i = i - 1
439
od
440
441
_close_finally_state_guard(skip_dispose_label)
442
443
brancher.label(try_label.middle)
444
445
add(Values.INSTRUCTION(ILOpCode.ENDFINALLY))
446
add(Values.HANDLER_END(dispose_region))
447
448
_loops.leave_loop()
449
450
gen_exception_handler_exit(outer_try, try_label, return_value)
451
452
if tail_value? then
453
add(tail_value.load())
454
fi
455
fi
456
457
if list.value? then
458
leave_block()
459
fi
460
si
461
462
pre(pragma: Statements.PRAGMA) -> bool is
463
process_pragma(pragma.pragma, true)
464
return false
465
si
466
467
visit(pragma: Statements.PRAGMA) is
468
process_pragma(pragma.pragma, false)
469
si
470
471
pre(labelled: Statements.LABELLED) -> bool is
472
super.pre(labelled)
473
474
return true
475
si
476
477
visit(labelled: Statements.LABELLED) is
478
_loops.next_name(labelled.label.name)
479
480
labelled.statement.walk(self)
481
482
super.visit(labelled)
483
si
484
485
visit(`break: Statements.BREAK) is
486
// A valued break delivers to the innermost *consuming* loop,
487
// skipping intermediate loops that are not expressions. A
488
// labelled break follows its resolved target; a bare one
489
// exits the innermost loop.
490
let target mut =
491
if let `break.expression? /\ !`break.resolved_target? then
492
_loops.find_value_loop()
493
elif let `break.resolved_target? then
494
_loops.find_by_node(`break.resolved_target!)
495
else
496
_loops.get_current_loop()
497
fi
498
499
if !target? then
500
// Reported at compile-expressions; nothing to emit.
501
return
502
fi
503
504
let brancher = get_brancher_for_block()
505
let expression = `break.expression
506
507
// A valued break delivers its value to the loop's
508
// convergence point (see _gen_loop_result). The three
509
// shapes mirror _gen_val_block_targeted_return: spill mode
510
// stores to the frame field; capture mode pushes and
511
// branches; capture crossing a `.try` stashes into the
512
// loop's cross-try TEMP and `leave`s to the join.
513
if
514
target.wants_value /\
515
expression? /\
516
expression.value?
517
then
518
let result_type = target.result_type!
519
520
let v =
521
if let value = expression.value then
522
_boxer.box_if_needed(value, result_type)
523
else
524
IR.Values.DEFAULT(result_type)
525
fi
526
527
if let spill_field = target.spill_field then
528
add(_build_frame_field_store(_current_state_machine_frame()!, spill_field, v))
529
530
if _loops.open_try_count > target.enclosing_try_count then
531
brancher.leave(target.result_label!)
532
else
533
brancher.branch(target.result_label!)
534
fi
535
elif _loops.open_try_count <= target.enclosing_try_count then
536
add(v)
537
brancher.branch(target.result_label!)
538
else
539
assert target.cross_try_temp? else "value-carrying loop missing cross-try TEMP"
540
assert target.cross_try_join? else "value-carrying loop missing cross-try join"
541
542
target.cross_try_used = true
543
target.cross_try_temp!.store(v)
544
brancher.leave(target.cross_try_join!)
545
fi
546
547
return
548
fi
549
550
// Bare break — including a valued one under a loop that
551
// consumes nothing, which compile-expressions has already
552
// reported. The natural-exit path yields absence.
553
if _loops.is_in_try then
554
brancher.leave(target.end)
555
else
556
brancher.branch(target.end)
557
fi
558
si
559
560
visit(`continue: Statements.CONTINUE) is
561
// Unresolved labels and loop-less continues were reported at
562
// compile-expressions; nothing user-facing is diagnosed here.
563
let target mut =
564
if let `continue.resolved_target? then
565
_loops.find_by_node(`continue.resolved_target!)
566
else
567
_loops.get_current_loop()
568
fi
569
570
if !target? then
571
return
572
fi
573
574
let brancher = get_brancher_for_block()
575
576
if _loops.is_in_try then
577
brancher.leave(target.start)
578
else
579
brancher.branch(target.start)
580
fi
581
si
582
583
si
584
si