Introduction to Tart

“Whenever a programmer has the urge to create a new programming language, the proper response is to lie down on the couch and wait until the feeling passes.” – Anonymous

Tart is a general-purpose, strongly-typed programming language.

Tart is intended for systems programming and high-performance applications such as audio synthesis applications, computer games, real-time video processing, and simulation. It can also be used as a systems programming language.

Tart borrows many ideas from other programming languages, including C, C++, Python, Java, C#, D, Haskell, Ruby, Scala and many others. At the same time, it also includes a number of features not seen in any of these languages.

Tart is a statically-typed language that has been designed for high efficiency. Tart allows you to get “close to the metal”, in the sense that you get access to the details of the native platform. At the same time, Tart is a very high level language which supports things like type deduction, multi-method dispatch and metaprogramming.

Tart is designed to be easy to learn, but its also designed to be “expert-friendly”. Like a musical instrument, Tart enables a true master to create “virtuoso performances” of excellence and creative power.

Motivations

Why create a new programming language, when there are already so many good ones out there?

The answer to this question is complex, because there’s no single motivating reason that stands out from all the others. Some of these motivations are:

  • The desire for a language that would combine the simplicity and readability of Python with the power of static typing and template metapgrogramming, as well as modern language features such as closures and generic functions.
  • The desire for a compiler that compiles to highly efficient native code instead of a VM.
  • The desire for a language which would fulfill the same role as C++, but designed from scratch with the benefit of hindsight.
  • The desire for a language which would fulfill the same role as Java, but more concise and requiring less verbose boilerplate.

Features of Tart

  • Able to be compiled directly to native code. Although there may also be versions of the Tart compiler that are targeted at virtual machines such as the JVM and CLR, Tart doesn’t require a VM.
  • Strong static typing. Tart’s type system is optimized for expressiveness and power. Types can be automatically inferred in many cases, which means that the code isn’t cluttered up by redundant type declarations.
  • Support for “effect declarations” - meaning that you can annotate what effects a particular function has, and those effects can propagate upward at compile time to the calling functions. So for example, if a function requires doing I/O, or is not thread-safe you can annotate this fact with an effect.
  • Built-in support for garbage collection - but also allows explicit allocation when required.
  • Able to interoperate with libraries written in other languages through the platform ABI using a foreign function interface (FFI).
  • Supports a variety of programming styles: object-oriented, functional programming, imperative programming, generic programming, metaprogramming, and so on.
  • Support for concurrency and multiprocessor programming.
  • Built-in support for reflection and type inspection.
  • Supports operator overloading via generic functions. Writing a new ‘+’ operator is simply a matter of writing a new specialization for “Operator.add”.
  • Support for closures and anonymous functions.
  • Regular, unambiguous syntax that is easy to write parsers for. It will be easy to support Tart in IDEs and refactoring tools. At the same time, Tart’s syntax has been designed with humans in mind - the syntax provides just enough visual variety to allow easy recognition of common coding idioms.
  • Direct support for Unicode. Tart’s source files are unicode files, and unicode characters are allowed in identifiers.

Things that Tart doesn’t do

Tart can’t do everything! Here’s some things that some other languages do that Tart doesn’t:

  • Tart doesn’t support run-time compilation of code or creation of classes, although some implementations may provide this an an extension. Many of the kinds of things you would want to do with run-time class creation (such as automatic creation of mock implementation classes for testing) can be done at compile time with Tart’s template system.
  • Tart doesn’t have a preprocessor like C and C++. It has macros, but they operate on the AST level.
  • Tart doesn’t allow C-style pointer math. It’s more like Java/C# in this respect.
  • Tart isn’t finished.

Tart is still in an early development stage. That means that some aspects of the language may change. At some point the design of Tart will be “frozen”, but for the moment there’s no guarantee that the code you write today will be compatible with future compilers.

About the name “Tart”

The name “Tart” was chosen because it has the following attributes:

  • Its a 4-letter word that is easy to say and remember.
  • Like some other successful programming languages, it shares its name with a tasty food product.
  • It’s a fruitful (pun intended) source of metaphor, which will be important for all those future tech magazine writers who are looking for a new angle for their latest article.
  • The name “tart” also implies promiscuity - which is appropriate, because Tart will interoperate with just about anybody.
  • Unlike many great name ideas, “tart” isn’t taken yet.

