Sere language reference

This is the user-facing description of Sere as the compiler implements it today. It is not a roadmap. Features that are tokenized but not implemented are called out at the end.

Looking for the details of one subject — every str or list method, the format mini-language, operator rules, pointers, or the exact dunder names? See reference/ for one page per topic. This file is the whole language at a glance; the reference pages are the deep dives.

Sere is a statically typed Python-superset. Indentation is significant. Programs compile to native code through LLVM 22. print is a language intrinsic (also used from the prelude); it is not a statement.

A minimal program:

sere
def main() -> i32:
    print("hello, sere")
    return 0

Compile:

powershell
sere examples\hello.sere -o hello.exe
.\hello.exe

The process exit code is main's i32 return value.


Contents

  1. Programs
  2. Lexical structure
  3. Types
  4. Names and bindings
  5. Expressions
  6. Statements
  7. Functions
  8. Decorators
  9. Classes, structs, and enums
  10. Modules and imports
  11. Memory and pointers
  12. Collections and strings
  13. Pattern matching
  14. Errors
  15. Macros
  16. Introspection and platform
  17. Intrinsics
  18. Standard library
  19. Native interop
  20. Diagnostics
  21. Reserved, not implemented
  22. Examples

Programs

A linked executable needs main. Return i32 (used as the process exit code) or void:

sere
def main() -> i32:
    return 0

or with arguments:

sere
def main(argv: list[str]) -> i32:
    return len(argv)

A file without main still typechecks and can emit LLVM; it does not produce a C main. Top-level statements in the entry file run as module initialization before main.

prelude.sere is injected automatically. Other stdlib modules are opt-in (import math).

A typed binding may omit an initializer; the slot is default-initialized. = always requires an expression.

sere
ptr: Unique[i32]          # ok, default
n: i32 = 0                # ok
# n: i32 =                # error

Indent with spaces only. Tabs are a diagnostic.


Lexical structure

Comments

# to end of line. # type: ignore and # type[NameError]: ignore suppress diagnostics (see Diagnostics).

Names

Identifiers: ASCII letters, digits, and _. Keywords are reserved.

Keywords

False  None  True
and  as  assert  async  await  break  case  class  const  continue
def  defer  del  do  elif  else  enum  except  extern  finally
for  from  if  import  in  is  lambda  macro  match
not  or  pass  raise  return  static  struct  super
try  type  while  with

const binds a readonly name. lambda is an anonymous function. do introduces a block expression (see do block expression). with requires __enter__ / __exit__ on the context type. async and await are supported — see Async / await status.

Literals

KindForms
Integerdecimal 42, hex 0xFF, binary 0b1010, octal 0o755, _ separators 1_000
Float1.0, 3e2, 1.0f, _ allowed
BoolTrue, False
NoneNone
String"...", multi-character '...', """...""" (multiline)
Byteone-character 'A' / '\n' — type i8, assignable to byte (u8)
F-stringf"hi {x}" with {expr} holes
Regexbacktick `\d+`, type regex

Operators

Arithmetic: + - * / // % ** Bitwise: & | ^ ~ << >> Comparison: == != < <= > >= is in Boolean: and or not Assignment: = += -= *= /= //= %= **= &= |= ^= <<= >>= Inc/dec: ++n n++ --n n-- Pointers: &x *p Cast: value as T Call/index: f(x) xs[i] xs[a:b] Walrus: name := expr Other: . , : -> => ! $ @ ... | (unions and bitwise or)

/ is true division. // is floor division.


Types

Primitives

TypeMeaning
voidNo value (function returns)
NonePython-style spelling of void; also the no-value literal
boolTrue / False
i8 i16 i32 i64Signed integers
u8 u16 u32 u64Unsigned integers
f32 f64IEEE floats
strString
byteAlias of u8
regexCompiled pattern (backtick literal)

Prelude aliases (unions):

sere
type Int = i8 | i16 | i32 | i64 | u8 | u16 | u32 | u64
type Float = f32 | f64

Pointers

TypeMeaning
Unique[T]Exclusive heap pointer; dropped at end of scope
Shared[T]Reference-counted heap pointer
Ptr[T]Raw pointer; caller frees

Collections

TypeMeaning
list[T]Runtime list
list[T, N]List of T with a fixed length N
array[T]Fixed array from array[T](...)
dict[K, V]Map

Callables

Functions, lambdas, classes, structs, and bound instance methods (self.method) can be passed as values. self is already applied, so def handler(self, x: i32) matches Callable[[i32], R].

