Hap: a simple concurrent programming language.

Started by
6 comments, last by EvincarOfAutumn 11 years ago

Well, I’ve gone and done it again—written a language when nobody asked me to. This one might be interesting to game developers, and I’m interested to hear feedback from anyone with an opinion about what ought to be done with it.

The language is called Hap and is designed as a readable dynamic language for game prototyping. It uses single-threaded cooperative multitasking to support simple event-based concurrency. The example from the README sums it up nicely:


var health = 100;

when (health <= 0) {
  print("Goodbye, cruel world!");
  exit;
}

print("Hello, sweet world!");

while (true)
  health = health - 1;

This code prints:


Hello, sweet world!
Goodbye, cruel world!

Hopefully that’s enough to get your attention. Another example shows how you can use the asynchronous control flow statements to write surprisingly readable code:


fun make_cycler(start, end) {

  var current = start;

  whenever (current > end)
    current = start;

  ret lam() {
    var previous = current;
    current = current + 1;
    ret previous;
  };

}

This function returns a function generating a cycle of values in the given range. For instance:


make_cycler(0, 2)

Will produce the sequence:


0, 1, 2, 0, 1, 2, …

Hap can be used on its own, but the idea is to support embedding in larger systems, much like Lua or JavaScript. Being only a few weeks old, the language naturally has some issues that would need to be addressed before it could be used in production. In particular:

  • There is no standard library, and thus no way to perform a number of common math and container operations.
  • Embedding the language is not as simple as it should be.
  • Asynchronous control flow is not first-class, i.e., there is no way to refer to or halt a “whenever” statement.
  • The prototype interpreter is not designed with performance in mind.

I will gladly address these and any other issues if there is interest in the general concept. Tell me what you think!

Advertisement

I'll drop kick my first thought: "Why?" and try to be more useful... :)

The first thing that jumped out at me: "fun"... Err, please oh please use something a bit more descriptive: at least "func" or better "function", it's not horrible to type out full words in the language once in a while. Proc and other descriptive names are valid choices also, but "fun" to me is the name of a variable that someone needs to be kicked for, unless it really does describe something 'fun', like the 'fun' meter or something.