Now, you may ask: Do the letters “TART” stand for anything? The answer is: Yes they do. In fact they stand for a lot of different things. And you, the Tart users will get to decide what some of those things are. You see, each release of the Tart compiler will include (and be named after) a new definition of what “Tart” stands for. This definition will be selected from suggestions sent in by Tart users. So start working on those backronyms!

Some Examples of Tart Code

Here’s some examples of Tart code to get you started.

Let’s start by declaring a new type. Let’s create a new structure, named Point, which contains two integers x and y:

// A basic structure declaration
struct Point {
  var x:int;
  var y:int;
}

p = Point(10, 10);

So far it looks pretty much like most programming languages that you’ve seen before. One difference is that the type comes after the name. The reason for this is that, unlike C and C++ where the name of the variable can sometimes occur in the middle of the type declaration (an example being int * x(const int)), in Tart the name and the type are always kept separate.

The colon character is used in Tart to mean “of type”. So x:int means a variable named ‘x’ of type int.

Tart supports both struct and class, but they don’t mean the same thing that they do in C++. Instead the distinction is more like the one in C#: A struct is a value type, while a class is a reference type. That means that normally when you assign one struct instance to another, you are doing a member-wise copy of the entire thing, whereas when you assign a class instance all that gets copied is a pointer to the object.

Variable declarations

In Tart, there are three keywords that are used to define a data element: var, let, and def. var defines a variable - in other words, a mutable data element. let defines an immutable constant. def defines a function or property.

Let’s start by looking at let and var. Both allow the type of a declaration to be deduced from its initialization expression:

// Type can be automatically deduced from the initialization expression.
let a1 = 1;
var a2 = "Hello, World";

// Type can also be explicitly stated
let b1:int = 2;
var b2:String = "A string";

// For a 'var', you don't have to specify a value, but for 'let' you do.
var c1:int;
// let: c2:int; -- error, no value specified.
// var: c2;     -- error - no type and no way to infer it.

As you can see, you don’t have to include the type if it can be inferred by the compiler. Also note that in the case of let, the compiler may or may not allocate storage for the variable, depending on the circumstance.

Array variables are declared using brackets after the type name:

// An array of unspecified length
var a1:int[];

// An array of fixed length 10
var a2:int[10];

// A two-dimensional array
var a3:int[10,10];

// An array of arrays
var a3:int[10][10];

// A two-dimensional array, where one dimension is unspecified:
var a4:int[,10];

You can also declare functions with let and var:

let f1 = fn (x:int, x:int) -> int {
  sys.stdout.println("Hello, World!");
  return 1;
}

The symbol -> means “returns type”. It’s used to indicate the return type of a function. So function (x:int, y:int) -> int is actually the complete type of the function.

The let syntax for functions is kind of cumbersome though, so we have def which is a shortcut:

def f1(x:int, y:int) -> int {
  sys.stdout.println("Hello, World!");
  return 1;
}

Of course, you can just leave off the return type, since its inferrable from the return statement:

def f1(x:int, y:int) {
  sys.stdout.println("Hello, World!");
  return 1;
}

The def keyword can also be used to define a property, which is a getter/setter pair:

def myProp:int {
  get { return p; }
  set (value) { p = value; }
}

myProp = 1;   // Sets p to 1.

The let and var keywords can be used almost anywhere. For example, they can be used inside a class to declare a class or struct member, as we saw before. They can also be used to declare local variables:

def square(x:int) {
  let x2 = x * x;
  return x2;
}

Note that even though let declares an immutable value, it does not have to be a compile-time constant. What let really means is “bind this name to this value in this scope”. Once bound, the binding cannot be changed. (In fact, if you only use let and never use var, you can do pure functional programming.)

Like many programming languages, you aren’t allowed to put an assignment in a conditional expression:

var m:Match;
if (m = re_ident.match(s)) // Illegal
   return m.group();

You can, however, introduce a new variable inside a conditional expression using let or var and assign to it. The scope of the variable includes the body of the if-statement:

if (let m = re_ident.match(s))
   return m.group();

In the case of the ‘for...in’ statement, there’s an implicit let for the iteration variable:

for (a in 1..10) {
  // do something
}

A short interlude

At this point, you may be wondering why have three keywords - let, var and def - when in fact with a smart compiler, one keyword could do the job. In fact, an earlier version of Tart didn’t use any keyword at all, just a punctuation symbol to introduce a new name. The problem with this earlier syntax is that the syntax was too regular - it was hard to tell one part of the program from another.

