#!/usr/bin/env ape
"""
Ape is a systems-level programming language
that is fast to write, compile, and run.
"""
# Declarations
x, y, z = 5, "Hello, world!", .an_enum_literal
s: type[.str] # zero-initialized
w: type[.int] = del # uninitialized
assert type(x) == type[.int]
# Shadowing
x = 5.0
assert type(x) == type[.float] # now a float!
# Explicit reassignment
x := 10
assert type(x) == type[.float] # still a float!
# Multiple call syntax, "bare" procedure calls
assert type(type) == type == type[.type]
# First-class *static* typing
id = type[.id]
int = type[.int]
int32 = int[bits=32]
float = type[.float]
str = type[.str]
list = type[.list]
tuple = type[.tuple]
assert int.sizeof == __cpu_bits__ // 8
# Pointers
a = 5
pointer_to_a = id(a)
assert type(pointer_to_a) == id[int]
b = pointer_to_a[0] # dereference
pointer_to_a[0] := 10 # reassign
# Polymorphic procedures
def min[T](xs: list[T]) -> T:
result = xs[0] # optional bounds checking
for xs[1:]: # implicit names, slice syntax
result := result if result < it else it
return result
# Overloadable procedures
def min[T](*xs: tuple[T, ...]) -> T:
return xs.list().min
# Overloadable operators
x: list[float, 4, 4]
x @= x # calls __imatmul__(id(x), x)
# Uniform call syntax
assert x.min == min(x)
"""
Note: There are no methods in Ape.
Every procedure defined in your scope
can instead be called with dot syntax.
The priority for x.y is defined as:
1. field y of x
2. call y[x]
3. call y(x)
Use x.y() to choose y(x) over y[x].
"""
# Lexical scoping
q = 10
with:
q = 12
finally: q := 13 # Deferred execution
q := 14
assert q == 10
# Casting
f = 5.0.(float[bits=32])
q := f.(int)
assert q == 5
# Compiler as a library
import compiler
def main(): pass
exe = compiler.compile(
entry=main,
os_target=.windows,
cpu_target=.x64,
exe_format=.pe64,
)
assert exe.generated_object.len > 1024
# Batteries optional but included
from basic import *
print("Hello, world!")
# Explicit allocators
print(42, allocator=page_allocator)
# or override for an entire block
with heap_allocator as allocator:
print("Hello,", "%".format(47))
"""
Note: There is no garbage collector in Ape.
All allocating procedures in basic are macros
with a default `allocator=nonlocal allocator`.
We implicitly imported `basic.allocator` which
defaults to `basic.temp_allocator`. New users
can remain oblivious to what this means outside
of learning to call temp_allocator.free_all()
every so often and can later graduate to
more advanced allocation schemes.
"""
# Metaprogramming example from basic
def format[fmt, T](
fmt: str,
*args: T, # T.kind == .tuple
allocator=nonlocal allocator,
) -> str[.dynamic] {macro=True}:
b: str[.dynamic]
b.allocator := allocator
yield:
i = argi = 0
tmp: str[.dynamic]
while i < fmt.len:
finally: i += 1
if fmt[i] == ord("%"):
if i + 1 > fmt.len or fmt[i + 1] != ord("%"):
yield if tmp.count > 0: b += yield tmp.str()
yield: b += args[argi].str()
argi += 1
continue
i += 1
tmp += fmt[i]
yield if tmp.count > 0: b += yield tmp.str()
assert argi == args.len
return b
"""
Note: `yield` flips back and forth between
compile-time and run-time contexts, and
tries to compute constants as much as possible.
Because T is a tuple, its length must be known
at compile time. If argi >= args.len in args[argi],
a compile error will be emitted.
"""