Venera


Venera Programming Language Draft Specification

Intro

Venera is a hobby low-level language, designed to be a unique style from C-like languages. It features a keyword heavy syntax. It maintains a C compatible ABI allowing C to invoke Venera and vice versa. Venera only targets x86-64 (x64). Pointers are always 64 bits (8 bytes) and memory is little-endian.

Grammar is denoted using an EBNF-like format. Lower case denotes a non-terminal, and upper case or quoted text ("::", "(") denotes terminals. A terminal is something written directly and cannot be expanded. It can be keywords, punctuation, etc. In code examples, // starts a comment, the same as in Venera itself (§1.3).

non-terminal ::=
    option-a      |
    option-b      |
    TERMINAL      |
    "("           |
    optional?     |
    zero-or-more* |
    one-or-more+

Diagnostics are reported with a code. The first digit is the compiler stage that reports it:

Code Stage
E0xxx Generic
E1xxx Lexing
E2xxx Parsing
E3xxx Analysis
E4xxx Code generation
W3xxx Analysis warnings
T0xxx Traps

1. Lexing

1.1 Source Text

Venera will reject any file that contains no characters. It will report error code E0002. When loading a file all \r characters will be stripped and every \t will be converted to four spaces.

1.2 Tokens

Stage one of the compiler is to generate tokens. Tokens are a collection of characters that build up words or symbols. A token will contain a lexeme which is the characters used to build up a token. For example, the following text would generate these tokens. IDENTIFIER: "var" INTEGER_LITERAL: 14.

var 14

The lexer will generate errors for unknown characters or for malformed literals.

1.3 Comments

Venera supports both line comments and multiline comments.

// The entire line is ignored

/*
    The characters between /* and */ are ignored
*/

The lexer will skip over comments and not generate tokens for them. If a multiline comment is not terminated, then error E1006 is reported.

1.4 Identifiers

Identifiers are a token which contains a name (it identifies something) and is not a keyword. They are used for variables, namespaces, functions, structs, etc. They are made up of both alphanumeric characters as well as underscores, however, it is important to note that the first character CANNOT be a digit; Otherwise a number token will be generated instead.

1.5 Keywords

Keywords are language specific identifiers. They cannot be used for variable names, etc.

PROGRAM DO END DEF RETURN NAME CONST WRITE INVOKE EXTERN REF DEREF
CAST IF ELSE WHILE BREAK CONTINUE OR AND NOT AT ARRAY SIZEOF LENGTHOF
NAMEOF STRUCT ENUM UNION NAMESPACE USE PUB VASTART VAARG VAEND INC DEC
IS AS
ADD SUB MUL DIV MOD
BAND BOR BXOR BNOT SHL SHR
TRAP
TRUE FALSE NULL
U8 U16 U32 U64 I8 I16 I32 I64 BOOL CHAR F32 F64 PTR VOID VALIST

ADD, SUB, MUL, DIV and MOD are compound operators, meaning they work similar to +=, etc in C. x ADD 1 is the same as x WRITE x + 1. See §6.4.

1.6 Literals

1.6.1 Integer

Integer literals can be either base-10 or base-16. A base-10, or decimal can be made up of any digit characters. A base-16, or hexadecimal can be made up of any digit character, as well as the letters a-f (both upper and lower case). Hexadecimal requires the $ prefix. I.e. $abcd

Suffix

Integers can take the following suffixes to modify the type:

Example:
    42        // from context, otherwise I32
    42u       // U32
    42l       // I64
    42ul      // U64
    $FF       // from context, otherwise I32
    $FFFFFFFF // from context, otherwise I64

1.6.2 Floating point

Floating point literals, or floats, are integers which can also take a decimal point. The decimal point is required otherwise an integer token will be generated. To avoid this, use the decimal point at the end. 123.

Suffix

Floats can take the following suffixes to modify the type:

Example:
    42   // integer, see above
    42.  // F64
    42.f // F32

1.6.3 Strings

String literals are any characters contained between " characters. Venera supports escape characters which allow strings to contain special characters. These are as follows:

\n - Newline         (ascii 0x0a)
\r - Carriage return (ascii 0x0d)
\t - Tab             (ascii 0x09)
\b - Backspace       (ascii 0x08)
\f - Form feed       (ascii 0x0c)
\v - Vertical tab    (ascii 0x0b)
\a - Alert           (ascii 0x07)
\\ - Backslash       (ascii 0x5c)
\" - Double quote    (ascii 0x22)
\' - Single quote    (ascii 0x27)
\0 - Null            (ascii 0x00)

If an escape sequence is unknown, say \q then code E1003 will be reported. If a string is not terminated, either end of file or end of line, then code E1004 is reported.

1.6.4 Characters

Character literals are similar to strings but they use ' instead. They can also store an escape sequence. If a character contains more than one character, or is not terminated, then E1005 is reported.

1.6.5 Booleans

Bool literals are the keywords TRUE or FALSE.

1.6.6 Null pointer

Null literal is the keyword NULL.

1.7 Punctuation

. , () ... $ ::
+ - * / %
== != < <= > >=

... is a variadic parameter, not three . characters.


2. Basic concepts

2.1 Versioning

Venera uses a semantic version system of MAJOR.MINOR.PATCH. Patch is used for bug-fixes or any small change which does not break backwards compatibility. A program written for 1.0.0 will compile with 1.0.1 Minor is for bigger changes which can break compatibility. This may change behaviour or remove features. A program written for 1.1.0 may not compile for 1.2.0. When loading a pre-compiled library, the compiler will refuse to load a library if it is compiled with an older major or minor version as it can't guarantee they are compatible. Major is for the biggest changes, which may completely overhaul the language. Unlike minor which may break backwards compatibility, a major change will break it. A 1.0.0 program will not compile on a 2.0.0 compiler.

2.2 Translation units and programs