Tart’s syntax is designed for humans as well as machines. One consequence of this is that the grammar is deliberately inconsistent - not greatly inconsistent, but just enough to provide a certain amount of variety to make common coding idioms pop out visually. Similarly, punctuation characters are used judiciously, just enough so as to support quick visual recognition of the structure of the code.

Lots of things can be omitted if they aren’t needed. The goal is to have a syntax that allows the programmer to express their intent clearly, without a lot of clutter getting in the way. But not so terse and compressed as to look like line noise.

Nullable types

In many programming languages, the special value null means ‘no object’. Any reference to an object can be assigned the null value, regardless of type.

In Tart, the null value is a distinct type, which is not convertable to the type of an object. This means that you cannot assign the value null to an object reference:

var x:String = null; // Error - can't assign 'null' to variable of type 'String'

What you can do, however, is define a reference that is a union of an object type and the null type by adding a ‘?’ suffix to the type:

var x:String? = lookupName();
var y = x;        // Type of y is also 'String?'
var z:String = x; // Error - x could be null

If the compiler can detect that a value is non-null, then this ‘nullable’ aspect is stripped off of the type:

var x:String? = lookupName();
if (x != null) {
  var z:String = x; // OK
}

Being able to express whether or not a value can be null allows the compiler to make a number of interesting optimizations and also allows the programmer to more easily reason about program correctness.

Nullable types also affect the behavior of type casts. Tart uses as to represent the type cast operator:

var obj:Object;
var s = obj as String; // Throw exception if obj is not a String

In the above example, obj is either successfully cast to String if the object is indeed a String, or if it is not, an IllegalTypecastError is thrown.

However, if you make the type nullable, then it acts more like a C++ dynamic_cast - it returns a null value if the cast operation failed:

var obj:Object;
var s = obj as String?; // Assigns null to s if obj is not a String.

More on function types

The keyword fn declares a variable having function type. It can be used to declare a named function or an anonymous function (also known as a ‘function literal’.)

Normally, its easier to use def to define a function and bind it to a name in one step. Def requires that the function have a name, however, and when working with with function types (where we don’t know the name of the specific function) then we have have to use fn.

The fn keyword is followed by an argument list. Normally the argument list will be in parentheses, however if there is only a single argument the parentheses can be omitted:

fn () -> int;   // No-argument function
fn x:int -> int;   // Single argument function
fn (x:int, y:int) -> int; // Two argument function

If you’re just declaring the type of a function, you can omit the argument name - but you still have to include the colon before the type name:

fn :int -> int;   // Single argument function type

The ‘->’ operator means ‘returns value of type’, and is right-associative. This allows you to define functions that return functions:

fn x:int -> fn y:int -> int;
fn x:int -> fn (y:int, z:int) -> int;

Note

Note for Haskell fans - the use of the -> operator looks similar to the way functions are defined in Haskell, but it’s actually quite different. In Haskell, functions can only have a single argument, and support for multiple arguments is done via currying. There are various reasons why Tart doesn’t do this [for one thing, it makes it difficult to support calls to the native ABI], but the consequence of this is that using -> alone to define a function in Tart would be syntactically ambiguous.

How would you declare the type of a function that takes another function as an argument? Like this:

fn(:fn i:int) -> int;

Note

The parentheses are needed because the precedence of the -> operator is higher than that of the fn keyword.

A function returning no value (i.e. a pure procedure) can be specified in the following ways:

fn :int -> void;  // A function returning void.
fn :int;          // Functions with no return type are assumed void,
fn :int -> ();    // A function returning no values.

If you don’t declare a return type, it is assumed to be void - unless the function has a body containing a return statement. If there is a return statement, then the type of the value being returned determines the function’s return type. If there’s more than one return statement, then it attempts to choose a return type that encompasses all of them, or signals an error if it can’t.

Note

The last syntax in the above example will make more sense once you’ve read the later section on functions with multiple return values.

Function arguments

In addition to regular positional arguments, functions support both keyword and variadic arguments. Variadic arguments are indicated via an ellipsis (...):

fn (format:int, args:int...);

Variadic arguments must be the last positional argument. The actual type of a variadic argument is a variable-length array of the given type. So in the example above, the actual type of args would be int[].