TypeMeaning
CallableAny callable; argument count and return type are unchecked
Callable[R]Any callable that returns R
Callable[[P...], R]Callable with those parameter types and return R
Callable[[P..., ...], R]Prefix parameters must match; extra arguments are allowed
Function / Function[R] / Function[[P...], R]Same shapes, but only functions and lambdas (not classes)
ClassAny class or struct type object
Class[T]The type object for T (same as type[T])
sere
def add(a: i32, b: i32) -> i32:
    return a + b

def apply(cb: Callable[[i32, i32], i32], x: i32, y: i32) -> i32:
    return cb(x, y)

def twice(cb: Callable[i32], n: i32) -> i32:
    return cb(n) + cb(n)

def call_any(cb: Callable) -> void:
    cb(1, 2)

n = apply(add, 1, 2)
m = twice(lambda (x: i32) -> i32: x + 1, 3)
call_any(add)

class Point:
    x: i32
    y: i32
    def __init__(self, x: i32, y: i32) -> void:
        self.x = x
        self.y = y

def make(cls: Class[Point], x: i32, y: i32) -> Point:
    return cls(x, y)

def construct(cb: Callable[[i32, i32], Point], x: i32, y: i32) -> Point:
    return cb(x, y)

p = make(Point, 1, 2)
q = construct(Point, 3, 4)

Callable[[i32, ...], i32] accepts def f(a: i32) -> i32 and def g(a: i32, b: i32) -> i32. A bare Callable result is Any; annotate Callable[R] when the return value is used as a typed result.

User types

  • class — identity (reference)
  • struct — copy-by-value
  • enum — discriminant; variants Color.Green
  • type Name = ... — alias or union

Unions

sere
n: i32 | f32 = 3
type Number = i32 | i64
wide: i64 = 10
total: i64 = small + wide    # integer widths mix in arithmetic

Cast with as or a constructor: n as i32, i32(tone), T(value), Unique[T](pointer).


Names and bindings

sere
n: i32 = 0
static module_count: i32 = 0

def bump() -> i32:
    static n: i32 = 0
    n = n + 1
    return n
  • Local: name: Type or name: Type = expr
  • Module-level static and function-level static persist
  • Functions and types can be aliased: donut = print

A declaration may be preceded by @ decorator lines. Reserved modifier decorators (@public, @private, @static, @abstract, @override, @frozen, @flags) and user-defined runtime decorators are covered in Decorators.


Expressions

Precedence, high to low (roughly): postfix → unary → as → range → * / // % ** → + - → shifts → & → ^ → | → comparisons / in / is → and → or → ternary a if c else b.

sere
xs: list[i32] = [1, 2, 3]
ages: dict[str, i32] = {"ada": 36}
empty: dict[str, i32] = dict[str, i32]()
arr: array[i32] = array[i32](1, 2, 3)
comp: list[i32] = [x for x in range(0, ..., 3)]
label: str = f"n={n}"
ok: bool = "ell" in hello and n > 0 and not False

range is an intrinsic that yields list[i32]:

CallMeaning
range(stop)0, 1, …, stop-1
range(start, stop)start … stop-1
range(start, stop, step)stepped

start ... stop desugars to range(start, stop). A bare ... in a call argument list is skipped, so range(0, ..., 3) is range(0, 3).

Ternary is Python-style: x if cond else y.

Calls

Positional and keyword arguments may mix; keyword arguments must name a parameter: scale(factor=3, value=1). Keyword arguments are not supported on indirect calls through a Callable or a function-typed value — use positional form there.

Comprehensions

[expr for name in iterable] builds a list. The iterable may be a range, list, or str:

sere
squares: list[i32] = [x * x for x in range(0, 5)]

Truthiness and equality

bool, integers, and floats test directly in if / while. is compares identity (enum variants, None); == compares value equality and works on strings, numbers, lists, dicts, and user types with __eq__-style arithmetic or comparison dunders.

Walrus

name := expr assigns and yields the value. If name already exists it must be assignable (and not const); otherwise it is declared with the inferred type:

sere
if (n := next()) > 0:
    print(n)

do block expression

do: turns a statement block into an expression. The statements run in their own scope and the value of the trailing expression statement becomes the value of the whole expression. A trailing if / elif / else works the same way when every branch produces a value of the same type.

sere
x = do:
    y = 10
    y += 2
    x = 5
    x + y   # 17: the last expression is the value
# `y` goes out of scope here

label = do:
    if x > 10:
        "big"
    else:
        "small"