Each file is a translation unit. A project is a directory containing a venera.toml manifest and a src directory. Each file contains one program. A program can optionally have a name which allows other programs to import it. Multiple units can share a program name, which allows them to be imported together.

program ::=
    use-directive* PROGRAM qualified-name? DO name-block? top-level-declaration* END

use-directive ::=
    USE qualified-name

qualified-name ::=
    IDENTIFIER ( "::" IDENTIFIER )*

top-level-declaration ::=
    public-definition    |
    function-definition  |
    extern-declaration   |
    namespace-definition |
    const-block          |
    struct-definition    |
    union-definition     |
    enum-definition

public-definition ::=
    PUB ( function-definition | extern-declaration | struct-definition | union-definition | enum-definition )

namespace-definition ::=
    NAMESPACE qualified-name DO name-block? top-level-declaration* END

If a program has no name it means it cannot be imported. Generally you will not use a name for Main.lna.

2.3 Multi-file linking

Every .lna file in the project's src folder is compiled, for both executables and libraries. A file does not need to be used anywhere to be compiled, so unused files are still checked for errors. An executable requires a Main.lna, which contains the Main function (§7.2). A library does not need one.

USE controls which programs a file can see. A file can only access the PUB declarations of programs it uses. If a program is used where it doesn't exist, then E3039 is raised. Files with the same program name can see each other's PUB declarations without a USE.

USE only makes the program's declarations available. It does not affect namespaces, and a program's name is not a namespace. Declarations are always referred to by their own namespace, wherever they come from.

// In the Std program
PROGRAM Std DO
    NAMESPACE Memory DO
        PUB DEF VOID Free(VOID PTR p) ...
    END
END

// In another file
USE Std
PROGRAM App DO
    DEF I32 Main DO
        ...
        INVOKE Memory::Free(p)    // not Std::Memory::Free
        ...
    END
END

If two visible declarations have the same qualified name, for example two used programs that both declare Memory::Free, then E3005 is reported.

Programs can use each other in a cycle. If A uses B and B uses A, both are allowed, and each file can use the other's PUB declarations, including calling each other's functions. This works because the compiler collects the declarations of every file before checking any function bodies, so the order files are compiled in does not matter. Structs and unions that contain each other by value are still an error (E3034, §3.4), even across files.

2.4 Visibility

Declarations within a file are private by default. Within a file, all declarations are reachable, regardless of public or private. A private declaration can only be used in the file it is declared in. This applies even to other files with the same program name (§2.2); they can only see each other's PUB declarations. When using a program, by default you can't access anything and you have to opt-in to public access. To make something public, use PUB before the declaration. PUB DEF ... PUB STRUCT ... Note that if a public symbol uses a private one, say as a parameter or member, then E3040 will be raised.

2.4.1 Namespaces

Namespaces append a qualified name to any names within them. Namespaces can be nested but also accept a qualified name.

NAMESPACE A DO NAMESPACE B DO ... // Allowed
NAMESPACE A::B DO ...            // Better

These do the same thing and both are accepted.

Namespaces and the declarations inside them are collected before any function bodies are checked (§2.3), so a name can be used before it is declared. When resolving an identifier it will check the current namespace and begin working up until it is found. Otherwise if a symbol is not found, then E3008 is thrown.

2.5 Scope

Venera uses DO and END for scoping. These work similar to { } in C. Each block will open up a new scope. This behaviour is reused for functions, namespaces etc. Blocks can be created at any time within a function. Each block can only define a variable once, otherwise E3005 is thrown.

A nested block can declare a variable with the same name as one in an outer scope. This is called shadowing. Inside the nested block, the name refers to the new variable and the outer one cannot be accessed. Once the block ends, the name refers to the outer variable again.

NAME DO
    I32 x
END
DO
    NAME DO
        F64 x // shadows the outer x
    END
    // x is the F64 here
END
// x is the I32 again

Function parameters belong to the function's outermost block, so declaring a variable with the same name as a parameter directly in the function body reports E3005. A nested block inside the function can shadow it.

2.6 NAME and CONST blocks

Venera does not use inline declaration like C, rather, variables are declared at the top of the scope. A NAME block declares all variables in the current scope as well as their types. Each scope can have at most one NAME block, and it must be the first thing in the scope, straight after the DO. This applies to blocks inside functions (§6.1), and also to the top level of a program and each namespace (§2.2), where the NAME block declares global variables. This is enforced by the grammar. Variables cannot be given an initial value in a NAME block; use WRITE after it. Initial values for variables may be added in a future version. A CONST block is the same but can only be used at the top level, and requires an initial value using IS.

name-block ::=
    NAME DO variable-declaration* END

const-block ::=
    CONST DO const-declaration* END

variable-declaration ::=
    type IDENTIFIER ( "," IDENTIFIER )*

const-declaration ::=
    type IDENTIFIER ( "," IDENTIFIER )* IS expression

You can separate the names with commas to declare multiple variables with the same type. This is the same as this in C.

int a, b;

One important difference from C is that PTR is part of the type, not the name, therefore the PTR applies to all names.

NAME DO
    I32 PTR a, b
END

is the same as

int *a, *b;

Local variables are not initialised. They will store garbage until written to. The compiler tracks uninitialised local variables and so usage without initialisation will result in E3021. Global variables (declared in a NAME block at the top level or in a namespace) start as zero. Every bit is 0, so numbers are 0, BOOLs are FALSE and pointers are NULL.

2.6.1 Constant expressions

A CONST initialiser must be a constant expression, which is evaluated by the compiler. An array size (§3.3) can use one by naming a CONST. A constant expression can only contain:

Variables, INVOKE, REF, DEREF, AT, field access and NULL are not allowed. Using anything else in a CONST reports E3045.

CONST DO
    U64 BufferSize IS 4 * 1024
    U64 HalfBuffer IS BufferSize / 2
    U64 HeaderSize IS SIZEOF Header
    CHAR PTR Greeting IS "hello"
END

