My LLVM Project

It was a little over two years ago that I saw Chris give a tech talk on LLVM at Google, and that was when I knew that there was a way that I could actually build the programming language that I'd been thinking about for so long.

Well, the compiler is still not done, even though I've been plugging steadily away at it in my free time. However, a lot of progress has been made recently, and I thought perhaps some folks might be interested in what I am trying to do.

The language is called "Tart", and the one-sentence description is "Tart is to C++ as Python is to Perl". Rather than go on about the philosophy of the language, however, I will show some code samples.

For example, here's what the "Iterator" interface looks like:

   interface Iterator[%T] {
     def next -> T or void;
   }

'%T' introduces a template parameter named T. The reason for the '%' prefix is to support partial specialization - you can mix template parameters and regular type expressions in the template argument list, as in "Iterator[List[%T]]". This is different from C++ where the template parameters and the specialization arguments are in separate lists.

Like C++, Tart is a statically-typed language. Unlike C++, however, Tart's type solver can deduce the template parameters of a class from the arguments to the constructor or a static method. So for example, a factory function such as Array.of(1, 2, 3) can deduce that we want an integer array since the arguments are integers. Of course, we can always be explicit and say Array[int].of(1, 2, 3). Tart's type solver also allows function template arguments to be inferred based on the type of variable that the function's return value is assigned to. (Java does this as well). Thus, if you have some function that takes a set of Strings, you can say "myFunction(EmptySet())", and not have to explicitly specify a template argument to EmptySet - it knows what kind of set you want.

Back to the example, the result of the iterator's 'next' method is a disjoint type (also called a discriminated union) written as "T or void". That is, it returns T as long as there are elements in the sequence, after which it returns void, i.e. nothing.

LLVM's ability to efficiently return small aggregate types is critical to making this perform well. Ideally, the Tart iterator protocol ought to be faster than either Java Enumerators (which requires two icalls per loop, one for hasNext() and one for next()), or Python's iterators (which depend on exceptions to signal the end of the sequence.)

To use the iterator interface, you can use the convenient "for .. in" syntax, similar to Python. Here's a snippet from one of my unit tests:

   def sum(nums:int...) -> int {
     var sum = 0;
     for i in nums {
       sum += i;
     }

     return sum;
   }

The 'sum' function takes a variable number of ints (The ... signals a varargs function), and returns an int.

The 'var' keyword declares a variable ('let', by contrast declares a constant, which can be local, similar to Java's 'final'). In this case, the type is omitted (you could say "var sum:int = 0" if you wanted to be explicit.)

You could also write this out longhand:

   def sum(nums:int...) -> int {
     var sum = 0;
     let iter = nums.iterate();
     repeat {
       classify iter.next() {
         as i:int {
           sum += i;
         }

         else {
           break;
         }
       }
     }

     return sum;
   }

Since the iter assignment never changes, we can use 'let' to bind it immutably to the variable.

'repeat' is an infinite loop, essentially the same as writing "while true".

'classify' is like a switch statement, except that the cases are types rather than values. It works with both disjoint types and polymorphic types, similar to what is seen in Scala and various functional languages such as OCaml. The variables in the individual 'as' clauses are never in scope unless the assignment to that variable actually succeeds, so there's no chance of seeing an uninitialized variable.

In any case, I don't want to go on about this too long - at least not until the compiler is in better shape to be shown to the world. I still need to work on closures, reflection, garbage collection, interface proxies, stack dumps, debug info, and a bunch of other stuff. (There's a Google code project for it, but I'm not encouraging people to go there until I have more of the language finished.)

-- Talin

For example, here's what the "Iterator" interface looks like:

interface Iterator[%T] {
def next -> T or void;
}

So this would be something like:

template <class T>
virtual class Iterator {
  T next(); // or void?
};

So the power of having two types of return parameters is that you save
function calls (hasNext())?

def sum(nums:int...) -> int {
var sum = 0;
for i in nums {
sum += i;
}
return sum;
}

I see you don't have types for variables, only for containers,
templates and functions. And yet you say your language is
statically-typed. If you do:

var foo = 10; // I'd presume it's an int
foo /= 3;

Would foo become a float?

foo /= 1e200;

Would it become a double?

What happens if you pass foo as an int, and inside the function it
becomes a double (without your consent, by a combination of
parameters) and you try to return it as an int?

'classify' is like a switch statement, except that the cases are types
rather than values. It works with both disjoint types and polymorphic
types, similar to what is seen in Scala and various functional languages
such as OCaml. The variables in the individual 'as' clauses are never in
scope unless the assignment to that variable actually succeeds, so
there's no chance of seeing an uninitialized variable.

Is it run-time or compile-time? The former is Java's insanceof/C++
RTTI, the later has not many uses...

In any case, I don't want to go on about this too long - at least not
until the compiler is in better shape to be shown to the world. I still
need to work on closures, reflection, garbage collection, interface
proxies, stack dumps, debug info, and a bunch of other stuff. (There's a
Google code project for it, but I'm not encouraging people to go there
until I have more of the language finished.)

I think it's great to write compilers to learn how languages work (I'm
doing my own too).

