-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathBytecodeSimulatorForJS.ns
More file actions
1549 lines (1464 loc) · 66.6 KB
/
Copy pathBytecodeSimulatorForJS.ns
File metadata and controls
1549 lines (1464 loc) · 66.6 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
Newspeak3
'Root'
class BytecodeSimulatorForJS usingPlatform: platform simulation: simulation = (
(* The V5 Simulator execution machine for the JS platform: steps the MM bytecode records emitted by dual compilation, over live NS2JS runtime objects. Ported from the upstream Simulator (upstream-V5Simulator-338f0bb3.ns.txt) per the machine mapping in CODEGEN_V5_PORT_PLAN_2026-07-14.md section 5 step 5.
UNDER CONSTRUCTION, first working slice: ordinary, self and super sends over the runtime-mixin metadata with upstream's access-modifier rules; local and non-local returns; closures created and activated in simulation; and the full-speed escape - a method found without a bytecode record (a _js-intrinsic body, an edited class) is invoked as its compiled JS function, so simulation seamlessly degrades to real execution method by method. Real closures receiving value in simulation resolve to their _js-intrinsic methods and therefore escape automatically; simulated closures are intercepted before lookup.
Not yet ported, failing with descriptive errors: eventual sends, cannotReturn:/nonBooleanReceiver: reification, and simulated closures escaping to full-speed code.
Sentinel NLR (M4) interplay: a sentinel record surfacing from a full-speed escape cannot target any simulated frame (activation ids are JS-frame ids), and it propagates out of the simulation AUTOMATICALLY -- the simulator is itself M4-compiled, so the checked send at each escape site (Message sendTo: and friends) returns the record straight out of escape:, step, and the driver loop to the simulation's real caller, which propagates it onward. KNOWN GAP: simulated unwind-protect frames (SimulationSupport surrogates) pending on the simulated stack do NOT run their protectors during such a flythrough; the abandoned simulated frames are heap data and simply become garbage. Accepted for now; revisit if a real scenario hits it. Exception handling IS simulated: on:do: rewrites to the run:protectedBy:handler: surrogate; any signal (stepped code and full-speed escapes alike throw through the step loop, since simulated frames are data) routes to the nearest matching handler frame by method identity, running intervening unwind protectors first. A stepping driver is included: start:send:with:, step, stepOver, stepThrough, stackDo:, atEnd, result. Unwind protection (ensure:/ifCurtailed:) IS ported: sends on simulated closures rewrite to the SimulationSupport surrogates, whose frames the non-local-return walk recognizes by method identity. Not yet wired into any runtime. *)
|
private List = platform collections List.
private InstructionStream = simulation InstructionStream.
private Message = platform kernel Message.
private Exception = platform kernel Exception.
|
) (
public class SimulatedClosure in: definingActivation_ initialBCI: initialBCI_ numArgs: numArgs_ numCopied: numCopied_ = (
(* The reification of a block created DURING simulation (upstream: VM primitive 145). Deliberately not interchangeable with real closures yet: passing one to full-speed code is an unported seam. *)
|
public definingActivation <SimulatedActivation> = definingActivation_.
public initialBCI <Integer> = initialBCI_.
public numArgs <Integer> = numArgs_.
(* A real sized JS array: List new: n is an EMPTY list of capacity n, so indexed stores into it fail. *)
public copied = _js call: (_js ident: 'Array') with: {numCopied_. }.
|
) (
public isKindOfSimulatedClosure ^<Boolean> = (
^true
)
) : ()
public class SimulationSupport new = Object new (
(* Kernel-method surrogates the Simulator activates and recognizes BY METHOD IDENTITY - the capability-clean analogue of upstream's primitive-number markers (162 etc.). These are plain Newspeak, so dual emission gives them bytecode records: the Simulator rewrites closure-protocol sends on simulated closures into activations of these methods, their normal control flow is simply stepped, and the non-local-return walk recognizes run:ensure:/run:ifCurtailed: frames as unwind protection. The probe methods at the bottom exercise the machinery from a workspace. *)
|
public protectorRan <Boolean> ::= false.
|
) (
public loop: body = (
[ body value ] repeat
)
public probeEnsureNormal = (
protectorRan:: false.
^[ 7 ] ensure: [ protectorRan:: true ]
)
public probeNLRThroughEnsure = (
protectorRan:: false.
[ ^42 ] ensure: [ protectorRan:: true ].
^0
)
public repeatUntil: condition = (
[ condition value ] whileFalse.
^nil
)
public repeatWhile: condition = (
[ condition value ] whileTrue.
^nil
)
public run: body ensure: protector = (
| result |
result:: body value.
protector value.
^result
)
public run: body ifCurtailed: protector = (
^body value
)
public run: body protectedBy: exceptionClass handler: handler = (
(* The on:do: surrogate: its frame carries the exception class and handler in temps 2 and 3; the Simulator's signal machinery recognizes the frame by method identity and runs the handler itself, so this body only evaluates the protected block. *)
^body value
)
public probeCaughtSignal = (
^[ Error signal: 'boom' ] on: Exception do: [:e | 99 ]
)
public probeEnsureDuringUnwind = (
protectorRan:: false.
^[ [ Error signal: 'x' ] ensure: [ protectorRan:: true ] ]
on: Exception
do: [:e | 7 ]
)
public probeUncaught = (
(* The handler class never matches (Message is not in any exception's class chain), so the signal must propagate out of the simulation to the caller as a real exception. *)
^[ Error signal: 'escaped' ] on: Message do: [:e | 0 ]
)
public while: condition do: body = (
[ condition value ] whileTrue: [ body value ].
^nil
)
public whileNot: condition do: body = (
[ condition value ] whileFalse: [ body value ].
^nil
)
) : ()
public class Simulator new = InstructionStream new (
|
public activation <SimulatedActivation>
protected root <SimulatedActivation>
protected support = SimulationSupport new.
protected supportMixin = runtimeMixinOf: (behaviorOf: support).
(* The kernel Exception mixin, to recognize Exception>>signal by method identity: since Stage 2 the kernel signal is plain Newspeak (handler-stack walk, resumable) for FULL-SPEED code, but simulation keeps the pre-Stage-2 semantics -- a simulated signal is rerouted to a JS throw, which the step guard converts to the surrogate-frame handler walk. *)
protected exceptionMixin = runtimeMixinOf: (behaviorOf: Exception new).
|
) (
activate: simulatedMethod receiver: newReceiver arguments: arguments = (
| newActivation |
(simulatedMethod selector = #signal and: [
_js operator: '===' with: simulatedMethod mixin and: exceptionMixin
]) ifTrue: [
(* Pre-Stage-2 semantics in simulation: throw to the step guard, whose handleSignal walks the SIMULATED handler frames (the on:do: surrogate). See the on:do: interception for the unification status. A statement throw, never ^-returned: `return throw x` is a JS syntax error. *)
_js throw: newReceiver
].
nil = simulatedMethod record ifTrue: [
^escape: simulatedMethod receiver: newReceiver arguments: arguments
].
newActivation:: SimulatedActivation new.
newActivation sender: activation.
newActivation method: simulatedMethod.
newActivation receiver: newReceiver.
newActivation size: simulatedMethod numTemps.
1 to: arguments size do: [:index |
newActivation tempAt: index put: (arguments at: index)
].
activation:: newActivation
)
activateClosure: closure arguments: arguments = (
| newActivation |
newActivation:: SimulatedActivation new.
newActivation sender: activation.
newActivation bci: closure initialBCI.
newActivation method: closure definingActivation method.
newActivation closure: closure.
newActivation receiver: closure definingActivation receiver.
arguments size = closure numArgs ifFalse: [
^simulatorLimitation: 'closure arity mismatch'
].
arguments do: [:argument | newActivation push: argument ].
1 to: closure copied size do: [:index |
newActivation push: (closure copied at: index)
].
activation:: newActivation
)
public bci = (
^activation bci
)
public bci: value = (
activation bci: value
)
behaviorOf: obj = (
(* The runtime class of any value: its JS prototype. Primitives box (their patched prototypes carry the kernel methods); the chain top normalizes to nil. *)
| proto = _js call: (_js ident: 'Object.getPrototypeOf') with: {obj. }. |
(_js operator: '===' with: proto and: (_js ident: 'null')) ifTrue: [ ^nil ].
^proto
)
dispatchOrdinary: selector receiver: messageReceiver arguments: arguments = (
(* Upstream ordinarySend: lookup: public finds, protected on the receiver class means DNU for an ordinary send. Interception of simulated closures happens before lookup, since they are not instances of the real Closure class. *)
| receiverBehavior lookupBehavior found |
(isSimulatedClosure: messageReceiver) ifTrue: [
(isValueSelector: selector arity: arguments size) ifTrue: [
^activateClosure: messageReceiver arguments: arguments
].
selector = #ensure: ifTrue: [
^dispatchOrdinary: #run:ensure:
receiver: support
arguments: {messageReceiver. arguments at: 1. }
].
selector = #ifCurtailed: ifTrue: [
^dispatchOrdinary: #run:ifCurtailed:
receiver: support
arguments: {messageReceiver. arguments at: 1. }
].
selector = #whileTrue: ifTrue: [
^dispatchOrdinary: #while:do:
receiver: support
arguments: {messageReceiver. arguments at: 1. }
].
selector = #whileFalse: ifTrue: [
^dispatchOrdinary: #whileNot:do:
receiver: support
arguments: {messageReceiver. arguments at: 1. }
].
selector = #whileTrue ifTrue: [
^dispatchOrdinary: #repeatWhile: receiver: support arguments: {messageReceiver. }
].
selector = #whileFalse ifTrue: [
^dispatchOrdinary: #repeatUntil: receiver: support arguments: {messageReceiver. }
].
selector = #repeat ifTrue: [
^dispatchOrdinary: #loop: receiver: support arguments: {messageReceiver. }
].
selector = #on:do: ifTrue: [
(* Simulation keeps its OWN (pre-Stage-2) exception semantics for now: the surrogate frame + handleSignal, with simulated signals rerouted to throws (see activate:). Full unification with the kernel's resumable handler stack is designed but deferred: a handler shell invoked from a full-speed signal would need its wrapper's NLR to unwind across a NESTED simulation loop into the outer chain, which the runToCompletion walk cannot do yet. *)
^dispatchOrdinary: #run:protectedBy:handler:
receiver: support
arguments: {messageReceiver. arguments at: 1. arguments at: 2. }
]
].
(true = messageReceiver or: [ false = messageReceiver ]) ifTrue: [
(booleanControl: selector on: messageReceiver arguments: arguments) ifTrue: [ ^self ]
].
receiverBehavior:: behaviorOf: messageReceiver.
lookupBehavior:: receiverBehavior.
[ nil = lookupBehavior ] whileFalse: [
found:: methodFor: selector in: lookupBehavior.
nil = found ifFalse: [
(#public = found accessModifier) ifTrue: [
^activate: found receiver: messageReceiver arguments: arguments
].
(#protected = found accessModifier) ifTrue: [
^escapeDnu: selector receiver: messageReceiver arguments: arguments
]
].
lookupBehavior:: behaviorOf: lookupBehavior
].
^escapeDnu: selector receiver: messageReceiver arguments: arguments
)
booleanControl: selector on: aBoolean arguments: arguments ^<Boolean> = (
(* The boolean control protocol, interpreted in-simulator. On psoup these are ordinary bytecode methods the upstream Simulator steps; on the JS platform they are _js-intrinsic (JS-only), and escaping them would pass simulated closures into full-speed code. Answers whether the selector was handled. *)
selector = #and: ifTrue: [
aBoolean
ifTrue: [ valueInSimulation: (arguments at: 1) ]
ifFalse: [ activation push: false ].
^true
].
selector = #or: ifTrue: [
aBoolean
ifTrue: [ activation push: true ]
ifFalse: [ valueInSimulation: (arguments at: 1) ].
^true
].
selector = #ifTrue: ifTrue: [
aBoolean
ifTrue: [ valueInSimulation: (arguments at: 1) ]
ifFalse: [ activation push: nil ].
^true
].
selector = #ifFalse: ifTrue: [
aBoolean
ifTrue: [ activation push: nil ]
ifFalse: [ valueInSimulation: (arguments at: 1) ].
^true
].
selector = #ifTrue:ifFalse: ifTrue: [
valueInSimulation: (arguments at: (aBoolean ifTrue: [ 1 ] ifFalse: [ 2 ])).
^true
].
selector = #ifFalse:ifTrue: ifTrue: [
valueInSimulation: (arguments at: (aBoolean ifTrue: [ 2 ] ifFalse: [ 1 ])).
^true
].
^false
)
valueInSimulation: valuable = (
(* Evaluate a niladic valuable so its result lands on the current simulated stack: simulated closures are activated (their return delivers the result); anything else is asked for value at full speed. *)
(isSimulatedClosure: valuable)
ifTrue: [ activateClosure: valuable arguments: (newArraySized: 0) ]
ifFalse: [
activation push: ((Message selector: #value arguments: (newArraySized: 0)) sendTo: valuable)
]
)
dup = (
activation push: activation top
)
escape: simulatedMethod <SimulatedMethod> receiver: receiver arguments: arguments = (
(* The full-speed escape: run a method that has no bytecode record as its compiled JS function and push the result. Public sends re-dispatch through the kernel Message (whose mangler is authoritative, binary selectors included); protected targets are fetched at the protected mangling on the receiver's prototype chain. PRIVATE members are NOT installed on prototypes at all: they live as functions on the DEFINING MIXIN object, taking the receiver as their first parameter (the compiler's early-bound $N[...](self) convention) -- fetching them off the receiver DNUs (bug seen 2026-07-18: an enclosing module's private slot accessor, record-less because synthetic, broke stepping through Workspace>>platform). Exceptions propagate through the normal signal path. Simulated closures crossing INTO full speed are wrapped in re-entrant shells (fullSpeedValue:) so the escaped code can invoke them like any closure. *)
| message shelled result |
shelled:: arguments collect: [:a | fullSpeedValue: a ].
message:: Message selector: simulatedMethod selector arguments: shelled.
#public = simulatedMethod accessModifier
ifTrue: [ result:: message sendTo: (fullSpeedValue: receiver) ]
ifFalse: [
#private = simulatedMethod accessModifier
ifTrue: [
| f = _js propertyOf: simulatedMethod mixin
at: message mangledSelector. |
result:: _js call: (_js propertyOf: f at: (_js literal: 'apply'))
with: {_js ident: 'null'. {receiver. } , shelled. }
]
ifFalse: [
| f = _js propertyOf: receiver
at: '_' , message mangledSelector. |
result:: _js call: (_js propertyOf: f at: (_js literal: 'apply'))
with: {receiver. shelled. }
]
].
activation push: result
)
escapeDnu: selector receiver: receiver arguments: arguments = (
(* Lookup failed in simulation: real dispatch implements the same rules and raises the real doesNotUnderstand:, so delegate wholesale. *)
activation push: ((Message selector: selector arguments: (arguments collect: [:a | fullSpeedValue: a ])) sendTo: receiver)
)
public fullSpeedValue: value = (
(* A value crossing from simulation into full-speed code: simulated closures become re-entrant shells (real compiled closures that re-enter simulation when invoked); everything else passes through. *)
(isSimulatedClosure: value) ifTrue: [ ^shellFor: value ].
^value
)
shellFor: simClosure <SimulatedClosure> = (
(* The simulated-closure-to-full-speed wrapper: a REAL closure of matching arity whose invocation runs the simulated closure in a nested simulation loop. The shell is a compiled JS function, so escaped code (kernel iteration, aliens, event handlers) can call it like any closure. *)
| n = simClosure numArgs. |
0 = n ifTrue: [ ^[ runToCompletion: simClosure withArguments: {} ] ].
1 = n ifTrue: [ ^[:a | runToCompletion: simClosure withArguments: {a. } ] ].
2 = n ifTrue: [ ^[:a :b | runToCompletion: simClosure withArguments: {a. b. } ] ].
3 = n ifTrue: [ ^[:a :b :c | runToCompletion: simClosure withArguments: {a. b. c. } ] ].
4 = n ifTrue: [ ^[:a :b :c :d | runToCompletion: simClosure withArguments: {a. b. c. d. } ] ].
^simulatorLimitation: 'simulated-closure shell arity above 4'
)
eventualSend: selector numArgs: numArgs = (
simulatorLimitation: 'eventual sends are not yet simulated'
)
behavior: behavior hasSelector: selector = (
| mms |
mms:: runtimeMethodsOf: behavior.
nil = mms ifTrue: [ ^false ].
mms do: [:mm |
(_js propertyOf: mm at: (_js literal: 'name')) = selector ifTrue: [ ^true ]
].
^false
)
behaviorNamed: mixinName startingAt: startBehavior = (
| behavior rm |
behavior:: startBehavior.
[ nil = behavior ] whileFalse: [
rm:: runtimeMixinOf: behavior.
nil = rm ifFalse: [
(mixinNameOf: rm) = mixinName ifTrue: [ ^behavior ]
].
behavior:: behaviorOf: behavior
].
^simulatorLimitation: 'enclosing mixin application not found: ' , mixinName
)
enclosingMixinNameOf: qualifiedName = (
(* The parent in the nesting tree, by qualified name: everything before the last backquote; nil at top level. *)
| last |
last:: 0.
1 to: qualifiedName size do: [:i |
(qualifiedName copyFrom: i to: i) = '`' ifTrue: [ last:: i ]
].
last = 0 ifTrue: [ ^nil ].
^qualifiedName copyFrom: 1 to: last - 1
)
enclosingObjectOf: behavior = (
(* The depth-1 enclosing instance: the runtime class carries the enclosing-objects array at a per-class synthetic property, indexed from the immediate enclosing object outward (self is not in the array). *)
| prop = mangleQualified: 'enclosingObjects`' , (mixinNameOf: (runtimeMixinOf: behavior)). |
^(_js propertyOf: behavior at: prop) at: 1
)
enclosingMixinOf: runtimeMixin boundIn: receiver = (
(* The runtime mixins carry no parent pointer; recover the parent application from its qualified name on the enclosing receiver's class chain. *)
| parentName = enclosingMixinNameOf: (mixinNameOf: runtimeMixin). |
nil = parentName ifTrue: [ ^nil ].
^runtimeMixinOf: (behaviorNamed: parentName startingAt: (behaviorOf: receiver))
)
implicitReceiverSend: selector numArgs: numArgs = (
(* Upstream's outward walk: at each lexical level, if the level's declarations include the selector, the send binds there (lexical rules); past the top level it degrades to an ordinary protected send on the original receiver. *)
| arguments candidateReceiver candidateMixin candidateApp nextMixin |
arguments:: popArguments: numArgs.
candidateReceiver:: activation receiver.
candidateMixin:: activation method mixin.
[
candidateApp:: findApplicationOf: candidateMixin
startingAt: (behaviorOf: candidateReceiver).
(behavior: candidateApp hasSelector: selector) ifTrue: [
^sendLexical: selector
to: candidateReceiver
arguments: arguments
wrt: candidateMixin
].
nil = (enclosingMixinNameOf: (mixinNameOf: candidateMixin)) ifTrue: [
^sendProtected: selector
to: activation receiver
arguments: arguments
startingAt: (behaviorOf: activation receiver)
].
candidateReceiver:: enclosingObjectOf: candidateApp.
nextMixin:: enclosingMixinOf: candidateMixin boundIn: candidateReceiver.
candidateMixin:: nextMixin
] repeat
)
mangleQualified: name = (
(* names mangleSynthetic: semantics: a dollar prefix, with colons and backquotes becoming dollars. *)
| s |
s:: '$'.
1 to: name size do: [:i |
| c = name copyFrom: i to: i. |
(c = ':' or: [ c = '`' ])
ifTrue: [ s:: s , '$' ]
ifFalse: [ s:: s , c ]
].
^s
)
mixinNameOf: runtimeMixin = (
(* Guarded: a runtime mixin without a name (mis-wired reflective structures) must surface as a descriptive limitation, not a TypeError deep in the enclosing walk. *)
| n |
n:: normalizeNull: (_js propertyOf: runtimeMixin at: (_js literal: 'name')).
nil = n ifTrue: [
simulatorLimitation: 'runtime mixin without a name in the lexical walk'
].
^n
)
public runtimeMixinForDebugOf: obj = (
(* The runtime mixin of obj's own class, exactly as the interpreter derives it -- the correct mixin for a doIt method compiled in obj's class scope (more robust than reading runtimeMixin off the NS Mixin object, which can be mis-wired for reflectively created classes). *)
^runtimeMixinOf: (behaviorOf: obj)
)
isSimulatedClosure: obj = (
(* A structural probe would misfire on arbitrary objects via dnuCatchers; compare the runtime class directly. *)
^_js operator: '==='
with: (_js call: (_js ident: 'Object.getPrototypeOf') with: {obj. })
and: (_js call: (_js ident: 'Object.getPrototypeOf')
with: {SimulatedClosure in: nil initialBCI: 1 numArgs: 0 numCopied: 0. })
)
isValueSelector: selector arity: arity = (
selector = #value ifTrue: [ ^0 = arity ].
selector = #value: ifTrue: [ ^1 = arity ].
selector = #value:value: ifTrue: [ ^2 = arity ].
selector = #value:value:value: ifTrue: [ ^3 = arity ].
selector = #valueWithArguments: ifTrue: [ ^1 = arity ].
^false
)
jump: delta = (
activation bci: activation bci + delta
)
localToTemp: offset = (
^(nil = activation closure
ifTrue: [ activation method numArgs ]
ifFalse: [ activation closure numArgs ]) + offset + 1
)
public method = (
^activation method
)
methodFor: selector in: behavior = (
(* Find the MM entry for selector in the behavior's runtime mixin and wrap it with its provenance. Behaviors outside the Newspeak world (patched JS prototypes, the implementation base) have no newspeakClass and contribute nothing. *)
| mms |
mms:: runtimeMethodsOf: behavior.
nil = mms ifTrue: [ ^nil ].
mms do: [:mm |
(_js propertyOf: mm at: (_js literal: 'name')) = selector ifTrue: [
^(SimulatedMethod record: (normalizeNull: (_js propertyOf: mm at: (_js literal: 'bytecode')))
mixin: (runtimeMixinOf: behavior)
selector: selector
accessModifier: (_js propertyOf: mm at: (_js literal: 'accessModifier')))
metadata: mm;
yourself
]
].
^nil
)
nonBooleanReceiver: nonBoolean = (
simulatorLimitation: 'branch on a non-boolean'
)
normalizeNull: value = (
(* Loose equality: JS null AND undefined both normalize to Newspeak nil. Undefined arises for absent properties (e.g. newspeakClass at the top of the runtime-class chain, ImplementationBase); strict === null let it through and the next property read exploded. *)
(_js operator: '==' with: value and: (_js ident: 'null')) ifTrue: [ ^nil ].
^value
)
ordinarySend: selector numArgs: numArgs = (
| arguments messageReceiver |
arguments:: popArguments: numArgs.
messageReceiver:: activation pop.
^dispatchOrdinary: selector receiver: messageReceiver arguments: arguments
)
outerSend: selector numArgs: numArgs depth: depth = (
| arguments receiver targetMixin count app |
arguments:: popArguments: numArgs.
receiver:: activation receiver.
targetMixin:: activation method mixin.
count:: 0.
[ count < depth ] whileTrue: [
count:: count + 1.
app:: findApplicationOf: targetMixin startingAt: (behaviorOf: receiver).
receiver:: enclosingObjectOf: app.
targetMixin:: enclosingMixinOf: targetMixin boundIn: receiver
].
^sendLexical: selector to: receiver arguments: arguments wrt: targetMixin
)
pop = (
activation pop
)
newArraySized: size = (
(* A real JS array (the runtime representation of Array), since these values flow into escaped full-speed code. Bare Array does not resolve on the JS platform. *)
^_js call: (_js ident: 'Array') with: {size. }
)
popArguments: numArgs = (
| arguments = newArraySized: numArgs. |
numArgs to: 1 by: -1 do: [:i | arguments at: i put: activation pop ].
^arguments
)
popIntoIndirectLocal: offset inVector: vectorOffset = (
| vector = activation tempAt: (localToTemp: vectorOffset). |
vector at: 1 + offset put: activation pop
)
popIntoLocal: offset = (
activation tempAt: (localToTemp: offset) put: activation pop
)
popJumpFalse: delta = (
| top = activation pop. |
true = top ifTrue: [ ^self ].
false = top ifTrue: [
activation bci: activation bci + delta.
^self
].
^nonBooleanReceiver: top
)
popJumpTrue: delta = (
| top = activation pop. |
true = top ifTrue: [
activation bci: activation bci + delta.
^self
].
false = top ifTrue: [ ^self ].
^nonBooleanReceiver: top
)
push: value = (
activation push: value
)
pushClosureNumCopied: numCopied numArgs: numArgs blockSize: blockSize = (
| newClosure |
newClosure:: SimulatedClosure in: activation
initialBCI: activation bci
numArgs: numArgs
numCopied: numCopied.
numCopied to: 1 by: -1 do: [:index |
newClosure copied at: index put: activation pop
].
activation push: newClosure.
activation bci: activation bci + blockSize
)
pushEnclosingObject: depth = (
| enclosingObject targetMixin count app |
enclosingObject:: activation receiver.
targetMixin:: activation method mixin.
count:: 0.
[ count < depth ] whileTrue: [
count:: count + 1.
app:: findApplicationOf: targetMixin startingAt: (behaviorOf: enclosingObject).
enclosingObject:: enclosingObjectOf: app.
targetMixin:: enclosingMixinOf: targetMixin boundIn: enclosingObject
].
activation push: enclosingObject
)
pushIndirectLocal: offset inVector: vectorOffset = (
| vector = activation tempAt: (localToTemp: vectorOffset). |
activation push: (vector at: 1 + offset)
)
pushLocal: offset = (
activation push: (activation tempAt: (localToTemp: offset))
)
pushMixin = (
activation push: activation method mixin
)
pushNewArray: size = (
activation push: (newArraySized: size)
)
pushNewArrayWithElements: size = (
| newArray = newArraySized: size. |
size to: 1 by: -1 do: [:index | newArray at: index put: activation pop ].
activation push: newArray
)
pushParameter: offset = (
| numArgs |
numArgs:: nil = activation closure
ifTrue: [ activation method numArgs ]
ifFalse: [ activation closure numArgs ].
activation push: (activation tempAt: numArgs - offset + 1)
)
pushReceiver = (
activation push: activation receiver
)
returnLocal: result = (
| sender = activation sender. |
nil = sender ifTrue: [ ^simulatorLimitation: 'return past the simulation root' ].
sender isDead ifTrue: [ ^simulatorLimitation: 'return to a dead activation' ].
activation terminate.
sender push: result.
activation:: sender
)
returnLocalReceiver = (
^returnLocal: activation receiver
)
returnLocalTop = (
^returnLocal: activation pop
)
aboutToReturn: result through: unwindFrame = (
(* An unwind-protect frame sits between the returning activation and its home: run the protector to completion, mark the frame so the re-walk passes it, then resume the non-local return. Upstream reaches this via Activation aboutToReturn:through:. *)
unwindFrame unwindProtectorRan: true.
runToCompletion: (unwindFrame tempAt: 2).
^returnNonLocal: result
)
isUnwindProtectFrame: frame ^<Boolean> = (
(* Method identity: the frame runs one of the SimulationSupport unwind surrogates and its protector has not run yet. *)
nil = frame method ifTrue: [ ^false ].
frame unwindProtectorRan ifTrue: [ ^false ].
(_js operator: '===' with: frame method mixin and: supportMixin) ifFalse: [ ^false ].
^frame method selector = #run:ensure: or: [ frame method selector = #run:ifCurtailed: ]
)
runToCompletion: valuable = (
(* Evaluate a niladic valuable in a nested simulation loop, answering its value; used where the main loop's control state must be preserved around the evaluation (unwind protectors). *)
| saved sentinel |
(isSimulatedClosure: valuable) ifFalse: [
^(Message selector: #value arguments: (newArraySized: 0)) sendTo: valuable
].
saved:: activation.
sentinel:: SimulatedActivation new.
activation:: sentinel.
activateClosure: valuable arguments: (newArraySized: 0).
[ sameActivation: activation as: sentinel ] whileFalse: [ step ].
activation:: saved.
^sentinel pop
)
returnNonLocal: result = (
(* Upstream's NLR including the unwind-protection walk: frames running the SimulationSupport unwind surrogates are recognized by method identity and their protectors run before the return proceeds. *)
| closure home sender unwind zap next |
closure:: activation closure.
home:: closure definingActivation.
closure:: home closure.
[ nil = closure ] whileFalse: [
home:: closure definingActivation.
closure:: home closure
].
unwind:: activation sender.
[ sameActivation: unwind as: home ] whileFalse: [
nil = unwind ifTrue: [
^simulatorLimitation: 'non-local return past the simulation root'
].
(isUnwindProtectFrame: unwind) ifTrue: [
^aboutToReturn: result through: unwind
].
unwind:: unwind sender
].
sender:: home sender.
(nil = sender or: [ sender isDead ]) ifTrue: [
^simulatorLimitation: 'non-local return to a dead or absent home sender'
].
zap:: activation.
[
next:: zap sender.
zap terminate.
zap:: next.
sameActivation: zap as: sender
] whileFalse.
sender push: result.
activation:: sender
)
returnNonLocalReceiver = (
^returnNonLocal: activation receiver
)
returnNonLocalTop = (
^returnNonLocal: activation pop
)
runtimeMethodsOf: behavior = (
| rm = runtimeMixinOf: behavior. |
nil = rm ifTrue: [ ^nil ].
^_js propertyOf: rm at: (_js literal: 'methods')
)
runtimeMixinOf: behavior = (
| nsClass |
nsClass:: normalizeNull: (_js propertyOf: behavior at: (_js literal: 'newspeakClass')).
nil = nsClass ifTrue: [ ^nil ].
^_js propertyOf: (_js propertyOf: nsClass at: (_js literal: '$mixin$slot'))
at: (_js literal: 'runtimeMixin')
)
sameActivation: a as: b = (
^_js operator: '===' with: a and: b
)
selfSend: selector numArgs: numArgs = (
^sendLexical: selector
to: activation receiver
arguments: (popArguments: numArgs)
wrt: activation method mixin
)
sendLexical: selector to: messageReceiver arguments: arguments wrt: runtimeMixin = (
| mixinApplication found |
mixinApplication:: findApplicationOf: runtimeMixin startingAt: (behaviorOf: messageReceiver).
found:: methodFor: selector in: mixinApplication.
nil = found ifFalse: [
(#private = found accessModifier) ifTrue: [
^activate: found receiver: messageReceiver arguments: arguments
]
].
^sendProtected: selector
to: messageReceiver
arguments: arguments
startingAt: (behaviorOf: messageReceiver)
)
findApplicationOf: runtimeMixin startingAt: startingBehavior = (
| behavior |
behavior:: startingBehavior.
[ nil = behavior ] whileFalse: [
(_js operator: '==='
with: (runtimeMixinOf: behavior)
and: runtimeMixin) ifTrue: [ ^behavior ].
behavior:: behaviorOf: behavior
].
^simulatorLimitation: 'mixin application not found on the receiver class chain'
)
sendProtected: selector to: messageReceiver arguments: arguments startingAt: startBehavior = (
| lookupBehavior found |
lookupBehavior:: startBehavior.
[ nil = lookupBehavior ] whileFalse: [
found:: methodFor: selector in: lookupBehavior.
nil = found ifFalse: [
(#private = found accessModifier) ifFalse: [
^activate: found receiver: messageReceiver arguments: arguments
]
].
lookupBehavior:: behaviorOf: lookupBehavior
].
^escapeDnu: selector receiver: messageReceiver arguments: arguments
)
public atEnd ^<Boolean> = (
^sameActivation: activation as: root
)
exceptionMatches: signaled class: exceptionClass ^<Boolean> = (
(* The kernel on:do: matching rule: walk the signaled object's class chain comparing identity; raw JS errors carry no newspeakClass and match as plain Exception. *)
| c |
c:: normalizeNull: (_js propertyOf: signaled at: (_js literal: 'newspeakClass')).
nil = c ifTrue: [ c:: Exception ].
[ nil = c ] whileFalse: [
(_js operator: '===' with: c and: exceptionClass) ifTrue: [ ^true ].
c:: c superclass
].
^false
)
handleSignal: signaled = (
(* A signal surfaced during a step (all stepped code and every full-speed escape throws through the step loop, since simulated frames are data, not JS frames). Find the nearest live handler frame below the current activation, run the intervening unwind protectors innermost-first, run the handler at full simulation, and return its result from the handler frame. No matching frame: rethrow to the real caller of the simulation. *)
| frame handler result sender zap next |
frame:: activation.
[ nil = frame ] whileFalse: [
(isLiveHandlerFrame: frame for: signaled) ifTrue: [
frame unwindProtectorRan: true.
runProtectorsFrom: activation to: frame.
handler:: frame tempAt: 3.
result:: runToCompletion: handler withArgument: signaled.
sender:: frame sender.
zap:: activation.
[ sameActivation: zap as: sender ] whileFalse: [
next:: zap sender.
zap terminate.
zap:: next
].
sender push: result.
activation:: sender.
^self
].
frame:: frame sender
].
_js throw: signaled
)
public home: frame ^<SimulatedActivation> = (
(* The method activation a block activation lexically belongs to; a method frame is its own home. *)
nil = frame closure ifTrue: [ ^frame ].
^home: frame closure definingActivation
)
isLiveHandlerFrame: frame for: signaled ^<Boolean> = (
nil = frame method ifTrue: [ ^false ].
frame unwindProtectorRan ifTrue: [ ^false ].
(_js operator: '===' with: frame method mixin and: supportMixin) ifFalse: [ ^false ].
frame method selector = #run:protectedBy:handler: ifFalse: [ ^false ].
^exceptionMatches: signaled class: (frame tempAt: 2)
)
public reached: goal ^<Boolean> = (
(* Upstream has:reachedOrSkipped:: true when the current activation IS the goal, or the goal is no longer on the sender chain (skipped via a non-local return or an unwind). *)
| a |
a:: activation.
(sameActivation: a as: goal) ifTrue: [ ^true ].
[ nil = a ] whileFalse: [
(sameActivation: a as: goal) ifTrue: [ ^false ].
a:: a sender
].
^true
)
public reachedHome: goal ^<Boolean> = (
(* Upstream has:reachedOrSkippedHome:: like reached:, but any activation whose home is the goal counts - stepping THROUGH a method lands in the blocks it activates. *)
| a |
a:: activation.
(sameActivation: (home: a) as: goal) ifTrue: [ ^true ].
[ nil = a ] whileFalse: [
(sameActivation: (home: a) as: goal) ifTrue: [ ^false ].
a:: a sender
].
^true
)
public result = (
(* The final value of a completed simulation. A simulated closure escaping as the result is shelled: the caller lives at full speed. *)
^fullSpeedValue: root pop
)
runProtectorsFrom: start to: handlerFrame = (
(* Run every not-yet-run unwind protector between the signaling activation and the handler frame, innermost first, marking each. *)
| frame |
frame:: start.
[ sameActivation: frame as: handlerFrame ] whileFalse: [
(isUnwindProtectFrame: frame) ifTrue: [
frame unwindProtectorRan: true.
runToCompletion: (frame tempAt: 2)
].
frame:: frame sender
]
)
runToCompletion: valuable withArgument: argument = (
| saved sentinel |
(isSimulatedClosure: valuable) ifFalse: [
^(Message selector: #value: arguments: {argument. }) sendTo: valuable
].
saved:: activation.
sentinel:: SimulatedActivation new.
activation:: sentinel.
activateClosure: valuable arguments: {argument. }.
[ sameActivation: activation as: sentinel ] whileFalse: [ step ].
activation:: saved.
^sentinel pop
)
public runToCompletion: simClosure withArguments: arguments = (
(* Run a simulated closure with the given arguments in a nested simulation loop, preserving the outer loop's control state; re-entrant (used by the full-speed shells, possibly after the owning thread finished). *)
| saved sentinel |
saved:: activation.
sentinel:: SimulatedActivation new.
activation:: sentinel.
activateClosure: simClosure arguments: arguments.
[ sameActivation: activation as: sentinel ] whileFalse: [ step ].
activation:: saved.
^sentinel pop
)
public rootActivation ^<SimulatedActivation> = (
(* Public access for SimulatedThread (cross-object sends need public). *)
^root
)
public isProtectorFrame: frame ^<Boolean> = (
(* Public access for SimulatedThread's unwind checks. *)
^isUnwindProtectFrame: frame
)
public newspeakClassApplying: runtimeMixin startingAt: startBehavior ^<NSClass | nil> = (
(* Presentation support (ActivationMirror definingClassMirror): the NS class whose runtime class on [startBehavior]'s chain applies [runtimeMixin]. Falls back to the newspeakClass superclass chain for receivers whose prototypes carry the kernel FLAT (primitives - no materialized applications). Answers nil rather than raising: displaying a transplanted frame must never fail on an unresolvable mixin. *)
| b nsClass rm |
b:: startBehavior.
[ nil = b ] whileFalse: [
(_js operator: '===' with: (runtimeMixinOf: b) and: runtimeMixin) ifTrue: [
^normalizeNull: (_js propertyOf: b at: (_js literal: 'newspeakClass'))
].
b:: behaviorOf: b
].
nil = startBehavior ifTrue: [ ^nil ].
nsClass:: normalizeNull: (_js propertyOf: startBehavior at: (_js literal: 'newspeakClass')).
[ nil = nsClass ] whileFalse: [
rm:: normalizeNull: (_js propertyOf: (_js propertyOf: nsClass at: (_js literal: '$mixin$slot'))
at: (_js literal: 'runtimeMixin')).
(_js operator: '===' with: rm and: runtimeMixin) ifTrue: [ ^nsClass ].
nsClass:: nsClass superclass
].
^nil
)
public applicationOf: runtimeMixin startingAt: behavior = (
(* Public access for the activation mirrors: the runtime class on the behavior's chain applying the given runtime mixin (whose newspeakClass identifies the defining NS class for a frame's method). *)
^findApplicationOf: runtimeMixin startingAt: behavior
)
public transplantMethodMixinNamed: mixinName selector: selector receiver: receiver ^<SimulatedMethod | nil> = (
(* Stage 3 transplant: the simulated method for a shadow frame - the named mixin's application located on the receiver's class chain, its MM record wrapped. Primitive receivers (numbers, strings, raw arrays) take the class-chain fallback: their kernel methods are patched FLAT onto the JS prototypes, so the named mixin application is never materialized as a prototype. nil when unresolvable (mixin not found, method edited away, or record-less): the transplant skips such frames. *)
| found |
found:: [ methodFor: selector
in: (behaviorNamed: mixinName startingAt: (behaviorOf: receiver)) ]
on: Exception
do: [:e | nil ].
nil = found ifTrue: [
found:: [ transplantViaClassChainNamed: mixinName selector: selector receiver: receiver ]
on: Exception
do: [:e | nil ]
].
(nil = found or: [ nil = found record ]) ifTrue: [ ^nil ].
^found
)
transplantViaClassChainNamed: mixinName selector: selector receiver: receiver ^<SimulatedMethod | nil> = (
(* The transplant fallback for receivers whose JS prototype chain does not materialize the named mixin application (kernel patched flat onto primitive prototypes). The NS class objects still carry the true hierarchy: walk the receiver's newspeakClass superclass chain to the mixin with the given name and read the MM entry off the mixin itself. *)
| nsClass rm mms found |
nsClass:: normalizeNull: (_js propertyOf: (behaviorOf: receiver)
at: (_js literal: 'newspeakClass')).
[ nil = nsClass ] whileFalse: [
rm:: normalizeNull: (_js propertyOf: (_js propertyOf: nsClass at: (_js literal: '$mixin$slot'))
at: (_js literal: 'runtimeMixin')).
nil = rm ifFalse: [
(normalizeNull: (_js propertyOf: rm at: (_js literal: 'name'))) = mixinName ifTrue: [
mms:: normalizeNull: (_js propertyOf: rm at: (_js literal: 'methods')).
nil = mms ifTrue: [ ^nil ].
mms do: [:mm |
(_js propertyOf: mm at: (_js literal: 'name')) = selector ifTrue: [
found:: SimulatedMethod record: (normalizeNull: (_js propertyOf: mm at: (_js literal: 'bytecode')))
mixin: rm
selector: selector
accessModifier: (_js propertyOf: mm at: (_js literal: 'accessModifier')).
found metadata: mm.
^found
]
].
^nil
]
].
nsClass:: nsClass superclass
].
^nil
)
public transplantFrameFor: simMethod <SimulatedMethod> receiver: receiver arguments: rawArgs <JSArray> ^<SimulatedActivation> = (
(* A restart-quality frame from shadow-stack capture: receiver and arguments as caught at unwind time, locals nil, bci 1 (exactly psoup restart:withMethod: state). Sender linking and adoption are the caller's business. *)
| a n limit i0 |
a:: SimulatedActivation new.
a method: simMethod.
a receiver: receiver.
a size: simMethod numTemps.
n:: simMethod numArgs.
limit:: _js propertyOf: rawArgs at: (_js literal: 'length').
limit < n ifTrue: [ n:: limit ].
1 to: n do: [:i |
i0:: i - 1.
a tempAt: i put: (_js propertyOf: rawArgs at: i0)
].
^a
)
public adoptBrokenStack: innermost <SimulatedActivation> = (
(* Adopt a transplanted stack: a fresh root sentinel beneath the bottom frame, the innermost as the current activation. The owning SimulatedThread presents it broken; restart:/return:from: re-suspend it for stepping. *)
| bottom |
root:: SimulatedActivation new.
bottom:: innermost.
[ nil = bottom sender ] whileFalse: [ bottom:: bottom sender ].
bottom sender: root.
activation:: innermost
)
public simulate: receiver send: selector with: arguments = (
(* Run a send in simulation to completion, answering the result. Equivalent to start:send:with: followed by stepping to the end. *)
start: receiver send: selector with: arguments.
[ atEnd ] whileFalse: [ step ].
^result
)
public sourceOf: frame ^<String | nil> = (
(* The source text of the frame's method: direct text when the method carries it (runtime-compiled doIts), else the deployed sources array via the MM source index. *)
| idx |
nil = frame method ifTrue: [ ^nil ].
nil = frame method sourceText ifFalse: [ ^frame method sourceText ].
nil = frame method metadata ifTrue: [ ^nil ].
idx:: _js propertyOf: frame method metadata at: (_js literal: 'source').
0 = idx ifTrue: [ ^nil ].
^_js propertyOf: (_js ident: 'sources') at: idx
)
ordinarySendSitesOf: m <SimulatedMethod> ^<List> = (
(* {bci. selector.} pairs for the ordinary-family sends (ordinary/self/implicit short and ext, super, outer) in the method's V5 bytecode, blocks included (their code shares the method's bytes). Common-selector, eventual and branch pseudo-sends are excluded - the same scope as the compiler's send-site instrumentation. bci = one-based offset of the instruction's first byte, the bciMap coordinate. *)
| bytes lits n sites i op raw idx len |
bytes:: m bytecode.
lits:: m literals.
n:: _js propertyOf: bytes at: (_js literal: 'length').
sites:: List new.
i:: 0.
[ i < n ] whileTrue: [
op:: _js propertyOf: bytes at: i.
raw:: nil.
(op >= 64 and: [ op <= 111 ]) ifTrue: [
raw:: _js propertyOf: lits at: (op bitAnd: 7)
].
(op >= 250 and: [ op <= 254 ]) ifTrue: [
idx:: (_js propertyOf: bytes at: i + 1) + (((_js propertyOf: bytes at: i + 2) bitAnd: 15) * 256).