MARLA

Messages And Registers: Language with Actors. Eine kleine Programmiersprache für Mikrocontroller. Sie fühlt sich beim Schreiben an wie Python und verhält sich wie C. Der Compiler übersetzt MARLA nach lesbarem C99; das baut dann avr-gcc für den Arduino Uno oder der normale C-Compiler für einen Testlauf auf dem PC.

So sieht MARLA aus

extern fn hal_led_write(on: bool)
extern fn hal_delay_ms(ms: u16)

const BLINK_MS: u16 := 500

fn main():
    let mut counter: u16 := 0
    loop:
        hal_led_write(true)
        hal_delay_ms(BLINK_MS)
        hal_led_write(false)
        hal_delay_ms(BLINK_MS)
        counter := counter + 1
        if counter = 100:
            counter := 0

Einrückung statt Klammern wie in Python, := für die Zuweisung, = für den Vergleich wie in Pascal, feste Integer-Typen wie u16. Ein Ausschnitt aus dem C, das der Compiler daraus macht:

#define ml_BLINK_MS ((uint16_t)(500))

#line 7 "blink.mar"
static void ml_main(void) {
    #line 8 "blink.mar"
    uint16_t ml_counter = ((uint16_t)0);
    #line 9 "blink.mar"
    for (;;) {
        #line 10 "blink.mar"
        hal_led_write(true);
        ...

Die #line-Zeilen sorgen dafür, dass Fehlermeldungen von C-Compiler und Debugger auf die Zeile im MARLA-Quelltext zeigen, nicht auf das erzeugte C.

Aktoren statt Threads

Auf einem Mikrocontroller passiert vieles gleichzeitig: Ein Timer läuft ab, ein Byte kommt über die serielle Schnittstelle, eine Taste wird gedrückt. In C landet das in Interrupt-Routinen, die sich mit der Hauptschleife über globale volatile-Variablen absprechen. Das ist eine klassische Fehlerquelle.

MARLA macht daraus Nachrichten. Ein Aktor hat einen privaten Zustand und einen Briefkasten (Mailbox) fester Größe. Der Timer schickt ihm ein Tick, er reagiert darauf:

extern fn hal_led_write(on: bool)

actor Blinker(mailbox: 4, full: DropOldest):
    an: bool := false

    fn receive(msg: TimerMsg):
        match msg:
            Tick:
                self.an := not self.an
                hal_led_write(self.an)

fn main():
    return

Welcher Timer wie oft tickt, steht in einer kleinen Board-Datei, nicht im Programm. (Dieses Beispiel läuft heute auf dem PC; auf dem Uno fehlt dafür noch ein Baustein, siehe Stand & Plan.) Was passiert, wenn der Briefkasten voll ist, muss jeder Aktor selbst festlegen: hier DropOldest, also die älteste Nachricht überschreiben. Alle Aktoren stehen schon beim Übersetzen fest; es gibt keinen Heap und kein dynamisches Erzeugen.

Fehler beim Übersetzen, nicht auf dem Board

Was auf dem Mikrocontroller schiefgeht, ist schwer zu finden. Deshalb prüft MARLA möglichst viel schon beim Übersetzen und sagt verständlich, was falsch ist:

typmix.mar:5:16: Fehler: Typkonflikt bei '+': 'u8' und 'u16' — keine impliziten Umwandlungen, expliziter Cast mit 'as' nötig
        let c := a + b
                   ^
nicht_mut.mar:4:5: Fehler: 'x' ist nicht veränderlich — deklariere 'let mut x'
        x := 2
        ^
doppelgleich.mar:4:10: Fehler: '==' gibt es nicht — meintest du '='?
        if x == 1:
             ^

Die Leitideen

  1. Keine versteckten Kosten. Jedes Sprachmittel hat eine offensichtliche, billige Übersetzung nach C. Kein Heap, kein Garbage Collector, keine Laufzeit-Magie.
  2. Fehler beim Übersetzen statt zur Laufzeit. Statische Typen, keine stillen Umwandlungen.
  3. Python-Gefühl, C-Semantik. Speicher liegt auf dem Stack oder global, Typen stehen beim Übersetzen fest.
  4. Nur portables C99. Alles Hardwarenahe geht über eine kleine HAL-Schicht pro Zielsystem.
  5. Lesbares C. Wer wissen will, was passiert, schaut einfach in die erzeugte Datei.

Weiter