Default argument values are specified using =:

fn (format:String, npos:int=0, sep=false);

Function arguments use the same kind type deduction as var and let statements: if the type of an argument is unspecified, the type will be derived from the type of the default value.

Like Python, parameters can be referred to by name as well as position:

print("Hello, World!\n", padding="");

Any parameter can be referred to by its keyword name. The normal mapping of arguments to formal parameters is that positional arguments are assigned first, in order, and then any keyword arguments are assigned to any remaining unfilled parameters.

Sometimes it is useful to specify a parameter that is “keyword only” meaning that it can only be specified via keyword instead of positionally. A semicolon can be used to segregate regular positional parameters from keyword-only parameters:

fn (format:String; npos:int=0, sep=false);

In the above example, only the format argument will be filled in by positional argument - to have more than one positional argument in this case would be an error.

Function arguments are also immutable by default, however you can add a var modifier to make them mutable:

def imperativeFactorial(var n:int) {
  for (a in 1..n) {
   n *= a;
  }
  return n;
}

Caveat

It’s likely that this feature will be removed, making arguments mutable unless declared otherwise. Mutable arguments turn out to be more useful than I had first anticipated.

You can also pass values by reference by adding the ref modifier:

def swap(ref a:int, ref b:int) {
  let temp = a; a = b; b = temp;
}

Ref is not a general-purpose referencing mechanism like the C++ ‘&’ operator, but only applies to function arguments. You can use ref to return multiple return values from a function (although there is a better way - see the next section.)

Unpacking sequences

Tart supports a Python-like ability to unpack variables from a sequence:

let a, b = [1, 2];
let a:int, b:int = [1, 2];

The last variable in the unpacking assignment can be a variadic argument, meaning it scoops up all the remaining values:

let a:int, b:int... = [1, 2, 3, 4];

As with function arguments, the ‘...’ syntax changes the type of the argument into an array of the explicitly declared type. So the type of b is actually int[], and would in the above example be assigned the value [2, 3, 4].

Variable unpacking works with regular assignment as well, allowing for the Python ‘swap idiom’:

a, b = b, a;

Returning multiple values

Although Tart doesn’t have a built-in ‘tuple’ class like Python, you can use the variable-unpacking syntax to return multiple values from a function. First, you need to declare the function signature as returning multiple values:

def returnStringAndInt() -> (String, int) {
  return "Hello", 12;
}

let a:String, b:int = returnStringAndInt();

Note

Because the comma operator has a low precedence, you’ll have to put parentheses around the list of return types if there is more than one.

Unlike Python, a function that returns multiple values does not actually create a ‘tuple’ object for the return results. Instead, one value is returned via the normal function return mechanism, and the other value is passed to the function by reference and filled in.

What’s nice about using the multiple-return-value syntax (as opposed to ref arguments) is that the compiler can know for certain that the variable has changed. This allows the compiler to do additional optimizations:

var a:String, b:int
a = 10; // Dead code
a, b = returnStringAndInt();

If a had been passed as a reference and filled in by the function, the compiler wouldn’t be able to know that the second line was dead code - because the compiler doesn’t know whether a was actually filled in or not.

Lists, maps, and comprehensions

Tart supports a literal syntax for lists and maps, similar to Python, Perl, and other dynamic languages:

let x:int[] = [1, 2, 3, 4];

let flavors:Map<String, int> = {
  "Apple", 1,
  "Cherry", 2,
  "Lemon", 3
};

Normally, it will deduce the type of the literal from the thing it’s being assigned to. In cases where that’s not sufficient, you can specify the type explicitly:

let x = [1, 2, 3, 4]:int[];

let flavors = {
    "Apple", 1,
    "Cherry", 2,
    "Lemon", 3
}:Map<String, int>;

Tart also supports a list comprehension syntax similar to that used in Python:

let x = [n for n in 0..10]:int[];

As with list literals, you can specify the type if you want to be explicit.

Declaring new types

We’ve already seen how to declare a struct. Let’s look at some other type declarations in a more complex example. Most of them should be relatively familiar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Declare a class. The base class is 'ListNode'.
class Shape(ListNode) {
    // Declare an enumeration
    enum Style {
       Filled,
       Hollow,
    }

    // Private member variables.
    private {
       // Allow the unit test access to these vars.
       friend ShapeTest;

       // Some variables.
       var fillStyle:Style;
       var fillColor:Color;

       // A variable with parameterized type.
       var children:List<Shape>;
    }

