What aBelle can do and how to write it, section by section, with the grammar at the end. An overview and the first programs are on the aBelle page; in Asmonite F1 explains the words of the line the cursor is in.
Provisional version. This is aBelle 0, the first stage of the language. Until the version is defined for good, things may still change: words, rules and what a compiler accepts. The examples in the ZIP always fit the compilers in the same ZIP.
# Hello, world! on the Windows console
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
: and is indented 4 spaces deeper than it; it ends where the indentation goes back. A tab in the indentation is an error.# starts a comment up to the end of the line (not in a string or a character).\ at its end); inside brackets an expression may go over several lines.| Type | What | Size |
|---|---|---|
i32 | a signed whole number, -2147483648 .. 2147483647; the default when no type is written | 4 bytes |
u32 | an unsigned whole number, 0 .. 4294967295; for addresses, bit patterns, registers | 4 bytes |
pchar | the address of a byte (a pointer to text or to a char array) | 4 bytes |
There is no type for truth values: a comparison gives 1 or 0; if and while take any value, and everything but 0 is true. True and False are the constants 1 and 0.
Conversions are always written: i32(x), u32(x), pchar(x) change the type only, not the bits.
Mixing: both sides of an operation or a comparison must have the same type; i32 with u32 is an error (so u32(a) + b). A number of the source (42, 'a') fits the other side.
**Arithmetic with pchar:** p + n and p - n (n an i32) give a pchar, p - q the distance as an i32; pchars can be compared. The number 0 is also a pchar (the address 0, "no pointer").
At the top level of the file:
MAXSYM = 4096 # a constant: no type, a fixed value (also an expression of constants) count: i32 = 0 # a global variable: with a type and a start value (a constant) names: i32[MAXSYM] # an array: MAXSYM numbers (i32 or u32), all 0 at the start src: char[200000] # an array of 200000 characters (bytes), all 0 at the start toks: i32[] # an array that grows (also u32[] and char[]), empty at the start
names[i] is word number i of the array (0 .. MAXSYM-1), of the type of the array. src[i] is byte number i behind a pchar: an i32 with the value 0 .. 255 (so src[i] == 'a' works without a conversion); when written, only the lower 8 bits count. An index outside is not checked.char exists only as the element of an array, not as the type of a variable. A char array without an index is the address of its first character, a pchar (hence the name): it can be passed on (ReadFile(h, src, ...)) and walked through with a pchar.i32 or u32 appears only with an index; it cannot be assigned or passed as a whole."Hello\n") is a pchar to bytes with a 0 at the end.i32: with 32 bits and a sign, / toward 0, >> keeps the sign, shifts go by 0 .. 31 (1 << 33 is 2). So 0x80000000 >> 4 is 0xF8000000; u32(x) >> 4 in a function shifts without a sign.Arrays that grow (toks: i32[], u32[], char[], without a size) have no fixed limit:
| Form | What it does |
|---|---|
append(a, x) | puts x at the end |
len(a) | the length, an i32 |
resize(a, n) | sets the length to n; new elements are 0 (resize(a, 0) empties the array) |
a[i] | read and written as in a fixed array, also a[i] += 1; the index is not checked |
append (or resize) gets twice the memory (at least 16 elements) and copies the elements there. On average every element is copied once at most. Without memory, or when the array would have 0x10000000 elements or more (also a negative length), the program ends with exit code 3.char[] without an index is, like a fixed char array, a pchar to its first character. **After append or resize the array may have moved**: a pchar taken before then points to nothing, an index stays valid.char: 1 byte). Under Windows the memory comes from HeapAlloc; it is freed at the end of the program. An array that grows stands at the top level only; it cannot be assigned or passed as a whole.Local variables come into being in a function at their first assignment; their type is that of the expression (a number gives i32), or it is written with it: p: pchar = 0. After that the variable keeps its type. The parameters are local variables too.
Global names in functions: a name of the top level is that global name in every function; an assignment changes the global variable. So a local variable cannot have the name of a global one. (This differs from Python, which needs global x.)
def add(a: i32, b: i32) i32:
return a + b
def show(s: pchar): # without a type behind the bracket there is no value
print(s)
def add(a, b) i32:). A parameter without a type is an i32. One result at most. Recursion is allowed.return; an if … elif … else that has an else and whose branches are all surely at their end; a while True: without a break of its own. A for loop is not: it may run 0 times.def main() i32:; its value is the exit code of the program.Functions of a DLL:
extern "kernel32.dll":
def ReadFile(h: u32, buf: pchar, n: u32, got: pchar, ov: u32) i32
def ExitProcess(code: u32)
They are called like your own functions. Behind the name of the DLL stands their convention: stdcall (the function takes its arguments off the stack itself, as the functions of Windows like kernel32.dll) or cdecl (the caller takes them away, as the functions of C like in msvcrt.dll). Without a word it is stdcall. A wrong word mixes up the stack: the program crashes, or its stack grows with every call. Under x64 there is one convention only, there both words make no difference.
extern "msvcrt.dll" cdecl:
def printf(text: pchar, ...) i32 # ... : any number of arguments behind
def strlen(s: pchar) i32
... as the last parameter (only in a cdecl block) takes any number of arguments more, each with a value (i32, u32, pchar). The program needs ExitProcess (and with growing arrays GetProcessHeap, HeapAlloc, HeapReAlloc) of kernel32.dll itself: it must not import them from another DLL.
| Statement | Example | |
|---|---|---|
| assignment | x = x + 1, names[i] = n, src[i] = 'a' | |
| compound assignment | i += 1, names[k] -= 2, flags \ | = 0x20, src[i] &= 0x7F |
| call | print("ok\n") (a result is dropped) | |
if / elif / else | see below | |
while | while i < n: | |
for | for i in 0..n:, for i in a..n: | |
append, resize | append(toks, k), resize(src, n) (arrays that grow) | |
break, continue | in the innermost loop | |
return | return, return x | |
pass | an empty block |
Compound assignment: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=. x += e means x = x + (e), with the same rules for the types as the operator; with names[i] += e and src[i] += e the address is computed once. The target must exist already (a variable, an array, a byte behind a pchar); no new local variable comes into being this way. It is a statement, not an expression. There is no ++ or --.
if c >= '0' and c <= '9':
kind = DIGIT
elif c == '_' or (c >= 'a' and c <= 'z'):
kind = NAME
else:
kind = OTHER
Counting loops: for i in a..n: counts i from a up to n - 1 (the end is not included, as in Python and Rust), so for i in 0..len: goes over the indexes of an array. a and n are expressions of type i32. n is computed once, before the first round; if a >= n, the loop does not run at all. i is a local variable of type i32 (it comes into being here if it is new; a global one is not possible); behind the loop it has the value n (or the one at the break). continue goes on with the next i.
On one line: behind the : of if, elif, else, while and for one simple statement may stand on the same line instead of an indented block (an assignment, a call, break, continue, return, pass), but no other if, while or for:
if c == 0: break if i == 0: return "zero" elif i == 1: return "one" else: return "many" while s[n] != 0: n += 1
From the weakest to the strongest precedence (as in Python):
| Precedence | Operators | |
|---|---|---|
| 1 | or | |
| 2 | and | |
| 3 | not | |
| 4 | == != < <= > >= (not chained: a < b < c is an error) | |
| 5 | \ | |
| 6 | ^ | |
| 7 | & | |
| 8 | << >> | |
| 9 | + - | |
| 10 | * / % | |
| 11 | -x ~x (in front) | |
| 12 | brackets, call f(x), index a[i], conversion u32(x) |
&: x & 1 == 0 means (x & 1) == 0.and and or look at the right side only when needed and give 1 or 0./ and % divide whole numbers. With i32 the result is cut toward 0 (-7 / 2 is -3, -7 % 2 is -1), as in C and in the hardware, unlike Python. A division by 0 is not checked.>> shifts with the sign for i32 and with zeros for u32. An overflow is not checked (the numbers wrap around as in the hardware).Numbers and characters: 42, 0x2A, 'a' (the value of the character, an i32). Text: "...". Both know \n, \r, \t, \0, \\, \', \" and \xHH.
include "wlan.abel" # next to this file include "board/uart.abel" # also in a folder below
include "name" stands alone on a line at the top level (not indented). The compiler reads the file at this place as if its text stood there: its constants, variables, arrays, extern blocks and functions belong to the program. There is one name space (no modules, no prefixes).include stands in (/ and \ both work); a name with a drive or from the root (C:\..., \...) counts as it is. No extension is added.include of the same file (also through another one) is passed over. An included file may use include itself.target stands only in the program itself, not in an included file.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
modules (include only puts in the text of a file), for over anything but a range a..n, struct, floating point, reg (for the boards), pointers to words (load32/store32), arrays as parameters, local arrays, freeing memory, more than one result, checks at run time.
program = { toplevel } .
toplevel = const | global | array | chars | extern | func | include .
include = "include" STRING NL . (not indented; the text of the file at this place)
const = NAME "=" expr NL . (expr of constants)
global = NAME ":" type "=" expr NL .
array = NAME ":" ("i32" | "u32") "[" [ expr ] "]" NL . (without expr: an array that grows)
chars = NAME ":" "char" "[" [ expr ] "]" NL .
extern = "extern" STRING [ "stdcall" | "cdecl" ] ":" NL INDENT { "def" NAME "(" [ xparams ] ")" [ type ] NL }
DEDENT .
xparams = "..." | param { "," param } [ "," "..." ] . (... only with 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 is the end of a line, INDENT and DEDENT are the more and less of indentation that the lexer makes of the spaces at the start of a line.
A program for the ESP32 says so in its first line with target "esp32"; target stands in the program itself only, not in an included file. Without target it is a program for Windows. abelle_esp32.exe compiles it into the code for the IRAM from 0x40080000 on and the data for the DRAM from 0x3FFB0000 on.
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 address) takes no memory; it reads and writes the registers of the chip in words of 32 bits.def name(...) at address) has no body: a function of the ROM or of a part that stays in the memory. It is called as the ROM calls its functions, with six arguments at most.main has no value and runs for ever. Functions of a DLL and arrays that grow are not there.