Curta - Let's make hard things easy
fuzzyastrocat (1228)

Note: There is a [fairly important] jam-related note at the bottom of this post.

Programming on Arduino has always been with C++. And that makes sense — A tiny microcontroller with highly limited memory has to be written in a low-level language, right?

Well, no. Enter Curta.

So, what's Curta?

Curta is a statically typed (but often dynamically inferred) event-driven embedded systems language designed to be intuitive and easy. But don't think "easy" means "simpler" — Curta doesn't remove needed features or try to oversimplify things. Instead, it gives you the power of Arduino's C++ in an easier-to-use package.

And, Curta does this while keeping memory usage relatively low. In an example program (in fact, the first example below), Curta used only 5% more sketch and global variable memory. Not bad for a type-inferred, high-level, event-driven language!

So what's all this about event-driven?

Curta is an event-driven language — i.e, when events in the outside world happen, you describe how Curta should react. For example, say you wanted to wait for a pin to go low and then do something. In Arduino C++, you'd do this:

     while(digitalRead(/*pin number*/) == LOW);
     // do stuff

But this is bad, because it blocks everything else in the program! In Curta, you react to the PinLow event:

     event PinLow :: (Int pin)
     on PinLow {
          if $pin == /*pin number*/ }
               :then {
                        // do stuff
               }
     }

That might look a little confusing, since you probably don't know what the Curta syntax is, but you probably get the idea.

Ok cool — so what else is special about Curta?

Glad you asked!
Curta has a variety of benefits:

Portable

Curta compiles to C++, so you don't need any special toolkits. This also makes it portable: if the system can run C++, it can run Curta.

Simple, consistent syntax

From the above, you might think that Curta's syntax is confusing. But really, there are only three simple constructs that the entire syntax uses.

Easily extendable

By no means am I going to claim that Curta is done. But what I will claim is that Curta can be easily extended — that is, external environment code can be written and Curta can interface with that code.

I could keep listing things, but don't worry I'll spare you :D

Phew, thanks! So... can I see some example code?

Sure! No language description is complete without some examples.

Suppose you want to echo pin data from one Arduino to another by using Serial. (Basically, one Arduino monitors pins as inputs. If any of the pins go high, it tells the other Arduino to set its corresponding pin high. It does the same for pins going low.) Here's how you'd do it in Curta:

Serial-sender:

    !include "std.rta" // Standard library

    inst PinNums :: List<Int> // Create two instances of lists
    inst PinStates :: List<Int>

    event Init :: ()
    on Init {
      PinNums
        :add {?4} // Store the pins we want to watch
        :add {?5}
      
      PinStates
        :add {?HIGH} // Initialize some default states
        :add {?HIGH}

      loop i: 0->PinNums:size } // Loop through all the pin numbers
        :then {
          Pins
            :get {? PinNums:get{?i} }
              :init {?INPUT_PULLUP} // Initialize each pin
        }

      Serial
        :begin {baudRate 9600} // Begin Serial comms
    }

    event PinLow :: (Int pin)
    on PinLow {
      loop i: 0->PinStates:size }
        :then {
          if PinNums  :get{?i} == $pin &&
            PinStates:get{?i} != LOW   } // If this is the pin that's low and it was not low last time we checked...
            :then {
              Serial // Output the data
                :print   {? "1"} // Invert, because PULLUP
                :println {? Str(i)}

              PinStates:set {?i} {? LOW} // Update state
            }
        }
    }

    // The following is the same as above, just swapping LOW for HIGH
    event PinHigh :: (Int pin)
    on PinHigh {
      loop i: 0->PinStates:size }
        :then {
          if PinNums  :get{?i} == $pin &&
            PinStates:get{?i} != HIGH   }
            :then {
              Serial
                :print   {? "0"}
                :println {? Str(i)}

              PinStates:set {?i} {? HIGH}
            }
        }
    }