Rules:

  • A name first bound inside the block lives only inside it and is dropped when the block ends. Assigning to a name that already exists in an enclosing scope updates that binding, exactly as it does inside an if block.

  • A block whose last statement is not an expression evaluates to None, so the value can be discarded:

    sere
    do:
        print("side effect only")
  • An if used as the trailing statement of a do: block must have an else branch and every branch must produce the same type; otherwise the block evaluates to None.

  • return, raise, break, and continue inside a do: block act on the enclosing function or loop.

  • The do: keyword and its indented block must begin where a statement can start (for example the right-hand side of =), not inside parentheses.


Statements

sere
pass
assert cond
assert cond, "failed"
return expr
break
continue
n = n + 1
n += 2
++n
n++

if / while / for

sere
if n == 1:
    n = n + 2
elif n == 0:
    pass
else:
    n = -n

while n < 4:
    n = n + 1

for n in range(0, 4):
    total += n
for item in xs:
    total += item
for ch in hello:
    pass

for iterates range(...), str (one-character strings), and list[T].

defer / del

sere
defer free(raw)
defer:
    print("done")
del xs[i]
del table[key]

defer queues its body and runs it in reverse order on every return (and on the implicit return at the end of the function). del removes a list index or dict key. del name is a diagnostic.

with / const / lambda / tuples

sere
const limit = 4
pair = (1, 2)
a, b = pair
add1 = lambda (x: i32) -> i32: x + 1
if (n := 3) > 0:
    print(n)
with Guard() as value:
    print(value)

Untyped lambda x: ... parameters are Any. Lambdas do not capture enclosing locals; pass values as parameters, or use a nested def, which can capture. with calls __enter__ and __exit__ on one evaluated context object.


Functions

sere
def scale(value: i32, factor: i32 = 2) -> i32:
    return value * factor

def identity[T](value: T) -> T:
    return value
  • Return type after -> may be omitted: main infers i32, __init__ infers void, other functions infer Any
  • Parameter types may be omitted (Any)
  • Default arguments are allowed
  • Keyword arguments at call sites: scale(factor=3, value=1)
  • Varargs and kwargs: def log(prefix: str, *parts: list[str], **opts: dict[str, str]) -> void
  • print(..., sep=" ", end="\n") — end="" suppresses the trailing newline
  • Generic type parameters: [T] on def or class
  • Methods take self as the first parameter
  • Nested def is allowed and may capture enclosing locals — useful for decorator wrappers and closures (lambdas still cannot capture)

Native:

sere
extern "C" "native_add"
def add(left: i32, right: i32) -> i32

The string is the link symbol. The def has no body.

Docstrings

A string literal as the first statement of a body is the declaration's docstring. It is removed from the body, so it costs nothing at run time, and it is carried on the declaration for the language server and __doc__.

sere
def serve_beer(name: str, age: i32) -> Result[str, str]:
    """Serve a beer when the guest is old enough.

    The drink is only poured after the guest's age has been checked, so the
    caller handles refusal as well as success.

    Args:
        name: Who is being served.
        age: The guest's age in years.

    Returns:
        Ok with a message when served, Err with the reason otherwise.

    Raises:
        ValueError: When the age is negative.

    Example:
        print(serve_beer("Ada", 30))
    """
    ...

Structure — the parts are all optional, and the order is free:

PartWritten asShown as
Summaryfirst paragraphthe line under the signature
Bodyany paragraph after the summaryprose, blank lines preserved
ArgumentsArgs:, Arguments:, Parameters:, Params:**Arguments** with one bullet per name
ReturnsReturns:, Return:, Result:**Returns**
RaisesRaises:, Throws:**Raises** with one bullet per error
ExampleExample:, Examples:, Usage:, Code:a sere code block
NoteNotes:, Note:, Warning:, Tip:, See also:that title as its own section
  • Section headers start at the left edge of the docstring; the entries under them are indented, one level, as in the example above.
  • An entry is name: description. A parameter may repeat its type as name (type): description; the hover shows the type from the signature, so writing it again is optional but allowed. Indented lines after an entry continue its description.
  • Only these titles open a section, so a sentence that happens to contain a colon stays prose.

Where it appears:

  • Hover over a declaration, a call, a method, or a constructor shows the signature, the description, then the arguments, returns, raises, and examples. A call shows the same documentation as the declaration it resolves to.
  • Completion shows the declared summary and sections next to the suggestion.
  • Signature help shows the same documentation under the parameter list.
  • The module's own leading docstring is __doc__.

@public/field declarations take no docstring; a class, struct, enum, function, and method all do.

Constrained generic parameters

Add : Type or : Type1 | Type2 after a generic parameter name to restrict its allowed types:

sere
def identity[T: i32 | f64](value: T) -> T:
    return value

class Box[T: i32 | str]:
    value: T

enum Value[T: i32 | str]:
    Item(T)

def main() -> i32:
    number: i32 = identity[i32](3)
    real: f64 = identity(2.5)
    box = Box[str]("hello")
    value = Value.Item("text")
    return 0

The same syntax works on structs and generic methods. Each parameter has its own constraint; unrestricted parameters can appear alongside constrained ones: class Pair[K: i32 | str, V].

Constraints are checked at compile time for explicit type arguments and for arguments inferred from function calls or enum payloads. identity[str]("no") and identity("no") both report TypeError. A constrained class or struct still requires explicit constructor type arguments, such as Box[i32](3).

Allowed type arguments match exactly after resolving aliases. A constraint of i32 | f64 rejects an inferred i64 or Any; a constraint naming a class does not also admit its subclasses. Convert the value first, or choose an allowed explicit type argument and use the usual argument-conversion rules. Concrete collection types are also valid, for example T: list[i32] | str. Constraints must name concrete types; Any, void, and other generic parameters are not allowed in the constraint itself. Omitting a constraint ([T]) retains unrestricted generic behavior.

The | in a constraint lists alternative type arguments. It does not change T into a union-valued variable: each specialization still has one chosen type. An annotation on a constrained record must supply its type arguments, too; bare Box cannot silently select Any.

See generic_constraints.sere for an executable example with functions, classes, methods, and enum payloads.

More on parameters, defaults, varargs, lambdas, and closures: reference/functions.md. Generic parameters and constraints: reference/generics.md.


Decorators

Sere has two kinds of decorator:

  • Reserved modifiers, handled at compile time: @public, @private, @static, @abstract, @override, @frozen, @flags.
  • Runtime decorators: any user callable applied with @name, @name(args), or @Class.method.

Both kinds may appear in the same stack. Decorators run bottom-up — the one closest to the declaration runs first, so @a above @b on f means f = a(b(f)).

Reserved decorators

DecoratorOnEffect
@public / @privatefield, def, property accessor, class, struct, enum, type, macro, module binding@private is not exported: import / from and module.name cannot see it (PermissionError). Private methods and setters are only usable inside the owning class.
@staticfieldSame as static name: T: one shared class variable
@abstractmethodEmpty/pass body must be overridden; a real body is a default hook
@overridemethodMarks an override
@frozenclassFields are not assignable after init
@flagsenumVariants are bit flags; Flag.A in mask is a bitwise test

Reserved decorator names cannot be redefined and never produce a runtime wrapper.

Runtime decorators

A runtime decorator is a callable that receives the decorated object and returns its replacement:

sere
def identity(fn: Callable) -> Callable:
    return fn

def logged(fn: Function[[i32], i32]) -> Function[[i32], i32]:
    def wrapper(n: i32) -> i32:
        print("logged", n)
        return fn(n)
    return wrapper

@identity
@logged
def bump(n: i32) -> i32:
    return n + 1
  • @name(args) is a factory: the call is evaluated and its result is the decorator.
  • @Class.method uses a class method as the decorator.
  • Decorating a class / struct passes the type object to the decorator.

Runtime decorators run once, at module initialization, before main. The wrapped value is stored and every later call to the name goes through it. A decorator whose static signature is unknown (it returns a bare Callable) leaves the original signature intact, Python-style. Nested def wrappers may capture the decorated function.

The full reference — type-checking rules, factory and class-method patterns, class decoration, and diagnostics — is in decorators.md.


Classes, structs, and enums

Class (identity)

sere
class Pet:
    name: str

    def __init__(self, name: str) -> void:
        self.name = name

    def id(self) -> i32:
        return 1

class Cat(Pet):
    def __init__(self, name: str) -> void:
        super().__init__(name)

class Box[T]:
    value: T

    def get(self) -> T:
        return self.value

class Animal:
    @abstract
    def speak(self) -> i32:
        pass

Construct with Pet("z") or Box[i32](4). Multiple bases are allowed (class Dog(Animal, Named)). import pets then class Cat(pets.Pet) is the same base as from pets import Pet then class Cat(Pet).

static fields are one shared value for the class (not per instance). Read and write them as MyClass.x or self.x:

sere
class Counter:
    @public static total: i32 = 0

    def __init__(self) -> void:
        Counter.total = Counter.total + 1

@static on the field is the same as the static keyword.

Properties

name.get and name.set are accessors for obj.name / obj.name = value. The backing field may reuse the same name. Inside the accessor (and in __init__ when a stored field exists), self.name is the field. Everywhere else it goes through the getter or setter.

