SmaCC Transformation Toolkit rewrite rules

The rewrite rules do the work of the transformation. They have two parts: a match and a transformation. The match checks the current AST node to see if the transformation should be run. If the match doesn't succeed, then the next rewrite rule is run. If the rewrite rule is the last rule, then the child nodes are processed.

As an example, consider a rewrite rule converting blocks defined by GtSmaCCTransformationExampleParser SmaCCGLRParser << #GtSmaCCTransformationExampleParser slots: {}; tag: 'Transformations'; package: 'GT4SmaCC-Examples' into Javascript:

This rule matches all nodes of type GtSmaCCTransformationExampleBlockNode GtSmaCCTransformationExampleStatementNode << #GtSmaCCTransformationExampleBlockNode slots: { #beginToken . #statements . #endToken }; tag: 'Transformations'; package: 'GT4SmaCC-Examples' , and when a block node is matched it runs the code in the transformation. That code replaces the begin and end keywords with {}. Once the begin and end keywords have been replaced, the rule does self continue . That looks for other rules that match the node and when no other rules are found, will process the children of the block node which are its statements. The code inside of the rules is evaluated in the context of a SmaCCRewriteMatchContext Object << #SmaCCRewriteMatchContext slots: { #rewriteEngine . #match . #nodes . #strings . #continuation }; sharedVariables: { #SilentProperties . #Continue . #Uninitialized }; package: 'SmaCC_Rewrite_Engine' class so self understands all messages defined on that class.

The matching part of a rewrite rule can contain code that only if it returns true runs the transformation. If it returns anything besides true or throws an error the transformation isn't run. For example, consider a rule that converts the . that terminates the statement in our GtSmaCCTransformationExampleParser SmaCCGLRParser << #GtSmaCCTransformationExampleParser slots: {}; tag: 'Transformations'; package: 'GT4SmaCC-Examples' . In Javascript those should be converted to ;. Instead of writing several rules that match only subclasses of GtSmaCCTransformationExampleStatementNode GtSmaCCTransformationExampleProgramNode << #GtSmaCCTransformationExampleStatementNode slots: {}; tag: 'Transformations'; package: 'GT4SmaCC-Examples' that contain a period, we can simply match any statement and check if it's period token isn't nil. For statements that don't have a period, this throws an error, but that is handled and treated as not matching:

The rewrite rules can also use patterns both for matching and transforming the code. For example, we can create a specific rule that matches the increment by one and converts it to use ++ in Javascript:

Many of the above rules use the SmaCCRewriteMatchContext>>#replace:with: replace: anObject with: aString anObject isNil ifTrue: [ ^ self ]. self delete: anObject. self insert: aString before: anObject method to do the edit operations. That method takes a AST node or token and replaces its source with the string passed in the second argument. If the argument is a node, then that node's source interval is updated so that future edit operations on the node will use the new source. Token's don't update their source interval when used in edit operations. However, if the token is part of the current matched node, then the matched node's source interval may be updated.

There are many other edit operation methods like SmaCCRewriteMatchContext>>#delete: delete: anObject | start stop | start := self startIndexFor: anObject. stop := self stopIndexFor: anObject. self deleteFrom: start to: stop , SmaCCRewriteMatchContext>>#insert:after: insert: aString after: anObject | position endPosition anIndex | anIndex := self stopIndexFor: anObject. anIndex isNil ifTrue: [ ^ nil ]. position := self source insert: aString at: (self source nextIndex: anIndex). position isNil ifTrue: [ ^ nil ]. endPosition := position + (aString size - 1). self updateStopPositionFor: ((anObject isKindOf: SmaCCParseNode) ifTrue: [ anObject ] ifFalse: [ match ]) to: endPosition whenEqualTo: anIndex. ^ position , SmaCCRewriteMatchContext>>#move:before: move: anObject before: anotherObject ^ self move: anObject before: anotherObject withWhitespace: '' , etc. The edit operations can be found in the source editing protocol of the SmaCCRewriteMatchContext Object << #SmaCCRewriteMatchContext slots: { #rewriteEngine . #match . #nodes . #strings . #continuation }; sharedVariables: { #SilentProperties . #Continue . #Uninitialized }; package: 'SmaCC_Rewrite_Engine' class. Most of them take AST nodes or tokens for their positions. When using operations such as SmaCCRewriteMatchContext>>#insert:after: insert: aString after: anObject | position endPosition anIndex | anIndex := self stopIndexFor: anObject. anIndex isNil ifTrue: [ ^ nil ]. position := self source insert: aString at: (self source nextIndex: anIndex). position isNil ifTrue: [ ^ nil ]. endPosition := position + (aString size - 1). self updateStopPositionFor: ((anObject isKindOf: SmaCCParseNode) ifTrue: [ anObject ] ifFalse: [ match ]) to: endPosition whenEqualTo: anIndex. ^ position , order matters. Order can be controlled by either the location of the rule script file or by moving the edit operation before or after a self continue or a self processChildren statement. The SmaCCRewriteMatchContext>>#continue continue continuation value method continues processing the current matching node in the script file. If you don't want to continue with the current matching node, you can process any other node by using SmaCCRewriteMatchContext>>#processChild: processChild: aSmaCCParseNode rewriteEngine rewriteNode: aSmaCCParseNode , SmaCCRewriteMatchContext>>#processChildren processChildren match nodesDo: [ :each | self processChild: each ] , or SmaCCRewriteMatchContext>>#processChildren: processChildren: aCollection aCollection do: [ :each | self processChild: each ] . If none of these methods are called, then processing of the current matching node and its children stops.