And on the receiver:

    // Much of the beginning is similar to before
    !include "std.rta"

    inst PinNums :: List<Int>

    event Init :: ()
    on Init {
      PinNums
        :add {?4}
        :add {?5}

      loop i: 0->PinNums:size }
        :then {
          Pins
            :get {? PinNums:get{?i} }
              :init {?OUTPUT}
        }

      Serial
        :begin {baudRate 9600}
    }

    event SerialLine :: (Str data)
    on SerialLine { // When we receive some Serial data...
      if $data:substring{?0}{?1} == "0" } // If it's telling us that the pin is low
        :then {
          Pins
            :get {?
              PinNums:get{?Int($data:substring{?1}{?3})} // Get the pin
            }
              :write {?LOW} // And set it low
        }
        :else { // Else, do the opposite
          Pins
            :get {?
              PinNums:get{?Int($data:substring{?1}{?3})}
            }
              :write {?HIGH}
        }
    }

You probably have a feel for how Curta works now, but I'll explain this for those interested:

Something of the form an_expression :some_name {optional args} {maybe more args}... is called a query. Queries allow us to manipulate objects or get data from them. For example, if I wanted to get the object for Pin #13, I would do:

    Pins :get {? 13}

What's the question mark for?

All arguments in Curta are named arguments by default. To suppress this, you can use a ? in place of the argument name. So, the above is equivalent to:

    Pins :get {pin 13}

because the name of the argument to get on object Pins is "pin".

I see some big queries, what's the deal with those?

There are two types of queries:

  • Query expressions. These are like the example above, and they return a value.
  • Query statements. These do not return a value, but instead provide a shorthand syntax to perform many queries on an object.

The "big queries" are query statements. Here's an example of one of those:

    Pins
            :get {? 13}
                    :init    {? INPUT}
                    :write {? HIGH}
            :get {? 12}
                    :write {? LOW}

Query statements utilize whitespace (audience gasps) to determine their structure. The above query becomes a tree:

                           /--------[ Pins ]---------\
                          /                           \
               /---[get 13]---\                    [get 12]
    [init INPUT]         [write HIGH]     [write LOW] -/

So the [init INPUT] query is performed on the return value of the [get 13] query being performed on [ Pins ]. Seems complicated at first, but it's actually super powerful and useful.

Why do some queries not have arguments?

Because some queries don't need them! For instance, size on a List:

    myList:size

This performs the size query on myList. (This probably explains the name "Query" more.)

So... what's the deal with the if-statement?

An if-statement (and the loop-statement) both can be queried! In fact, that's the only way you can do anything useful with them. For instance, suppose I wanted to translate the following C++ code to Curta:

    if(1 == 1){ /* stuff */ }
    else if (1 == 2){ /* more stuff */ }
    else if (1 == 3){ /* more more stuff */ }
    else { /* finally more stuff */ }

In Curta, that becomes:

    if 1 == 1 }
            :then { /* stuff */ }
            :or 1 == 2 { /* more stuff */ }
            :or 1 == 3 { /* more more stuff */ }
            :else { /* finally more stuff */ }

...which gives a sense of ownership to the "if".

But... why the random curly-bracket?

Ah yes. Well, because there's no easy way to tell the difference between

    if 1 == (my_object
            :then ...)

and

    (if 1 == my_object)
            :then ...

without it.

I have more questions!

Great! Feel free to comment on this post, I'll answer them as promptly as I can. There's also a lot we haven't covered here (like the templating, delays, forcing events, the loop statement, loop jumps, exit...), so if you're interested I can explain that.
EDIT: I've updated the post to include some more about the language.

I want to get coding!

Great! Head on over to https://repl.it/@fuzzyastrocat/Curta, make a fork, and read README.md to get started. You do need an Arduino or other Arduino-compatible board. You'll also need to install the LinkedList Arduino package to run Curta.

I want to learn more!

Awesome! Let's take a look at some of Curta's other features. Note: this is getting into the more complicated parts of Curta, if I've explained it poorly please comment and I can clarify.

The object system

In the above examples, we've already been using Curta's object system. However, we haven't used any custom objects, which is what this will cover.

Let's make an object which holds an int and a string. You should be allowed set the int and the string or get the string (but not the int). Why are we doing this? Well, it provides a nice example :D

To accomplish this random task, we do two things:

    decl MyWeirdContainer
        @inner_theint: 0
        @inner_thestring: ""

        :set (Int newint, Str newstr) -> None
        :thestring () -> Str

First, we declare the structure of our new type. We see it has two properties: @inner_theint and @inner_thestring, prefixed with "inner_" so that we can make a query called "thestring" and not have overlap. (note that property declarations MUST come before query declarations). We now see that properties are preceded by @; contrast this to arguments, which are always preceded by $. All object properties are always private. This is not a limitation, it is a feature — more on that in the next section. Next, we define the possible queries on this type: it can be setted, or we can get thestring.

