Quote:Original post by Rebooted
I'm interested. Do you have an up-to-date spec/informal semantics? Where is the interpreter?
The interpreter is currently on my other computer. It's a five-file C# project, and it is extremely limited (it fully implements the language semantics, but it has almost no error diagnosis when errors happen, and the type system is not in place yet). I am currently working on a bootstrapped compiler written directly in Xhed.
The semantics are extremely simple: programs manipulate values, which are typical ML-like values:
- Base value types (int, bool, real, char).
- Streams (aka arrays, aka vectors) of an underlying type and specified size. Streams are indexed starting at one, instead of zero.
- Tuples.
- Closures, which are (env, argnames, body), where "env" is the environment of the closure, "argnames" are the names of the arguments, and "body" is the expression inside the closure.
Expressions are evaluated inside environments (which map variable names to variable values). Aside from the usual operational semantics of binary and unary operators, you have:
- eval(env, literal) = value(literal)
- eval(env, variable) = env[variable]
- eval(env, let x = a in b) = eval(env + [x -> eval(env,a)], b)
- eval(env, { args -> expr }) = closure(env, args, expr).
- eval(env, f a) = call(f, eval(env, a))
Calling a closure removes the first argument from "args" and adds it to the inner closure environment, bound to the passed argument. Then, if there are no arguments left, it evaluates the closure body in that inner environment. Otherwise, returns the new closure.
There are a handful of builtins: @x is the size-of operator, returns the number of elements in stream x, x
evaluates to the i-th element of the stream x, which means that the "front" element is x[1] and the "back" element is x[@x], make n f creates an array such that x
= f i and @x = n, and while c f i returns the first x = fn i such that c x = false (by order of increasing n ≥ 0). All other standard library functions are implemented in terms of these built-ins (but are, obviously, to be optimized by the compiler).
Quote:So the language is completely pure? No mutable references or input/output? Is the purpose to perform efficient/parallel computation on values input from a host language? That's quite interesting, and I think you could do quite a lot on optimisation and parallelism if so.
Yes, the language is completely pure. There are several optimizations: obviously, there are the basic ones (trivial parallelization of make-map-reduce sequences), the ones related to concurrent evaluation (f x + f y being computed on two threads and then added together), and the ones related to stream processing which are currently not very often seen:
public sort = { s -> # Create a heap from the stream s Xh.Stream.fold Xh.Heap.add Xh.Heap.empty s # Create a sorted array back from the stream >> Xh.Stream.generate { h -> ! (Xh.Heap.is_empty h) } { h -> Xh.Heap.top h, Xh.Heap.pop h }}public example = { s -> s >> foo >> sort >> bar}
The point is that the stream argument of fold has a "streaming" format, which means that its elements are read in sequential order one after another without any lookahead. Similarly, the output stream of generate has a "streaming" format, which means that its elements are written in sequential order. So, let's assume that foo and bar respectively generate and read "streaming" format streams. Then, I could split the example function in two threads running on two cores.
Thread one executes foo, and outputs the generated elements of the output stream to a pipeline one after another as soon as they are available. Then, it executes bar, telling it to read from another pipeline whenever it needs a new element to work on.
Thread two executes sort, which will read an element from the input pipeline and place it in the heap whenever it's available, then pop elements from the heap and put them in an output pipeline as fast as possible.
Since the pipelines of the two threads are connected, the results are as expected. Since the threads run in parallel, the total time required between foo and bar evaluation is the O(log n) insertion time of a single element, which could be lowered to O(1) with an even more clever data structure.
Quote:Type system: do you have algebraic types and HM polymorphism? Weren't you planning on dependent types at one point, too?
I have plans for HM polymorphism and recursive types, though not necessarily sum types (which can be represented as visitors).
Quote:Consider adding support for first-class continuations rather than exceptions. People seem to overlook them, but they're right up there with first-class functions and typing when it comes to increasing language expressiveness. They're what I miss from Scheme when programming in Haskell. SML/NJ has typed first-class continuations. If you are not familiar with them, this is quite a good explanation.
I'll look into it. I am familiar with continuations, but am generally unable to read LISP code for long durations without my eyes bleeding. [smile]