Visibility is per accessor: a public getter with a private setter is read-only from outside the class.

sere
class Vec2:
    @private x: i32
    @private y: i32

    def __init__(self, x: i32, y: i32) -> void:
        self.x = x
        self.y = y

    @public x.get:
        return self.x

    @public y.get:
        return self.y

    @public x.set(value: i32) -> void:
        self.x = value

    # equivalent: x.set(self, value: i32) -> void

v.x        # getter
v.x = 10   # setter; the setter always receives self

A getter may omit () and -> T (the field type is used). A setter takes one value besides self and returns void. A getter without a setter is read-only. A computed property may omit the field and only declare accessors.

Struct (value)

sere
struct Point:
    x: i32
    y: i32

    def length_sq(self) -> i32:
        return self.x * self.x + self.y * self.y

p: Point = Point(1, 2)
q: Point = p     # copy
q.x = 9          # p.x stays 1

Structs cannot inherit.

Enum

sere
enum Color:
    Red
    Green = 2
    Blue

enum Message:
    Quit
    Move(x: i32, y: i32)
    Write(str)

    def is_quit(self) -> bool:
        match self:
            case Message.Quit:
                return True
            case _:
                return False
  • Unit variants: Color.Green
  • Payload variants: Message.Move(1, 2)
  • .name → str, .value → discriminant, i32(tone) → tag
  • Unit / @flags enums (no payload) are integers in context: pass to i32 parameters, assign to i32, compare with ints, and use | & ^ without .value
  • Color.variants() → list[str]
  • tone is Color.Green compares identity of the variant
  • @flags on an enum marks it as a flag set; Flag.A in mask is a bitwise test

Dunder methods

If a type defines these, the corresponding syntax uses them:

MethodSyntax
__init__T(...)
__len__len(x)
__getitem__ / __setitem__x[i] / x[i] = v
__contains__v in x
__enter__ / __exit__with x as name:
__add__ / __radd__ and other arithmetic+ - * / // % ** and comparisons

See reference/classes.md for the complete dunder and operator-overloading tables, and reference/enums.md for payloads and @flags.


Modules and imports

sere
import util
import util as u
from util import double
from html_lang import html, Html
from math import sqrt
from window import Window as BaseWindow
from string import *

import gl binds only the module name. A local class Window is a different type from gl.Window.

from SomeClass import someMethod in the same file hoists a class method to module scope so it can be exported. from module import Name as Alias binds the export under Alias.

Public top-level names are exported by default. __exports__ can also list names brought in with from other import Name (or as Alias) so a barrel file can re-export them:

sere
def version() -> str:
    return "1"

from Greeter import hello
from window import Window as BaseWindow

__exports__ += [hello, BaseWindow]   # keep version, also re-export these
# __exports__ = [hello]              # export only hello

Search order: directory of the importing file (and libs/ next to it), the current working directory, then the stdlib next to sere (or SERE_STDLIB in tests). util.sere and util.slib both provide module util. .sere wins when both exist. A folder named util with util/util.sere or util/lib.sere is also import util (native .c / .lib in that folder or native/ are compiled and linked). The compiler extracts .slib files next to themselves under .sere-lib/ and links any native objects they contain. .slib packs only the entry and the local modules it actually imports, plus compiled native objects — not the rest of the tree. The language server uses the same search path, so drop-in .slib files and folder libraries complete and hover like ordinary modules.

Module globals (always in scope):

NameMeaning
__name__"__main__" for the entry file, otherwise the module stem
__file__Source path
__package__Package string
__doc__Leading docstring if present
__debug__True in a debug-oriented build flag
__sere_version__Compiler version string

Compile-time host flags (bool). A false if branch is not typechecked:

FlagMeaning
__windows__ __linux__ __macos__ __unix__OS
__x86_64__ __arm64__Architecture
__platform__"windows" / "linux" / "macos"
__arch__"x86_64" / "arm64" / "unknown"
sere
if __windows__:
    windows.message_box("hi")
if cfg!(linux):
    pass

cfg!(windows) (and linux, macos, unix, x86_64, arm64, debug) is a prelude macro that expands to the matching dunder.


Memory and pointers

sere
owned: Unique[i32] = unique[i32](42)
*owned = 43
value: i32 = load(owned)

raw: Ptr[i32] = alloc[i32]()
store(raw, 7)
*raw = 8
free(raw)

