aBelle: die Sprachbeschreibung

Was aBelle kann und wie man es schreibt, Abschnitt für Abschnitt, mit der Grammatik am Ende. Einen Überblick und die ersten Programme zeigt die aBelle-Seite; in Asmonite erklärt F1 die Wörter der Zeile, in der der Cursor steht.

Vorläufige Fassung. Dies ist aBelle 0, die erste Stufe der Sprache. Bis die Fassung endgültig festgelegt ist, kann sich noch etwas ändern: Wörter, Regeln und was ein Compiler annimmt. Die Beispiele im ZIP passen immer zu den Compilern im selben ZIP.

Inhalt

Ein erstes Beispiel

# Hello, world! auf der Konsole von Windows
extern "kernel32.dll":
    def GetStdHandle(n: i32) u32
    def WriteFile(h: u32, buf: pchar, n: u32, written: pchar, ov: u32) i32

written: char[4]

def strlen(s: pchar) i32:
    n = 0
    while s[n] != 0:
        n = n + 1
    return n

def print(s: pchar):
    WriteFile(GetStdHandle(-11), s, u32(strlen(s)), written, 0)

def main() i32:
    print("Hello, world!\n")
    return 0

Zeilen, Blöcke, Kommentare

  • Eine Zeile endet mit LF oder CR LF; die Datei ist UTF-8 (Text und Kommentare dürfen Umlaute haben).
  • Eine Anweisung je Zeile. Ein Block folgt auf eine Zeile, die mit : endet, und ist um 4 Leerzeichen weiter eingerückt als sie; er endet, wo die Einrückung zurückgeht. Ein Tabulator in der Einrückung ist ein Fehler.
  • Leere Zeilen und Zeilen nur mit einem Kommentar zählen für die Einrückung nicht.
  • # beginnt einen Kommentar bis zum Zeilenende (nicht in einem String oder Zeichen).
  • Eine Zeile lässt sich nicht fortsetzen (kein \ am Ende); innerhalb von Klammern darf ein Ausdruck aber über mehrere Zeilen gehen.

Typen

TypWasGröße
i32ganze Zahl mit Vorzeichen, -2147483648 .. 2147483647; der Standard, wenn kein Typ dasteht4 Bytes
u32ganze Zahl ohne Vorzeichen, 0 .. 4294967295; für Adressen, Bitmuster, Register4 Bytes
pchardie Adresse eines Bytes (ein Zeiger auf Text oder ein char-Feld)4 Bytes

Es gibt keinen Typ für Wahrheitswerte: Ein Vergleich ergibt 1 oder 0; if und while nehmen jeden Wert, und alles außer 0 ist wahr. True und False sind die Konstanten 1 und 0.

Umwandeln ist immer ausdrücklich: i32(x), u32(x), pchar(x) ändern nur den Typ, nicht die Bits.

Mischen: Die beiden Seiten einer Rechnung oder eines Vergleichs müssen denselben Typ haben; i32 mit u32 ist ein Fehler (also u32(a) + b). Eine Zahl im Quelltext (42, 'a') passt sich der anderen Seite an.

**Mit pchar rechnen:** p + n und p - n (n ein i32) ergeben einen pchar, p - q den Abstand als i32; pchars lassen sich vergleichen. Die Zahl 0 ist auch ein pchar (die Adresse 0, „kein Zeiger“).

Konstanten, Variablen, Felder

Auf der obersten Ebene der Datei:

MAXSYM = 4096              # eine Konstante: kein Typ, ein fester Wert (auch ein Ausdruck aus Konstanten)
count: i32 = 0             # eine globale Variable: mit Typ und Anfangswert (eine Konstante)
names: i32[MAXSYM]         # ein Feld: MAXSYM Zahlen (i32 oder u32), am Anfang alle 0
src: char[200000]          # ein Feld von 200000 Zeichen (Bytes), am Anfang alle 0
toks: i32[]                # ein Feld, das wächst (auch u32[] und char[]), am Anfang leer
  • names[i] ist das Wort Nummer i des Feldes (0 .. MAXSYM-1), vom Typ des Feldes. src[i] ist das Byte Nummer i hinter einem pchar: ein i32 mit dem Wert 0 .. 255 (so geht src[i] == 'a' ohne Umwandlung); beim Schreiben zählen nur die unteren 8 Bit. Ein Index außerhalb wird nicht geprüft.
  • char gibt es in aBelle 0 nur als Element eines Feldes, nicht als Typ einer Variablen. Ein char-Feld ohne Index ist die Adresse seines ersten Zeichens, ein pchar (daher der Name): es lässt sich übergeben (ReadFile(h, src, ...)) und mit einem pchar durchlaufen.
  • Ein Feld von i32 oder u32 steht nur mit Index im Programm; es lässt sich nicht als Ganzes zuweisen oder übergeben.
  • Text im Quelltext ("Hallo\n") ist ein pchar auf Bytes mit einer 0 am Ende.
  • Der Compiler rechnet Konstanten wie i32: mit 32 Bit und Vorzeichen, / zur 0 hin, >> behält das Vorzeichen, geschoben wird um 0 .. 31 (1 << 33 ist 2). Daher ist 0x80000000 >> 4 gleich 0xF8000000; ohne Vorzeichen schiebt u32(x) >> 4 in einer Funktion.