My points about the language:

First, It's much more like a cross between Python/Perl and Java than
C++. It's not strongly typed and yet it use generic programming. As
long as it's not creating *types* with templates (like C++) but
creating only generic algorithms (like Java), it's ok.

It has too much syntactic sugar and names changed for no apparent
reason, it drives the user away from using your language. A good deal
of Java's success is because they used a syntax very similar to C++.

My (very personal) point of view is that, if you're not bringing
anything new, help the others that are instead of re-inventing the
wheel. But again, if your point is (as mine) to learn, well done! It's
much better than my own language! :wink:

cheers,
--renato

Reclaim your digital rights, eliminate DRM, learn more at
http://www.defectivebydesign.org/what_is_drm

If your goal is to create a usable language, your best bet is to use
it. Create a significant project and code it in your language. This
will show you what the language/standard library still need, what
coding tasks are too awkward to carry out, what practical advantages
your language offers over alternatives, and so forth.

A compiler for your language is an obvious first project to try out.
This will exercise your language/standard library in parsing,
interacting with external libraries (including but not limited to
LLVM), file handling, data structure manipulation, and much more.
Replace your original compiler piece by piece, and see how easy your
language is to work with. Pay attention to the tasks that are painful
or repetitive, and evolve you language to make those tasks easier.

Renato Golin wrote:

  

For example, here's what the "Iterator" interface looks like:

  interface Iterator[%T] {
    def next -> T or void;
  }
    
So this would be something like:

template <class T>
virtual class Iterator {
  T next(); // or void?
};

So the power of having two types of return parameters is that you save
function calls (hasNext())?
  

There are other benefits as well. Having disjoint types makes library APIs cleaner - often a function will need to return either a result, or an error code. In C++, this has to be handled either via a reference parameter (which is inefficient because the compiler's live variable analysis can't determine if the value was assigned or not) or by returning a struct.

  def sum(nums:int...) -> int {
    var sum = 0;
    for i in nums {
      sum += i;
    }
    return sum;
  }
    
I see you don't have types for variables, only for containers,
templates and functions. And yet you say your language is
statically-typed. If you do:
  

The type of a variable is optional - if omitted, it is deduced from the initializer.

var foo = 10; // I'd presume it's an int
foo /= 3;

Would foo become a float?

foo /= 1e200;

Would it become a double?

What happens if you pass foo as an int, and inside the function it
becomes a double (without your consent, by a combination of
parameters) and you try to return it as an int?

'classify' is like a switch statement, except that the cases are types
rather than values. It works with both disjoint types and polymorphic
types, similar to what is seen in Scala and various functional languages
such as OCaml. The variables in the individual 'as' clauses are never in
scope unless the assignment to that variable actually succeeds, so
there's no chance of seeing an uninitialized variable.
    
Is it run-time or compile-time? The former is Java's insanceof/C++
RTTI, the later has not many uses...

It's like Java's "instanceof", i.e. a runtime type test.

In any case, I don't want to go on about this too long - at least not
until the compiler is in better shape to be shown to the world. I still
need to work on closures, reflection, garbage collection, interface
proxies, stack dumps, debug info, and a bunch of other stuff. (There's a
Google code project for it, but I'm not encouraging people to go there
until I have more of the language finished.)
    
I think it's great to write compilers to learn how languages work (I'm
doing my own too).

My points about the language:

First, It's much more like a cross between Python/Perl and Java than
C++. It's not strongly typed and yet it use generic programming. As
long as it's not creating *types* with templates (like C++) but
creating only generic algorithms (like Java), it's ok.
  

It's not meant to "look" like C++ - it's meant to occupy the same ecological niche. I have many years of experience programming in Java, C# and Python, and I'm happy using those languages to write things like web servers and desktop applications. But there are some tasks which I *wouldn't* use those languages for - XBox games, MPEG decoders, AudioUnits, signal processing, and so on. For those kinds of tasks, I would normally use C++ - but over the years I have collected hundreds of gripes with C++, which I'd like to fix.

It has too much syntactic sugar and names changed for no apparent
reason, it drives the user away from using your language. A good deal
of Java's success is because they used a syntax very similar to C++.

My (very personal) point of view is that, if you're not bringing
anything new, help the others that are instead of re-inventing the
wheel. But again, if your point is (as mine) to learn, well done! It's
much better than my own language! :wink:

I've only shown a small part of what I have. This forum is not the place for language advocacy, I don't want to start a discussion on the merits of my language or any other. I'm absolutely convinced that I have something to contribute, and I'm going to forge ahead despite the naysayers :slight_smile:

Mainly, I just wanted to share with the LLVM community how I am using LLVM, and I don't want the topic to stray too far from that.