Tingle Prototype Features


At version 0.9.5, the core of Tingle is complete and unlikely to
change much in the future.  Its collection of data types and primitive
operations is very limited though, making it a demo version of what
a Tingle 1.0 could be.

The file language.txt presents an ideal version of the language; and 
this file prototype.txt documents the quirks and experimental features
of the prototype, which are still subject to change.  We start with
a rather sketchy language reference.


1  Informal language reference

Data types:

    String, Integer, Boolean, Object, Nil

Literals:

    True, False, Nil

    Strings take double quotes.

Primitive functions:

    Name       Arity

    print        Any
    read           0
    +              2
    -              2
    *              2
    /              2   
    %              2   (rest after integer division)
    ++             2   (concatenation)
    unary -        1
    unary +        1
    unary !        1
    ==             2   (pointer equality when objects; asnew preserves equality)
    !=             2
    >              2
    >=             2
    <              2
    <=             2
    abs            1
    first          1   (first letter of a string)
    rest           1
    length         1
    isString       1
    isInteger      1
    isBoolean      1
    isObject       1
    isNil          1
    toString       1
    toInteger      1
    companion      1   (returns companion object of the class of an instance)
    random         0

   The above functions are the only real functions in the system (as
   opposed to methods).  They are only available through scoping.

    &&             2
    ||             2

   The logical and and or are really syntax rather than functions, to
   support evaluation of their right argument by need.  

Ideas and classes:

   as x { ... }
   class x { ... }

   These are equivalent, but "as" is prefered, to contrast with class
   composition.  Declarations inside an idea block may be separated by
   semicolons, but do not need to be.

   in x { ... }
   at x { ... }

   class <name> = <name> ...

   Class composition is actually an ordinary executable statement,
   which returns Nil; it is just the parser that refuses to accept it
   anywhere but at top level; and the REPL knows not to print its
   result.  (In earlier versions, this syntax was accepted everywhere
   where a statement could appear.)

Methods:

   About methods we should only note that declaring local variables
   is done as follows:

      def ( parameter, parameter, ...
          | local variable, local variable, ... ) {
      ... }

   A method body is a statement, customarily a block statement.

Data:

   Generating trivial getters and setters:

      var <name> , ...
      var <name> | <method> | , ...
      var <name> | <method>, <method> | , ...
      var <name> = initializer , ...
      var <name> | <method> | = initializer , ...
      var <name> | <method>, <method> | = initializer , ...

   The first method name is for the getter, the second for the setter.
   If only 1 method name is given, it is a getter.

   The setter returns "self".

Access control:

   The keyword "var" can be preceded by "private".

   The keyword "def" can be preceded by "private" or "protected" or
   "semiprotected"/"semi", and so can getter and setter names.

Statements:

   <expression>

   { <statement> ; ... }       (return value of the last expression
                                or Nil for the empty block)

   The difference between statements and expressions is syntactical
   only; statements have a return value just like expressions.

Expressions:

   <name>

   <name> = <expression>

   <name> . <method> ( <expression> ... )

   <method> ( <expression> ... )

   super . <method> ( <expression> ... )

   self . <method> ( <expression> ... )

   self

   <scoping> <name>

   <scoping> <name> = <expression>

   <scoping> <method> ( <expression> ... )

   new <classname>

   new // create a new object of the same class as the current one
       // or of the associated class when used in a companion

   renew <expression> <classname>

   asnew <expression> <classname>

   with <ideaname | classname> <statement>

   if <expression> then <statement>

   if <expression> then <statement> else <statement>

   while <expression> <statement>

   do <statement> while <expression>

   <literal>

   ( <expression> , ... )

   ( <expression> , ... ) . <method> ( <expression> ... )

   Arithmetic expressions can be formed out of these building blocks
   using the usual infix notation.  Method sends can be cascaded.

   Note that you can use "new" with a single idea in the prototype,
   meaning: "create an object of an ad hoc class composed solely out
   of this idea".  I never bothered to have companions generated for
   these, though.  Nor are they affected by layer activation.  So
   only for quick and dirty experiments.

Scoping:

   ::
   <ideaname> ::
   <ideaname> :: <ideaname> ::  etc.

   When classes are used as ideas, class names may be given too.