local: i32 = 10
stack: Ptr[i32] = &local
*stack = *owned
IntrinsicResult
unique[T](value)Unique[T]
shared[T](value)Shared[T]
alloc[T]()Ptr[T]
load(p)T
store(p, v)void
free(p)void
&xPtr[T] for an addressable lvalue
*pload T; *p = v stores

Details, including the collector API and import heap: reference/memory.md.

Output parameters

Use Ptr[T] for a function that writes into a caller's variable. Pass its address with &; no out keyword is required. Declare the variable before the call. Use *parameter = value or store(parameter, value) to write it.

sere
def get_name(out_name: Ptr[str]) -> void:
    *out_name = "Sere"

def main() -> i32:
    name: str = ""
    get_name(&name)
    print(name)
    return 0

The same syntax works for fields, list elements, forwarding to other functions, callbacks, and native C functions. Ptr[str] points to a Sere string value (data pointer plus length), not a C character buffer. Match the native function's ABI.

Writable pointer parameters require the exact pointee type: Ptr[i32] cannot implicitly become Ptr[i64], Ptr[Any], or a pointer to a base class. Unique[T] and Shared[T] can be borrowed as Ptr[T]; raw pointers do not implicitly acquire ownership. Owning-pointer parameters are borrowed during a call and are not released on function return. Prefer Ptr[T] in functions that only need access. free() accepts raw Ptr[T] allocations; owning pointers are released automatically.

Null dereferences through *, load(), and store() raise RuntimeError. Taking a mutable address of a constant and directly returning a local variable's address are rejected. Raw pointers still require lifetime discipline: do not keep an address after its storage goes out of scope, free a borrowed address, or keep an element pointer while resizing its list. These checks are not a borrow checker and do not detect all dangling aliases or use-after-free errors. Explicit pointer casts remain a low-level escape hatch.

alloc / free go through the installed collector (import gc). Default collector is "none" (tracked malloc; you free it). Switch with gc.use("mark_sweep") or gc.use("arena"). Arenas and pools: import heap. Custom collectors: implement SereGcVTable in C, call sere_gc_install from sere_mod_init, link with sere main.sere --link my_gc.lib.


Collections and strings

sere
xs: list[i32] = [1, 2, 3]
xs.append(4)
xs[0] = 10
print(len(xs), xs[1], xs[1:], xs[:2], xs[1:3], xs[:])

hello: str = "Hello"
assert hello[0] == "H"
assert hello[-1] == "o"
assert hello[1:4] == "ell"
assert "ell" in hello
assert hello + "!" == "Hello!"
assert hello * 2 == "HelloHello"

len works on list, array, dict, str, and types with __len__. xs.append(v) and append(xs, v) both add to a list.

Dicts: ages["ada"] = 37. Empty: dict[str, i32]().

Details: strings.md, lists.md, dicts.md, numbers.md, formatting.md, bytes.md, operators.md.


Pattern matching

sere
match moved:
    case Message.Move(x, y):
        assert x == 1
    case Message.Quit if n > 0:
        pass
    case _:
        assert False

case _ is the wildcard. Enum payloads bind names in the arm. case pat if expr is a guard.

When the subject is an enum, the match must be exhaustive: every variant is covered, or the last arm is case _. Missing variants are a compile-time diagnostic (match is not exhaustive; missing Color.Blue). Non-enum subjects have no exhaustiveness requirement.

Payload bindings are plain names (case Message.Move(x, y)); the binding type is the declared payload type. Arms are checked in order; the first matching arm runs.


Errors

sere
try:
    raise TypeError("nope")
except TypeError as e:
    print(e.message)

try needs except and/or finally. Optional else (no error) and finally (always). except Type matches that class and its subclasses. Bare except: catches everything. as e binds an instance with .message.

sere
try:
    risky()
except ValueError as e:
    print(e.message)
except RuntimeError:
    print("known failure")
else:
    print("no error")
finally:
    print("always")

Multiple except clauses are allowed; the first matching handler runs. Exceptions propagate out of functions and for / while bodies until a handler or the top level. defer bodies run on the way out, including when an exception unwinds the function.

Builtin exception classes (all subclass Exception): SyntaxError, IndentationError, NameError, AttributeError, TypeError, IndexError, ImportError, ValueError, AssertionError, PermissionError, RuntimeError, RecursionError, NotImplementedError.

sere
class Boom(ValueError):
    pass

raise Boom("bad")
raise "boom"                 # Exception
raise TypeError              # empty message
assert False, "fail"         # AssertionError, catchable

raise stringifies the first constructor argument (or a bare str). Bare raise uses an empty Exception. panic("msg") still aborts. Prelude macros:

