Querying the runtime

TL;DR

This page shows how we can query the run-time stack of execution contexts using the thisContext keyword.

What is a context?

A context holds all the dynamic state associated with the execution of a method or a block. At run time, this information exists in the virtual machine, not in the image. This means that we need to reify a context in order to query and manipulate it. This is achieved using the thisContext keyword, which reifies the current execution context.

If you inspect the following snippet, you will obtain a reified instance of the execution context which you can explore.

thisContext.
  

This is an instance of Context Object variableSubclass: #Context instanceVariableNames: 'sender pc stackp method closureOrNil receiver' classVariableNames: 'PrimitiveFailToken SpecialPrimitiveSimulators TryNamedPrimitiveTemplateMethod' package: 'Kernel-Methods' . Each context has compiled code (the getter is method or compiledCode).

thisContext method.
  

Each context also has a sender, which is another context:

thisContext sender.
  

The chain of senders forms a stack of execution contexts.

thisContext stack.
  

Contexts are especially useful for accessing information that can only be obtained at runtime.

Accessing the current method

A simple usage of thisContext is to obtain the compiled code of the currently executing method:

thisMethodFromContext
	<gtExample>
	| method |
	method := thisContext method.
	self assert: method selector 
			equals: #thisMethodFromContext.
	^ method
    

Accessing temporaries

A context holds the entire execution state of a running method. For example, you can access the values of temporary variables by sending tempNamed:

thisContext
	"NB: as soon as we return the context, it will be dead, so the values of variables will no longer be visible."

	<gtExample>
	| context foo |
	foo := 42.
	context := thisContext.
	self assert: context isDead not.
	self assert: context method selector 
			equals: #thisContext.
	self assert: (context tempNamed: #foo) equals: 42.
	^ context
    

Caveat: If you inspect the example above, the variables will all be nil. This is because the context will be dead after being returned.

thisContextIsDead
	<gtExample>
	| context |
	context := self thisContext.
	self assert: context isDead.
	^ context
    

If instead we make a copy of the context before returning it, we will be able to still see the variable values.

thisContextCopy
	"Here we return a copy of the context, which will contain all the temporary variables with their values at the point of return."

	<gtExample>
	| context foo |
	foo := 42.
	context := thisContext.
	self assert: context isDead not.
	self assert: context method selector 
			equals: #thisContextCopy.
	self assert: (context tempNamed: #foo) equals: 42.
	^ context copy
    
thisContextCopyIsNotDead
	<gtExample>
	| context |
	context := self thisContextCopy.
	self assert: context isDead not.
	^ context
    

Accessing the stack

You can obtain the full run-time stack by sending stack to thisContext, or you can ask for just a stack up to a certain size.

thisStack
	<gtExample>
	<noTest>
	| stack |
	stack := thisContext stackOfSize: 10.
	self assert: stack size equals: 10.
	self assert: stack first method selector equals: #thisStack.
	^ stack
    

Note that at run time, the stack we obtain will be live, but when we inspect the example above, we are just seeing a stack of dead contexts. We can copy all the contexts to get a live stack with their full states.

thisStackCopy
	"Return a copied stack of live contexts."
	<gtExample>
	<noTest>
	| stack |
	stack := thisContext stackOfSize: 10.
	self assert: stack size equals: 10.
	self assert: stack first method selector 
			equals: #thisStackCopy.
	^ stack collect: #copy
    

Finding senders

We can walk up the stack to find an earlier method of interest, for example, an <gtExample> that directly or indirectly invokes the current method, such as ContextExamples>>#findExampleInStack findExampleInStack "Walk up the stack looking for a method with a gtExample pragma." | context | context := thisContext. [ context sender isNil ] whileFalse: [ context := context sender. (context method hasPragmaNamed: #gtExample) ifTrue: [ ^ context method ] ]. ^ self error: 'Example not found in stack'

Here's a sample usage:

thisMethodFoundDeeplyInStack
	<gtExample>
	| method |
	method := self searchIndirectly.
	self assert: method selector equals: #thisMethodFoundDeeplyInStack.
	^ method
    

Conditional breakpoints

Searching for an earlier method in the stack can be interesting for setting conditional breakpoints. Here is an example of a conditional halt: HaltDemo>>#haltIfCalledFrom: haltIfCalledFrom: aSelector "Walk up the stack looking for a Context with the argument selector." | context | context := thisContext. [ context sender isNil ] whileFalse: [ context := context sender. context selector = aSelector ifTrue: [ Halt signal ] ]

Since foo is not called from bar here, it will not halt.

foo
	"Does not halt"
	<gtExample>
	self haltIfCalledFrom: #bar.
	^ 'foo'
    

On the other hand, bar calls foo in this example, so here it will halt.

bar
	"Will halt"
	<gtExample>
	<noTest>
	^ self foo , 'bar'
    

Finding defining methods

Here is a practical example of using thisContext to find defining methods for custom tools.

In GT, you can ALT-click on a View or an Action to see its defining method. This works pretty much as we have seen. When a View is created, it collects its defining method by searching through the stack for a method with the <gtCollector> pragma.

Have a look at GtPhlowDefiningMethodsCollector>>#collect collect | aMethods | aMethods := OrderedCollection new. 2 to: self stack size do: [ :anIndex | | aCompiledMethod | aCompiledMethod := (self stack at: anIndex) blocDefiningMethod. (aCompiledMethod hasPragmaNamed: #gtCollector) ifTrue: [ ^ self definingMethodsFrom: aMethods ]. aMethods addFirst: aCompiledMethod asRingDefinition ]. 2 to: self stack size do: [ :anIndex | | aCompiledMethod | aCompiledMethod := (self stack at: anIndex) blocDefiningMethod. aCompiledMethod isDoIt ifTrue: [ "For doits right now we return the compiled method, as wrapping doits in a Ring definition does work ok"^ aCompiledMethod ]. (aCompiledMethod hasPragmaNamed: 'gtExample') ifTrue: [ ^ aCompiledMethod asRingDefinition ] ]. ^ self definingMethodsFrom: aMethods to see how it's done.

For a sample usage, see GtPhlowProtoView>>#list list ^ GtPhlowListView new originalView: self; definingMethod: (GtPhlowDefiningMethodsCollector forContext: thisContext) collect , which invokes the defining method collector when a list view is created.

The <gtCollector> pragma is used in GtPhlowView>>#on:perform: on: anObject perform: aMessageSymbol <return: #GtPhlowView> <gtCollector> ^ self on: anObject perform: aMessageSymbol withSomeArguments: { self. self ensureContext } , which performs the view methods. This way the collector starts where the factory method is called, and stops where the view method is performed.