-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathcoro_transform.nim
More file actions
2649 lines (2515 loc) · 112 KB
/
Copy pathcoro_transform.nim
File metadata and controls
2649 lines (2515 loc) · 112 KB
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
#
#
# Hexer Compiler
# (c) Copyright 2025 Andreas Rumpf
#
# See the file "license.txt", included in this
# distribution, for details about the copyright.
#
##[
Shared coroutine state-machine transform.
This module contains the body-walking dispatcher, state-machine
generators, frame-type generator, wrapper-proc generator, and for-loop
trampoline for coroutine-shaped routines. Both flavours of coroutines
share it:
- `.passive` procs / `.passive` iters (driven from `complete()`,
factory-allocated frame, owned by the trampoline).
- `.closure` iters (Nim-compatible resumable iter values; eager
value-owned frame is planned for a follow-up).
The flavour-specific bits — recognising a `.passive` call, emitting the
call itself, lowering `delay`/`delay0`/`suspend`, the
proctype-to-wrapper-signature rewrite, the top-level coroutine
entrypoint — live behind a `Hooks` proc-field record on `Context`.
Consumers (cps.nim for `.passive`, eventually lambdalifting.nim for
`.closure` iters) install their own hooks before invoking `tr`.
Default hook implementations behave as "no `.passive` in scope". A
consumer can install only the hooks it actually needs; defaults answer
"no" / "pass through" for the rest.
]##
import std / [assertions, sets, tables, hashes, syncio]
when defined(nimony):
{.feature: "lenientnils".}
include ".." / lib / nifprelude
include ".." / lib / compat2
import ".." / lib / symparser
import ".." / nimony / [nimony_model, decls, programs, typenav, sizeof, expreval, xints, builtintypes, langmodes, renderer, reporters, typeprops]
import ".." / finalir / finalir_model
import passes, defaultvalues, constparams, duplifier, closuretypes
export closuretypes # the lowered-closure shape, shared with the passes below
include ".." / nimony / nif_annotations
## Note: `ContinuationName` lives in `builtintypes` (re-imported via the
## `nimony / [..., builtintypes, ...]` line above); we don't redefine it
## here.
const
ContinuationProcName* = "ContinuationProc.0." & SystemModuleSuffix
RootObjName* = "CoroutineBase.0." & SystemModuleSuffix
## Misleadingly named: this is `CoroutineBase`, used for
## `(ptr CoroutineBase)` throughout the coroutine internals. Kept
## for source compatibility — the iter-value env slot uses
## `BareRootObjName` (real RootObj) instead.
# `BareRootObjName` (the real `RootObj`, the iter-value tuple's env slot),
# `ClosureEnvParamName` and `addClosureEnvParam` (the env param appended to a
# lowered closure signature, distinct from the coroutine's `this.0` env
# below) live in `closuretypes`, which sits below `lifter` so every pass that
# must emit the identical env slot can reach it. Re-exported above.
EnvParamName* = "`this.0"
## The coroutine's env param. Lives here — like `RootObjName` and the
## wrapper-signature shape — so lambdalifting's pass-2 lowering stays in
## lock-step with it off one definition.
FnFieldName* = "fn.0"
EnvFieldName* = "env.0"
CallerFieldName* = "caller.0"
YieldedFieldName* = "yielded.0"
CalleeFieldName* = "callee.0"
ResultParamName* = "`result.0"
ResultFieldName* = "`result.0"
CallerParamName* = "`caller.0"
AllocFrameProcName* = "allocFrame.0." & SystemModuleSuffix
DeallocFrameProcName* = "deallocFrame.0." & SystemModuleSuffix
type
EnvField* = object
objType*: SymId
field*: SymId
typeAsSym*: SymId
pragmas*, typ*: Cursor
def*: int
use*: int
RoutineKind* = enum
IsNormal, IsIterator, IsPassive
ProcContext* = object
localToEnv*: Table[SymId, EnvField]
constrFields*: HashSet[SymId]
## The frame fields the frame constructor already mentions, filled
## in by `patchParamList`. `completeFrameConstr` defaults every
## *other* field of the frame type against this set, which is what
## keeps the constructor total.
cf*: TokenBuf
resultSym*: SymId
counter*: int
labelCounter*: int = 1
loopHeads*: seq[int]
## One entry per enclosing loop, innermost last: the state label of a
## SUSPENDING loop, or `KeptLoop` for one that stays a single
## `(loop ...)` construct. A Final IR `(continue .)` is the back-edge of
## the INNERMOST loop only, so it lowers against the top of this stack:
## a `jmp` to that state label, or — for a kept loop — the marker copied
## through untouched, because `coroTr` is what translates that one. A
## kept loop has to push too: without an entry of its own its back-edge
## would lower against the enclosing suspending loop and turn the inner
## loop into a single iteration per outer round.
resultIsTuple*: bool
## The result slot is a `(ErrorCode, T)`, i.e. this was a `.raises`
## routine that returns a value and the `eraiser` gave its signature
## the success tuple. The code lives at index 0. A `void` raising
## coroutine's slot is a bare `ErrorCode` and is written whole.
resultSlotType*: TokenBuf
## The return type `patchParamList` built the result param from. Needed
## again by `generateCoroutineType` for a routine that HAS a result slot
## but no `result` local to lift into it — which is what a `void`
## `.raises` routine is once its signature returns an `ErrorCode`.
kind*: RoutineKind
isClosureIter*: bool
## True for `.closure` iters specifically. Drives the resume-slot
## writeback at yield sites and the two-branch wrapper body.
## `.passive` iters use the factory model and don't set this.
capturedEnvField*: SymId
## Set for a `.closure` iter whose body captures locals of the
## ENCLOSING proc: the coro frame grows one extra field holding the
## erased `(ref RootObj)` pointer to that proc's lambdalifting
## environment. Whoever creates the frame (lambdalifting, at the
## point where the iter VALUE is built) fills it in; every
## `(envp EnvType field)` in the body reads the capture back
## through it. `SymId(0)` = this iter captures nothing.
TrHook* = proc (c: var Context; dest: var TokenBuf; n: var Cursor) {.nimcall.}
TrPassiveCallHook* = proc (c: var Context; dest: var TokenBuf; n: var Cursor; target: Cursor) {.nimcall.}
SymPredHook* = proc (c: var Context; s: SymId): bool {.nimcall.}
CursorPredHook* = proc (c: var Context; n: Cursor): bool {.nimcall.}
Hooks* = object
## Flavour-specific call-backs. Installed by the consumer before
## any shared transform proc runs.
isPassiveProc*: SymPredHook
## True if `s` denotes a routine with
## `{.passive.}` (used by `tr`'s
## Symbol path to rewrite the sym to
## its `init.` wrapper).
isPassiveCall*: CursorPredHook
## True if `n` is a call whose target
## has `{.passive.}` (i.e. a
## suspension point).
trPassiveCall*: TrPassiveCallHook
## Emit a `.passive` call. `target`
## is the lvalue receiving the call's
## result, or `default(Cursor)` for a
## void call.
trDelay*: TrHook ## handles `(delay …)`
trDelay0*: TrHook ## handles `(delay0)`
trSuspend*: TrHook ## handles `(suspend)`
trProctype*: TrHook ## handles ProctypeT / ItertypeT bodies
trCoroutine*: TrHook ## handles ProcS/FuncS/MethodS/
## ConverterS/IteratorS — decides
## whether the routine is a coroutine
## and emits the state machine if so.
Context* = object
counter*: int
ptrSize*: int
## Target pointer size in bytes. Needed to give `int`/`uint`/`float`
## a concrete width when a default value for one is synthesized.
nextTemp*: int
## Continues the outer pipeline's xelim temp counter through the
## nested per-coroutine Final-IR runs (treIteratorBody) — restarting at
## 0 re-mints `x.N SymIds that collide with still-live outer temps.
typeCache*: TypeCache
sizeofCache*: SizeofCache
## Memoizes the type sizes `nextArgRole` asks about while `trGoto`
## decides which actuals have to arrive as an address.
thisModuleSuffix*: string
procStack*: seq[SymId]
currentProc*: ProcContext
continuationProcImpl*: Cursor
shouldPublish*: seq[tuple[sym: SymId, start: int]]
coroTypes*: TokenBuf
hooks*: Hooks
awaitingSuspendPark*: bool
## Set by `(delay0)`; consumed by the following `(suspend)` to
## decide between real parking and a synchronous state transition.
pendingCapturedEnvField*: SymId
## Inbox for the NEXT `transformCoroutineDecl` call: the consumer
## (lambdalifting) knows whether the iter it is about to hand us
## captures, we don't. Moved into `currentProc` on entry and
## cleared, so a following non-capturing iter can't inherit it.
proc generateContinuationProcImpl*(): Cursor =
## Load the `ContinuationProc` typedef body from system, returned as
## a Cursor pointing at the proctype literal. Cps and lambdalifting
## both feed this into `Context.continuationProcImpl`; the value is
## used by `contNextState` / `stashResumeFn` / wrapper emission as
## the cast target for state-proc symbols.
let symId = pool.symId(ContinuationProcName)
let impl = programs.tryLoadSym(symId)
if impl.status == LacksNothing:
let t = asTypeDecl(impl.decl)
if t.kind == TypeY:
return t.body
return default(Cursor)
proc coroTr*(c: var Context; dest: var TokenBuf; n: var Cursor)
{.ensuresNif: addedAny(dest).}
proc coroTrSons*(c: var Context; dest: var TokenBuf; n: var Cursor)
## `coroTr` / `coroTrSons` (rather than the unqualified `tr` / `trSons`)
## so they don't overload-collide with the `tr` / `trSons` that
## `lambdalifting` and other consumers naturally name their local
## body walkers.
# ---------------------------------------------------------------------
# Naming helpers
# ---------------------------------------------------------------------
proc coroHelperName*(routineSym: SymId; tag, fallbackSuffix: string): SymId =
## Mint the name of a coroutine helper (`coro` frame type, `init` wrapper,
## `s<state>` state proc) derived from `routineSym`.
##
## The suffix is the DEFINING module's, never the transforming module's:
## the helpers are generated once, by the module that declares the routine,
## so a caller in another module has to arrive at the same name for the two
## to link up. `fallbackSuffix` covers a bare symbol (no module segment),
## which is what a symbol this pass minted itself looks like.
##
## `symWithoutModule` — not `symVersionedBasename` — because it
## preserves an intermediate `I<hash>` segment: two instantiations of one
## generic (`gen.12.Iaaaa.mod`, `gen.12.Ibbbb.mod`) would otherwise share
## the stem `gen.12` and collide on every helper name.
let owning = pool.symModule(routineSym)
let module = if owning.len > 0: owning else: fallbackSuffix
result = pool.symId(
derivedName(pool.symWithoutModule(routineSym), tag) & "." & module)
proc coroTypeForProc*(c: Context; procId: SymId): SymId =
coroHelperName(procId, "coro", c.thisModuleSuffix)
proc coroWrapperProc*(c: Context; procId: SymId): SymId =
coroHelperName(procId, "init", c.thisModuleSuffix)
proc stateToProcName*(c: Context; sym: SymId; state: int): SymId =
coroHelperName(sym, "s" & $state, c.thisModuleSuffix)
proc localToFieldname*(c: var Context; local: SymId): SymId =
var name = pool.symBasename(local)
name.add "`f."
name.add $c.counter
inc c.counter
name.add "."
name.add c.thisModuleSuffix
result = pool.symId(name)
proc coroWrapperForExternIter*(iterSym: SymId): SymId =
## Context-free spelling of `coroWrapperProc` for lambdalifting, which
## holds its own `Context` type and so cannot pass ours. Same name, same
## rule — the iterator's own module suffix.
coroHelperName(iterSym, "init", "")
proc coroTypeForExternIter*(iterSym: SymId): SymId =
## Context-free spelling of `coroTypeForProc`; see above.
coroHelperName(iterSym, "coro", "")
proc coroEnvFieldForIter*(iterSym: SymId): SymId =
## The frame field holding a capturing `.closure` iter's env pointer.
## Derived from the iter sym exactly like the frame type and the
## wrapper, so the frame-CREATION site (lambdalifting) and the
## frame-READING sites (the state machine, generated here) arrive at
## the same name without having to agree on an order.
coroHelperName(iterSym, "cenv", "")
proc publishWrapperSignature*(routineSym: SymId; moduleSuffix: string) =
## Publish a placeholder signature for a coroutine's `init` wrapper so
## downstream passes (eraiser / duplifier / destroyer / constparams) can
## resolve its type via `tryLoadSym` even though no wrapper DECL exists in
## this process. Hexer-generated symbols never enter a module's sem index,
## which is the one thing both callers are up against:
##
## * lambdalifting expands a same-module `.closure` iter corofor into a
## trampoline that names the wrapper before cps has emitted it;
## * cps compiles a call to a FOREIGN `.passive` proc, whose wrapper the
## DEFINING module's hexer run emits — there is no later point in *this*
## run at which it becomes loadable.
##
## `tryLoadSym` on the wrapper is the whole guard. A module-suffix test
## would be wrong in both directions: the foreign case is exactly the one
## that needs publishing, and the same-module case is already covered by
## the wrapper resolving once cps has emitted it.
##
## The shape mirrors what `generateCoroutineHelpers` emits: original params,
## then `(param result (ptr T))` if non-void, then
## `(param caller Continuation)`, return type `Continuation`, and the
## routine's OWN pragmas — copied, not synthesized, because a foreign
## wrapper's placeholder is never replaced by the real signature in this
## process and `constparams.trCall` reads `raises` off it to decide whether
## the call returns an `(ErrorCode, T)` tuple. Body is empty (`.`); for a
## same-module routine cps's `publishSignature` overwrites this entry.
let wrapperSym = coroHelperName(routineSym, "init", moduleSuffix)
if tryLoadSym(wrapperSym).status == LacksNothing:
return # already published: an earlier call, or cps's own emission
let res = tryLoadSym(routineSym)
if res.status != LacksNothing:
return # signature unrecoverable; let the downstream lookup fail loudly
let fn = asRoutine(res.decl)
let info = NoLineInfo
var buf = createTokenBuf(40)
buf.addParLe ProcS, info
buf.addSymDef wrapperSym, info
buf.addDotToken() # exported
buf.addDotToken() # pattern
buf.addDotToken() # typevars
buf.copyIntoKind ParamsU, info:
var p = fn.params
if p.kind != DotToken:
p = sub(p) # peek walk, never left
while p.hasMore:
assert p.substructureKind == ParamU
takeInto buf, p:
buf.takeTree p # name
buf.takeTree p # exported
buf.takeTree p # pragmas
buf.takeTree p # type
buf.takeTree p # default value
# `fn` is the NIMONY declaration — this is the foreign case, so nothing
# has lowered it — hence the mapping is applied here rather than read off
# an already-lowered return type the way `generateCoroutineHelpers` does.
let raises = hasPragma(fn.pragmas, RaisesP)
var ret = fn.retType
if raises or not isVoidType(ret):
buf.copyIntoKind ParamU, info:
buf.addSymDef pool.symId(ResultParamName), info
buf.addDotToken() # export
buf.addDotToken() # pragmas
buf.copyIntoKind PtrT, info:
addLengReturnType(buf, ret, fn.pragmas, info)
buf.addDotToken() # default value
buf.copyIntoKind ParamU, info:
buf.addSymDef pool.symId(CallerParamName), info
buf.addDotToken() # export
buf.addDotToken() # pragmas
buf.addSymUse pool.symId(ContinuationName), info
buf.addDotToken() # default value
buf.addSymUse pool.symId(ContinuationName), info
addPragmasWithoutRaises(buf, fn.pragmas)
buf.addDotToken() # effects
buf.addDotToken() # body — empty, cps replaces with the real body
buf.addParRi() # close proc
programs.publish(wrapperSym, buf, SemcheckSignatures)
# ---------------------------------------------------------------------
# Iter-value tuple-type emitters
#
# `.closure` iter values are lowered to `(tuple <wrapper-proctype>
# (ref RootObj))` — structurally identical to closure procs, so the
# lifter handles destroy/copy/sink hooks for iter values uniformly.
#
# Two entry points: one consumes an `(itertype …)` cursor in-place (used
# in type slots), the other re-builds the shape from an iterator sym's
# decl (used at iter-sym-as-value sites and iter-nil tupconstrs).
# Lambdalifting and cps both call these to keep the wrapper-signature
# shape in lock-step with `generateCoroutineHelpers`.
# ---------------------------------------------------------------------
proc emitIterTupleType(dest: var TokenBuf; params, retType: Cursor; info: NifLineInfo) =
## Emit
## `(closureTuple (proctype . (params <orig>... (param result ptr T) (param caller Continuation)) Continuation <pragmas>) (ref RootObj))`
## for an iterator with these params and this return type. `params` is the
## `(params ...)` tree or a dot token; both cursors are read-only peeks.
##
## NOTE: parameter types are copied verbatim (`takeTree`) on the
## assumption that iter param types are scalar. If we ever support
## nested itertypes in param positions we'll need to recurse via a
## proctype-walker here.
dest.copyIntoKind ClosureTupleT, info:
dest.copyIntoKind ProctypeT, info:
dest.addDotToken() # nilability tag
dest.copyIntoKind ParamsU, info:
if params.isTagLit:
var p = params
p = sub(p) # peek walk, never left
while p.hasMore:
assert p.substructureKind == ParamU
takeInto dest, p: # param tag
dest.takeTree p # name
dest.takeTree p # exported
dest.takeTree p # pragmas
dest.takeTree p # type (assumed scalar)
dest.takeTree p # default value
# result becomes a ptr parameter (skipped when return type is void):
if not isVoidType(retType):
dest.copyIntoKind ParamU, info:
dest.addSymDef pool.symId(ResultParamName), info
dest.addDotToken() # export
dest.addDotToken() # pragmas
dest.copyIntoKind PtrT, info:
var r = retType
dest.takeTree r
dest.addDotToken() # default value
# caller parameter is always last:
dest.copyIntoKind ParamU, info:
dest.addSymDef pool.symId(CallerParamName), info
dest.addDotToken() # export
dest.addDotToken() # pragmas
dest.addSymUse pool.symId(ContinuationName), info
dest.addDotToken() # default value
dest.addSymUse pool.symId(ContinuationName), info
# Pragmas: ALWAYS emit `(pragmas (closure))` regardless of whether
# the source itertype was `.closure` or `.passive`. Two reasons:
# (a) cps's `trProctype` re-walks types and treats ProctypeT with
# `(pragmas (passive))` as an unlifted passive proctype — it
# would wrap our already-lifted proctype in another result-ptr
# + caller-Continuation param pair, corrupting the type sym.
# (b) `(closure)` is the canonical "this is a closure-shaped fn
# pointer" marker used by `isClosure`, cps's `HconvX` path,
# etc. Both `.closure` and `.passive` iter values share the
# SAME tuple ABI, so they share the same lifted-tuple shape.
dest.copyIntoKind PragmasU, info:
dest.copyIntoKind ClosureP, info: discard
addRootRef dest, info
proc emitIterTupleTypeFromParams*(dest: var TokenBuf; n: var Cursor; info: NifLineInfo) =
## `emitIterTupleType` for the `(itertype ...)` tree at `n`, which is
## consumed: the cursor is left past its closing ParRi.
assert n.typeKind == ItertypeT
var params = default(Cursor)
var retType = default(Cursor)
n.into: # past itertype tag
if n.hasMore:
skip n # past nilability tag
assert n.hasMore, "itertype without params"
params = n
skip n
assert n.hasMore, "itertype without a return type"
retType = n
skip n
# drop the source pragmas and anything else (effects/body slots)
while n.hasMore: skip n
emitIterTupleType(dest, params, retType, info)
proc emitIterTupleTypeFromSym*(dest: var TokenBuf; iterSym: SymId; info: NifLineInfo) =
## `emitIterTupleType` for an iterator sym's decl. Used at
## iter-sym-as-value and iter-nil sites where we don't have an itertype
## tree on hand.
let res = tryLoadSym(iterSym)
assert res.status == LacksNothing, "iter sym not loaded: " & pool.symString(iterSym)
let fn = asRoutine(res.decl)
emitIterTupleType(dest, fn.params, fn.retType, info)
proc isClosureIterSym*(s: SymId): bool =
## True for `.closure` iter decls only — those are the ones that lower
## to the iter-value tuple (Nim-compatible ref-based env). `.passive`
## iters stay as plain function pointers via cps's `trProctype`, so
## they don't go through the tupconstr emission in lambdalifting's
## tre Symbol path.
let res = tryLoadSym(s)
if res.status == LacksNothing and res.decl.symKind == IteratorY:
let routine = asRoutine(res.decl)
return hasPragma(routine.pragmas, ClosureP)
return false
# ---------------------------------------------------------------------
# Predicates
# ---------------------------------------------------------------------
proc isProc*(c: var Context; s: SymId): bool =
let res = tryLoadSym(s)
if res.status == LacksNothing:
result = res.decl.symKind == ProcY
else:
let info = getLocalInfo(c.typeCache, s)
result = info.kind == ProcY
proc isClosureIter*(s: SymId): bool =
## True for any coroutine-shaped iter decl — `.closure` or `.passive`.
## `tr`'s Symbol path uses this to rewrite the iter sym (as it
## appears in value positions) to its wrapper sym.
let res = tryLoadSym(s)
if res.status == LacksNothing and res.decl.symKind == IteratorY:
let routine = asRoutine(res.decl)
return hasPragma(routine.pragmas, ClosureP) or
hasPragma(routine.pragmas, PassiveP)
return false
proc getNextState*(buf: TokenBuf; n: Cursor): int =
var pos = cursorToPosition(buf, n)
while pos < buf.len:
# raw linear scan: only TagLit tokens carry a tagId (suffix/literal bits
# alias it), and the head may carry a line-info suffix before its child
if buf[pos].kind == TagLit and buf[pos].tagId == TagId(LabS):
let operand = readonlyCursorAt(buf, pos + tokenWidth(readonlyCursorAt(buf, pos)))
# Skip `xelim`'s structured merge labels: only the CPS state machine's
# own integer-labelled `lab` names a state (`doc/final_ir.md`).
if operand.kind == IntLit:
return int(operand.intVal)
inc pos
return -1
proc coroTrSons*(c: var Context; dest: var TokenBuf; n: var Cursor) =
copyInto dest, n:
while n.hasMore:
coroTr(c, dest, n)
# ---------------------------------------------------------------------
# IR emitters — operate on (ptr CoroutineBase) frames via `this.0`
# ---------------------------------------------------------------------
proc contNextState*(c: var Context; dest: var TokenBuf; state: int; info: NifLineInfo) =
assert state >= 0
if cursorIsNil(c.continuationProcImpl):
bug "could not load system.ContinuationProc"
dest.copyIntoKind OconstrX, info:
dest.addSymUse pool.symId(ContinuationName), info
dest.copyIntoKind KvU, info:
dest.addSymUse pool.symId(FnFieldName), info
dest.copyIntoKind CastX, info:
dest.copyTree c.continuationProcImpl
dest.addSymUse stateToProcName(c, c.procStack[^1], state), info
dest.copyIntoKind KvU, info:
dest.addSymUse pool.symId(EnvFieldName), info
dest.copyIntoKind CastX, info:
dest.copyIntoKind PtrT, info:
dest.addSymUse pool.symId(RootObjName), info
dest.addSymUse pool.symId(EnvParamName), info
proc stashResumeFn*(c: var Context; dest: var TokenBuf; state: int; info: NifLineInfo) =
## For `.closure` iters: emit
## `this.caller.fn = cast[ContinuationProc](next_state)`
## so the wrapper, on a subsequent iter-value call, reads this slot
## to find where to resume. `caller.env` doubles as the ownership
## marker:
## - nil → wrapper-allocated, frame deallocated at final state.
## - !nil → iter-value-owned, final state just returns (nil, nil);
## the ref destructor handles dealloc via finalizeCoroutine.
if not c.currentProc.isClosureIter: return
if cursorIsNil(c.continuationProcImpl):
bug "could not load system.ContinuationProc"
dest.copyIntoKind AsgnS, info:
dest.copyIntoKind DotX, info:
dest.copyIntoKind DotX, info:
dest.copyIntoKind DerefX, info:
dest.addSymUse pool.symId(EnvParamName), info
dest.addSymUse pool.symId(CallerFieldName), info
dest.addIntLit 1, info # CallerFieldName lives on the CoroutineBase super
dest.addSymUse pool.symId(FnFieldName), info
dest.addIntLit 0, info # FnFieldName is a direct field of Continuation
if state < 0:
dest.addParPair NilX, info
else:
dest.copyIntoKind CastX, info:
dest.copyTree c.continuationProcImpl
dest.addSymUse stateToProcName(c, c.procStack[^1], state), info
proc emitAllocFrame*(c: var Context; dest: var TokenBuf; calleeSym: SymId; info: NifLineInfo) =
## Emit: cast[ptr CalleeCoroutine](allocFrame(sizeof(CalleeCoroutine)))
dest.copyIntoKind CastX, info:
dest.copyIntoKind PtrT, info:
dest.addSymUse coroTypeForProc(c, calleeSym), info
dest.copyIntoKind CallX, info:
dest.addSymUse pool.symId(AllocFrameProcName), info
dest.copyIntoKind SizeofX, info:
dest.addSymUse coroTypeForProc(c, calleeSym), info
proc emitDeallocFrame*(c: var Context; dest: var TokenBuf; info: NifLineInfo) =
## Emit: deallocFrame(cast[ptr CoroutineBase](this))
dest.copyIntoKind CallS, info:
dest.addSymUse pool.symId(DeallocFrameProcName), info
dest.copyIntoKind CastX, info:
dest.copyIntoKind PtrT, info:
dest.addSymUse pool.symId(RootObjName), info
dest.addSymUse pool.symId(EnvParamName), info
proc emitStopContinuation*(dest: var TokenBuf; info: NifLineInfo) =
## Emit `Continuation(fn: nil, env: nil)` — the sentinel "no caller"
## continuation passed to closure-iterator init wrappers.
dest.copyIntoKind OconstrX, info:
dest.addSymUse pool.symId(ContinuationName), info
dest.copyIntoKind KvU, info:
dest.addSymUse pool.symId(FnFieldName), info
dest.addParPair NilX, info
dest.copyIntoKind KvU, info:
dest.addSymUse pool.symId(EnvFieldName), info
dest.addParPair NilX, info
proc emitFinalReturn*(c: var Context; dest: var TokenBuf; info: NifLineInfo) =
## Emit the terminating return for a coroutine state machine.
##
## `.passive` procs / `.passive` iters: save `this.caller`,
## deallocFrame, return the saved caller — control flows back to the
## passive caller's continuation.
##
## `.closure` iters: `caller.fn` is the *resume slot* (overwritten at
## every yield), so we cannot read it back for the final return — it
## points at the last-yielded state proc, not at Stop. Instead emit a
## literal `Continuation(fn: nil, env: nil)` directly. `caller.env`
## is the ownership marker:
## - `caller.env == nil` → wrapper-allocated frame; deallocFrame
## here.
## - `caller.env != nil` → iter-value-owned; the ref destructor
## deallocs via `finalizeCoroutine`, so we just return Stop
## without freeing.
if c.currentProc.isClosureIter:
let envSym = pool.symId(EnvParamName)
let callerFld = pool.symId(CallerFieldName)
let envFld = pool.symId(EnvFieldName)
# if (*this).caller.env == nil: deallocFrame
dest.copyIntoKind IteV, info:
dest.copyIntoKind EqX, info:
dest.addParPair PointerT, info
dest.copyIntoKind DotX, info:
dest.copyIntoKind DotX, info:
dest.copyIntoKind DerefX, info:
dest.addSymUse envSym, info
dest.addSymUse callerFld, info
dest.addIntLit 1, info # CallerFieldName lives on the super
dest.addSymUse envFld, info
dest.addIntLit 0, info # EnvFieldName is direct field of Continuation
dest.addParPair NilX, info
dest.copyIntoKind StmtsS, info:
emitDeallocFrame(c, dest, info)
dest.addDotToken()
dest.copyIntoKind RetS, info:
emitStopContinuation(dest, info)
return
let tmpVar = pool.symId("`tmpCaller." & $c.currentProc.counter)
inc c.currentProc.counter
dest.copyIntoKind VarS, info:
dest.addSymDef tmpVar, info
dest.addDotToken() # exported
dest.addDotToken() # pragmas
dest.addSymUse pool.symId(ContinuationName), info
dest.copyIntoKind DotX, info:
dest.copyIntoKind DerefX, info:
dest.addSymUse pool.symId(EnvParamName), info
dest.addSymUse pool.symId(CallerFieldName), info
dest.addIntLit 1, info # field is in superclass
emitDeallocFrame(c, dest, info)
dest.copyIntoKind RetS, info:
dest.addSymUse tmpVar, info
proc emitStackFrameTag*(c: var Context; dest: var TokenBuf; coroVar: SymId; info: NifLineInfo) =
## Emit: coroVar.callee = nil
## Marks the frame as stack-allocated so deallocFrame is a nop.
dest.copyIntoKind AsgnS, info:
dest.copyIntoKind DotX, info:
dest.addSymUse coroVar, info
dest.addSymUse pool.symId(CalleeFieldName), info
dest.addIntLit 1, info # field is in superclass
dest.addParPair NilX, info
proc emitItEnv(dest: var TokenBuf; info: NifLineInfo;
itSym, envFieldSym: SymId) =
dest.copyIntoKind DotX, info:
dest.addSymUse itSym, info
dest.addSymUse envFieldSym, info
dest.addIntLit 0, info # direct field of Continuation
proc emitWhileBegin*(dest: var TokenBuf; info: NifLineInfo;
itSym, myEnvSym, exitLab: SymId) =
## Open half of the corofor trampoline (shared by cps's `.passive`
## and lambdalifting's `.closure` expansions). Emits, in the Final IR:
##
## let myEnv = it.env
## try:
## loop:
## it = advance(it)
## ite iterStopped(it): jmp exitLab
## ite iterYielded(it, myEnv):
## <body-stmts goes here — emit between begin and end>
## continue
## lab exitLab
##
## The caller follows with body emission, then `emitWhileEnd` with the same
## `exitLab`.
let envFieldSym = pool.symId(EnvFieldName)
let advanceSym = pool.symId("advance.0." & SystemModuleSuffix)
let stoppingSym = pool.symId("iterStopped.0." & SystemModuleSuffix)
dest.copyIntoKind LetS, info:
dest.addSymDef myEnvSym, info
dest.addDotToken() # exported
dest.addDotToken() # pragmas
dest.copyIntoKind PtrT, info:
dest.addSymUse pool.symId(RootObjName), info
emitItEnv(dest, info, itSym, envFieldSym)
dest.addParLe TryS, info
dest.addParLe ScopeS, info # try body
dest.addParLe LoopV, info
dest.addParLe ScopeS, info # loop body
dest.copyIntoKind AsgnS, info:
dest.addSymUse itSym, info
dest.copyIntoKind CallS, info:
dest.addSymUse advanceSym, info
dest.addSymUse itSym, info
dest.copyIntoKind IteV, info:
dest.copyIntoKind CallS, info:
dest.addSymUse stoppingSym, info
dest.addSymUse itSym, info
dest.copyIntoKind StmtsS, info:
dest.copyIntoKind JmpS, info:
dest.addSymUse exitLab, info
dest.addDotToken()
dest.addParLe IteV, info
dest.copyIntoKind CallS, info:
dest.addSymUse pool.symId("iterYielded.0." & SystemModuleSuffix), info
dest.addSymUse itSym, info
dest.addSymUse myEnvSym, info
dest.addParLe StmtsS, info # body-stmts open
proc emitWhileEnd*(dest: var TokenBuf; info: NifLineInfo; itSym, exitLab: SymId) =
## Close half of the corofor trampoline. Balances `emitWhileBegin`'s
## opens and emits `finally: finalizeCoroutine(addr it)`.
let finalizeSym = pool.symId("finalizeCoroutine.0." & SystemModuleSuffix)
dest.addParRi() # close body StmtsS
dest.addDotToken() # no else
dest.addParRi() # close IteV
dest.copyIntoKind ContinueV, info:
dest.addDotToken()
dest.addParRi() # close loop-body ScopeS
dest.addParRi() # close LoopV
dest.copyIntoKind LabS, info:
dest.addSymDef exitLab, info
dest.addParRi() # close try-body ScopeS
dest.copyIntoKind FinU, info:
dest.copyIntoKind StmtsS, info:
dest.copyIntoKind CallS, info:
dest.addSymUse finalizeSym, info
dest.copyIntoKind HaddrX, info:
dest.addSymUse itSym, info
dest.addParRi() # close try
# ---------------------------------------------------------------------
# trCoroFor — expand a `(corofor ...)` into the trampoline
# ---------------------------------------------------------------------
proc trCoroFor*(c: var Context; dest: var TokenBuf; n: var Cursor) =
## Expand `(corofor (call iter args... (haddr forLoopVar)) (block ...))`
## into the trampoline:
##
## var it: Continuation = `iter.init.<suffix>`(args..., addr forLoopVar,
## StopContinuation)
## try:
## while true:
## it = advance(it)
## if finished(it): break
## <body>
## finally:
## finalizeCoroutine(addr it)
let info = n.info
n.into: # skip (corofor
# ---- first child: (call iter-or-tupat args... (haddr forLoopVar)) ----
assert n.exprKind in CallKinds, "corofor: expected iter call as first child"
let callStart = n # past CallS tag
n = sub(n)
# The branch we take here is the ONLY reliable signal for whether
# the arg list has an upstream env-arg (case 3, non-Symbol target).
# Probing the last arg for TupatX is unsound: a regular `(tupat
# someTuple 0)` arg would falsely match.
var targetBuf = createTokenBuf(4)
var upstreamEnvArg = false
if n.kind == Symbol and isClosureIter(n.symId):
# Direct `.passive` (or `.closure`) iter DECL call — route through the
# iter's init wrapper.
targetBuf.addSymUse coroWrapperProc(c, n.symId), n.info
inc n
elif n.kind == Symbol:
# Iter-VALUE local: a `.passive` iter value is a bare function pointer
# to the wrapper (no env tuple — see cps's `trProctype`), so call it
# directly. The wrapper always allocates a fresh frame, so no caller
# env is needed; `emitStopContinuation` below supplies the sentinel.
targetBuf.addSymUse n.symId, n.info
inc n
else:
upstreamEnvArg = true
targetBuf.takeTree n
# Cursors are stable — walk once to count args and remember the
# cursor at the last (haddr) position; emit later via `addSubtree`.
let argsStart = n
var lastArgPos = default(Cursor)
var argCount = 0
while n.hasMore:
lastArgPos = n
skip n
inc argCount
n = callStart; skip n # close iter call
# Structural invariant from the corofor producer: trailing arg is
# `(haddr forLoopVar)`, optionally preceded by an env-arg when the
# target was pre-extracted. Don't probe `HaddrX` — a regular iter
# arg of `addr` shape would falsely match.
let trailingCount = if upstreamEnvArg: 2 else: 1
assert argCount >= trailingCount, "corofor: iter call missing args"
let realArgCount = argCount - trailingCount
let itSym = pool.symId("`coroIt." & $c.currentProc.counter)
inc c.currentProc.counter
c.typeCache.registerLocal(itSym, VarY, default(Cursor))
dest.copyIntoKind VarS, info:
dest.addSymDef itSym, info
dest.addDotToken() # exported
dest.addDotToken() # pragmas
dest.addSymUse pool.symId(ContinuationName), info
dest.copyIntoKind CallS, info:
dest.add targetBuf
# Through `coroTr`: in a coroutine the `for` loop's variable is a frame
# field, and `(haddr x)` has to name it there. Copied verbatim, the
# iterator wrote through a pointer to a local that no longer exists.
var w = argsStart
for i in 0 ..< realArgCount:
coroTr(c, dest, w)
var addrW = lastArgPos
coroTr(c, dest, addrW)
emitStopContinuation(dest, info)
let myEnvSym = pool.symId("`coroEnv." & $c.currentProc.counter)
inc c.currentProc.counter
c.typeCache.registerLocal(myEnvSym, LetY, default(Cursor))
let exitLab = pool.symId("`coroExit." & $c.currentProc.counter)
inc c.currentProc.counter
emitWhileBegin(dest, info, itSym, myEnvSym, exitLab)
while n.hasMore:
coroTr(c, dest, n)
emitWhileEnd(dest, info, itSym, exitLab)
# ---------------------------------------------------------------------
# Shared call / asgn / local dispatchers — route to .passive hooks
# when the call/asgn rhs is a passive call.
# ---------------------------------------------------------------------
proc trCall*(c: var Context; dest: var TokenBuf; n: var Cursor) =
let fn = n.childCursor
let typ = c.typeCache.getType(fn, {SkipAliases})
if procHasPragma(typ, PassiveP):
var retType = getType(c.typeCache, n)
# `retType` is the NIMONY answer. A `.raises` callee hands back an
# `ErrorCode` beside it — or instead of it — so the temp receiving the call
# has to be the Leng type, and a callee that returns nothing still has one.
# See `builtintypes.addLengReturnType`.
let raises = procHasPragma(typ, RaisesP)
let hasResult = raises or not isVoidType(retType)
if hasResult:
let info = n.info
dest.copyIntoKind ExprX, info:
let tmpVar = pool.symId("`tmpCpsResult." & $c.currentProc.counter)
inc c.currentProc.counter
var target = createTokenBuf(1)
target.addSymUse tmpVar, info
dest.copyIntoKind VarS, info:
dest.addSymDef tmpVar, info
dest.addDotToken() # exported
dest.addDotToken() # pragmas
if raises:
# spelled out rather than `addLengReturnType`, because the value
# half still has to go through `coroTr`'s proctype rewriting
if isVoidType(retType):
dest.addSymUse pool.symId(ErrorCodeName), info
else:
dest.copyIntoKind TupleT, info:
dest.addSymUse pool.symId(ErrorCodeName), info
coroTr c, dest, retType
else:
coroTr c, dest, retType # type
dest.addDotToken()
c.hooks.trPassiveCall(c, dest, n, beginRead target)
dest.addSymUse tmpVar, info
else:
c.hooks.trPassiveCall(c, dest, n, default(Cursor))
else:
coroTrSons(c, dest, n)
proc trLocalValue*(c: var Context; dest: var TokenBuf; n: var Cursor; lhs: Cursor) =
if c.hooks.isPassiveCall(c, n):
c.hooks.trPassiveCall(c, dest, n, lhs)
else:
dest.copyIntoKind AsgnS, n.info:
dest.copyTree lhs
coroTr c, dest, n
proc trAsgn*(c: var Context; dest: var TokenBuf; n: var Cursor) =
var rhs = n.childCursor
skip rhs
if c.hooks.isPassiveCall(c, rhs):
var lhsTransformed = createTokenBuf(6)
n.into:
coroTr c, lhsTransformed, n
c.hooks.trPassiveCall(c, dest, n, beginRead lhsTransformed)
else:
copyInto dest, n:
coroTr c, dest, n
coroTr c, dest, n
proc trLocal*(c: var Context; dest: var TokenBuf; n: var Cursor) =
let sym = n.childCursor.symId
let kind = n.symKind
let info = n.info
let field = c.currentProc.localToEnv.getOrDefault(sym)
if field.def != field.use:
n.into:
skip n, SkipName # name
skip n, SkipExport # exported
skip n, SkipPragmas # pragmas
c.typeCache.registerLocal(sym, kind, n)
skip n, SkipType # type
if n.kind == DotToken:
inc n
else:
var lhs = createTokenBuf(6)
lhs.copyIntoKind DotX, info:
lhs.copyIntoKind DerefX, info:
lhs.addSymUse pool.symId(EnvParamName), info
lhs.addSymUse field.field, info
trLocalValue(c, dest, n, beginRead lhs)
else:
var pcall = false
var callExpr = default(Cursor)
copyInto dest, n:
let target = n
takeTree dest, n # name
takeTree dest, n # export marker
takeTree dest, n # pragmas
c.typeCache.registerLocal(sym, kind, n)
let isPassive = procHasPragma(n, PassiveP)
# Use trProctype hook for the type slot so inline itertypes /
# passive proctypes get the wrapper-signature rewrite — same
# coverage as the TypeS path. sem often inlines named iter types
# into use-site type slots, so without this the let's type
# disagrees with what `consume(g: MyIter)`-style param types
# get.
c.hooks.trProctype(c, dest, n) # type
pcall = c.hooks.isPassiveCall(c, n)
if pcall:
callExpr = n
dest.addDotToken()
skip n
elif isPassive and n.kind == Symbol:
# rhs is a `.passive` proc/iter sym used as a value — rewrite to
# its init wrapper, NOT `target.symId` (that's the local var).
# A non-Symbol rhs (e.g. `nil`, or another value of the same
# type) is left to `coroTr`: the lowered passive type is a bare
# wrapper proctype, so a plain `(nil)` is already well-typed.
dest.addSymUse coroWrapperProc(c, n.symId), info
inc n
else:
coroTr(c, dest, n)
if pcall:
var symBuf = createTokenBuf(1)
symBuf.addSymUse target.symId, info
c.hooks.trPassiveCall(c, dest, callExpr, beginRead symBuf)
# ---------------------------------------------------------------------
# State-machine entries — body-level structural lowering of yield/return
# ---------------------------------------------------------------------
proc declareContinuationResult*(c: var Context; dest: var TokenBuf; info: NifLineInfo) =
dest.copyIntoKind ResultS, info:
dest.addSymDef pool.symId("result.0"), info
dest.addDotToken() # exported
dest.addDotToken() # pragmas
dest.addSymUse pool.symId(ContinuationName), info
dest.addDotToken() # default value
proc newLocalProc*(c: var Context; dest: var TokenBuf; state: int; sym: SymId) =
c.awaitingSuspendPark = false
const info = NoLineInfo
let procBegin = dest.len
dest.addParLe ProcS, info
let name = stateToProcName(c, sym, state)
dest.addSymDef name, info
for i in 0..<3:
dest.addDotToken() # exported, pattern, typevars
dest.copyIntoKind ParamsU, info:
dest.copyIntoKind ParamY, info:
dest.addSymDef pool.symId(EnvParamName), info
dest.addDotToken() # export
dest.addDotToken() # pragmas
dest.copyIntoKind PtrT, info:
dest.addSymUse coroTypeForProc(c, sym), info
dest.addDotToken() # default value
dest.addSymUse pool.symId(ContinuationName), info
dest.addDotToken() # pragmas
dest.addDotToken() # effects
publishSignature dest, name, procBegin
dest.addParLe StmtsS, info # body
declareContinuationResult c, dest, info
when defined(cpsDebugStates):