Now we must declare how these queries work:

    impl MyWeirdContainer :set {
        new @inner_theint: $newint
        new @inner_thestring: $newstr
    }

    impl MyWeirdContainer :thestring {
        return @inner_thestring
    }

Here, we first declare that we are implementing the set query. Recall that above, we defined the arguments to :set as "newint" and "newstr". Here we reference these with the $ sigil — recall that arguments are referenced with $ while properties are referenced with @. We also see a new keyword — new (haha pun). new introduces mutability into Curta, by allowing re-assignment of properties. Here we assign $newint to@inner_theint and $newstr to@inner_thestring, respectively.

The second query, :thestring, is fairly straightforward.

So how can we use this? Well, we create an instance of MyWeirdContainer:

    inst MyInstance :: MyWeirdContainer

    on Init {
            MyInstance :set {newint 10} {newstr "Hi"}

            MyInstance:thestring // evaluates to "Hi"
    }

We can make as many instances as we want (this is the same thing as our first example, where we made instances of the List type, except now it's our custom type. In fact, List is technically a custom type, but defined in std.rta as an external environment type.)

Something easier — variables

Now that we've discussed perhaps the most difficult part of Curta, let's talk about something easier: variables. Variables can be introduced with the let statement:

    let a_var as "It's a var!"
    in {
        Serial
            :println {? a_var}
    }

They work mostly the way you expect. But if you try something like this, you'll get a LexingError:

    let a_var as "It's a var!"
    in {
        a_var = "It's a mutable var!"
    }

It should throw an error on the =. Why? Because Curta has no re-assignment operator! Once you have declared a variable with let, you cannot mutate it. This brings up two important points:

  • Curta has no mutable variables.
  • All mutability (and in some respects, functional impurity) is contained inside object properties.

So what does this mean practically? Well, it means you can just use one global object to store all your variables in. However, you also can write every Python program in one line of code and name all your variables gibberish, but that's not good practice. Instead, Curta encourages an "encapsulated" style of programming — you put everything related to one aspect of your program (say, a temperature meter) in an object, keeping the mutable variables out of the global namespace.

Extensibility

Curta has built-in extensibility features. You can write C++ code in the outside environment and Curta can interact with that code. There are two ways for Curta to interact without outside code:

Constants

These are things like Arduino's INPUT and LOW. They can be imported by simply doing the following:

    define ext INPUT as Int

(Note: std.rta already defines these for you, this is just an example)

Objects

This is how you can import more complicated things (such as methods) into Curta. Suppose you have the following C++ code:

    struct SomeStruct {
        int a = 0;
        
        void set_a(int val){
            a = val;
        }

        int get_a(){
            return a;
        }
    }

We can import this into Curta with the following:

    decl ext SomeStruct
            :set_a (Int val) -> None
            :get_a () -> Int

Note that we don't need to declare the property a, since all properties are private!

We also must physically import that C++ code, so you would paste it at the top of Curta's generated C++. (If not, Arduino's C++ would throw a compiler error SomeStruct does not name a type when you try to make a new instance of SomeStruct).

Wrap up

That's all I'll cover here. Like I said above, if you have more questions feel free to comment and I can explain more parts of the language. While the learning curve may seem steep, it actually becomes intuitive once you use it enough.

Something weird happened, what do I do?

Just comment on here with the bug, I'll fix it as promptly as possible! Make sure to always use https://repl.it/@fuzzyastrocat/Curta, it will contain the most up-to-date version.

You said something about a Jam note...

Oh right, almost forgot! Two things:

The team is @curtadev, repl is repl.it/@curtadev/Curta. However, all new curta developments will happen on repl.it/@fuzzyastrocat/Curta.
Thanks to @hydrobolic for lots of feedback and suggestions.

Judges note: I'm just entering this for fun mainly. Why? Because I don't think Curta lends itself well to repl.it. The goal of repl.it is to be able to get coding quickly, all online. But Curta requires a physical piece of hardware to run on... so you can't fulfill the "get coding quickly" or the "all online" part. However, I wouldn't be opposed to winning one of the "individual prizes and categories" :D

You are viewing a single comment. View All