Let's dive right in with a simple SABLE example, the ubiquitous "Hello, World" program. To cover more territory, this is larger than the usual fare, demonstrating a conditional, the 3 kinds of methods, and a few other things.
The colored headings are not part of the program, strictly speaking, but provide context for the code units. A graphical development environment will present this information differently than a sequential document.
SABLE assemblyHelloWorld.exe {~ console entryClass: #HelloWorld.HELLO method: #main:; reference: 'mscorlib.dll'; use: #System} {@ System.Reflection.ASSEMBLY_VERSION: '1.0.0.0'} SABLE namespace: #HelloWorld; classesHELLO {~ object public} HELLO staticMethods secret in: 'entrypoint'main: args {ARRAY[STRING]} ^{INT32}. |name| := args notEmpty then: [args first] else: ['World']. {HELLO} new sayHelloTo: name. ^0 HELLO constructors public in: 'constructors'new. "{Base} new." "This call to the no-argument base-class constructor is implied." HELLO instanceMethods public in: 'operations'sayHelloTo: name {STRING}. {~ cilName: 'SayHello'} CONSOLE writeLine: 'Hello, {0}!' with: name.
This shows one way to introduce classes and methods, since some information is specified outside their definitions. We'll see a more terse approach later.
The first thing you'll notice is that Smalltalk-style messages are everywhere. They not only describe program behavior, they are used to define aspects of the code units. We'll cover message syntax shortly.
The next thing you may notice is the use of {~ ...} in several places. This construct is called a pragma. SABLE uses pragmas extensively to specify various properties of defined assemblies, classes, and methods, things for which other languages use reserved words. Pragmas contain, not keywords, but messages to compiler-implemented objects, where the tilde plays the roll of the pragma message receiver. Naturally, different messages apply when defining an assembly than within a class or method; the appropriate pragma receiver is used depending on the context.
The most interesting thing about pragmas is that they are defined in the SABLE language, using classes and methods. For example, a library defines CLASS_SHAPE_PRAGMA_RCVR which declares methods >#object, >#structure, >#interface, etc.; an instance is used as the first pragma receiver in a class definition, accepting a message specifying what "shape" of class is being declared.
The assembly definition shows an attribute for the assembly version. Attributes are similar to pragmas in both usage and form, leading with an at sign (@) as receiver. The attribute receiver acts like a Builder, receiving messages which describe metadata attributes, always returning itself to receive the next message. Messages can represent attribute classes, constructors, and property setter methods. A detailed description of how the attribute builder behaves is beyond the scope of this document, but when you see attributes used, the meaning will be clear enough.
Every method is categorized into a single grouping; this provides the following properties for the method outside of its definition:
Any assembly for which you don't have SABLE source code is called an external library. SABLE programs can call the accesssible methods in external libraries, no matter what language they were written in. As a result, SABLE programs can build user interfaces, access databases, invoke web services, etc.
The example above sends >#writeLine:with: to write to the console; this calls the corelib method called (in C#) System.Console.WriteLine(string,object). SABLE uses naming standards chosen for readability, not for matching those of other languages. So how does SABLE know what method to call?
An external library is first processed by a tool to produce SABLE "headers", creating the names by which classes and methods are known in SABLE. Over time, you can change the names if needed, making them even better. The >#cilName: pragma connects the SABLE definition with the CIL definition. Here are the relevant headers for the WriteLine method:
SABLE namespace: #System; classesCONSOLE {~ object static} {~ cilName: 'System.Console'} CONSOLE staticMethods in: 'writing text'writeLine: format {STRING} with: arg0 {OBJECT?}. {~ cilName: 'WriteLine'}
Methods written in SABLE may also have a cilName. Using the cilName set in >#sayHelloTo:, our example above is callable from C# with this code:
HELLO speaker = new HELLO(); speaker.SayHello("SABLE");
A Block is a sequence of statements in square brackets. A block may accept arguments (examples appear later); they may result in a value, in which case the last statement is that result (if it's an expression). A block establishes a scope; any variables declared in a block exist only until the block ends.
The blocks in >#main: are Runtime Blocks. They are always inlined, which means their code is emitted directly in the method where they appear; they're never passed around as objects. Their types are represented by a special parameterized class added to the core library, BLOCK. A runtime block can be used only as an argument to primitive and macro messages, or as receiver of messages defined in class BLOCK.
A Primitive is a method added into the core library which the SABLE compiler processes, emitting code to create the specified result. For example, above we invoked BOOLEAN>~>#then:else:, declared as follows.
~[R].then: trueBlock {BLOCK[^R]} else: falseBlock {BLOCK[^R]} ^{R}. {~ primitive: #ifTrue:ifFalse:; nonfunctional} {~ generic: R is: #Voidable}
This declares a method parameter, R. Its #Voidable constraint allows the parameter to be bound to type {VOID} representing no result value. In our example, the parameter type of >#then:else: was {STRING}. We didn't have to say so explicitly; the compiler inferred that from the context. It's extremely rare to need to specify message parameter types (so we won't cover it), but SABLE does provide a simple syntax for it.
A Macro is simply an inlined method. This is a hugely powerful feature that simplifies client code. Since they are always inlined, they can be added to external libraries, making them even easier to use. (This has implications when sharing source code, but there are solutions not covered here.)
Macros have one purpose, but it's a big one. Macros encapsulate the common usage patterns of a class, making those patterns simpler to express. As such, they allow a class to offer a very wide vocabulary with a rich set of access options, without adding new methods to the class in CIL. And combined with blocks, macros allow you create your own control structures; we'll revisit these important concepts later.
It's easy to define new macros, simply use a pragma message. ARRAY[T] defines these instance macros which we used above. As is typical, they are defined in terms of other operations on the same class. (All LISTs have these, actually.)
notEmpty ^{BOOLEAN}. {~ macro} "Does this collection have any elements?" ^My length ~= 0 first ^{T}. {~ macro} "The first element in the array, which must exist." {~ require: 'not empty' as: [Me notEmpty]} ^Me at: 0
Without these, we would have to write:
|name| := args length > 0 then: [args at: 0] else: ['World'].
The differences may be small in this case, but they add up. (And the difference is bigger in most cases, especially when blocks are involved.) This is an extremely valuable feature which we'll see again and again. Using macros can reduce the size of your code with no sacrifice to runtime performance. If developer productivity is correlated to the amount of code you have to write, then SABLE macros can make you more productive.
Macros are not the place for complex algorithms. To paraphrase an old adage, "Keep your methods simple and your macros simpler."