sere
todo!("not yet")
unreachable!()
dbg!(total)          # prints and yields total

Which failures are catchable and which are fatal at runtime: reference/exceptions.md.


Macros

Macros run after parse, before type checking. They are hygienic by default (names introduced in a quote do not capture caller names). Expansion depth is capped (diagnostic RecursionError).

Quote

sere
macro twice(x):
    quote:
        ($x) + ($x)

total: i32 = twice!(n)

Splice with $x. Repeats: $($x),* inside quote. $type is available when the macro sets typed: true (the type of the first argument).

Match (token trees)

sere
macro vec:
    match:
        ($($x:expr),*) => quote:
            [$($x),*]

xs: list[i32] = vec!(1, 2, 3)

Specs include expr, ident, literal.

Indent / raw / pipeline

Statement form: name: plus an indented body. Expression form only after =:

sere
node: Html = html:
    <div>{title}</div>

n: i32 = pipeline:
    1
    |> add2
    |> wrap(4)

Do not write if left < right: as a macro; ident: newline after a comparison is the suite colon.

Macro properties:

sere
macro html:
    syntax: raw          # raw | tokens | pipeline | (default sere)
    interpolate: brace   # brace | dollar
    wrapper: Html        # constructor around the result
    typed: true

Invocations:

  • name!(...) name!{...} name![...]
  • indent name: (statement, or initializer after =)

Import macros like any name: from html_lang import html, Html.

More on quote/splice, token-tree matching, and macro properties: reference/macros.md. Modules and import search order: reference/modules.md.


Introspection and platform

Always in scope:

sere
typeof(small)              # str
isinstance[i32](small)
isinstance(small, i32)
isinstance(window, gl.Window)
isinstance(xs, list[f32])
typeof(window) is gl.Window
dir(Box)                   # list[str]
dir()
inspect(scale)             # str
sizeof[i32]()              # i64
alignof[i32]()             # i64
x.__name__  x.__type__  x.__module__  x.__qualname__
Type.__name__

from inspect import label is extra helpers, not the builtins.


Intrinsics

These names are compiler primitives (see IntrinsicKind):

unique shared alloc free load store len print str repr parse try_parse append list / list construction array dict range typeof isinstance dir inspect sizeof alignof panic super

print accepts any printable value, including pointers. str(x) converts. repr(x) also converts (debug-oriented spelling for a value).

Conversion intrinsics

CallMeaning
str(x)Convert any value to its str form
repr(x)Convert any value to its str form (debug spelling)
parse[T](text)Parse text as T (parse[i32]("123")), `T
try_parse[T](text)Parse text as T, yielding `T

Integers parse from decimal (plus 0x / 0b / 0o prefixes); floats parse from decimal and exponent forms. bool parses "True" / "False".

Input

CallMeaning
input(prompt: str)Print prompt, read one line from stdin, return it as str (from the prelude)

Standard library

Injected: stdlib/prelude.sere (abs, min, max, clamp, sign, input, Int / Float, the Exception class hierarchy, macros dbg! todo! unreachable! cfg!).

Import the rest:

ModuleRole
ioread_line, eprint
fs path os env sysFiles, paths, process, host
string bytes encoding regexText and binary
math vec matrix ml arraysNumeric / linear algebra
hash random time log bitUtilities
gc heap memoryCollectors, arenas (memory is documentation)
inspectExtra labels (label, describe)
utilTiny helpers (double); used by import examples
html_langhtml: raw macro + Html
windowsWin32 message box, beep, clipboard, … (stub off Windows)
glOpenGL 2.1+ (WGL window, should_close, shaders, VBO/VAO, textures, FBO, input)
qt6Qt 6 widgets; linked automatically if the compiler was built with Qt

Failed C bindings typically return "" / 0 / False rather than throwing. Gate OS-only code with if __windows__:.

More on how stdlib is wired: stdlib.md.


File checksums

hash.file(path) -> i64 streams a file's raw bytes in 64 KiB chunks and returns its FNV-1a 64-bit checksum. It supports binary files and does not load the whole file into memory. Invalid paths and open/read failures raise hash.FileHashError. Use it for change detection; FNV-1a is not a cryptographic digest.

sere
import hash

checksum: i64 = hash.file("src/main.sere")

Native interop

sere
extern "C" "sere_gc_collect"
def collect() -> void

Link extra object files or libs:

powershell
sere src\main.sere --link libs\native.lib -o bin\app.exe