Felder, die wachsen (toks: i32[], u32[], char[], ohne Größe) haben keine feste Grenze:

FormWas sie tut
append(a, x)hängt x hinten an
len(a)die Länge, ein i32
resize(a, n)setzt die Länge auf n; neue Elemente sind 0 (resize(a, 0) leert das Feld)
a[i]lesen und schreiben wie bei einem festen Feld, auch a[i] += 1; der Index wird nicht geprüft
  • Ist kein Platz mehr, holt append (oder resize) doppelt so viel Speicher (mindestens 16 Elemente) und kopiert die Elemente dorthin. Im Mittel wird so jedes Element höchstens einmal kopiert. Gibt es keinen Speicher mehr, oder soll das Feld 0x10000000 Elemente oder mehr haben (auch eine negative Länge), endet das Programm mit dem Exit-Code 3.
  • Ein char[] ohne Index ist wie ein festes char-Feld ein pchar auf sein erstes Zeichen. **Nach append oder resize kann das Feld umgezogen sein**: ein vorher geholter pchar zeigt dann ins Leere, ein Index bleibt gültig.
  • Ein Element ist 4 Bytes groß (char: 1 Byte). Der Speicher kommt unter Windows von HeapAlloc; er wird am Ende des Programms frei. Ein Feld, das wächst, steht nur auf der obersten Ebene; es lässt sich nicht als Ganzes zuweisen oder übergeben.

Lokale Variablen entstehen in einer Funktion bei ihrer ersten Zuweisung; ihr Typ ist der des Ausdrucks (eine Zahl ergibt i32), oder er steht dabei: p: pchar = 0. Danach behält die Variable ihren Typ. Die Parameter sind auch lokale Variablen.

Globale Namen in Funktionen: Ein Name, den es auf der obersten Ebene gibt, ist in jeder Funktion dieser globale Name; eine Zuweisung ändert die globale Variable. Eine lokale Variable kann also nicht so heißen wie eine globale. (Das weicht von Python ab: Dort braucht man global x.)

Funktionen

def add(a: i32, b: i32) i32:
    return a + b

def show(s: pchar):        # ohne Typ hinter der Klammer gibt es keinen Wert
    print(s)
  • Der Typ des Ergebnisses steht hinter der Klammer (def add(a, b) i32:). Ein Parameter ohne Typ ist ein i32. Höchstens ein Rückgabewert. Rekursion ist erlaubt.
  • Eine Funktion mit Typ kommt nie hinten aus ihrem Rumpf heraus, sonst meldet der Compiler „f returns a i32: its end needs a return“. Sicher zu Ende ist: ein return; ein if … elif … else, wenn es ein else hat und jeder Zweig sicher zu Ende ist; ein while True: ohne eigenes break. Eine for-Schleife nicht, sie kann 0-mal laufen.
  • Eine Funktion darf benutzt werden, bevor sie in der Datei steht.
  • Das Programm beginnt bei def main() i32:; ihr Wert ist der Exit-Code des Programms.
  • Die Argumente werden von links nach rechts ausgewertet.

Funktionen einer DLL:

extern "kernel32.dll":
    def ReadFile(h: u32, buf: pchar, n: u32, got: pchar, ov: u32) i32
    def ExitProcess(code: u32)

Sie werden aufgerufen wie eigene Funktionen. Hinter dem Namen der DLL steht ihre Konvention: stdcall (die Funktion nimmt ihre Argumente selbst vom Stack, so die Funktionen von Windows wie kernel32.dll) oder cdecl (der Aufrufer nimmt sie weg, so die Funktionen von C wie in msvcrt.dll). Ohne Angabe gilt stdcall. Eine falsche Angabe bringt den Stack durcheinander: das Programm stürzt ab oder sein Stack wächst mit jedem Aufruf. Unter x64 gibt es nur eine Konvention, dort machen beide Wörter keinen Unterschied.

extern "msvcrt.dll" cdecl:
    def printf(text: pchar, ...) i32       # ... : beliebig viele Argumente dahinter
    def strlen(s: pchar) i32

