Pepsi and Coke (No. 4)

I thought I was going to write about the "syntax" thing, but I think it is worthwhile to clarify myself on objects and the execution system first.

So, objects. After spending a few days on Jolt Coke, I went back and re-read the page at http://www.piumarta.com/pepsi/pepsi.html and http://www.piumarta.com/pepsi/coke.html (read the documents; what a concept). And, I found there are a lot of stuff I ignored first^^; Anway, here is what are important to get the feel for it.

  1. Objects in the Jolt/Coke system is based on the "Id" Object model.
  2. There is a "hard-wired" language (default language) called "IdSt" on this model.
  1. The Id object model is "a kind of" prototype-based model. But, there is a concept of a "family" of objects. The objects in a family have shared behavior. The shared behavior is provided by a "vtable" object.
  2. In IdSt, there is a default tree of families of objects that looks like an inheritance hierarchy of a typical OO language.
  3. For IdSt, there is a static compiler that can create an executable binary.
  4. The Jolt system (the interactive shell) is written in IdSt and compiled to be an executable binary.
  5. In the Jolt binary, IdSt objects are "there for your service". From the "above" (i.e, the code running on the interactive shell), you can have access to these objects. The stuff you write in []'s are these objects and messages to them.
  6. You can also modify the behavior of these objects at compile time and runtime. You can go down pretty deep. (And, Jolt is just a prototype. We are supposed to be able to go down even deeper in the future systems.)

To understand the Id object model (1), and the IdSt language (2), you should just read the page, read the code and try by yourself.

The concept of family of objects (3) is a bit different from Smalltalk's class hierarchy. If you think about a class-instance model, there is an object that is bound to an globally accessible identifier, and it holds the method dictionary that dictates the behavior of guys in the family. In IdSt, there is globally accessible one in the family via an identifier, but it is just like other ones in the family. In the other words, there are bunch of objects that share the same vtable, and only one happens to have global name. (That one is the prototype of the family.) For example, there is a code snippet from "main.st" that shows how the Singleton pattern is implemented:

Options : Object ( verbose )
Options verbose	[ ^verbose ]
Options new
[
    self := super new.
    verbose := false.
]
[ Options := Options new ]

The first line defines a new family called "Options" that is a subclass, err, a family, that delegates to "Object". An object in the family will have an instance variable called "verbose". (And, the first line actually create the prototype object in the family.) The second line defines a simple getter for the instance variable. The next definition of "new" defines what to do when a new instance is being created. "self := super new" may look strange, but it simply substitutes the default receiver for the rest of method. "verbose := false" sets the instance variable of the newly created instance to false. (Note from Göran: And since this method is meant to return a new instance it will do so, since there is an implicit return of self at the end, which now refers to the new instance instead of the prototype Options instance.)


Then, the last line is executed. The line creates a new instance and replaces the original prototype with it. (It is not exactly a singleton, as you can substitute the prototype, but anyway...)

And, do you remember my "vector-map" example? Like #collect: in Smalltalk, I wanted the result returned from "vector-map" to be the same kind as the "list" argument's, but I was using the definition below that always returns an Array, no matter what kind of collection is given.

(define vector-map
  (lambda (func list)
     (let ((s [list size])
           (ret [Array new: [list size]])
	   (idx '0))
	(while [idx < s]
	   [ret at: idx put: (func [list at: idx])]
	   (set idx [idx + '1]))
	ret)))

The right code should use "[list new: [list size]]", instead of "[Array new: [list size]]". The instances share the same vtable, and know how to do prototypical operations like "new:".

There is a default hierarchy of families (4). If you look at examples/jolt/Object.st, there is a portion of code:

Object : _object ()				
  UndefinedObject : Object ()
  Magnitude : Object ()
    Time : Magnitude ( _seconds _nanoseconds )
    Number : Magnitude ()
      SmallInteger : Number ()
      Float : Number ()
  Symbol : Object ( _size _elements )		
  Association : Object ( key value )
  Collection : Object ()
    SequenceableCollection : Collection ()
      ArrayedCollection : SequenceableCollection ( size )
        Array : ArrayedCollection ( _oops )
        ByteArray : ArrayedCollection ( _bytes )
          String : ByteArray ()
      OrderedCollection : SequenceableCollection ( firstIndex lastIndex array )
    IdentitySet : Collection ( tally lists )
      IdentityDictionary : IdentitySet ()
        FastIdentityDictionary : IdentityDictionary ()
  SlotDictionary : Object ( _size _tally _keys _values default )
  StaticBlockClosure : Object ( _function _arity )
    BlockClosure : StaticBlockClosure ( outer state _nlr )
  SinkStream : Object ()
  ReadStream : Object ( collection position readLimit )
    WriteStream : ReadStream ( writeLimit )
    FileStream : ReadStream ( file )
    ConsoleFileStream : FileStream ( _prompt )
  File : Object ( _fd )
  Function : Object ()
  Random : Object ( seed a m q r )

Note that this hierarchy will be changed, and also you can change it as you like. Nothing is special about this structure (err, probably a few things are special). Also note that there are a few more family definitions in other files in the source code of current Jolt system.

The static compiler that compiles the Jolt system and creates an executable is "idc" (4) (5). Check the documents.

The IdSt objects that implement the Jolt system are alive and accessible from the interactive shell (6). You can import a prototype (or any object) by using "import" function from the interactive shell:

(define Array (import "Array"))

The definition of "import" is in boot.k. It is very simple. (As I gather, "_libid_import" imports a symbol from the main running executable.) You can define a new family with form:

(define-type	MyType		Object (inst var names))

and add a method to it with form:

(define [MyType someMethodNameLikeFooBar: anArg]
    [anArg + '1])

You can modify the behavior of interpreter quite deeply. For example, there is no expression that terminates the interactive shell and let us suppose you would like to implement one. If you look at examples/jolt/main.st, there is the default read-eval-print loop of the interactive shell.

The core part of it (it is in the equivalent of the main() function of the program) reads:

[(expr := scanner next) isNil]
     whileFalse:
     [Options verbose ifTrue: [StdErr print: expr; cr].
     expr := expr eval.
        echo ifTrue: [StdErr nextPutAll: ' => '; print: expr; cr]].

It (literally) reads the next expression from "scanner" (a CokeScanner), evaluates it, and prints it (if "echo" is true).

As you see, the whileTrue: loop above terminates when it gets nil from the scanner. So, if you type the following two lines into the shell:

(define CokeScanner (import "CokeScanner"))
(define [CokeScanner next] 0)

the definition of CokeScanner's next is changed so that it always returns NULL (or nil), therefore the loop terminates. Or, you can define:

(define bye (lambda () (define [CokeScanner next] 0)))

and type "(bye)" to quite the shell.

BTW, I haven't measure things, but as I gather, if you re-define the parts of Jolt written in IdSt from the interactive shell, things may run "faster". The code generated by the idc compiler doesn't look awfully efficient, but the dynamic code generator (it is not very well optimized by the way) bypasses some stuff.

Okay, this was the my basic "objects and how it works" explanation.

A unrelated note: a stumble block I tripped a few times was that the comparison operators in the []-world may return naked 0. For example, "['1 > '2]" returns 0 (or nil). You can simply think that the returned object is UndefinedObject and its representation in the ()-world is 0. As Alex pointed out, you can write:

  (define [UndefinedObject m] [StdOut nextPutAll: '"hello world\n"])

then you can call that method on nil:

  [['5 > '6] m]

I'm getting some understanding on how the Compiler and native code generation works. Tomorrow, I may write about that, or stick to the original plan and write about the syntax stuff.