Skip to content
← Back

src/syntax/process/generate-il/generate_il_generators.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
// Generator state machines: the outer-method stub, the MoveNext body walk and the deferred
18
// state-machine class emission.
19
partial GENERATE_IL is
20
// Generator IL emission:
21
//
22
// 1. The outer user-facing method gets a stub body
23
// (`newobj $StateMachine.ctor(); ret`) emitted inline,
24
// so the AST visit order is unchanged for the caller.
25
// 2. The function body is walked here (recursively, via
26
// self) into a fresh IR.Values.BLOCK that represents
27
// MoveNext's body. Yield statements emit their state-
28
// machine IL into this block as they are walked.
29
// 3. The state-machine class itself is deferred — captured
30
// in `_pending_state_machines` and emitted at the SAME
31
// level as the user's class (after `visit(`class)`
32
// closes the user class), since CIL doesn't allow
33
// classes nested inside method bodies.
34
_pre_generator_function(function: Definitions.FUNCTION, symbol: Semantic.Symbols.Function, state_machine: Semantic.Symbols.STATE_MACHINE) -> bool is
35
let frame = state_machine.frame
36
37
if !frame? then
38
_logger.error(function.location, "generator must return Pipe[T]")
39
40
return true
41
fi
42
43
// Realise the frame's symbol-level declarations (fields
44
// + ctor) once. Required for both the outer method's
45
// newobj reference and the state-machine class itself.
46
frame.declare()
47
48
// --- Outer user-facing method ---
49
//
50
// Forward this method's own arguments, construct the
51
// frame, return it.
52
_emit_generator_outer_method_srm(symbol, frame, state_machine)
53
54
// --- Walk the body into MoveNext's block ---
55
//
56
// The block lands captured on PENDING_STATE_MACHINE
57
// alongside the state machine; `_emit_pending_state_
58
// machines` writes it out inside the class definition
59
// once the enclosing user class has closed.
60
//
61
// For a generic generator, move each function-T symbol to
62
// a class-level position on the frame, so any IR.Values
63
// inside MoveNext that baked in function-T (callvirt owner
64
// types on the for-each dispatch, locals init for the
65
// iterator current, etc.) reach `!N` — MoveNext has no
66
// type parameters of its own — rather than `!!N`.
67
state_machine.install_body_emission_overrides()
68
69
enter_scope(function)
70
71
let move_next_block = Values.BLOCK()
72
73
enter_block(move_next_block)
74
75
// `.maxstack` must precede any IL opcodes in the
76
// method body. Add it before the dispatch placeholder
77
// so the populated dispatch IL lands after the
78
// directive at the start of MoveNext.
79
add(Values.MAX_STACK(64))
80
81
// Cache the state field to a CLR local at MoveNext
82
// entry. Every yield keeps it in sync via `dup; stloc;
83
// stfld` so per-region dispatches and state-guarded
84
// finally bodies read a stable value. Same pattern as
85
// the async path — needed to allow yield inside `.try`
86
// without branching into a protected region from
87
// outside (ECMA-335 forbids that, and the previous
88
// centralized-dispatch shape had exactly that bug).
89
let outer_dispatch_holder =
90
_open_state_machine_entry(frame, frame.state_field, ".gen_state")
91
92
// Stash the previous generator-yield-return label so
93
// nested generator functions (rare) don't trip over
94
// each other. visit(YIELD) lazily allocates a label the
95
// first time it needs to `leave` from inside a `.try`.
96
let prev_yield_return = _current_generator_yield_return_label
97
_current_generator_yield_return_label = null
98
99
// A frame whose previous pass ran out is put back as
100
// constructed here, on the next MoveNext, rather than where
101
// the stream ended: a bare `return` can sit under a `try`
102
// whose finally still reads the locals, so the end paths only
103
// mark the frame done and the rewind waits for the next pull.
104
let int_type = _innate_symbol_lookup.get_int_type()
105
let brancher = get_brancher_for_block()
106
let not_done = LABEL()
107
108
brancher.branch(BRANCH.NE, cast Value(Values.Load.TEMP(".gen_state", int_type)), cast Value(Literal.NUMBER(-1, int_type)), not_done)
109
add(_build_frame_rewind(frame))
110
brancher.label(not_done)
111
112
// Unpack any destructured formal arguments into their leaf
113
// frame fields before the body reads them. The plain path's
114
// AST walk does this from visit(Variables.VARIABLE); this
115
// path drives its own walk and never reaches those nodes.
116
_gen_destructured_formal_prologue(function)
117
118
// Walk the body. Visitors fire pre/visit on every
119
// node; visit(YIELD) is the interesting one — it
120
// appends state-machine IL into current_block
121
// (which IS move_next_block).
122
if function.body? then
123
function.body.walk(self)
124
fi
125
126
// Fell-off-end trailer: mark done, return false. The next
127
// MoveNext rewinds the frame and starts the body over, as any
128
// pipe does once it has run out. Plain `ret` since we're at the
129
// MoveNext outer scope (no open `.try`).
130
add(_build_frame_field_store(
131
frame, frame.state_field, Values.INSTRUCTION(ILOpCode.LDC_I4_M1)))
132
add(Values.INSTRUCTION(ILOpCode.LDC_I4_0))
133
add(Values.RET())
134
135
// If any yield inside an inner `.try` used the
136
// leave-return-true label, emit it now (outside any try)
137
// followed by `ldc.i4.1; ret`. Reached only via `leave
138
// L:` from inside a protected region.
139
if let yield_return_label = _current_generator_yield_return_label then
140
add(Values.MARK_LABEL(yield_return_label))
141
add(Values.INSTRUCTION(ILOpCode.LDC_I4_1))
142
add(Values.RET())
143
fi
144
145
_current_generator_yield_return_label = prev_yield_return
146
147
_pop_async_dispatch_holder(outer_dispatch_holder)
148
149
leave_block()
150
leave_scope(function)
151
152
// Uninstall — re-installed by _emit_state_machine_class
153
// around move_next_block.gen() so deferred IR.Value.gen()
154
// calls inside MoveNext still see the override. Between
155
// here and there, other class members emit normally.
156
state_machine.uninstall_body_emission_overrides()
157
158
_pending_state_machines.add(PENDING_STATE_MACHINE(state_machine, move_next_block))
159
160
// Skip the framework's default body walk + the framework's
161
// visit(function) close-out path. Our visit(function)
162
// sees the state-machine and returns early.
163
return true
164
si
165
166
_visit_generator_function(function: Definitions.FUNCTION, symbol: Semantic.Symbols.Function, state_machine: Semantic.Symbols.STATE_MACHINE) is
167
// _pre_generator_function emitted the outer method and
168
// captured the move_next block onto _pending_state_machines.
169
// Nothing more to do at AST visit time.
170
si
171
172
// `newobj` operand for the SM frame's constructor. For a
173
// generic frame, constructs a specialized type so the
174
// class-T's appear in the signature; for a non-generic frame,
175
// just the default ctor's IL reference. Exceptions in lookup
176
// are logged and a sentinel returned.
177
// A generator's outer method, encoded rather than written. Its
178
// whole body is: forward this method's own arguments, construct
179
// the frame, return it.
180
//
181
// The row is the function's own, so this deposits its offset the
182
// same way an ordinary method body does.
183
_emit_generator_outer_method_srm(
184
symbol: Semantic.Symbols.Function,
185
frame: Semantic.Symbols.STATE_MACHINE_FRAME,
186
state_machine: Semantic.Symbols.STATE_MACHINE
187
) is
188
let assembly_emitter = _context.srm_assembly_emitter
189
let body = IR.Emitter.SRM_METHOD_BODY_EMITTER()
190
191
// An instance generator hands `this` to the frame's
192
// constructor, which stores it into _outer_self so the
193
// body's `self` access can read back through the frame.
194
let slot mut = if symbol.is_emitted_with_receiver then 1 else 0 fi
195
196
if frame.outer_self_field? then
197
body.ldarg(0)
198
fi
199
200
for i in 0..symbol.argument_names.count do
201
body.ldarg(slot)
202
203
slot = slot + 1
204
od
205
206
body.new_object(
207
_context.resolve_call_target(
208
_specialized_frame_constructor(frame, state_machine, symbol.location)))
209
210
body.ret()
211
212
assembly_emitter.handles.set_body_offset(symbol, body.flush(assembly_emitter))
213
assembly_emitter.handles.set_il_outputs(symbol, body.il_outputs)
214
si
215
216
// The frame constructor as it must be named from outside: on the
217
// frame's own instantiation when the frame is generic, and on
218
// the frame itself otherwise.
219
_specialized_frame_constructor(
220
frame: Semantic.Symbols.STATE_MACHINE_FRAME,
221
state_machine: Semantic.Symbols.STATE_MACHINE,
222
location: Source.LOCATION
223
) -> Semantic.Symbols.Function =>
224
_specialized_constructor(
225
frame,
226
frame.constructor,
227
state_machine.get_construction_type_arguments(),
228
location)
229
230
// The same, for any frame: a constructed generic's constructor
231
// is a different symbol from the open type's, and only the
232
// constructed one names a type that exists at run time.
233
_specialized_constructor(
234
frame: Semantic.Symbols.Classy,
235
default_constructor: Semantic.Symbols.Function,
236
construction_type_args: Collections.List[Semantic.Types.Type]?,
237
location: Source.LOCATION
238
) -> Semantic.Symbols.Function is
239
// With no type arguments there is nothing to specialize
240
// over, and the constructed form would be a second symbol
241
// for the same constructor — one the numbering pass never
242
// saw, so it has no row and gets named as if it were
243
// imported.
244
if !construction_type_args? \/ construction_type_args.count == 0 then
245
return default_constructor
246
fi
247
248
if let specialized =
249
_specialized_frame_type(frame, construction_type_args, location)
250
then
251
if let function: Semantic.Symbols.Function = specialized.find_member("init") then
252
return function
253
fi
254
fi
255
256
return default_constructor
257
si
258
259
_emit_state_machine_class(state_machine: Semantic.Symbols.STATE_MACHINE, move_next_block: Values.BLOCK) is
260
let frame = state_machine.frame
261
262
assert frame? else "state machine has no frame at IL emission"
263
264
// Local field types snapshot at the compile-expressions
265
// walk moment; narrowing can retype the source local
266
// later, so re-read the settled types before emitting.
267
frame.refresh_local_field_types()
268
269
// Re-install the body-emission override so deferred
270
// IR.Value.gen() inside MoveNext renders function-T as
271
// class-level `!N`. Uninstalled at the bottom of this
272
// method.
273
state_machine.install_body_emission_overrides()
274
275
// .ctor — parameterless, chains to Object::.ctor.
276
_gen_state_machine_ctor_srm(frame)
277
278
// MoveNext — populated during the body walk.
279
_emit_frame_member_body_srm(
280
frame, IR.Emitter.FrameMember.MOVE_NEXT, move_next_block)
281
282
// Other Iterator[T] / IDisposable members.
283
_gen_state_machine_accessors_srm(frame)
284
285
state_machine.uninstall_body_emission_overrides()
286
si
287
288
// The IL that puts a frame back as constructed: the state to its
289
// initial value, each argument field restored from its pristine
290
// twin (the body may have written the working copy - a parameter
291
// is just a local to it), and every hoisted field back to its
292
// default, so a body that re-runs from the top behaves exactly as
293
// a freshly constructed one would. Re-entering at state 0 re-runs
294
// initializers for locals it re-declares; a deferred-init local
295
// without one reads the default here rather than a stale value
296
// from the previous pass. Emitted as the Reset member, and at the
297
// top of MoveNext for a frame whose previous pass ran out, since a
298
// pipe starts over once it has run out.
299
_build_frame_rewind(frame: Semantic.Symbols.STATE_MACHINE_FRAME) -> Values.BLOCK is
300
let block = Values.BLOCK()
301
302
block.add(_build_frame_field_store(
303
frame, frame.state_field, Values.INSTRUCTION(ILOpCode.LDC_I4_0)))
304
305
let initial_fields = frame.argument_initial_fields
306
let index mut = 0
307
308
for arg_field in frame.argument_fields do
309
block.add(_build_frame_field_store(
310
frame, arg_field, _build_frame_field_load(frame, initial_fields[index])))
311
312
index = index + 1
313
od
314
315
for local_field in frame.local_fields do
316
block.add(_build_frame_field_store(
317
frame, local_field, Values.DEFAULT(local_field.type!)))
318
od
319
320
return block
321
si
322
323
// A frame member the lowering synthesises, encoded into a body
324
// of its own and deposited against the (frame, member) key the
325
// structure walk reads when it writes the row. There is no
326
// symbol to key on: the member exists only in the emitted
327
// assembly.
328
_emit_frame_member_body_srm(
329
frame: Semantic.Symbols.STATE_MACHINE_FRAME,
330
member: IR.Emitter.FrameMember,
331
block: Values.BLOCK
332
) is
333
let assembly_emitter = _context.srm_assembly_emitter
334
let previous = _context.current_srm_body_emitter
335
336
let body = IR.Emitter.SRM_METHOD_BODY_EMITTER()
337
338
_context.current_srm_body_emitter = body
339
340
block.gen(_context)
341
342
_context.current_srm_body_emitter = previous
343
344
assembly_emitter.handles.set_frame_member_body(
345
frame, member, body.flush(assembly_emitter))
346
si
347
348
// The frame's constructor: chain to Object, then copy each
349
// constructor argument into the field the lowering declared for
350
// it. It is a declared symbol with a row of its own, so its body
351
// is deposited the same way any other method's is.
352
_gen_state_machine_ctor_srm(frame: Semantic.Symbols.STATE_MACHINE_FRAME) is
353
let assembly_emitter = _context.srm_assembly_emitter
354
let body = IR.Emitter.SRM_METHOD_BODY_EMITTER()
355
356
body.ldarg(0)
357
body.call(_context.resolve_object_constructor())
358
359
// Argument layout mirrors what STATE_MACHINE_FRAME.declare
360
// set up: [$outer_self,] arg0, arg1, ... The constructor is
361
// an instance method, so its declared arguments start at
362
// slot 1.
363
let slot mut = 1
364
365
if let outer_self_field = frame.outer_self_field then
366
body.ldarg(0)
367
body.ldarg(slot)
368
body.stfld(_context.resolve_field_target(outer_self_field))
369
370
slot = slot + 1
371
fi
372
373
// The pristine twins, index-aligned with `argument_fields`.
374
let initial_fields = frame.argument_initial_fields
375
376
for arg_field in frame.argument_fields do
377
body.ldarg(0)
378
body.ldarg(slot)
379
body.stfld(_context.resolve_field_target(arg_field))
380
381
slot = slot + 1
382
od
383
384
// Copy each freshly stored argument into its pristine twin,
385
// so the emitted Reset can put the working fields back to
386
// what the generator was constructed with after the body
387
// has written them.
388
let index mut = 0
389
390
for arg_field in frame.argument_fields do
391
body.ldarg(0)
392
body.ldarg(0)
393
body.ldfld(_context.resolve_field_target(arg_field))
394
body.stfld(_context.resolve_field_target(initial_fields[index]))
395
396
index = index + 1
397
od
398
399
body.ret()
400
401
assembly_emitter.handles.set_body_offset(
402
frame.constructor, body.flush(assembly_emitter))
403
assembly_emitter.handles.set_il_outputs(
404
frame.constructor, body.il_outputs)
405
si
406
407
408
// The iterator surface every consumer reaches a frame through.
409
// Each body is fixed and small, so they are built here rather
410
// than walked: get_Current reads the current field,
411
// GetEnumerator returns self, Dispose does nothing, Reset zeroes
412
// the state, and ToString joins the pipe.
413
_gen_state_machine_accessors_srm(frame: Semantic.Symbols.STATE_MACHINE_FRAME) is
414
let assembly_emitter = _context.srm_assembly_emitter
415
let handles = assembly_emitter.handles
416
417
// get_Current — read the current field.
418
let get_current = IR.Emitter.SRM_METHOD_BODY_EMITTER()
419
get_current.ldarg(0)
420
get_current.ldfld(_context.resolve_field_target(frame.current_field))
421
get_current.ret()
422
handles.set_frame_member_body(
423
frame,
424
IR.Emitter.FrameMember.GET_CURRENT,
425
get_current.flush(assembly_emitter))
426
427
// GetEnumerator — the frame implements Iterator[T] itself,
428
// so it is its own enumerator.
429
let get_enumerator = IR.Emitter.SRM_METHOD_BODY_EMITTER()
430
get_enumerator.ldarg(0)
431
get_enumerator.ret()
432
handles.set_frame_member_body(
433
frame,
434
IR.Emitter.FrameMember.GET_ENUMERATOR,
435
get_enumerator.flush(assembly_emitter))
436
437
// Dispose — a no-op. A generator abandoned mid-flight does
438
// not run the user's finally bodies.
439
let dispose = IR.Emitter.SRM_METHOD_BODY_EMITTER()
440
dispose.ret()
441
handles.set_frame_member_body(
442
frame, IR.Emitter.FrameMember.DISPOSE, dispose.flush(assembly_emitter))
443
444
// Reset — rewind to a freshly constructed frame, then return.
445
let reset = _build_frame_rewind(frame)
446
reset.add(Values.RET())
447
_emit_frame_member_body_srm(frame, IR.Emitter.FrameMember.RESET, reset)
448
449
// ToString — a generator is a Pipe[T], so stringify it the
450
// way every other Pipe does. The trait's default is a
451
// default interface method and does not override
452
// Object.ToString, so each implementing class needs its own.
453
let to_string = IR.Emitter.SRM_METHOD_BODY_EMITTER()
454
to_string.ldarg(0)
455
to_string.call(_context.resolve_pipe_join(frame.class_element_type))
456
to_string.ret()
457
handles.set_frame_member_body(
458
frame, IR.Emitter.FrameMember.TO_STRING, to_string.flush(assembly_emitter))
459
si
460
si
461
si