... als letzter Parameter (nur in einem cdecl-Block) erlaubt beliebig viele weitere Argumente, jedes mit einem Wert (i32, u32, pchar). ExitProcess (und mit wachsenden Feldern GetProcessHeap, HeapAlloc, HeapReAlloc) braucht das Programm selbst aus kernel32.dll: aus einer anderen DLL darf es sie nicht importieren.

Anweisungen

AnweisungBeispiel
Zuweisungx = x + 1, names[i] = n, src[i] = 'a'
Zuweisung mit Rechnungi += 1, names[k] -= 2, flags \= 0x20, src[i] &= 0x7F
Aufrufprint("ok\n") (ein Ergebnis wird verworfen)
if / elif / elsesiehe unten
whilewhile i < n:
forfor i in 0..n:, for i in a..n:
append, resizeappend(toks, k), resize(src, n) (Felder, die wachsen)
break, continuein der innersten Schleife
returnreturn, return x
passein leerer Block

Zuweisung mit Rechnung: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=. x += e heißt x = x + (e), mit denselben Regeln für die Typen wie der Operator; bei names[i] += e und src[i] += e wird die Adresse nur einmal ausgerechnet. Das Ziel muss es schon geben (eine Variable, ein Feld, ein Byte hinter einem pchar); eine neue lokale Variable entsteht so nicht. Es ist eine Anweisung, kein Ausdruck. ++ und -- gibt es nicht.

if c >= '0' and c <= '9':
    kind = DIGIT
elif c == '_' or (c >= 'a' and c <= 'z'):
    kind = NAME
else:
    kind = OTHER

Zählschleifen: for i in a..n: zählt i von a bis n - 1 (das Ende gehört nicht dazu, wie in Python und Rust), for i in 0..len: also über die Indizes eines Feldes. a und n sind Ausdrücke vom Typ i32. n wird einmal ausgerechnet, vor der ersten Runde; ist a >= n, läuft die Schleife gar nicht. i ist eine lokale Variable vom Typ i32 (sie entsteht hier, wenn es sie noch nicht gibt; eine globale geht nicht); nach der Schleife hat sie den Wert n (oder den beim break). continue geht mit dem nächsten i weiter.

Auf einer Zeile: Hinter dem : von if, elif, else, while und for darf statt eines eingerückten Blocks eine einfache Anweisung auf derselben Zeile stehen (eine Zuweisung, ein Aufruf, break, continue, return, pass), aber kein weiteres if, while oder for:

if c == 0: break
if i == 0: return "zero"
elif i == 1: return "one"
else: return "many"
while s[n] != 0: n += 1

Ausdrücke

Vom schwächsten zum stärksten Vorrang (wie in Python):

VorrangOperatoren
1or
2and
3not
4== != < <= > >= (nicht verkettet: a < b < c ist ein Fehler)
5\
6^
7&
8<< >>
9+ -
10* / %
11-x ~x (vorn)
12Klammern, Aufruf f(x), Index a[i], Umwandlung u32(x)
  • Anders als in C binden die Vergleiche schwächer als &: x & 1 == 0 heißt (x & 1) == 0.
  • and und or werten rechts nur aus, wenn nötig, und ergeben 1 oder 0.
  • / und % teilen ganzzahlig. Bei i32 wird zur 0 hin abgeschnitten (-7 / 2 ist -3, -7 % 2 ist -1), wie in C und auf der Hardware, anders als in Python. Teilen durch 0 wird nicht geprüft.
  • >> schiebt bei i32 mit dem Vorzeichen, bei u32 mit Nullen. Ein Überlauf wird nicht geprüft (die Zahlen laufen über wie in der Hardware).

Zahlen und Zeichen: 42, 0x2A, 'a' (der Wert des Zeichens, ein i32). Text: "...". In beiden gelten \n, \r, \t, \0, \\, \', \" und \xHH.

Mehrere Dateien: include

include "wlan.abel"          # neben dieser Datei
include "board/uart.abel"    # auch in einem Ordner darunter
  • include "name" steht allein auf einer Zeile auf der obersten Ebene (nicht eingerückt). Der Compiler liest die Datei an dieser Stelle, als stünde ihr Text dort: ihre Konstanten, Variablen, Felder, extern-Blöcke und Funktionen gehören zum Programm. Es gibt nur einen Namensraum (keine Module, keine Präfixe).
  • Der Name gilt von der Datei aus, in der das include steht (/ und \ gehen beide); ein Name mit Laufwerk oder ab der Wurzel (C:\..., \...) gilt so, wie er ist. Eine Endung wird nicht ergänzt.
  • Jede Datei wird einmal gelesen: ein zweites include derselben Datei (auch über eine andere) übergeht der Compiler. Eine eingebundene Datei darf selbst include benutzen.
  • Eine eingebundene Datei beginnt und endet auf der obersten Ebene; ein Block reicht nicht über ihr Ende.
  • target steht nur im Programm selbst, nicht in einer eingebundenen Datei.
  • Meldungen nennen die Datei, in der der Fehler steht: wlan.abel(12): ....
  • Im Debugger von Asmonite: Hält das Programm in einer eingebundenen Datei, zeigt der Editor diese Datei an Stelle des Programms, schreibgeschützt. Breakpoints, F4, das Watch-Fenster und die Werte unter der Maus gelten dort wie im Programm. Hält es wieder im Programm oder endet es, ist das Programm wieder da.

