Lynxer

Documentation

The Lynxer language

This page describes the Lynxer language as the interpreter actually parses and runs it, with every example verified against the interpreter. For the behaviour that is deliberately constrained or not implemented, see limitations.

Related pages: functions, types, lists, structs, classes, enums, vargroups, modules, and builtins.

A first program

global setup() {
    println("ready");
}

global main() {
    println("Hello, Lynxer!");
}

Requires the interpreter (see install) and runs with lynxer hello.lynx. If setup or main is missing, the parser reports it with a source location, for example lynxer: hello.lynx:2:2: program must define global setup().

Program structure

A file is a sequence of top-level declarations, in this order:

  1. global setup() — must be the first declaration. The interpreter raises global setup() must be the first declaration otherwise.
  2. Anything else — file-level funcs, global functions, struct, class and enum definitions before main. A class's parent must be declared before the child class.
  3. global main() — the default entry point, and the last declaration. A declaration after main fails with declarations may not follow global main().

Executable code outside a function body is a syntax error (expected top-level function declaration).

setup() and main() may take defaulted parameters; they are filled in before the body runs:

global setup(int retries = 3) {
    println("retries ", retries);   // retries 3
}

global main() {
    println("main");
}

overrideMain

When setup() calls overrideMain("name"), the named global function runs instead of main, and main becomes optional:

global setup() {
    overrideMain("start");
}

global start() {
    println("started");   // printed
}

global main() {
    println("unused");
}

Comments and docstrings

// a single-line comment

///
  a multi-line delimiter comment;
  the closing delimiter is the same slash run
///

////
  a file docstring. `--list-stdlibs` prints this block for a module.
////

Comment markers inside string literals are ordinary text.

Variables and declarations

global setup() {
    int count = 0;
    const str APP = "demo";
    println(APP, " ", count);   // demo 0
}

global main() {
    float ratio = 1.5;
    ratio += 0.5;
    println(ratio);             // 2
}

Ownership and borrowing

Variables own their value. A few built-ins make moves and temporary aliases explicit, which matters when a value is expensive to copy.

int source = 42;
int destination = 0;
varTransfer(source, destination);   // source is moved; destination owns 42

int borrower = 0;
varBorrow(destination, borrower);   // borrower is a read-only alias
println(borrowing(borrower));        // true
println(beingBorrowed(destination)); // true
varEndBorrow(borrower);              // borrower becomes an independent copy

Shared variables

Prefix a typed declaration with shared to make it a mutable alias of an existing variable. Assignments through either name update the same storage:

int x = 42;
shared int y = x;
y = 100;       // x is now 100 too
x = 7;         // y observes the same value
unshare(y);    // y becomes independent, keeping its current value
y = 200;       // x stays 7, y is 200

The initializer must be a variable name, the declared type must accept the source's value, and the source must be readable (not moved). unshare(name) detaches the alias; the detached variable keeps its current value and declared type. shared is a declaration form of varBorrowMutate, and unshare is an alias of varEndBorrow — so a shared variable reports borrowing(alias) and beingBorrowed(source) as true until it is detached.

The arguments to these built-ins are variable names, not values. A runnable example is lynxer/examples/ownership.lynx.

Types

The complete type list, ranges, and conversion rules live in types. The scalar names are int, float, num, bool, str, char, numBool, bit, byte, int8/int16/int32/int64, uint8/uint16/uint32/uint64, float32/float64, plus any, none, list, tuple, codeblock, and the named struct/class/enum types.

Operators

Lynxer uses keyword operators: is / isnt, and / or / not, bitand / bitor / bitxor, bitleft / bitright, and so on. The symbolic C-style spellings (==, !=, &&, ||, &, |, ^, <<, >>, …) still parse but are deprecated and print a warning; a bare ! is a syntax error.

int n = 6;
if((n % 2 is 0) and n > 4){
    println("even and greater than four");
}
println(n bitand 3);   // 2

→ Full operator reference, precedence and the deprecation table

Control flow

try / catch

try {
    println(1 /% 0);
} catch (str error) {
    println("caught ", error);   // caught division by zero
}

try handles runtime errors. The binding clause is optional; the caught value is the message string. raise(message) raises one, and exceptionInfo() returns the traceback inside a handler.

→ Full try / catch reference: raise, exceptionInfo(), nesting, and what is not caught

return

return expr; returns a value; a bare return; returns nothing. The value is checked against the function's return annotation, if any.

Functions

See functions for a full page on each form. There are three function declaration forms and a scoped named lambda form:

FormWrittenWhereCalled from
Global functionglobal name(...)between setup and mainglobal.name(...) or a bare name in the same file
File-level functionfunc name(...)between setup and maina bare name in the same file; global.module.name(...) from an importer
Local functionlocal name(...)inside another function bodya bare name, or local.name(...)
Named lambdalambda name(...)inside a function bodyits name in the declaring scope

global func name(...) is not valid syntax; use func name(...) for a file-level function.

func double(int n) -> int {
    return n * 2;
}

global add(int a, int b = 1) -> int {
    return a + b;
}

global main() {
    local square(int n) -> int {
        return n * n;
    }
    println(double(4));        // 8
    println(global.add(2));    // 3
    println(square(3));        // 9
}

Named lambdas are local to their declaring function scope and are not exported; nested function scopes cannot see them. See functions for an example. Functions and lambdas can also be stored as function values, returned, and passed as arguments; see function values.

Parameters and defaults

Return annotations

Only the arrow form is accepted:

global f(int x) -> int { return x + 1; }

Codeblocks

A function may declare caller-supplied blocks after its parameters and invoke them with exec(). See codeblocks for the full reference.

Scoping

Lynxer has no nested block scopes, and blocks do not shadow:

global setup() {
    int total = 0;
}

func add(int n) {
    total += n;        // updates the top-level variable
    int scratch = n;   // local to add(); not visible outside
}

global main() {
    add(2);
    add(3);
    println(total);    // 5
}

String interpolation

inter"..." interpolates {expr} inside a string, with \{, \}, and \\ as escapes:

global main() {
    int n = 3;
    str message = inter"n is {n}";
    println(message);          // n is 3
    println(inter"also {n}"); // also 3
}

inter"..." is a string expression, so it can be used wherever a string value is accepted, including output calls, assignments, and return values:

global describe(int n) -> str {
    return inter"value: {n}\n";
}

The embedded expressions run when the string is evaluated. Each result is converted to text and inserted between the literal parts.

Modules

import("name") and importAs("name", "alias") load .lynx source modules or native .so libraries. They are statements, so they run from any function body — most often setup() — not at file scope. Both arguments must be string literals. See modules.

Aggregate types

Not supported

See limitations for the complete list. The most common surprises are \x/\u escapes, the .lynxc bytecode format, and the not-planned modules (tkinter, tkinterPlus, turtle, venv).