A CONST can use another CONST declared later in the file, as long as they do not depend on each other in a cycle. A cycle such as A IS B + 1 and B IS A + 1 reports E3056.

Constant expressions follow the same rules as runtime ones. Integer overflow wraps (§5.9). Division by zero reports E3051 and an out of range shift reports E3052, since these would be traps at runtime.


3. Types

3.1 Fundamental types

type ::=
    base-type type-suffix*

base-type ::=
    primitive-type |
    VOID           |
    VALIST         |
    qualified-name

type-suffix ::=
    PTR                                        |
    ARRAY ( INTEGER_LITERAL | qualified-name ) |
    "(" ( parameter-type-list | ELLIPSIS )? ")"

primitive-type ::=
    integral-type | CHAR | BOOL | F32 | F64

integral-type ::=
    U8 | U16 | U32 | U64 |
    I8 | I16 | I32 | I64
U8 U16 U32 U64  // Unsigned integers
I8 I16 I32 I64  // Signed integers
CHAR            // 8-bit unsigned integer
BOOL            // 8-bit true/false value
F32 F64         // Floating point single/double precision
VOID            // Nothing. Can only be used for functions with no returns and for pointers to an unknown type
VALIST          // A variadic argument list, the same as C's va_list (§6.8)

3.2 Pointer types

type ::=
    base-type type-suffix*

type-suffix ::=
    PTR |
    (* other alternatives: see §3.1 *)

A pointer is a memory address to a specific type. I32 PTR means at that address is an I32. Pointers can point to pointers. Any type can be a pointer. VOID PTR is used as a universal pointer type. Any other pointer can be implicitly cast to and from it. To dereference a pointer, the DEREF keyword is used. If a VOID PTR is dereferenced, then E3018 is thrown. Additionally if VOID PTR is used in arithmetic, it will throw E3019. To get a pointer to something you can use REF. REF i == &i

3.3 Array types

type ::=
    base-type type-suffix*

type-suffix ::=
    ARRAY ( INTEGER_LITERAL | qualified-name ) | (* qualified-name must name an integer CONST *)
    (* other alternatives: see §3.1 *)

Array elements start at 0, so an I32 ARRAY 10 has the indices 0 to 9. An array in Venera is bounds checked, meaning it is impossible to read or write out of bounds. If the index is a constant, an out of bounds index is reported at compile time with E3027. Otherwise it is checked at runtime and an out of bounds index raises trap T0002 (§8). Arrays can be automatically decayed to a pointer which strips that bounds check. An array can be cast to a pointer, but a pointer cannot be cast to an array as it can't guarantee that the size would be correct. Arrays can be used as a parameter for a function or as a struct member.

An array parameter is passed by reference. The caller passes a pointer to its array, so any writes inside the function change the caller's array. Since the length is part of the parameter's type, LENGTHOF and bounds checks still work inside the function. The argument must be an array of exactly the same type and length, otherwise E3009 is reported.

DEF VOID Fill(I32 ARRAY 10 values) DO
    values AT 0 WRITE 5     // changes the caller's array
    RETURN VOID
END

An array that is a struct member is stored inside the struct, so passing the struct by value copies the array. The size of an array must be known at compile time. The size is either an integer literal or the name of an integer CONST. To use an expression, declare it as a CONST first (§2.6.1). If the name is not a CONST then E3046 is reported. The size must be greater than 0, otherwise E3055 is reported. I32 ARRAY 0 is not allowed. To pass an array to a C function, you will have to pass it as a pointer and a size. You can use LENGTHOF to get the length of an array. Again, you lose this once it is decayed to a pointer. The length is tracked by the compiler, not stored in memory. An array cannot be assigned to or returned by value. These can throw E3009 or E3049.

3.4 Struct and unions

struct-definition ::=
    STRUCT IDENTIFIER DO field-declaration* END

union-definition ::=
    UNION IDENTIFIER DO field-declaration* END

field-declaration ::=
    type IDENTIFIER ( "," IDENTIFIER )*

A struct is a collection of variables. The members are placed in memory one after another, with padding and alignment as required. The size of a struct is the total size of each member, again with padding and alignment included. A union on the other hand has each member stored in the same memory address. The size of a union is the size of the largest member. A member cannot contain the current struct/union without it being a pointer, since this would result in the structure having an infinite size. You also have to be aware of cyclic dependencies:

STRUCT A DO
    B b
END

STRUCT B DO
    A a
END

Without one of the members being a pointer, this is a compiler error, again for the same reason above. This one will give the code E3034

3.5 Enum types

enum-definition ::=
    ENUM integral-type? IDENTIFIER DO enum-member ( "," enum-member )* ","? END

enum-member ::=
    IDENTIFIER ( IS "-"? INTEGER_LITERAL )?

An enum is a way of applying a name to an integral value. Enums in Venera are type safe and work like C++'s enum class. An enum is required to have at least one member, otherwise E2027 is reported. By default, an enum is stored as an I32, however you can specify any integer type to change how large the underlying type is. The first member will have the value 0 and the value is incremented for each next member, unless a value is specified. At the next valueless member, it will begin to increment again off of the last value. If a member's value does not fit in the underlying type (e.g. -1 in an ENUM U8), E3050 is reported. Enum members must have a unique name, or else E3047 is thrown. Enums support the bitwise operators BAND, BOR, BXOR and BNOT, so they can be used as flags. See §5.10.

3.6 Function types

type-suffix ::=
    "(" ( parameter-type-list | ELLIPSIS )? ")" |
    (* other alternatives: see §3.1 *)

parameter-type-list ::=
    type ( "," type )* ( "," ELLIPSIS )?

A function type is a pointer to a function. It must be followed by at least one PTR. Not declaring it as a pointer will report E3048. A function pointer can be invoked like a normal function, using the INVOKE keyword. Unlike a normal function where the parenthesis are optional if empty, a function pointer requires the parenthesis otherwise the compiler can get confused on the type.