Wörter, die belegt sind

and, append, at, break, cdecl, char, continue, def, elif, else, extern, False, for, i32, if, in, include, len, not, or, pass, pchar, resize, return, stdcall, target, True, u32, while

Was aBelle 0 nicht hat

Module (include fügt nur den Text einer Datei ein), for über anderes als einen Bereich a..n, struct, Gleitkomma, reg (für die Boards), Zeiger auf Wörter (load32/store32), Felder als Parameter, lokale Felder, Freigeben von Speicher, mehrere Rückgabewerte, Prüfungen zur Laufzeit.

Grammatik

program    = { toplevel } .
toplevel   = const | global | array | chars | extern | func | include .
include    = "include" STRING NL .                       (nicht eingerückt; der Text der Datei an dieser Stelle)
const      = NAME "=" expr NL .                          (expr aus Konstanten)
global     = NAME ":" type "=" expr NL .
array      = NAME ":" ("i32" | "u32") "[" [ expr ] "]" NL .   (ohne expr: ein Feld, das wächst)
chars      = NAME ":" "char" "[" [ expr ] "]" NL .
extern     = "extern" STRING [ "stdcall" | "cdecl" ] ":" NL INDENT { "def" NAME "(" [ xparams ] ")" [ type ] NL }
             DEDENT .
xparams    = "..." | param { "," param } [ "," "..." ] .    (... nur mit cdecl)
func       = "def" NAME "(" [ params ] ")" [ type ] ":" block .
params     = param { "," param } .
param      = NAME [ ":" type ] .
type       = "i32" | "u32" | "pchar" .
block      = NL INDENT stmt { stmt } DEDENT .
stmt       = simple NL | if | while | for .
simple     = target [ ":" type ] "=" expr | target augop expr | call | "break" | "continue" | "return" [ expr ]
           | "pass" | ("append" | "resize") "(" NAME "," expr ")" .
augop      = "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>=" .
target     = NAME [ "[" expr "]" ] .
if         = "if" expr ":" body { "elif" expr ":" body } [ "else" ":" body ] .
while      = "while" expr ":" body .
for        = "for" NAME "in" expr ".." expr ":" body .
body       = block | simple NL .
expr       = and { "or" and } .
and        = not { "and" not } .
not        = "not" not | cmp .
cmp        = bor [ ("==" | "!=" | "<" | "<=" | ">" | ">=") bor ] .
bor        = bxor { "|" bxor } .
bxor       = band { "^" band } .
band       = shift { "&" shift } .
shift      = sum { ("<<" | ">>") sum } .
sum        = term { ("+" | "-") term } .
term       = unary { ("*" | "/" | "%") unary } .
unary      = ("-" | "~") unary | primary .
primary    = NUMBER | CHAR | STRING | "True" | "False" | "(" expr ")" | NAME [ "(" [ args ] ")" | "[" expr "]" ]
           | type "(" expr ")" | "len" "(" NAME ")" .
call       = NAME "(" [ args ] ")" .
args       = expr { "," expr } .

NL ist das Ende einer Zeile, INDENT und DEDENT sind das Mehr und Weniger an Einrückung, die der Zerleger aus den Leerzeichen am Zeilenanfang macht.

Programme für ein Board

Ein Programm für den ESP32 sagt das in seiner ersten Zeile mit target "esp32"; target steht nur im Programm selbst, nicht in einer eingebundenen Datei. Ohne target ist es ein Programm für Windows. abelle_esp32.exe übersetzt es in den Code für das IRAM ab 0x40080000 und die Daten für das DRAM ab 0x3FFB0000.

target "esp32"

gpio: u32[0x140] at 0x3FF44000              # the registers of the GPIO as an array at their address
def ets_delay_us(us: u32) at 0x40008534     # a function of the ROM, without a body

def main():
    gpio[0x24 / 4] = 1 << 2                 # GPIO 2 is an output
    while True:
        gpio[0x08 / 4] = 1 << 2
        ets_delay_us(500000)
        gpio[0x0C / 4] = 1 << 2
        ets_delay_us(500000)