aBelle: the language description

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.

Contents

A first example

# 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

Lines, blocks, comments

  • A line ends with LF or CR LF; the file is UTF-8 (text and comments may hold umlauts).
  • One statement per line. A block follows a line that ends with : and is indented 4 spaces deeper than it; it ends where the indentation goes back. A tab in the indentation is an error.
  • Empty lines and lines with only a comment do not count for the indentation.
  • # starts a comment up to the end of the line (not in a string or a character).
  • A line cannot be continued (no \ at its end); inside brackets an expression may go over several lines.

Types

TypeWhatSize
i32a signed whole number, -2147483648 .. 2147483647; the default when no type is written4 bytes
u32an unsigned whole number, 0 .. 4294967295; for addresses, bit patterns, registers4 bytes
pcharthe 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").

Constants, variables, arrays

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.
  • In aBelle 0 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.
  • An array of i32 or u32 appears only with an index; it cannot be assigned or passed as a whole.
  • Text in the source ("Hello\n") is a pchar to bytes with a 0 at the end.
  • The compiler computes constants as 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:

FormWhat 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
  • When there is no room left, 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.
  • A 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.
  • An element has 4 bytes (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.)

Functions

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)
  • The type of the result stands behind the bracket (def add(a, b) i32:). A parameter without a type is an i32. One result at most. Recursion is allowed.
  • A function with a type never comes out at the end of its body, else the compiler says "f returns a i32: its end needs a return". Surely at its end is: a 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.
  • A function may be used before it stands in the file.
  • The program starts at def main() i32:; its value is the exit code of the program.
  • The arguments are evaluated from left to right.

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.

Statements

StatementExample
assignmentx = x + 1, names[i] = n, src[i] = 'a'
compound assignmenti += 1, names[k] -= 2, flags \= 0x20, src[i] &= 0x7F
callprint("ok\n") (a result is dropped)
if / elif / elsesee below
whilewhile i < n:
forfor i in 0..n:, for i in a..n:
append, resizeappend(toks, k), resize(src, n) (arrays that grow)
break, continuein the innermost loop
returnreturn, return x
passan 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

Expressions

From the weakest to the strongest precedence (as in Python):

PrecedenceOperators
1or
2and
3not
4== != < <= > >= (not chained: a < b < c is an error)
5\
6^
7&
8<< >>
9+ -
10* / %
11-x ~x (in front)
12brackets, call f(x), index a[i], conversion u32(x)
  • Unlike C the comparisons bind weaker than &: 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.

More than one file: include

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).
  • The name counts from the file the include stands in (/ and \ both work); a name with a drive or from the root (C:\..., \...) counts as it is. No extension is added.
  • Each file is read once: a second include of the same file (also through another one) is passed over. An included file may use include itself.
  • An included file starts and ends at the top level; a block does not reach over its end.
  • target stands only in the program itself, not in an included file.
  • Messages name the file the error is in: wlan.abel(12): ....
  • In the debugger of Asmonite: when the program stops in an included file, the editor shows that file in place of the program, read only. Breakpoints, F4, the watch window and the values under the mouse work there as in the program. When it stops in the program again or ends, the program is back.

Reserved words

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

What aBelle 0 does not have

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.

Grammar

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.

Programs for a board

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)