I32 PTR   // Is this an I32 pointer or a parameterless function which returns I32?
I32() PTR // Now it is clear it is a function pointer

3.7 Sizeof

SIZEOF type
SIZEOF unary-expression

SIZEOF is used to get the total size of a variable or type in bytes. If used on an array, it will return the size of the underlying type, multiplied by the length. If used on a pointer, it will return 8, since Venera only targets x64. The result is a U64, and is a compile time constant. If used on VOID then it will report E3028 as void has no size.

3.8 Lengthof

LENGTHOF unary-expression

LENGTHOF is used to get the element count of an array. It cannot be used on a pointer as only arrays have size information attached. The result is a U64, and is a compile time constant. In the future, LENGTHOF will also support enums.


4. Conversions

Venera has two ways of converting values. Implicit and explicit. Implicit coercion happens automatically wherever a value of one type is used where another is expected: WRITE, initialisation, argument passing and RETURN. Explicit cast requires the CAST operator (§4.3). Coercion is a strict subset of what CAST can do.

4.1 Implicit coercion

A value of type from will be cast to to if:

4.2 Narrowing conversion

As mentioned above, numeric types can implicitly converted however this can result in data loss. For example, if an I64 is narrowed to an I16, then 6 bytes are being erased, regardless of if they store data or not. These cases report W3000. Unsuffixed integer literals take the type of their destination (§4.5), so writing 10 to an I16 never narrows. Narrowing only applies to values with a fixed type, such as variables, function results and suffixed literals.

The rules for narrowing are as follows:

When mixing signed and unsigned integers where the unsigned type is at least as wide, the signed operand is converted to unsigned (§4.4) and W3001 is reported, since negative values will be lost.

4.3 Explicit cast

CAST type AS unary-expression

CAST is an explicit cast which will cast without any warnings. It is the equivalent of a C-style cast or C++'s static_cast. The following are allowed:

From To
BOOL Integer
Integer BOOL
Enum Integer
Integer Enum
Pointer Pointer
Pointer U64
U64 Pointer
Integer Integer
Integer Float
Float Integer
Float Float
Array Pointer (only the underlying element type)
NULL Pointer

Integer refers to any integral type, including CHAR. I.e. casting an I32 to U8. Same with float referring to F32 or F64. And the same with pointers. Any cast not listed here will result in E3015

4.3.1 Float to integer

Converting a float to an integer, either with CAST or implicitly (§4.1), first truncates toward zero, then saturates. Saturating means a value outside the integer type's range is clamped to the nearest value it can hold. NaN becomes 0.

Value U8 I8
12.9 12 12
-12.9 0 -12
300.0 255 127
-300.0 0 -128
infinity 255 127
-infinity 0 -128
NaN 0 0

Unlike C, this is never undefined and never traps.

4.4 Rule for operators

When mixed types are used with a binary operation or comparison, a common type will be used.

a b Common type
I16 I64 I64
I64 U32 I64
I32 U32 U32
I32 U64 U64
U8 U8 U8

CHAR counts as a U8. Unlike C, small integers are not promoted to I32 first, so U8 + U8 is a U8. When a signed operand is converted to an unsigned common type, W3001 is reported (§4.2).

An unsuffixed integer literal does not count as a mixed type, see §4.5.

4.5 Unsuffixed integer literals

An integer literal without a suffix does not have a fixed type. It takes the type of where it is used:

The context passes through parentheses and through operators where both operands are literals, so b WRITE (1 + 2) * b uses both 1 and 2 as the type of b.

NAME DO
    U8 b
    I64 big
END

b WRITE 10          // 10 is a U8
b WRITE b + 1       // 1 is a U8, no narrowing
big WRITE 1 SHL 40  // 1 is an I64, so the shift does not overflow
b WRITE b + 300     // E3050, 300 does not fit in a U8
b WRITE -1          // E3050, -1 does not fit in a U8

If the literal, or a constant expression made only of literals, does not fit in the type it takes, E3050 is reported. A - directly before a literal is part of the check, so -1 cannot be used as a U8.

Suffixed literals (42u, 42l, ...) always have their fixed type and never take one from context. Float literals are not affected by this rule. An unsuffixed float literal is always an F64, so f WRITE f * 2.5 with an F32 f narrows (W3000); use 2.5f instead.


5. Expressions

5.1 Grammar and precedence

expression ::=
    logical-and-expression ( OR logical-and-expression )*

logical-and-expression ::=
    not-expression ( AND not-expression )*

not-expression ::=
    NOT not-expression |
    comparison-expression

comparison-expression ::=
    bit-or-expression ( ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) bit-or-expression )?

bit-or-expression ::=
    bit-xor-expression ( BOR bit-xor-expression )*

bit-xor-expression ::=
    bit-and-expression ( BXOR bit-and-expression )*

bit-and-expression ::=
    shift-expression ( BAND shift-expression )*

shift-expression ::=
    arithmetic-expression ( ( SHL | SHR ) arithmetic-expression )*

arithmetic-expression ::=
    term ( ( "+" | "-" ) term )*

term ::=
    unary-expression ( ( "*" | "/" | "%" ) unary-expression )*

unary-expression ::=
    "-" unary-expression                 |
    DEREF unary-expression               |
    BNOT unary-expression                |
    REF postfix-expression               |
    CAST type AS unary-expression        |
    SIZEOF type                          |
    SIZEOF unary-expression              |
    LENGTHOF unary-expression            |
    VAARG IDENTIFIER type                |
    NAMEOF unary-expression              |
    postfix-expression

postfix-expression ::=
    factor ( ( AT index ) | ( "." IDENTIFIER ) )*

index ::=
    "-"? factor

factor ::=
    "(" expression ")" |
    primary

primary ::=
    INTEGER_LITERAL | FLOAT_LITERAL | STRING_LITERAL | CHAR_LITERAL |
    TRUE | FALSE | NULL                                             |
    qualified-name                                                  |
    INVOKE lvalue-chain argument-list?