    // A member function.
    def draw(dc:DrawContext) {
       dc.setFillStyle(fillStyle);
       for (child in children) {
          child.draw(dc);
       }
    }

    // A method with variable number of arguments
    // The 'children' argument's actual type is Shape[]
    def addChildren(children:Shape ...) {
       // Explicitly qualified self.children to disambiguate
       // from same-named parameter.
       self.children.extend(children);
    }
}

A couple of things are worth noticing in this example:

You can declare a block of variables as private, rather than having to put the word private in front of every variable name. Within a private or protected block, you can declare friend classes that have direct access to just these class members.

Note

The last point deserves some additional explanation: With C++, you can only declare a friend of an entire class. In Java, you can’t declare a friend at all. Both of these feature choices cause programmers to expose too much encapsulated data. In the case of C++, you can’t expose a private member without exposing everything. In Java, not having the ability to expose private data to certain classes causes Java programmers to declare class members public far more than the would otherwise.

Like most object-oriented languages (other than C++), all functions are dynamically overridable (virtual in C++ parlance) unless declared final.

C# requires that you add the modifier overload to any function that overloads a same-named function in the superclass. This prevents accidental hiding of superclass functions.

Annotations

None of the language features described so far are all that different from what you can do in C++. And that’s OK - because a lot of the intent behind Tart is to produce a “cleaned up” C++.

But there’s a few tricks that Tart can do that are rather interesting.

Tart uses the C# syntax for declaring annotations:

[throws(ArrayBoundsException)]
def lookup(index:int) {
    return table[index];
}

However, in this case throws isn’t just a normal annotation, it’s an ‘effect’. Effects are special, in that they are ‘viral’ - they propagate to the caller, much like the way checked exceptions work in Java:

// This function implicitly has the 'throws(ArrayBoundsException)' effect, which it inherits
// by virtue of the fact that it called lookup().
def lookup2() {
    // Calling a function with an effect gives us that effect too.
    return lookup(0);
}

As you may notice, there are two big differences between effects and Java checked exceptions. The first is that effects aren’t hard-wired into the language - you can invent your own kinds of effects, just by deriving from the “EffectAnnotation” class. The second difference is that you don’t have to explicitly declare the effects in the signature of the calling function - instead it’s calculated for you.

There are also special kinds of annotations that remove effects: Just like try/catch can remove the effect of a ‘throws’ in Java, you can have statements that remove effects from a block of code. You can also have annotations that verify that a particular effect is present or absent:

[assertEffect(threadSafe)]
def threadSafeFunc() {
    // m.withLock removes the 'not threadsafe' effect.
    m.withLock({
       notThreadSafeFunc();
    });
}

So if you really do want to say “This function throws no exceptions other than DivideByZeroException”, and make sure that none of the functions that it is calling can throw that exception either, you can do so - and if that constraint is violated, a compilation error will let you know about it.

One of the big reasons for the “Effects” feature is that it allows you to reason about concurrent programs. Tart doesn’t have any special syntax for threading or synchronization - those are just library functions. What it does have is a way to make meaningful statements about the concurrent behavior of a function or class, and act on those assertions later.

Generic Functions

Like C++, Tart supports compile-time overloading of functions. So for example, suppose I have a toString() function that does something different depending on whether it is given an int, a float, or a string:

def toString(val:int) -> String {
    // Convert int to string
}

def toString(val:float) -> String {
    // Convert float to string
}

def toString(val:String) -> String {
    return val;
}

This works well, but only if the type of the argument is known at compile time. What if we want to select the method based on the actual runtime type of the object, as opposed to merely it’s declared type?

We can tell Tart to dynamically dispatch based on the actual type of the argument by using the virtual keyword. But wait – it’s not like C++! We use the virtual keyword on the argument, to indicate that this argument is dynamically dispatched:

// Declare a generic function
def toString(virtual val:object) -> String;

// Specialize for some type. We don't need to say virtual again.
overload toString(val:HashTable) -> String {
    // Convert HashTable to string
}

Note that if the compiler can figure out at compile time which function would be called, then it won’t use multi-method dispatch. So for example, if I say toString(5), it will know to use the int version, since integers can’t be subclassed.