Name spaces:

   <ideaname> @ <namespace>
   <classname> @ <namespace>
   
   namespace <namespace>

Comments:

   C++ style:  /* */ and //

The read-eval-print loop:

   At the prompt, the prototype accepts as/in/at blocks, "class ="
   declarations, declarations of global data and methods outside of
   all classes, and statements and expressions; and you can change
   name spaces.  Name space declarations do not go with textual
   blocks, making them easier to use in a read-eval-print loop: they
   stay valid until the next name space declaration.

Source files:

   For source files, there is the extra rule that executable
   statements at top level (i.e. not nested inside another construct)
   have to be preceded by the keyword "run".  One might use "run" with
   a block statement as a "main" function.

   Name space declarations and "class =" declarations can be followed
   by a period (".") but this is optional.

   At the end of each source file, the current namespace is reset
   to "main": namespaces do not carry over into the next file or the
   read-eval-print loop.


2  Adding code directly into a class

Left unspecified in the Introduction to Tingle, here is the current
semantics of adding code directly into a class:

as Widget@WidgetLibrary {
   as myWidget {
      ...
   }
   def grok_this() { ... super.method() ... }
}

Scoping: the method grok_this() is in scope for the intersection of
Widget and myWidget, and hides any top method with the same name in
the class Widget.  The body of grok_this() itself also only has access
to the top methods of Widget.  Thus super.method() can only refer to
a method outside of the scope Widget, which in the example can only
mean a globally defined method.

Top method: it is as if grok_this() is in the intersection of all
classes out of which Widget itself is composed.  One could expect that
if a method with the same name already existed in that intersection,
it would be overwritten; and that would be a perfectly good semantics.
That is not what happens in Tingle though: it means that two methods
with the same name are both present in the same intersection, which of
course means that neither can be top.

The reason is that Tingle allows classes to be recomposed at run time,
so that methods that are defined directly in a class can "move" to
another intersection.


3  Fun with laziness

Initializers are evaluated the first time a variable is read.  This
means that we have some laziness in the language, and that -- purely
to illustrate the semantics, and in no way implying that this
represents a recommended programming style -- we are going to
construct a lazy stream of fibonacci numbers using initializers.  Note
that initializers of variables that are assigned to before the first
time they are read from, will never be executed, so that the
initializer of "prev" will only ever be executed for the initial node:

as node {
   var value | getValue, setValue | = 1

   var prev  | getPrev, setPrev | = (new node).setValue(0)
   var next  | getNext, setNext | =
      (new node).setPrev(self)
                .setValue(prev.getValue()+value)
}

var fib = new node

run while True {
   print(fib.getValue(), "\n"); fib = fib.getNext()
}

Also note that lazy initializers can lead to correct-but-unexpected
behaviour during interactive sessions.  Compare:

       > var a = 1       > var a = 1
                         > a
                         1
       > var a = 2       > var a = 2
       > a               > a
       2                 1

In these examples, we redeclare a variable and replace not its value,
but its lazy initializer.  In the transcript to the left, "a" has
not been dereferenced before it was redeclared, so the original
initializer has not been evaluated and its value has not been stored
as the value of the variable yet.  At the right, it has, and so
the new initializer never gets used.  (You are unlikely to make this
mistake in program files though, as declarations and executable
statements such as assignments are clearly separated there.)


4  To do

Tingle currently allows nesting with classes without any limitations,
in effect totally blurring the difference between ideas and classes;
which is nice for experimentation, but way too powerful for a
practical language: to maintain programmer sanity, we should really
enforce that all the ideas making up a class used for scoping are from
another name space.

The Tingle interpreter does not check whether classes are subtypes of
all classes that are made up of a subset of their set of ideas; it
may be useful to at least have a facility to generate warnings.  There
are several possible causes for classes not to be subtypes of their
parts:

- composition can "cancel" methods because of an unresolved conflict
  in determining the top method;
- a protected or private method can hide a "lower" public one;
- methods can be overridden with methods with a different amount of
  parameters.

As Tingle is not fit for subclassing for construction, it might be
nice to introduce some kind of delegation, for instance with instance
variables that are declared with "fwd" instead of "var".  These
could export their top methods to the scope that the objects
themselves are in.


Dirk van Deun, 2007-2012 (dirk at dinf.vub.ac.be)