argument-list ::=
    "(" ( expression ( "," expression )* )? ")"

5.2 REF and DEREF

REF x will get a pointer to x (variable or function). It is the same as &x in C. DEREF x will dereference x. It requires it to be at least one level of pointer. DEREF cannot be used on a VOID PTR and will report E3018. Using it on a non-pointer reports E3013. Returning a reference to a local variable is illegal and will report E3022.

5.3 NOT

NOT expr requires expr to evaluate to BOOL. If it does not, then E3014 is reported.

5.4 CAST

CAST is used for converting an expression from one type to another. It takes a type and an expression to convert: CAST I32 AS x. See §4.3.

5.5 SIZEOF

SIZEOF will get the total size of a variable or type. See §3.7.

5.6 LENGTHOF

LENGTHOF will get the length of an array. See §3.8.

5.7 VAARG

VAARG IDENTIFIER type

Gets the next variadic argument as type from the VALIST identifier. type must be a type that survives C's argument promotions, so F32, BOOL, CHAR and integers smaller than 32 bits are not allowed (§9.5).

5.8 Indexing and field access

target AT index
target . field

The target of AT must be an array or a pointer, otherwise E3025 is reported. It cannot be a VOID PTR (E3019). The index must be an integer. The target of . must be either a struct, union or pointer to one, otherwise E3032 is reported. E3033 may be thrown if the struct/union does not have a field of that name.

5.9 Binary operators

If both operands are BOOL then ==, !=, AND, OR are allowed. AND and OR only accept BOOL operands, otherwise E3014 is reported. AND and OR are logical operators. Only the bare minimum is evaluated. A() AND B(). If A() returns false then B() is not invoked. BOOL's do not support arithmetic so TRUE + FALSE is illegal and also comparing a BOOL to a non-bool type.

When one operand is NULL the other side must be a pointer type. Only == and != are allowed. If one operand is a pointer (not NULL literal) then arithmetic is unlocked. Pointer arithmetic allows a pointer to add or subtract an integer, say for indexing into a block of memory. Two pointers cannot be added, however they can be subtracted, assuming they're the same pointer type. If a pointer is VOID PTR then no arithmetic is allowed. Similarly, comparison operators on pointers require them to be the same type.

Pointer arithmetic counts in elements, not bytes, the same as C. With an I32 PTR p, p + 5 moves forward 5 I32s, which is 20 bytes. Subtracting two pointers gives the number of elements between them as an I64. It can be negative if the right pointer is after the left.

NAME DO
    I32 ARRAY 10 arr
    I32 PTR a, b
END
a WRITE REF arr AT 2
b WRITE REF arr AT 7
// b - a is 5 (20 bytes apart), a - b is -5

If one side is an enum then the other operand must be the exact same enum. You cannot use an integer. It must be cast to the enum type first.

When the operands are numbers, the modulo operator is not allowed if one of the operands is a floating point number. They must both be integers.

Integer arithmetic always wraps using two's complement, in every build. Overflow is never undefined and never traps. For example, a U8 holding 255 plus 1 gives 0, and -x where x is the smallest I32 gives the same value back. The same applies to ADD, SUB, MUL, INC and DEC. Dividing the smallest signed value by -1 also wraps, giving the smallest value back, and MOD gives 0. Integer division or MOD by zero raises trap T0001 (§8). If the divisor is a constant 0, E3051 is reported at compile time instead. Float division by zero is not a trap. It follows IEEE 754 and gives infinity or NaN.

5.10 Bitwise operators

a BAND b    // bitwise and          (a & b in C)
a BOR b     // bitwise or           (a | b in C)
a BXOR b    // bitwise exclusive or (a ^ b in C)
BNOT a      // bitwise not          (~a in C)
a SHL n     // shift left           (a << n in C)
a SHR n     // shift right          (a >> n in C)

Bitwise operators only work on integers and enums. Using them on a float reports E3026, and on a BOOL reports E3014. For logical operations on BOOL use AND, OR and NOT.

Unlike C, the bitwise operators bind tighter than comparisons, so flags BAND $FF == 0 means (flags BAND $FF) == 0. From loosest to tightest: BOR, BXOR, BAND, then SHL/SHR, then arithmetic (§5.1).

BAND, BOR and BXOR follow the same common-type rules as arithmetic (§4.4). If one operand is an enum, the other must be the same enum, and the result is that enum. BNOT on an enum also gives that enum. These results may not match any member of the enum (e.g. Flag::Read BOR Flag::Write), see §5.13.1.

SHL and SHR require integer operands; they cannot be used on enums. The result has the type of the left operand; the right operand can be any integer type. SHR on a signed type is arithmetic (the sign bit is copied in). On an unsigned type it is logical (zeros are shifted in).

Bits shifted out by SHL are discarded. Shifting by a negative amount, or by at least the bit width of the left operand (e.g. x SHL 32 on an I32), raises trap T0003 (§8). If the amount is a constant, E3052 is reported at compile time instead.

There are no compound forms of the bitwise operators. Use x WRITE x BOR y.

5.11 Unary minus

The unary minus -expr is only valid on numeric values. If expr is a pointer, or enum etc, then one of these errors will be thrown, depending on the type: E3014, E3019, E3038.

5.12 Function calling

INVOKE lvalue-chain argument-list?

The lvalue-chain can name either a function (optionally namespace-qualified, e.g. INVOKE Std::Memory::Free(p)) or a function pointer. If a function takes zero parameters then the parenthesis are optional INVOKE Foo. If the passed arguments do not match the required amount then E3010 will be reported, unless the function has a variadic argument. Each argument must be the parameter's type, or coerce to it (§4.1), otherwise E3011 is reported.

5.13 Enum access

Namespace::Path::EnumName::MemberName

An enum member is accessed with the enum's name, a ::, then the member's name. The enum's name can be namespace qualified.

Colour::Red
Graphics::Colour::Red

5.13.1 NAMEOF