Also, it will only use dynamic dispatching for types which are subclasses of the virtual argument. So since HashTable is a subclass of Object, it will use dynamic dispatching for all arguments of type HashTable.

It gets even more interesting if you have a function where some arguments are virtual and some are not. In this case, what happens is that it tries to find all of the matching overloads at compile time based on the declared types of the arguments. If one or more of those functions has some arguments that are declared virtual, then it will then use dynamic dispatching at runtime to select the correct specialization based on the runtime types of those arguments.

Statements

Tart has all the usual statements that you would expect: if/else, for, while, do/while, as well as a few new ones such as repeat.

Here are a few examples:

// if/else statement
if a > 2 {
  ++a;
} else {
  --a;
}

// while loop
while let m = re.match(str):
  console.stdout.writeLn(m.group(1));

// do/while loop
do {
  let x = buffer[i++];
} while (i < length) ;

// 'repeat' is a simple 'do-forever' statement.
repeat {
  let s = console.stdin.readLn();
  if s == null:
    break;
}

Tart supports both forms of for (the C++ and Python forms), just like Java & JavaScript do:

// C-style for loop
var total:int = 0;
for (var i:int = 0; i < 10; ++i) {
  total += i;
}

// Python-style for-loop
var fact = 1;
for n in 1 .. 10:
  fact *= n;

One thing to observe is that unlike C, you don’t need to surround the test expression of an if or while statement in parentheses. However, the test expression must be followed by a colon, unless it’s followed by a block statement { ... } in which case the colon can be omitted.

Because the break, continue and return statements are so often used conditionally, the Perl syntax of post-statement conditions is supported for these statement types:

break if a > 10;
continue if a < 10 and a not in 0..5;
return 10 if a == 10;

In the case of the throw statement, there’s no special syntax, but you can use the when() method of the exeption class:

// Throw IllegalArgumentError when index < 0
IllegalArgumentError.when(index < 0);

This latter syntax is especially convenient for implementing pre- and post-conditions within a function body.

Another useful statement is the with statement:

// Declare a new variable 'fh' and assign an open file handle to it.
with fh = File.open("unicode.txt) {
  // Do something with fh.
  // It will be closed when the block exits.
}

The with statement can be used to guarantee that the appropriate cleanup code is called after you are finished with an object. In the above example, the file handle fh will be closed upon exit from the with block, regardless of how the block was existed (even if via return or an exception.)

The with statement can also influence the set of effect annotations that propagate outward from within the contained block. Similar to the way a try statement can filter out an exception effect, a with statement that acquires and then releases a mutex could potentially remove a ‘thread-unsafe’ effect.

Operator Overloading

Tart supports a limited form of operator overloading. This is important for things like complex number classes, or vectors - data types which need a way to define mathematical operations which shouldn’t be built in to the language.

Unlike C++, however there’s no special syntax for operator overloading. Quite the converse - the ‘+’ operator is merely syntactic sugar for a call to Operator.add, in other words the function add() in the Operator namespace. The add() functions defines overloads for all of the appropriate built-in types, and new overloads for user-created types can be added, either as static overloads or as generic functions. Of course, the compiler will try to inline such functions whenever possible, so that 1 + 2 will still be a compile-time constant.

Tart doesn’t allow you to define new and strange operators (for all the usual reasons.) But for people who want to experiment with DSLs (Domain Specific Languages), Tart has a set of semantically unassigned operators - that is, built-in operators such as ‘:=’ which have no assigned meaning. These operators are available for application use.

One set of operators defined by Tart are the ‘possible relation’ operators which are used in interval arithmetic. So <=? means ‘possibly less than’.

Not all operators are overloaded via generic functions. There are a few unary operators that are defined as class methods.

The design criteria for whether an operator is implemented as a generic function or a member function can be summarized in the following rule: “If it’s something done with the object, then it’s a generic function; If it’s something the object itself does, then it’s a member function.” So for example, “adding” is something that uses an object - one speaks of adding two numbers, rather than (Smalltalk and Ruby notwithstanding) numbers adding themselves together. On the other hand, the ability to “call” an object is seen as a facility that the object itself provides.

To make an object callable, define a member function with no name:

class Foo {
    def (s:String) {
        // ...
    }
}

f = Foo();
f("Hey there");

You can also overload the array subscript operator. The syntax is similar to defining a property:

class <T> StringMap {
    def [key:String]:T {
      get { /* getter code */ }
      set { /* setter code */ }
    }
}