Beyond that, I'd tend to wonder why you are writing a custom interpreter? I've put together a couple similar items to this (specialized network DSL's, not intended as generic languages) and it was extremely simple to pop it into the LLVM JIT and not have to write anything except the front end parser. It's just a thought since it tends to make things quite a bit easier on you.

I'll drop kick my first thought: "Why?" and try to be more useful... smile.png


Thank you! smile.png

The first thing that jumped out at me: "fun"... Err, please oh please use something a bit more descriptive: at least "func" or better "function", it's not horrible to type out full words in the language once in a while. Proc and other descriptive names are valid choices also, but "fun" to me is the name of a variable that someone needs to be kicked for, unless it really does describe something 'fun', like the 'fun' meter or something.


That’s a valid point. I don’t want to bikeshed over syntax, as it’s easy to change, but here’s my rationale for using short keywords:
  • You use them frequently, so they ought to be succinct.
  • They’re unlikely to conflict with good, useful identifiers. None of this “klass” or “function_” business.
  • They’re just abbreviations, not abstract mnemonics or operators.

Beyond that, I'd tend to wonder why you are writing a custom interpreter? I've put together a couple similar items to this (specialized network DSL's, not intended as generic languages) and it was extremely simple to pop it into the LLVM JIT and not have to write anything except the front end parser. It's just a thought since it tends to make things quite a bit easier on you.


Also fair. I’ve written a number of compilers and interpreters and this is just what felt right for a prototype. Whenever a statement is evaluated outside an atomic section, the interpreter runs an interrupt during which the active asynchronous conditions are evaluated. It was very easy to write a simple VM with this kind of custom behaviour built in—easier than implementing it atop an existing model. In the early stages of a language, there is more value in flexibility. Only as the language matures can you know what the “right” way is.

(The GameDev editor didn't like quoting the reply, it made a mess. So just replying freeform.)

As to the name thing. I agree for commonly used items but in the case of functions, usually the cost of typing even "this_is_a_function" would be trivial when compared to any real non-example code don't you think? I know it is trivial but 'fun' just strikes me as wrong. At first I thought it was just a typo, then I checked the linked example and hey, it wasn't a typo. I know this is trivial, and I don't mean to harp, it's just that I would reserve shorter abbreviations like that for the items you really will be using every couple lines, this should not really be one of those cases though in my thinking. "Var" I'm fine with, I see the utility. My .02 on the given reasoning.

As to the interpreter stuff. Yep, experimenting is a great thing and not being tied to something is even better. Having said that, please understand I'm making a suggestion I believe would save you time even in the experimental phase and I'm not arguing the reasons or goals. (Unless you just "want" to write a VM for fun? Always a valid reason.) I wrote a domain language with basically this same concept, it was specific to networking and all about string processing so a bit less performance critical. Initially I thought there was no way LLVM would be able to support the language but luckily before I went back to a custom VM/Interpreter, I realized how easy it was to do basically anything I wanted and probably a couple things I would be hard pressed to write myself. In the long run, what I thought would take a week to prototype I manged in about 2 days. It really is a pretty impressive infrastructure and if you go through a couple of the tutorials, you can likely get a decent starting understanding in less than a day. The key is to not get fixated on the standard method of usage those tutorials provide and treat it as more of a tool than a library, this would require a bit to explain though so I'll leave off here. :)

I like the when/whenever concept. I have a similar brewing idea for Epoch but in a more generalized CSP/message passing style.

I'm unfortunately way too busy working on my own language to mess with someone else's more than briefly, but I did want to say that the idea of trigger-based instead of polling-based game logic makes me more than a little excited.


As for LLVM... the learning curve can be a bit steep if you want to do anything nontrivial, and as I've been whining about in my journal/Twitter feed for the last week, things like garbage collection are outright painful... but it's still a great kit and well worth targeting if you want free performance boosts :-)

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

As for LLVM... the learning curve can be a bit steep if you want to do anything nontrivial, and as I've been whining about in my journal/Twitter feed for the last week, things like garbage collection are outright painful... but it's still a great kit and well worth targeting if you want free performance boosts :-)

Not meaning to argue, but I had no problem picking up the basics in a day or two. I probably wasn't clear about my usage though, I basically just wrote garbage collection and such using C++ functions which I registered with the JIT. I didn't ever bother with the LLVM C style concepts of variables and such nor the C++ concepts of instances of higher order types, I just called back to my registered functions for all that. I did real LLVM code for all the actual math, comparisons and such, but all variable access went back to my registered functions. As I said, it doesn't produce blazing fast code (faster than any VM I could write though probably) but it really was downright simple compared to any prior time I'd written my own VM. I just had to write a wrapper runtime instead of all the nitpicks.

I know it is trivial but 'fun' just strikes me as wrong. At first I thought it was just a typo, then I checked the linked example and hey, it wasn't a typo.

Hah, looking like a mistake isn’t good. Then again, “fn” has precedent in Clojure and Rust, so perhaps “fun” isn’t too far off the mark. The “function” keyword in JavaScript must have looked perfectly fine when the language was designed, but it’s a pain now that the use of the language has evolved. I’ll think about it and take input from others. Syntax is unimportant and then it’s important, you know?

As to the interpreter stuff. Yep, experimenting is a great thing and not being tied to something is even better. Having said that, please understand I'm making a suggestion I believe would save you time even in the experimental phase and I'm not arguing the reasons or goals.

I appreciate it! I have toyed with LLVM before and will most certainly move to it in the future. (ApochPiQ took a similar route with his Epoch language, for example.) What’s done is done, and the existing interpreter certainly isn’t bad—just not ideal.

Now, the things that concern me most are the design of a standard library and support for embedding. I would be interested to hear your opinion on those. As far as a standard library goes, what I’m working on at the moment is quite simply exporting native functions through a global object. For example:


var keys = hap.map.keys;
var map = { this: "this", that: "that" };

# ["this", "that"]
trace keys(map);

The “hap” object would give you a few broad namespaces such as “map”, “list”, and “io”. When embedding Hap, you would use the same mechanism to export native functions to your Hap code. You could also make foreign function declarations by leaving off the body of a function definition. In both cases, marshalling would be done by the Hap library.

I would also like to support immutable names by way of a “val” keyword (à la Scala). In the future I will add support for type annotations, and move to static typing with global type inference—I have an appropriate inference algorithm. This may seem radical, but it offers distinct advantages. For one thing, I don’t think it’s possible to manage the complexity of Hap-style concurrency and dynamic typing together. These small concurrency examples are simple to understand, but I fear they won’t scale without some help.

I like the when/whenever concept. I have a similar brewing idea for Epoch but in a more generalized CSP/message passing style.

I'm unfortunately way too busy working on my own language to mess with someone else's more than briefly, but I did want to say that the idea of trigger-based instead of polling-based game logic makes me more than a little excited.

The current implementation runs a periodic interrupt and polls events, but it’s a relatively minor change to do it push-based, and I mean to. Hap is deliberately not general at the moment, for the sake of getting something out the door—it’s an iterative process. smile.png

The original idea included “repeat when” and “repeat whenever” for running loops in response to events, but these proved challenging to implement correctly with the current model, so I held off. “repeat when” would begin evaluating when a condition became true, and repeatedly evaluate until that condition became false. “repeat whenever” would do the same thing, but also resume execution when the condition became true again.

This topic is closed to new replies.

Advertisement