NAMEOF gives the name of an enum member as a CHAR PTR string.

NAMEOF Colour::Red
NAMEOF c

If it is given a member directly, like Colour::Red, then the name is known at compile time. If it is given a enum value variable, then the name is looked up at run time. If the value does not match any member then the string "UNKNOWN" is returned. Using NAMEOF on something that is not an enum will raise E3038.


6. Statements

6.1 Grammar

block ::=
    DO name-block? statement* END

statement ::=
    RETURN expression                    |
    RETURN VOID                          |
    BREAK                                |
    CONTINUE                             |
    VASTART IDENTIFIER                   |
    VAEND IDENTIFIER                     |
    TRAP STRING_LITERAL                  |
    block                                |
    INVOKE lvalue-chain argument-list?   |
    lvalue write-value                   |
    WHILE expression block               |
    if-statement

6.2 Blocks and scope

A block in Venera is declared using DO and END as stand-ins for C's {}. Within a function, a block will create a new scope. A variable declared inside a block is not usable outside. A block can start with one NAME block (§2.6).

6.3 Returning

RETURN VOID
RETURN expression

A function with the return type VOID must use RETURN VOID as due to the lack of semicolons, return must take something. If an expression is used then E3001 will be reported. If a function does return something then the expression returned must be the same type, or a type which can be coerced. Error E3003 will be reported otherwise. Using RETURN VOID in it reports E3002. A function that returns something must RETURN on every path, otherwise E3000 is reported. A path that ends in TRAP does not need a RETURN. If a function returns early then E3016 may be reported for unreachable code.

6.4 Writing and compound operations

lvalue ::=
    DEREF unary-expression |
    lvalue-chain

lvalue-chain ::=
    qualified-name ( ( AT index ) | ( "." IDENTIFIER ) )*

write-value ::=
    WRITE expression               |
    compound-assignment expression |
    INC                            |
    DEC

compound-assignment ::=
    ADD | SUB | MUL | DIV | MOD

While + is used as a normal expression (a + 1), for a compound operator (a += 1) ADD is used instead. a += 1 becomes a ADD 1. INC/DEC cannot be used post/prefix like a++ or ++a. It cannot be used in an expression. a + b INC is not allowed. INC/DEC are standalone statements.

Write can be used with AT. array AT 3 WRITE b will write b to the element at index 3, which is the fourth element since indexes start at 0. Write cannot target a constant value, otherwise E3044 will be reported.

6.5 Dereference write

lvalue ::=
    DEREF unary-expression |
    lvalue-chain

DEREF can be used to write through a pointer. If used on a non-pointer then E3013 is reported. It can be chained for nested pointers.

6.6 IF

if-statement ::=
    IF expression DO name-block? statement* ( ELSE IF expression DO name-block? statement* )* ( ELSE DO name-block? statement* )? END

The condition of an if statement must evaluate to a bool, otherwise E3023 will be reported. Unlike C where a block is not needed, Venera requires a DO after each condition. Each clause runs until the next ELSE, and a single END closes the whole IF.

IF a DO
    ...
ELSE IF b DO
    ...
ELSE DO
    ...
END

Uninitialised variable tracking is merged across branches, so if the ELSE writes but IF doesn't, it will still be tracked as uninitialised. The flag will be dropped only if every branch writes to it.

6.7 WHILE BREAK CONTINUE

WHILE expression block

The condition of a while loop must evaluate to a bool, otherwise E3024 will be reported. A while loop will run the block for as long as the condition is true. Exceptions are for CONTINUE and BREAK. CONTINUE will skip the rest of the block and check the condition again. BREAK will stop the loop, regardless of whether the condition is true or false. Using BREAK outside a loop reports E3030, and CONTINUE outside a loop reports E3031.

6.8 VASTART / VAEND

VASTART IDENTIFIER
VAEND IDENTIFIER

identifier must be a type VALIST since VASTART and VAEND operate on a variadic parameter list, otherwise E3041 is reported. To begin using a VALIST you must use VASTART before usage, and then clean it up at the end with VAEND. VASTART can only be used inside a function with a ... parameter, otherwise E3042 is reported.

6.9 Function calling as a statement

Unlike C, an expression cannot be a statement, as a result, this is a separate parsing node. It is used the exact same as the expression call. See §5.12.

6.10 TRAP

TRAP STRING_LITERAL

TRAP stops the program immediately with the given message, raising trap T0000 (§8).

IF kind == Kind::Unknown DO
    TRAP "unhandled kind"
END

TRAP is never removed, in either debug or release builds. Any statements after it in the same block are unreachable (E3016).


7. Declarations

7.1 Function definitions

function-definition ::=
    DEF type IDENTIFIER parameter-list? block

parameter-list ::=
    "(" ( parameter ( "," parameter )* ( "," ELLIPSIS )? | ELLIPSIS )? ")"

parameter ::=
    type IDENTIFIER

A parameter must not be of type VOID, unless it is a pointer. Only one variadic args (...) can be used per parameter list, and it must be last. Venera does not use function overloading so a function name must be unique regardless of return type or parameters. Functions of the same name can be used as long as they are in a different namespace.

7.2 Entry point

For an executable, a function called exactly Main must exist. It must be declared in src/Main.lna (§2.3), at the top level and not inside a namespace. If there is no such function then E3017 is reported. It must return I32 and can take either no parameters or exactly I32 argc, CHAR PTR PTR argv. The following errors may be reported if Main does not match: E3017, E3004, E3035. A library, either SHARED or STATIC, does not follow these rules. Main is optional and can take any parameters. Main is required to return a value. Unlike C, it will not automatically return 0. E3000 will be reported otherwise.

The compiler generates the C entry point main for an executable. It is a small function which first runs any program initialisation, then invokes Main, and returns Main's result as the process exit code. If Main takes no parameters, argc and argv are not passed to it. Main itself keeps the symbol name Main (§9.1). Since the generated function uses the symbol main, a function named main in an executable reports E3053.