m = StringMap();
let result = m["first"];

Types and Reflection

Tart supports reflection as a standard part of the language. Type literals are represented using the typeof operator:

let t:Type = typeof(Integer);
sys.stdout.println("Name = ", t.name);
sys.stdout.println("isArray = ", t.isArray);
sys.stdout.println("isRefType = ", t.isRefType);

The Type type implements a large set of methods for discovering things about types, including properties, methods, constructors, and so on. Type literals are constants, and many of their properties are also constants, meaning that you can use those properties in compile-time expressions.

Some examples of using the reflection functions:

// Lookup a property by name
let p:PropertyDescriptor = typeof(List).getProperty("length");
let list:List<String> = Collections.newArrayList();
let length = p.getValue(list);

// List all property names
for (p in typeof(List).properties) {
  sys.stdout.println(p.name);
}

Calling C functions

Tart makes it easy to call library functions written in C. Unlike Java, you don’t need to write a special “wrapper” around the C library. Instead, you declare these functions directly in Tart using the Extern attribute:

[Extern("img_open")]
def OpenImage(name:native ubyte[]^, length:int);

The argument to Extern is the C linkage name of the function. This causes the Tart compiler to emit a reference to that name in the module being compiled. Note that within Tart programs, you still use the declared function name (OpenImage in the example.)

The Extern attribute can be applied to class methods as well as standalone functions. If the method isn’t static, then the self parameter will be passed as the first argument.

The Tart compiler doesn’t do anything special to the arguments being passed to the library function. In other words, it passes each argument exactly the same way as it would if calling a function written in Tart. For primitive types, such as integers or native arrays, the memory layout is no different than the corresponding C type. Tart structs are also similar in memory layout to their C counterparts, so it should be possible to pass them to C library functions as well.

For object types, such as String, Tart passes a pointer to the object. Unfortunately, most native libraries won’t know how to interpret a Tart class. Currently the only way around this is to write a wrapper around the native function. Note that this wrapper could be written in C, or it could be written in Tart, whichever is most convenient. For a C wrapper, you would import a header that defines the various Tart types that you need to work with. For a Tart wrapper, you would convert the arguments into types that the native C function understands.

At some point, there will most likely be support for automatic translation of arguments, probably using some kind of per-parameter annotation on the external function declaration.

Note

the issue of interaction between garbage collection and native functions has not been adequately dealt with in the current design (since the collector hasn’t been written yet.) Initially, a non-copying mark & sweep collector will be used, however a copying collector may be more efficient especially for young generation objects. The compiler will need to auto-generate wrapper code that locks objects in memory to handle this case.

Macros

Tart macros are functions which execute in the compiler rather than at runtime. They are introduced with the macro keyword:

macro MyMacro(t:Type) -> Type {
  // ...
}

Macros are somewhat restricted in what they can do compared to functions. They generally have to be written in a pure functional style, and can’t do i/o or call library functions. They can, however, return function objects or even classes as results.

One thing that is interesting about macros is that the arguments are not evaluated before calling the macro. In other words, when you call a macro with an argument of “1 + 2”, it does not pass the value “3”, but rather the unevaluated expression “1 + 2” (technically what gets passed is an AST fragment.) These expressions will be substituted inline in the body of the macro. This means that you can control how many times (if at all) the expression and its associated side effects are evaluated.

Take for example the cond macro, which is part of the Tart core library:

macro <T> cond(condition:bool, trueVal:T, falseVal:T) -> T {
  if condition { return trueVal; }
  else { return falseVal; }
}

The cond macro operates exactly like the ternary ? operator in C.

Note

The name “cond” is taken from the LISP macro which does exactly the same thing.) Because it’s a macro, the side effects of trueVal will occur only if the condition is true, and the side effects of falseVal will occur only if the condition is false.

Another thing to notice is that the cond macro is a template as well as a macro. More about templates in the next section.

The $ operator gives direct access to the AST node for a macro parameter. This allows you to call methods of the AST node rather than the expression that the AST node represents. Currently the only available method is toString() which allows you to generate a string representation of the expression. This is used by the assert macro, which is another part of the Tart core library:

macro <T> assert(expression:T) {
  if not expression {
    throw AssertionError($expression.toString());
  }
}

Templates and Metaprogramming

Parameterized Types

Syntatically, Tart’s support for parameterized types is a hybrid of what is found in Java and C#:

let x:List<String> = Collections.emptyList();

Like C++ and C# (and unlike Java), parameterized functions may end up generating multiple versions of the code for different types based on usage. The reason for this is that Java’s ‘erasure’ concept severely limits what can be done with parameterized types - in Java, the only valid operations on a parameterized type are those operations that are valid on the upper bound - i.e. the non-parameterized version of the type.

That being stated, the compiler will attempt to ‘collapse’ different implementations in an optimized build when possible.

Like Java and C# (unlike C++) type parameters can be deduced from the result type of a function. In the above example, Collections.emptyList() is an overloaded function that returns the proper type of list based on what it is being assigned to. It might be implemented like this:

namespace Collections {
  def <T> emptyList() {
     return List<T>();
  }
}

Templates

C++ templates are powerful, but many programmers find them to be complex. The truth is that C++ templates aren’t hard to understand because they are too powerful - they are hard to understand because they are not quite powerful enough. A lot of the complexity of C++ templates (as seen if you’ve ever tried to read the source code to BOOST) comes from trying to get around their limitations.

Templates are part of Tart’s macro system. They are similar to C++’s templates in that they are resolved based on parameterized types. But within the body of the template you can have Tart macros and other things that you can’t do in a C++ program.

One goal of Tart’s template system is to be able to generate new classes from old. Say for example you want to take an abstract interface and generate a mock implementation for testing. This mock implementation merely records all of the method calls and plays them back later for verification. In Java, this can be done via run-time creation of classes using reflection. In Tart, we do this at compile time by using a template that invokes a mapping transformation on the members of the class. The input to the template is a class (the interface), and the output is a class (the mock implementation). For each public member of the input class, we apply an inner template to it, producing a mock implementation of that member; the concatenation of all of those resulting members is the output class.

The same technique could be used to generate call stubs for RPC calls.

Tart’s templates are closer in concept to C++’s templates than to Java or C#. Specifically, each new template instantiation causes the template body to be cloned. While theoretically this can cause code bloat, in practice a good linker is capable of merging identical sequences of assembly instructions (which is what most template expansions produce.)

This cloning process occurs before evaluation and type resolution. This is important, because it allows for a programming technique called duck typing. The term comes from Python, and is inspired by the old adage “if it looks like a duck, walks like a duck, and quacks like a duck, then it’s a duck.” In the context of templates it means that if a template expects a class with a “quack()” method, then any class having a quack() method will work, regardless of what its base classes are. With Java-style templates, if you wanted to call the “quack()” method, you would have to specify the template argument as being restricted to some common base class or interface having a “quack()” method, and then only subclasses of that base class could be used to instantiate the template.

Tart templates do borrow one important feature from Java, however, which is the ability to deduce template arguments from the return type of a function. Thus, if you say something like:

let list:List<int> = List();

Notice that you didn’t have to supply the type parameter when you called the List constructor? The Tart compiler knows what kind of list is required to fulfill the right-hand side of the assignment. Note that the reverse also works:

let list = List<int>();

Declaring Templates

Classes, structs, interfaces, and functions can be templates. The syntax for declaring a template is to put the template arguments immediately after the declaration keyword, enclosed in “angle-brackets”:

class <T> List(Iterable<T>) { ... }

You can also specify a type for the template parameter, just like for a function parameter:

class <T:Type> List(Iterable<T>) { ... }

Note

There will also be a syntax for restricting the type, but the details haven’t been worked out.

Explicit specialization

The “angle-bracket” syntax is also used to explicitly specialize a type, however the syntax is slightly different depending on whether the typename occurs in type context or expression context. Type context is any place where a type name would be expected - in a class’s list of base classes, in the type field of a variable, and so on. Expression context is any place where an expression would be expected.

In type context, the syntax is just regular angle brackets:

let i:Iterable<String> = names.split(",");
var l:List<Iterable<String>>;

Note

Note in the last example that you do not need to insert a space between the two closing ‘>’ characters.

However, in expression context, an extra period must be added before the leading angle bracket:

var g = List.<String>();

The reason for this is due to the fact that Tart syntax is a context-free grammar (unlike C++), and the ‘.’ is needed to disambiguate the use of the ‘<’ symbol as the start of a type variable list rather than as a less-than operator. (Note that Java uses this same syntax.)

Implicit specialization

Partial specialization