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.
# 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
: 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.# beginnt einen Kommentar bis zum Zeilenende (nicht in einem String oder Zeichen).\ am Ende); innerhalb von Klammern darf ein Ausdruck aber über mehrere Zeilen gehen.| Typ | Was | Größe |
|---|---|---|
i32 | ganze Zahl mit Vorzeichen, -2147483648 .. 2147483647; der Standard, wenn kein Typ dasteht | 4 Bytes |
u32 | ganze Zahl ohne Vorzeichen, 0 .. 4294967295; für Adressen, Bitmuster, Register | 4 Bytes |
pchar | die 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“).
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.i32 oder u32 steht nur mit Index im Programm; es lässt sich nicht als Ganzes zuweisen oder übergeben."Hallo\n") ist ein pchar auf Bytes mit einer 0 am Ende.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:
| Form | Was 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 |
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.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.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.)
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)
def add(a, b) i32:). Ein Parameter ohne Typ ist ein i32. Höchstens ein Rückgabewert. Rekursion ist erlaubt.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.def main() i32:; ihr Wert ist der Exit-Code des Programms.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.
| Anweisung | Beispiel | |
|---|---|---|
| Zuweisung | x = x + 1, names[i] = n, src[i] = 'a' | |
| Zuweisung mit Rechnung | i += 1, names[k] -= 2, flags \ | = 0x20, src[i] &= 0x7F |
| Aufruf | print("ok\n") (ein Ergebnis wird verworfen) | |
if / elif / else | siehe unten | |
while | while i < n: | |
for | for i in 0..n:, for i in a..n: | |
append, resize | append(toks, k), resize(src, n) (Felder, die wachsen) | |
break, continue | in der innersten Schleife | |
return | return, return x | |
pass | ein 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
Vom schwächsten zum stärksten Vorrang (wie in Python):
| Vorrang | Operatoren | |
|---|---|---|
| 1 | or | |
| 2 | and | |
| 3 | not | |
| 4 | == != < <= > >= (nicht verkettet: a < b < c ist ein Fehler) | |
| 5 | \ | |
| 6 | ^ | |
| 7 | & | |
| 8 | << >> | |
| 9 | + - | |
| 10 | * / % | |
| 11 | -x ~x (vorn) | |
| 12 | Klammern, Aufruf f(x), Index a[i], Umwandlung u32(x) |
&: 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.
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).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.include derselben Datei (auch über eine andere) übergeht der Compiler. Eine eingebundene Datei darf selbst include benutzen.target steht nur im Programm selbst, nicht in einer eingebundenen Datei.wlan.abel(12): ....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
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.
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.
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)
name: u32[n] at adresse) belegt keinen Speicher; es liest und schreibt die Register des Chips in Wörtern zu 32 Bit.def name(...) at adresse) hat keinen Rumpf: eine Funktion des ROM oder eines Teils, der im Speicher bleibt. Sie wird aufgerufen, wie das ROM seine Funktionen aufruft, mit höchstens sechs Argumenten.main keinen Wert und läuft für immer. Funktionen einer DLL und Felder, die wachsen, gibt es dort nicht.