7.3 External functions

extern-declaration ::=
    EXTERN DEF type IDENTIFIER parameter-list? ( AS STRING_LITERAL )?

Declares a function without a body. Tells the compiler that this function's source comes from somewhere else, usually a library. The AS part gives the function a different name in Venera from its native symbol name. For example, if a C function is called my_c_function, then Venera can do

NAMESPACE Venera DO
    EXTERN DEF VOID CFunction AS "my_c_function"
END

Now you can invoke it via Venera::CFunction and directly call my_c_function. If an external function is not resolved, then the LLVM linker will report an error, not the Venera compiler.

7.4 Public visibility

Currently, only DEF, EXTERN DEF, STRUCT, UNION and ENUM allow the PUB access specifier. This specifier allows other files to access these items (§2.4). A PUB function is also exported from the compiled binary (§9.2). In the future NAME and CONST may be added. A future INTERN specifier is planned, which will make a declaration visible to every file in the same program but not to programs that USE it, and not exported from a library.

7.5 Library declaration files

In languages like C#, a library carries a description of its types and functions, so you can use it just by referencing it. Venera uses standard .dll/.so/.lib files, which only contain LLVM-IR symbol names, not Venera, and no type information. Instead, building a library will also generate a .vni (Venera interface) file. A .vni file is a platform independent description of the library's public declarations: function signatures, structs, unions and enums. It tells the compiler what exists in the library, so it can type check code that uses it without the library's source. The linker then resolves the symbols themselves and may report errors. .vni files only contain public (PUB) declarations.

7.6 Build system

Venera primarily compiles via projects. Using venera init or venera new will create project files. A project will be rooted with venera.toml which contains information about the project, such as the name, optimisation for debug and release builds and whether this is an executable, static or shared library. venera.toml will also specify dependencies. These can be source venera projects, prebuilt libraries, both on disk, or via package manager or git. A src folder will be created. This is where all .lna files will be stored. A target folder is where binaries will be. This can contain .exe, .dll/.so, .o, .vni, .lib, etc.

venera build [--release] will be used to build the project. venera run [--release] will build and then run the generated application (only for executables, not libraries)


8. Traps

A trap is a runtime check that has failed. Venera has no exceptions, so currently a trap cannot be caught or recovered from. When a trap is raised the program prints a message to stderr and then calls abort(). Catching traps is planned for a future version (§8.4). Using abort() means a debugger will stop at the trap, and the operating system can produce a crash dump.

8.1 Trap codes

Traps use the T prefix, separate from compiler errors and warnings.

Code Raised by
T0000 A TRAP statement (§6.10)
T0001 Integer division or MOD by zero (§5.9)
T0002 Array index out of bounds (§3.3)
T0003 Shift by a negative amount or by at least the bit width (§5.10)

All checks are active in both debug and release builds, and cannot be turned off. If a check can be decided at compile time, the compiler reports an error instead of emitting a trap.

The following are not checked:

8.2 Trap output

In a release build the message contains the trap code, the message and where it was raised:

trap T0001: division by zero
    at src/Maths.lna:12:9 in Maths::Average

In a debug build a stacktrace of the call stack is printed after the location:

trap T0002: index 10 out of bounds for array of length 10
    at src/Buffer.lna:30:17 in Buffer::Get
stacktrace:
    0: Buffer::Get      src/Buffer.lna:30:17
    1: Parser::Next     src/Parser.lna:88:12
    2: Main             src/Main.lna:9:5

For a TRAP statement, the message is the string given to it.

8.3 Traps and C

A trap inside Venera code always aborts the whole process, even when that code was called from C. The trap handler is emitted by the compiler into every module, so a Venera library does not need a separate runtime library to be linked.

8.4 Future: catching traps

A future version will allow traps to be caught. The design is not decided yet, but it must keep these properties:


9. C interoperability

Venera uses the C ABI of the target platform, so C can call Venera and Venera can call C without any wrappers.

9.1 Symbol names

The symbol name of a function is its qualified name with each :: replaced by _.

DEF I32 Add(I32 a, I32 b) ...           // Add

NAMESPACE Maths DO
    PUB DEF F64 Average(F64 a, F64 b) ...   // Maths_Average
END

NAMESPACE Maths::Vector DO
    PUB DEF F64 Length(F64 x, F64 y) ...    // Maths_Vector_Length
END

From C these can be called directly:

double Maths_Average(double a, double b);

Since _ is also allowed in identifiers, two different qualified names can produce the same symbol, for example Maths::A_B and Maths_A::B. If two PUB functions produce the same symbol then E3053 is reported. Private functions are not checked since they are not visible outside their file (§9.2). EXTERN DEF uses its own name, or the name given with AS (§7.3), and is not affected by namespaces.

9.2 Linkage

A PUB function is exported. It is visible to other programs and, for a library, is exported from the .dll/.so and written to the .vni file (§7.5). A function without PUB is private to its file (§2.4). It has internal linkage, so it is not exported and is not visible to the linker. Two files can each have a private function with the same name, and a private function can share a name with a C library function, without a clash.

9.3 Types

Venera C
I8 I16 I32 I64 int8_t int16_t int32_t int64_t
U8 U16 U32 U64 uint8_t uint16_t uint32_t uint64_t
CHAR char (as an unsigned 8-bit value)
BOOL _Bool / bool (1 byte, 0 or 1)
F32 F64 float double
T PTR T *
VOID PTR void *
VALIST va_list
Enum Its underlying integer type (§3.5)
Struct / union A C struct / union with the same members, in order
Function type A C function pointer

Structs and unions have the same size, alignment and padding as the equivalent C type on the target platform. Passing and returning them by value follows the platform's C calling convention. Arrays cannot be passed to C directly. Pass a pointer and a length instead (§3.3). An array parameter of a Venera function (§3.3) is passed as a pointer to its first element, so from C an I32 ARRAY 10 parameter is an int32_t *.

