Tingle Prototype Features At version 0.9.8, 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. In fact, the above paragraph has not changed except for the version number since version 0.9.1 in the year 2010. 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) clone 1 (copies ordinary objects; forbidden for companions) random 0 Using first and rest you can convert strings into lists (made up of cons cell objects) of character strings, and continue from there to do string manipulation. 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 = ... 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 can also be done as follows: def ( parameter, parameter, ... . local variable, local variable, ... ) { ... } This syntax has become redundant now that blocks can declare local variables themselves, but there is no good reason to abolish it. You can also use a semicolon or a broken bar instead of a period to separate parameters from locals. It should be noted that the syntax for declaring local variables at the block level is a rather superficial convenience. For efficiency, local variables declared in blocks are "pushed up" to the method level: they remain present during the execution of the whole method, even when they are not in scope and therefore not accessible. (For this purpose, initializers defined as in the next section are treated as method bodies, as are "run" blocks and executable statements typed directly at the prompt.) This also implies that local variables declared in blocks are initialized to Nil only once per method activation, not every time control enters the block. Of course, if you are not in the habit of reading local variables before writing to them, you should not even notice. A method body is a statement, customarily a block statement. Data: Generating trivial getters and setters: var , ... var : , ... var : / , ... var : / , ... var = initializer , ... var : = initializer , ... var : / = initializer , ... var : / = initializer , ... The first method name is for the getter, the second for the setter. 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: { ; ... } (return value of the last expression or Nil for the empty block) { | , ... | ; ... } (with local variables) Statements have a return value just like expressions. In older versions there actually was a reason for distinguishing statements from expressions. Expressions: = . ( ... ) ( ... ) super . ( ... ) self . ( ... ) self = ( ... ) new new // create a new object of the same class as the current one // or of the associated class when used in a companion renew asnew with if then if then else while do while for in // expression should evaluate to an iterator, being an object // with a public next() method: the loop variable will be set to // the consecutive values returned by this next() method and // the body statement executed for each, until next() returns Nil ( , ... ) ( , ... ) . ( ... ) 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: :: :: :: :: etc. When classes are used as ideas, class names may be given too. Name spaces: @ @ 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. Name space declarations can also be followed by a colon. That looks cooler. 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 For loops For loops in Tingle work with iterators, as in Python. A small example will illustrate this: // LIBRARY namespace iterator: as range { var current: / semi setCurrent, end: / semi setEnd def next() if current == end then Nil else current = current + 1 } in range { def make(start, end) (new).setCurrent(start - 1) // no problem with bignums .setEnd(end) } class Range = range // MAIN PROGRAM namespace main: class Range = Range@iterator // import run for value in Range.make(1, 20) print(value, " ") 5 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-2015 (dirk at dinf.vub.ac.be)