Packed .slib files and folder libraries (mylib/lib.sere plus mylib/*.c or mylib/native/) compile and link their C automatically. Prefer packing the compiled .lib / .a into the .slib so consumers stay on one file.

Optional module init: define void sere_mod_init(void) in C. The runtime provides an empty default. A strong definition from --link overrides it. sere_mod_init runs from generated main before Sere globals.

Headers: include/sere/api/sere_mod.h, sere_gc.h.


Diagnostics

Reported as error[NameError]: ... (and other exception names).

ExceptionTypical cause
SyntaxErrorParse
IndentationErrorMixed or inconsistent indent
NameErrorUnknown name, type, function, macro, module
AttributeErrorUnknown field or method
TypeErrorWrong type, operand, or argument
IndexErrorBad index or slice
ImportErrorMissing module or prelude
ValueErrorInvalid or uninferable value
AssertionErrorBad assert
PermissionErrorPrivate field
RuntimeErrorControl-flow / compiler internal
RecursionErrorMacro expansion limit
NotImplementedErrorUnsupported or leftover construct

Suppression:

sere
# type[NameError]: ignore          # whole file if at the top
def main() -> i32:
    n: i32 = "nope"                # TypeError still reported
    return missing                 # NameError ignored (file rule)

    return missing  # type: ignore              # this line
    # type: ignore
    return missing                              # next statement

# type[Exception]: ignore hides every diagnostic in scope. # type: ignore[NameError] is accepted. Unknown names in the ignore list are ValueError.

sere --analyze file.sere prints JSON diagnostics. The editor uses sere --lsp.


Reserved, not implemented

Sere is a typed Python superset, not CPython. These remain out of scope or incomplete. They diagnose instead of generating silent wrong code:

  • keyword-only parameters (after *args), global / nonlocal
  • yield / generator functions (a nested def is fine — see Functions)
  • Unmodified CPython stdlib (use Sere modules such as requests and wsgi)
  • Lambda capture of enclosing locals (pass parameters instead, or use a nested def, which can capture)
  • del name (only del xs[i] / del d[k])
  • print x as a statement (print is a call)

If a construct parses but lowering is incomplete, you get NotImplementedError rather than silent wrong code.

Async / await status

async def, await, and the Task[T] / Future[T] types parse and type-check. Calling an async def yields a Task[T]; await unwraps it back to T and may only appear inside an async def:

sere
async def number() -> i32:
    return 42

async def combine() -> i32:
    a = await number()
    b = await number()
    return a + b

async def main(argv: list[str]) -> i32:
    print(await combine())
    return 0

See reference/async.md for Task[T], await placement, and the list of unimplemented constructs.

The runtime lowering uses LLVM switched-resume coroutines (the coro-early, coro-split, and coro-cleanup passes). Calling an async def spawns the coroutine lazily: its body does not run until the returned Task[T] is awaited (or driven by the async entry point). await resumes the child task, reads its typed result from the coroutine promise, and destroys the frame. The child's frame is allocated by the GC allocator and its root is released when the task completes.


Examples

Under examples/:

FileShows
hello.sereprint, f-strings, prelude math
control.sereif / while / break / continue
lang.serefor, defaults, enums
features.sereunions, comprehensions, default constructors
types.sereInt / Float aliases (no main)
collections.serelist, array, dict, @public / @private, static
strings.serequotes, index, slice, in, +, *
structs.serevalue types
enums.serepayloads, match, .name / .value
enum_print.sereprint an enum, main -> void
oop.sereinheritance, super, generics, ++
point.sereclass methods returning self type
properties.serex.get / x.set used as v.x / v.x = 10
dunders.sere__getitem__ / __len__ / __contains__
errors.seretry / raise
memory.sereUnique, Ptr, & / *
introspect.seretypeof, dir, module dunders
imports.sereimport / from
aliases.serefunction aliases
macros_*.serequote, match, HTML, pipeline, hygiene
native_add.sereextern "C"
stdlib_mods.sere / stdlib_more.serefs, math, string, regex, hash, …
numeric.serevec, matrix, ml, bytes
gc_mem.serecollectors, arenas, pools
qt6_app.sereQt widgets
gl_info.sere / gl_triangle.sere / platform.sereGL API surface / triangle / host flags
colors.sereunit enum
pythonish.sereuntyped params, inferred locals, const, //= **=
walrus.sere / lambda.sere / tuples.sere:=, lambda, tuple unpack
defer.sere / with_ctx.sere / del_list.seredefer, with, del
flags_enum.sere@flags and in
requests.sere / wsgi.sereHTTP client and WSGI-shaped server types

Internals of the compiler: README.md in this folder. How to add a keyword or module: extending.md.