Sake = Rake for Smalltalk
A while ago I tried looking at how an object model for a Rake-tool would look like in Smalltalk. Here are the somewhat polished results of that experiment.
One area where Ruby fares definitely better than Smalltalk is in creating DSLs. This is mostly because of the "implied self" of Ruby's syntax, which however is also a major source of complication in parsing Ruby. Therefore, the Sake object model is also an experiment in using class extensions creatively to implement Smalltalk DSLs.
Sake has not been implemented yet, and won't be anytime soon by me. But feel free to take up the challenge: I'm curious to see ways in which this sketch can be improved.
A key concept in Sake is the Database. These are basically dictionaries that can have a parent, and where the "current" database can be accessed with dynamic binding.
There are databases for variables, targets, rules, etc. Each inherits from Database and adds method specific to the kind of object that is stored.
These are the equivalent of Make variables. They allow macro replacement including "implicit variables" like Make's $@ or $<.
They are polymorphic with one-argument blocks. There are two main kinds of evaluator other than blocks:
- objects in the Evaluator hierarchy (one concrete subclass I can think of: CommandEvaluator)
An evaluator receives a target; String>>#value: uses it to support interpolation of implicit variables as mentioned earlier.
String >> value: aTarget "e.g. '$(COMPILE)' value: aTarget" ^(VariableDatabase new parent: VariableDatabase current at: 'source' put: [ aTarget prerequisites first ]; at: 'target' put: [ aTarget target ]; at: 'prerequisites' put: [ aTarget prerequisites modified ]; at: 'modified' put: [ aTarget modifiedPrerequisites ]; ...) evaluate: self
Here is how you disable macro expansion:
String >> unexpanded "e.g. 'gcc' unexpanded" ^[ :target | self ]
An additional method supported by all evaluators:
evaluatedFor: aTarget "e.g. '$(COMPILE)' evaluated, see below for usage" ^(self value: aTarget) unexpanded
Some evaluators might also have a zero-argument version, invoked with #value:
String >> value ^VariableDatabase current evaluate: self String >> evaluated ^self value unexpanded
CommandEvaluators are a decorator for other Evaluators, and are created with factory method #run on other Evaluators:
BlockClosure >> run "e.g. 'pwd' run" ^CommandEvaluator for: self String >> run "e.g. 'pwd' run" ^CommandEvaluator for: self CommandEvaluator >> value "e.g. 'pwd' run value" ^output of executing `self command value' CommandEvaluator >> value: aTarget "e.g. 'pwd' run value: aTarget" ^output of executing `self command value: aTarget'
Note the duplication in #run (and though I didn't write it, in other methods such as #evaluated). Traits would probably be a good idea here; alternatively, we could wrap Strings into an Evaluator subclass (e.g. StringEvaluator) and add #asEvaluator factory methods.
Evaluators are strongly related to variables used by macro expansion, since a variable's value is an evaluator. Therefore, variables are defined with other extension methods on String:
String >> evaluates: anEvaluator "e.g. 'COMPILE' evaluates: 'gcc'. in Makefiles, COMPILE = gcc" VariableDatabase current at: self put: aString String >> is: anEvaluator "e.g. 'COMPILE' is: 'gcc' (in Makefiles, 'COMPILE := gcc'), or 'DIR' is: 'pwd' run (in Makefiles, 'DIR := $(shell pwd)')." ^self evaluates: anEvaluator evaluated String >> runs: aString "e.g. 'DIR' runs: 'pwd' (in Makefiles, 'DIR = $(shell pwd)')." ^self evaluates: aString run
The Command hierarchy provides a way to run commands, and do the build.
- BlockCommand has pluggable behavior (might replace it with many subclasses?)
- other types (example: MakeDirectoryCommand)
- can be created from Evaluators
It is *not* polymorphic with one-argument block (should use a different selector like #runOn:) and, this time, I decided to create a BlockCommand subclass instead of adding extension methods (just for the sake of diversity; actual implementation will probably do one thing consistently):
BlockClosure >> asCommand ^self argumentCount = 0 ifTrue: [ BlockCommand on: [ :target | self value ] ] ifFalse: [ BlockCommand on: self ]
Or you can create them from evaluators...
Evaluator >> asCommand ^BlockCommand on: [ :target | target run: (self value: target) ] String >> asCommand ^BlockCommand on: [ :target | self linesDo: [ :line | target run: (line value: target) ] ]
... or from Arrays too:
Array >> asCommand ^BlockCommand on: [ :target | self do: [ :line | target run: (line value: target) ] ]
Of course, one might prefer to have different subclasses instead of always using BlockCommand.
Like commands define how to build, targets define what to build. They are mostly defined with factory methods on Strings.
String >> builtFrom: anObject "e.g. 'test' does: 'gst-sunit -f tests.st MyTests*'" (TargetDatabase current at: self) command: anObject asCommand String >> does: anObject "e.g. 'test' does: 'gst-sunit -f tests.st MyTests*'" (TargetDatabase current at: self) command: anObject asCommand; unattachFromFile; "same as Makefile's .PHONY" String >> dependsOn: aStringOrArray "e.g. 'file.o' dependsOn: 'foo.h'." aStringOrArray addDependencyTo: (TargetDatabase current at: self)
A glimpse of more complex stuff: automatic dependency tracking, custom commands
String >> scanDependencies "e.g. 'file.c' scanDependencies." | list allTargets | allTargets := TargetDatabase current targetsDependingOn: self. DependencyScanner new applyTo: self value: [ :result | allTargets do: [ :target | list addDependencyTo: target ] ] String >> requireDirectory "e.g. 'foo' requireDirectory" ^self builtFrom: MakeDirectoryCommand new
A note on string/array polymorphism
Some examples above depend on something like this:
Collection >> addDependencyTo: aTarget self do: [ :each | each addDependencyTo: aTarget ] String >> addDependencyTo: aTarget aTarget addDependency: self
Patterns are one of the ingredients for generic rules. Here is an example implementation of a pattern factory:
String >> pattern (or maybe #asPattern??) (self occurrencesOf: $%) = 1 ifFalse: [ ^'^', ((self escapeRegex copyReplacingRegex: '%' with: '(.*)'), '$') asRegex pattern ]. (self occurrencesOf: $%) > 1 ifTrue: [ self error: '...' ]. self isEmpty ifTrue: [ ^'(.*)' asRegex pattern ]. ('.,' includes: self first) ifFalse: [ self error: '...' ]. ^('^(.*)', self escapeRegex, '$') asRegex pattern
Patterns have a single abstract method:
Pattern >> ifMatching: aString do: aBlock ^self subclassResponsibility
Rules and transformers
Rules and transformers are the other two ingredients for generic rules. The same methods that were added to String to create targets, can be added to Pattern to create rules.
Like Targets, Rules also hold their prerequisites, which are transformers, not just strings. Transformers transform a target name into the name of its prerequisites.
Pattern >> does: anObject "e.g. '.o' pattern builds: '.c' running: '$(CC) -o $(target) $(source)'" ^(RuleDatabase current at: self) command: anObject asCommand Pattern >> dependsOn: aStringOrArray "e.g. '.o' pattern dependsOn: '.c'" aStringOrArray addDependencyTo: (RuleDatabase current at: self) Rule >> addDependency: prereq "'.o' pattern dependsOn: '.c'" dependencies add: prereq asTransformer Rule >> tryApplyingTo: aString pattern ifMatching: aString do: [ :result || target | target := TargetDatabase current at: aString. dependencies do: [ :transf | transf applyTo: result value: [ :result | target addDependency: result ] ] ]
Transformers also have a single abstract method too:
Transformer >> applyTo: aMatchResult value: aBlock "aMatchResult is a RegexResults object or something polymorphic with it." ^self subclassResponsibility
Here is an example of creating transformers:
String >> asTransformer (self ~ '%[0-9]') ifTrue: [ ^InterpolateValuesTransformer on: self ]. (self occurrencesOf: $%) = 1 ifTrue: [ ^InterpolateValuesTransformer on: (self copyReplacingRegex: '%' with: '%1') ]. (self occurrencesOf: $%) > 1 ifTrue: [ self error: '...' ]. ^InterpolateValuesTransformer on: '%1', self InterpolateValuesTransformer >> applyTo: aMatchResult value: aBlock aBlock value: (string % aMatchResult)
Collections (not strings) are also polymorphic with transformers:
Collection >> asTransformer "e.g. #('.c' '.h') asTransformer" ^self collect: [ :each | each asTransformer ] Collection >> applyTo: aMatchResult value: aBlock self do: [ :each | each applyTo: aMatchResult value: aBlock ] String >> applyTo: aMatchResult value: aBlock self shouldNotImplement
Again, might want to add a separate OneToManyTransformer subclass (created by Collection>>#asTransformer) instead.
DependencyScanner (see earlier) is also a Transformer!
Rule >> scanDependencies self addDependency: DependencyScanner new
Comments are welcome!