9.4 String literals

A string literal has the type CHAR PTR and is null-terminated, so it can be passed straight to C functions such as puts. String literals are stored in read-only memory. Writing through one directly reports E3020. Writing through a copy of the pointer is not detected and will normally crash the program.

9.5 Variadic arguments

Arguments passed to a ... parameter follow C's default argument promotions:

This means INVOKE printf("%f\n", f) works as expected with an F32 f. Because the arguments are promoted, VAARG (§5.7) must read the promoted type. Using VAARG with F32, BOOL, CHAR or an integer smaller than 32 bits reports E3054. Read the promoted type and CAST it instead:

c WRITE CAST CHAR AS VAARG args I32

Annex A — Grammar

(* FULL CAPS and quoted literals ("::", "(") are terminals.  These are emitted directly by the lexer *)
(* lower case are non-terminals meaning they are rules which can be broken down *)

program ::=
    use-directive* PROGRAM qualified-name? DO name-block? top-level-declaration* END

use-directive ::=
    USE qualified-name

qualified-name ::=
    IDENTIFIER ( "::" IDENTIFIER )*

top-level-declaration ::=
    public-definition    |
    function-definition  |
    extern-declaration   |
    namespace-definition |
    const-block          |
    struct-definition    |
    union-definition     |
    enum-definition

public-definition ::=
    PUB ( function-definition | extern-declaration | struct-definition | union-definition | enum-definition )

namespace-definition ::=
    NAMESPACE qualified-name DO name-block? top-level-declaration* END

function-definition ::=
    DEF type IDENTIFIER parameter-list? block

extern-declaration ::=
    EXTERN DEF type IDENTIFIER parameter-list? ( AS STRING_LITERAL )?

parameter-list ::=
    "(" ( parameter ( "," parameter )* ( "," ELLIPSIS )? | ELLIPSIS )? ")"

parameter ::=
    type IDENTIFIER

struct-definition ::=
    STRUCT IDENTIFIER DO field-declaration* END

union-definition ::=
    UNION IDENTIFIER DO field-declaration* END

field-declaration ::=
    type IDENTIFIER ( "," IDENTIFIER )*

enum-definition ::=
    ENUM integral-type? IDENTIFIER DO enum-member ( "," enum-member )* ","? END

enum-member ::=
    IDENTIFIER ( IS "-"? INTEGER_LITERAL )?

name-block ::=
    NAME DO variable-declaration* END

const-block ::=
    CONST DO const-declaration* END

const-declaration ::=
    type IDENTIFIER ( "," IDENTIFIER )* IS expression

variable-declaration ::=
    type IDENTIFIER ( "," IDENTIFIER )*

argument-list ::=
    "(" ( expression ( "," expression )* )? ")"

type ::=
    base-type type-suffix*

base-type ::=
    primitive-type |
    VOID           |
    VALIST         |
    qualified-name

type-suffix ::=
    PTR                                        |
    ARRAY ( INTEGER_LITERAL | qualified-name ) |
    "(" ( parameter-type-list | ELLIPSIS )? ")"

primitive-type ::=
    integral-type | CHAR | BOOL | F32 | F64

parameter-type-list ::=
    type ( "," type )* ( "," ELLIPSIS )?

block ::=
    DO name-block? statement* END

statement ::=
    RETURN expression                    |
    RETURN VOID                          |
    BREAK                                |
    CONTINUE                             |
    VASTART IDENTIFIER                   |
    VAEND IDENTIFIER                     |
    TRAP STRING_LITERAL                  |
    block                                |
    INVOKE lvalue-chain argument-list?   |
    lvalue write-value                   |
    WHILE expression block               |
    if-statement

if-statement ::=
    IF expression DO name-block? statement* ( ELSE IF expression DO name-block? statement* )* ( ELSE DO name-block? statement* )? END

lvalue ::=
    DEREF unary-expression |
    lvalue-chain

lvalue-chain ::=
    qualified-name ( ( AT index ) | ( "." IDENTIFIER ) )*

index ::=
    "-"? factor

write-value ::=
    WRITE expression               |
    compound-assignment expression |
    INC                            |
    DEC

compound-assignment ::=
    ADD | SUB | MUL | DIV | MOD

expression ::=
    logical-and-expression ( OR logical-and-expression )*

logical-and-expression ::=
    not-expression ( AND not-expression )*

not-expression ::=
    NOT not-expression |
    comparison-expression

comparison-expression ::=
    bit-or-expression ( ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) bit-or-expression )?

bit-or-expression ::=
    bit-xor-expression ( BOR bit-xor-expression )*

bit-xor-expression ::=
    bit-and-expression ( BXOR bit-and-expression )*

bit-and-expression ::=
    shift-expression ( BAND shift-expression )*

shift-expression ::=
    arithmetic-expression ( ( SHL | SHR ) arithmetic-expression )*

arithmetic-expression ::=
    term ( ( "+" | "-" ) term )*

term ::=
    unary-expression ( ( "*" | "/" | "%" ) unary-expression )*

unary-expression ::=
    "-" unary-expression                 |
    DEREF unary-expression               |
    BNOT unary-expression                |
    REF postfix-expression               |
    CAST type AS unary-expression        |
    SIZEOF type                          |
    SIZEOF unary-expression              |
    LENGTHOF unary-expression            |
    VAARG IDENTIFIER type                |
    NAMEOF unary-expression              |
    postfix-expression

postfix-expression ::=
    factor ( ( AT index ) | ( "." IDENTIFIER ) )*

factor ::=
    "(" expression ")" |
    primary

primary ::=
    INTEGER_LITERAL | FLOAT_LITERAL | STRING_LITERAL | CHAR_LITERAL |
    TRUE | FALSE | NULL                                             |
    qualified-name                                                  |
    INVOKE lvalue-chain argument-list?

integral-type ::=
    U8 | U16 | U32 | U64 |
    I8 | I16 | I32 | I64