Sake = Rake for Smalltalk

Tagged:  •    •    •  

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.

Database

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.

Evaluators

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:

  • Strings
  • 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

Commands

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.

Targets

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

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!

Sake is allready a Rake for Ruby .... see http://errtheblog.com/posts/60-sake-bomb

You should rename!

/anon

... which does not have anything to do with whatever I described in this blog post.

(Not logged in) Paolo

User login