Reflectively reading "super"

MD
Marcus Denker
Fri, Jun 2, 2023 9:16 AM

After we improved how thisContext and self is read reflectively, we should look at “super”.

Super is always interesting, as , at first, you think that it is some magic object “self but with the class of the superclass”, or something? Like a “perspective" on that object?

But it is much more trivial than that, and we can see that when we look at SuperVariable. This is one nice aspect of reflective implementations: you can just look at the code to understand...

So we know that in Pharo ‘super’ is not some hard-coded magic, but just a Variable, with an implementation in class SuperVariable. The Compiler delegates code generation to that object, and after [PR #13812] https://github.com/pharo-project/pharo/pull/13812, reading super in the debugger is using #readInContext: like temporary variables do.

So let’s see what super does regarding code generation.

SuperVariable>>#emitValue:
	"super references the receiver, send that follows is a super send (the message lookup starts in the superclass, see OCASTTranslator>>#emitMessageNode:)"
	methodBuilder pushReceiver

Who calls this? is is the OCASTTranslator, the visitor that takes a name-analyzed AST, visits that an calls methods on IRBuilder to create byte-code:

visitVariableNode: aVariableNode

	"Invalid variable should fail"
	aVariableNode variable isInvalidVariable ifTrue: [ ^ self emitRuntimeError: aVariableNode  ].

	"Because we are producing a CompiledMethod, register undeclared variables to `Undeclared`"
	aVariableNode variable isUndeclaredVariable ifTrue: [
		aVariableNode variable register ].

	aVariableNode variable emitValue: methodBuilder

So in the end, the OCASTTranslator just delegates code generation to the Variable. The self variable got set in the name analyzer, just like any other variable by looking it up in the current scope:

resolveVariableNode: aVariableNode
	| var |
	var := (scope lookupVar: aVariableNode name)
		ifNil: [ self undeclaredVariable: aVariableNode ].
	aVariableNode variable: var.
	^var
``

lookupVar is sent to the current scope, but if the variable is not defined there, it goes to the parent scope until it reaches “Smalltalk gloabls”, which (in Pharo) knows all the pseudo variables:

SystemDictionary>>#lookupVar: name
"Return a var with this name.  Return nil if none found"
^self reservedVariables at: name ifAbsent: [self bindingOf: name]


(we still call these “ReservedVariables”, but with this scheme they are not reserved at all, they are just variables of the global scope, just like “Object”. Shadowing rules apply (thus we can shadow these variables with a DoItVariable, to force reflective read as explained in https://blog.marcusdenker.de/improving-thiscontext-in-the-debugger-using-first-class-variables )


So how comes super sends are different if super is just read the same as self? 

The magic happens just for sends, as you can see in OCASTTranslator>>#emitMessageNode:


emitMessageNode: aMessageNode

aMessageNode isCascaded ifFalse: [
	self visitNode: aMessageNode receiver].
aMessageNode arguments do: [:each |
	self visitNode: each].
aMessageNode isSuperSend
	ifTrue: [methodBuilder send: aMessageNode selector toSuperOf: aMessageNode superOf ]
	ifFalse: [methodBuilder send: aMessageNode selector]

with:

RBMessageNode>>#isSuperSend
^ self receiver isSuperVariable



So back to reflective read. As super is just self when reading, we need to implement readInContext: the same as in SelfVariable:

readInContext: aContext
"super in a block is the receiver of the home context
For clean blocks it might not be known (nil)"
^aContext home ifNotNil: [:home | home receiver ]


PR is here: https://github.com/pharo-project/pharo/pull/13878




After we improved how thisContext and self is read reflectively, we should look at “super”. Super is always interesting, as , at first, you think that it is some magic object “self but with the class of the superclass”, or something? Like a “perspective" on that object? But it is much more trivial than that, and we can see that when we look at SuperVariable. This is one nice aspect of reflective implementations: you can just look at the code to understand... So we know that in Pharo ‘super’ is not some hard-coded magic, but just a Variable, with an implementation in class SuperVariable. The Compiler delegates code generation to that object, and after [PR #13812] https://github.com/pharo-project/pharo/pull/13812, reading super in the debugger is using #readInContext: like temporary variables do. So let’s see what super does regarding code generation. ``` SuperVariable>>#emitValue: "super references the receiver, send that follows is a super send (the message lookup starts in the superclass, see OCASTTranslator>>#emitMessageNode:)" methodBuilder pushReceiver ``` Who calls this? is is the OCASTTranslator, the visitor that takes a name-analyzed AST, visits that an calls methods on IRBuilder to create byte-code: ``` visitVariableNode: aVariableNode "Invalid variable should fail" aVariableNode variable isInvalidVariable ifTrue: [ ^ self emitRuntimeError: aVariableNode ]. "Because we are producing a CompiledMethod, register undeclared variables to `Undeclared`" aVariableNode variable isUndeclaredVariable ifTrue: [ aVariableNode variable register ]. aVariableNode variable emitValue: methodBuilder ``` So in the end, the OCASTTranslator just delegates code generation to the Variable. The self variable got set in the name analyzer, just like any other variable by looking it up in the current scope: ``` resolveVariableNode: aVariableNode | var | var := (scope lookupVar: aVariableNode name) ifNil: [ self undeclaredVariable: aVariableNode ]. aVariableNode variable: var. ^var `` lookupVar is sent to the current scope, but if the variable is not defined there, it goes to the parent scope until it reaches “Smalltalk gloabls”, which (in Pharo) knows all the pseudo variables: ``` SystemDictionary>>#lookupVar: name "Return a var with this name. Return nil if none found" ^self reservedVariables at: name ifAbsent: [self bindingOf: name] ``` (we still call these “ReservedVariables”, but with this scheme they are not reserved at all, they are just variables of the global scope, just like “Object”. Shadowing rules apply (thus we can shadow these variables with a DoItVariable, to force reflective read as explained in https://blog.marcusdenker.de/improving-thiscontext-in-the-debugger-using-first-class-variables ) So how comes super sends are different if super is just read the same as self? The magic happens just for sends, as you can see in OCASTTranslator>>#emitMessageNode: ``` emitMessageNode: aMessageNode aMessageNode isCascaded ifFalse: [ self visitNode: aMessageNode receiver]. aMessageNode arguments do: [:each | self visitNode: each]. aMessageNode isSuperSend ifTrue: [methodBuilder send: aMessageNode selector toSuperOf: aMessageNode superOf ] ifFalse: [methodBuilder send: aMessageNode selector] ``` with: ``` RBMessageNode>>#isSuperSend ^ self receiver isSuperVariable ``` So back to reflective read. As super is just self when reading, we need to implement readInContext: the same as in SelfVariable: ``` readInContext: aContext "super in a block is the receiver of the home context For clean blocks it might not be known (nil)" ^aContext home ifNotNil: [:home | home receiver ] ``` PR is here: https://github.com/pharo-project/pharo/pull/13878