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 determine whether the transformation should run. If the match does not succeed, the next rewrite rule is run. If the current rewrite rule is the last one, the child nodes are then processed.

As an example, consider a rewrite rule that converts 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' . When a block node is matched, it runs the transformation code. 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, processes the children of the block node, which are its statements. The code inside the rules is evaluated in the context of SmaCCRewriteMatchContext Object << #SmaCCRewriteMatchContext slots: { #rewriteEngine . #match . #nodes . #strings . #continuation }; sharedVariables: { #SilentProperties . #Continue . #Uninitialized }; package: 'SmaCC_Rewrite_Engine' , so self understands all messages defined on that class.

The matching part of a rewrite rule can also contain code that controls whether the transformation runs. The transformation runs only if that code returns true. If it returns anything other than true, or throws an error, the transformation is not run. For example, consider a rule that converts the . terminating a statement in 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 whether its period token is not nil. For statements that do not have a period, this throws an error, but that is handled and treated as not matching:

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

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

There are many other edit operation methods, such as 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 , and SmaCCRewriteMatchContext>>#move:before: move: anObject before: anotherObject ^ self move: anObject before: anotherObject withWhitespace: '' . The edit operations can be found in the source editing protocol of SmaCCRewriteMatchContext Object << #SmaCCRewriteMatchContext slots: { #rewriteEngine . #match . #nodes . #strings . #continuation }; sharedVariables: { #SilentProperties . #Continue . #Uninitialized }; package: 'SmaCC_Rewrite_Engine' . Most of them take AST nodes or tokens to identify positions. The edited source for a node can be retrieved with SmaCCRewriteMatchContext>>#sourceFor: sourceFor: anObject anObject isNil ifTrue: [ ^ '' ]. ^ (self sourceFrom: (self startIndexFor: anObject) to: (self stopIndexFor: anObject)) asString .

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 either by the location of the rule in the script file or by moving the edit operation before or after a self continue or self processChildren statement. The SmaCCRewriteMatchContext>>#continue continue continuation value method continues processing the current matching node in the script file. If you do not want to continue with the current matching node, you can process another 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 is called, processing of the current matching node and its children stops.