Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions Sindarin-Tests/SindarinBytecodeToASTCacheTest.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Class {
#name : #SindarinBytecodeToASTCacheTest,
#superclass : #TestCase,
#instVars : [
'cache',
'compiledMethod'
],
#category : #'Sindarin-Tests'
}

{ #category : #running }
SindarinBytecodeToASTCacheTest >> setUp [
"Hooks that subclasses may override to define the fixture of test."
super setUp.
compiledMethod := ScriptableDebuggerTests >> #helperMethod12.
cache := SindarinBytecodeToASTCache generateForCompiledMethod: compiledMethod
]

{ #category : #helpers }
SindarinBytecodeToASTCacheTest >> testCacheInInterval: interval equalsNode: aNode [
interval do: [ :i |
self assert: (cache nodeForPC: i) identicalTo: aNode ]
]

{ #category : #tests }
SindarinBytecodeToASTCacheTest >> testCachedMethodNode [
self assert: cache methodNode identicalTo: compiledMethod ast
]

{ #category : #tests }
SindarinBytecodeToASTCacheTest >> testFirstBCOffsetTest [
self assert: cache firstBcOffset equals: compiledMethod initialPC
]

{ #category : #tests }
SindarinBytecodeToASTCacheTest >> testHigherThanLastBCOffsetAccessTest [
| pc |
pc := cache lastBcOffset + 5.
self
assert: (cache nodeForPC: pc)
identicalTo: (compiledMethod sourceNodeForPC: pc)
]

{ #category : #tests }
SindarinBytecodeToASTCacheTest >> testLastBCOffsetTest [
self
assert: cache lastBcOffset
equals:
compiledMethod ast ir startSequence withAllSuccessors last last
bytecodeOffset
]

{ #category : #tests }
SindarinBytecodeToASTCacheTest >> testLowerThanFirstBCOffsetAccessTest [
self
testCacheInInterval: (0 to: cache firstBcOffset - 1)
equalsNode: compiledMethod ast
]

{ #category : #tests }
SindarinBytecodeToASTCacheTest >> testNodeForBCOffsetRangeTest [
"As we associate each node to each possible bytecode offset that can refer to it,
we have to check that associations are consistent between a range and a node"

| pcRange |
pcRange := 0 to: cache lastBcOffset.
pcRange do: [ :pc |
self
assert: (cache nodeForPC: pc)
identicalTo: (compiledMethod sourceNodeForPC: pc) ]
]

{ #category : #tests }
SindarinBytecodeToASTCacheTest >> testNodeForBCOffsetTest [
| pc |
pc := 51.
self
assert: (cache nodeForPC: pc)
identicalTo: (compiledMethod sourceNodeForPC: pc)
]
91 changes: 91 additions & 0 deletions Sindarin/SindarinBytecodeToASTCache.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"
I cache a mapping between possible bytecode offsets and the AST nodes they correspond to for a given compiled method.

Instanciate me using my class method generateForCompiledMethod: and give me as parameter a compiled method.

Use me through the node access API method nodeForPC: and give me a program counter as parameter.

I store:
- firstBcOffset: The first bytecode pc. If you try to access a pc below that first pc, I return the method node.
- lastBcOffset: The last bytecode pc. If you try to access a pc after this last pc, I return the node associated with the last pc.
- bcToASTMap: A map associating each possible pc between firstBcOffset and lastBcOffset and the corresponding ast node.
- the methode node.
"
Class {
#name : #SindarinBytecodeToASTCache,
#superclass : #Object,
#instVars : [
'firstBcOffset',
'lastBcOffset',
'bcToASTMap',
'methodNode'
],
#category : #Sindarin
}

{ #category : #initialization }
SindarinBytecodeToASTCache class >> generateForCompiledMethod: compiledMethod [
^self new generateForCompiledMethod: compiledMethod
]

{ #category : #accessing }
SindarinBytecodeToASTCache >> bcToASTMap [
^ bcToASTMap
]

{ #category : #private }
SindarinBytecodeToASTCache >> fillMissingBCOffsetsWithLastBCOffsetNodes [
"It happens that different bytecode offsets map to the same AST node.
These cases are detected when there is no node mapped between, for example, bcOffset 46 and bcOffset 50.
In that case, we take every possible bytecode index between 46 and 50 (i.e., 47, 48, 49),
and we map them to the same node as the last mapped bytecode offset, here 46."

| sortedBCOffsets |
sortedBCOffsets := bcToASTMap keys asSortedCollection.
1 to: sortedBCOffsets size - 1 do: [ :index |
| bcAtIndex bcAtNextIndex |
bcAtIndex := sortedBCOffsets at: index.
bcAtNextIndex := sortedBCOffsets at: index + 1.
bcAtIndex < bcAtNextIndex ifTrue: [
bcAtIndex to: bcAtNextIndex - 1 do: [ :i |
bcToASTMap at: i put: (bcToASTMap at: bcAtIndex) ] ] ]
]

{ #category : #accessing }
SindarinBytecodeToASTCache >> firstBcOffset [
^ firstBcOffset
]

{ #category : #initialization }
SindarinBytecodeToASTCache >> generateForCompiledMethod: compiledMethod [
| methodIR currentBcOffset |
methodNode := compiledMethod ast.
methodIR := methodNode ir.
bcToASTMap := Dictionary new.
firstBcOffset := compiledMethod initialPC.
currentBcOffset := firstBcOffset.
methodIR startSequence withAllSuccessors do: [ :seq |
seq do: [ :ir |
ir ifNotNil: [
bcToASTMap at: ir bytecodeOffset ifAbsentPut: [ ir sourceNode ].
currentBcOffset := ir bytecodeOffset + 1 ] ] ].
lastBcOffset := currentBcOffset - 1.
self fillMissingBCOffsetsWithLastBCOffsetNodes
]

{ #category : #accessing }
SindarinBytecodeToASTCache >> lastBcOffset [
^ lastBcOffset
]

{ #category : #accessing }
SindarinBytecodeToASTCache >> methodNode [
^ methodNode
]

{ #category : #'node access' }
SindarinBytecodeToASTCache >> nodeForPC: pc [
pc < firstBcOffset ifTrue: [ ^ methodNode ].
pc > lastBcOffset ifTrue: [ ^ bcToASTMap at: lastBcOffset ].
^ bcToASTMap at: pc
]
32 changes: 29 additions & 3 deletions Sindarin/SindarinDebugger.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Class {
#instVars : [
'process',
'debugSession',
'stepHooks'
'stepHooks',
'nodeMapForMethod',
'debugStarted'
],
#category : #Sindarin
}
Expand Down Expand Up @@ -96,7 +98,7 @@ SindarinDebugger >> contextIsAboutToSignalException: aContext [
"Returns whether aContext is about to execute a message-send of selector #signal to an instance of the Exception class (or one of its subclasses)"

| node |
node := aContext method sourceNodeForPC: aContext pc.
node := (self nodeMapForMethod: aContext method) nodeForPC: aContext pc.
node isMessage
ifFalse: [ ^ false ].
node selector = #signal
Expand Down Expand Up @@ -165,6 +167,7 @@ SindarinDebugger >> debug: aBlock [
debugSession deactivateEventTriggering.
[ self selector = #newProcess] whileFalse: [ self step]. "Step the process to get out of the on:do: context added at the bottom of its stack"
[self selector = #newProcess] whileTrue: [ self step ]. "Step the process so that it leaves BlockClosure>>#newProcess and enters the block for which a process was created"
debugStarted := true.
^ self
]

Expand All @@ -177,6 +180,7 @@ SindarinDebugger >> debugSession [
{ #category : #initialization }
SindarinDebugger >> initialize [
stepHooks := OrderedCollection new.
debugStarted := false
]

{ #category : #stackAccess }
Expand Down Expand Up @@ -248,7 +252,29 @@ SindarinDebugger >> method [
SindarinDebugger >> node [
"Returns the AST node about to be executed by the top context of the execution"

^ self context method sourceNodeForPC: self context pc
debugStarted ifFalse: [
"Until the debug session is started, node is returned using the unoptimized version"
"Sindarin starts by executing controlled steps to exit the caller code (e.g. tests)
before entering the actual debugged code, and we do not want to cache nodes for the
caller code."
^ self context method sourceNodeForPC: self context pc ].
"Once the hand is given to the tool or user, we start caching nodes for better performance"
^ self nodeMap nodeForPC: self context pc
]

{ #category : #astAndAstMapping }
SindarinDebugger >> nodeMap [
^self nodeMapForMethod: self context method
]

{ #category : #astAndAstMapping }
SindarinDebugger >> nodeMapForMethod: aCompiledMethod [
nodeMapForMethod ifNil: [ nodeMapForMethod := Dictionary new ].
^ (nodeMapForMethod
at: aCompiledMethod methodClass name
ifAbsentPut: [ Dictionary new ])
at: aCompiledMethod selector
ifAbsentPut: [ SindarinBytecodeToASTCache generateForCompiledMethod: aCompiledMethod ]
]

{ #category : #'graphical debugger' }
Expand Down