Welcome to AlexScript

AlexScript is a general-purpose, dynamically typed, interpreted programming language with Polish-language syntax. Where most languages spell their keywords in English (if, else, class, function), AlexScript spells them in Polish (jesli, albo, klasa, funkcja). The semantics, however, will feel familiar to anyone who has used Ruby, Python, or JavaScript: first-class functions, classes with single inheritance, modules with mixins, exceptions, closures, an interactive REPL, async/await, and a built-in debugger.

This guide takes you from the very first pokazl "Hello" all the way through classes, modules, exceptions, and asynchronous programming. Each section builds on the previous one, so if you read it top to bottom you should never hit a concept you haven’t seen before. If you’re already familiar with another scripting language, you can skim the early sections and dive in wherever a topic catches your eye.

A few conventions used throughout the guide. AlexScript source files use the .as extension. Code examples are shown in alexscript blocks; the output, where shown, follows the example. Comments in code start with # for a single line, or /* ... */ for a block comment. Error messages produced by AlexScript at runtime — including all the ones quoted in this tutorial — are in Polish, since the language itself is Polish-centric.

Running AlexScript

To run an AlexScript program, save it to a file with the .as extension and pass the file to the interpreter:

alexscript hello.as

The first line of output will be a confirmation message that the file was loaded — you can ignore it for now. After that, your program runs.

You can also pass a one-liner directly as a string argument, which is useful for quick experiments:

alexscript pokazl "test"'

The REPL

If you launch the interpreter without any file argument, you drop into an interactive REPL (Read-Eval-Print Loop):

alexscript

Inside the REPL, every expression you type is evaluated immediately and its result is printed. The REPL also keeps state across lines — variables you declare stay available until you reset:

> niech x = 10
> niech y = 20
> x + y
=> 30

The result of the last expression is automatically bound to the underscore variable _, so you can chain off the previous answer:

> 5 * 7
=> 35
> _ + 100
=> 135

The REPL recognizes a small set of special commands that aren’t AlexScript code:

The REPL is a great place to try out language features as you read through this guide. If you’re unsure how something behaves, type it in and see.

Hello, World

The traditional first program in AlexScript:

pokazl "Hello, World"

Save it to hello.as and run it. The pokazl keyword prints its argument followed by a newline. There is also pokaz, which prints without a newline:

pokaz "Hello, "
pokaz "World"
pokazl ""

Both pokaz and pokazl accept any value — strings, numbers, booleans, arrays, objects, even instances of your own classes. AlexScript will format each value sensibly for you:

pokazl 42
pokazl 3.14
pokazl prawda
pokazl [1, 2, 3]
pokazl { "imie": "Anna", "wiek": 30 }

Variables

Variables are declared with the niech keyword. AlexScript is dynamically typed: you don’t write the type explicitly — it’s inferred from the value you assign.

niech x = 5
niech pi = 3.14
niech imie = "Alex"
niech aktywny = prawda
niech nieznany = nic

After declaration, you can reassign a variable without niech:

niech licznik = 0
licznik = licznik + 1
licznik = licznik + 1
pokazl licznik    # 2

The distinction between niech variable = ... (declares a new variable) and variable = ... (modifies an existing one) is fundamental in AlexScript. We’ll come back to it under Variable scope.

Constants

Identifiers written entirely in uppercase letters are treated as constants. Once assigned, a constant cannot be reassigned — any attempt to do so raises a runtime error.

niech MAX_USERS = 100
niech PI = 3.14159

# This will fail:
# MAX_USERS = 200
# BladWykonania: Nie mozna zmienic wartosci stalej MAX_USERS

Constants are by convention used for values that should never change at runtime — configuration limits, mathematical constants, error codes, and similar.

A constant can hold any kind of value: a number, a string, an array, an object, even a function. The “constant” rule is about rebinding the name, not about deep immutability of the value behind it. An array assigned to a constant can still have its elements modified through methods like dodaj — the binding is fixed, the contents are not.

Global variables

By default, variables declared with niech inside a function or block are local to that scope. Use globalna niech at the top level (or inside a function, to declare a global from there) to make a variable accessible from any scope:

globalna niech LICZNIK_GLOBALNY = 0

funkcja zwieksz() {
    LICZNIK_GLOBALNY = LICZNIK_GLOBALNY + 1
}

zwieksz()
zwieksz()
pokazl LICZNIK_GLOBALNY    # 2

Use globals sparingly. They make code harder to reason about and harder to test. Most of the time, passing values as function arguments or storing them in objects is a better choice.

Variable scope

AlexScript uses lexical (block) scoping. A variable declared inside a { ... } block is visible only within that block:

jesli prawda {
    niech wewnetrzna = 1
    pokazl wewnetrzna    # 1
}

# pokazl wewnetrzna   # BladNazwy — wewnetrzna is invisible here

This applies to every kind of block — jesli branches, dopoki loops, dla loops, function bodies, and so on. Variables declared inside the body don’t leak out:

dla niech indeks = 0; 3; 1 {
    niech temp = indeks * 2
}

# pokazl temp     # BladNazwy — temp was scoped to the loop body

Inner scopes can read variables from outer scopes, but niech always creates a new binding in the current scope:

niech x = 10

jesli prawda {
    niech x = x + 1   # creates a new local x; outer x is unchanged
    pokazl x          # 11
}

pokazl x              # 10

To modify the outer variable, assign without niech:

niech x = 10

jesli prawda {
    x = x + 1         # mutates the outer x
}

pokazl x              # 11

This distinction — niech variable = ... declares a new variable, plain variable = ... modifies an existing one — is one of the most common sources of confusion in AlexScript. Read it twice, and remember it.

Reserved words

A small set of identifiers cannot be used as variable names because they are keywords or operators in the language. The full list:

niech, globalna, jesli, albo, albojesli, to, prawda, falsz, i, lub, dopoki, petla, dla, w, funkcja, nic, zakoncz, nastepny, pokaz, pokazl, zwroc, wyjscie, wczytaj, import, proba, zlap, wkoncu, rzuc, klasa, super, sam, statyczna, prywatne, abstrakcyjna, modul, dolacz, debug, fn, asynchroniczna, czekaj, istnieje.

This particularly catches people out for the very short ones. You cannot name a variable i — it’s the logical AND operator. You cannot name a variable w — it’s the membership keyword for dla loops. Naming a variable to will also fail, since to is the inline-conditional keyword.

If you really want a one-letter loop counter, use j, k, n, or idx. If you want to name a variable “in”, spell it differently — for example wejscie. The interpreter is forgiving about what you name things, but it can’t let you reuse the names that mean something to it.

Data Types

AlexScript has a small, regular set of built-in types. Every value belongs to exactly one of them.

Logiczna (boolean)

The boolean type has two values: prawda (true) and falsz (false). Booleans are produced by comparison operators (==, <, >, etc.) and the logical operators (i, lub, !).

niech aktywny = prawda
niech zakonczony = falsz
pokazl aktywny i !zakonczony    # prawda

Calkowita (integer)

Whole numbers, with arbitrary precision. There is no fixed upper bound — AlexScript integers grow as large as memory allows.

niech maly = 42
niech duzy = 1000000000000000000
niech ujemny = -7
pokazl duzy * duzy

Zmiennoprzecinkowa (float)

Numbers with a decimal point. Backed by 64-bit floating point.

niech pi = 3.14159
niech temperatura = -2.5
pokazl pi * 2.0

Mixing integers and floats in arithmetic produces a float.

Napis (string)

Sequences of characters, written in double quotes. AlexScript strings support escape sequences (\n, \t, \", \\) and string concatenation with +, but the most ergonomic way to embed values is string interpolation — see the Strings section.

niech imie = "Anna"
niech powitanie = "Cześć, #{imie}!"
pokazl powitanie    # Cześć, Anna!

Nic (null)

The single value nic represents “no value” or “absence”. It’s what you get from a function that doesn’t explicitly return, from accessing a missing object key, and from variables you initialize without a meaningful value yet.

niech wynik = nic

jesli wynik == nic {
    pokazl "brak wyniku"
}

Tablica (array)

Ordered, zero-indexed collections that can hold values of any types — including a mix of types in the same array.

niech liczby = [1, 2, 3, 4, 5]
niech mieszane = [1, "dwa", 3.0, prawda, nic]
niech pusta = []

pokazl liczby[0]      # 1
pokazl mieszane[1]    # dwa

Arrays come with a rich set of built-in methods — dlg, dodaj, usun, mapuj, filtruj, sortuj, and many more — covered in detail in Arrays.

Obiekt (object)

Key-value collections, sometimes called hashes or dictionaries in other languages. Keys are strings; values can be of any type.

niech osoba = {
    "imie": "Anna",
    "wiek": 30,
    "miasto": "Warszawa"
}

pokazl osoba["imie"]    # Anna
osoba["wiek"] = 31
osoba["email"] = "[email protected]"

Inspecting types at runtime

Every value responds to a typ() method that returns its type name as a string:

pokazl (42).typ()       # calkowita
pokazl 3.14.typ()       # zmiennoprzecinkowa
pokazl "tekst".typ()    # napis
pokazl prawda.typ()     # logiczna
pokazl nic.typ()        # nic
pokazl [1, 2].typ()     # tablica
pokazl ({"a": 1}).typ() # obiekt

For numeric literals you sometimes need parentheses to disambiguate the parser, as in (42).typ().

Operators

Arithmetic

niech a = 7
niech b = 3

pokazl a + b      # 10   addition
pokazl a - b      # 4    subtraction
pokazl a * b      # 21   multiplication
pokazl a / b      # 2    integer division (both operands are integers)
pokazl a % b      # 1    modulo
pokazl a ** b     # 343  exponentiation

When either operand is a float, division returns a float:

pokazl 7 / 3        # 2
pokazl 7.0 / 3      # 2.3333333333333335
pokazl 7 / 3.0      # 2.3333333333333335

The unary minus negates a number:

niech x = 5
pokazl -x           # -5

The ** operator works with floats too, which gives you a concise way to write roots — x ** 0.5 is the square root of x, x ** (1.0 / 3) is the cube root:

pokazl 9 ** 0.5         # 3.0
pokazl 27 ** (1.0 / 3)  # 3.0000000000000004

The + operator also concatenates strings:

pokazl "Hello, " + "World"

To convert a number to a string for concatenation, use .napis():

niech wiek = 30
pokazl "Mam " + wiek.napis() + " lat"

In practice, string interpolation is usually nicer than +-concatenation for building messages from mixed values.

Comparison

pokazl 5 == 5     # prawda
pokazl 5 != 3     # prawda
pokazl 5 > 3      # prawda
pokazl 5 < 3      # falsz
pokazl 5 >= 5     # prawda
pokazl 5 <= 4     # falsz

Comparison works on numbers and strings (lexicographic for strings). Equality works on every type and is structural — two arrays with the same elements compare equal, and so do two objects with the same keys and values:

pokazl [1, 2, 3] == [1, 2, 3]    # prawda
pokazl {"a": 1} == {"a": 1}      # prawda

Logical

niech a = prawda
niech b = falsz

pokazl a i b          # falsz   logical AND
pokazl a lub b        # prawda  logical OR
pokazl !a             # falsz   logical NOT

The i and lub operators short-circuit: if the left operand of i is falsz, the right operand is not evaluated; same for lub if the left is prawda.

funkcja drukuj_i_zwroc(x) {
    pokazl "obliczam: #{x}"
    zwroc x
}

# Right side never runs:
pokazl falsz i drukuj_i_zwroc(prawda)
# Output:
# falsz

jesli and dopoki only accept boolean conditions — there is no implicit “truthiness” of strings or numbers like in Python or JavaScript. jesli "tekst" { ... } will raise a runtime error rather than silently treating the non-empty string as true.

Bitwise

For integers, AlexScript provides full bitwise operations. They work like in C, Python, or Ruby — on the binary representation of the number.

niech a = 12          # 1100
niech b = 10          # 1010

pokazl a & b          # 8     bitwise AND  (1000)
pokazl a | b          # 14    bitwise OR   (1110)
pokazl a ^ b          # 6     bitwise XOR  (0110)
pokazl ~a             # -13   bitwise NOT
pokazl a << 2         # 48    left shift   (110000)
pokazl a >> 1         # 6     right shift  (110)

The shift operators require the right operand to be a non-negative integer; shifting by a negative count raises a runtime error.

The << operator has a second meaning: when the left operand is an array, it appends to that array. The interpreter picks the right meaning based on the type of the left operand — there’s no ambiguity at runtime.

There are also bit-level instance methods on integers — bit(n), ustaw_bit(n), wyczysc_bit(n), przelacz_bit(n), policz_bity(), dlugosc_bitowa(), binarnie(), szesnastkowo(), osemkowo() — useful when you want to work with individual bits without bit-twiddling expressions:

niech n = 13          # 1101
pokazl n.binarnie()         # "1101"
pokazl n.bit(0)             # 1
pokazl n.bit(1)             # 0
pokazl n.policz_bity()      # 3
pokazl n.dlugosc_bitowa()   # 4

Compound assignment

The familiar shorthand operators +=, -=, *=, /=, %= are supported:

niech x = 10
x += 5      # 15
x -= 3      # 12
x *= 2      # 24
x /= 4      # 6
x %= 4      # 2

Ternary operator

The ternary expression warunek ? wartosc_jesli_prawda : wartosc_jesli_falsz evaluates exactly one of two branches based on a condition:

niech wiek = 18
niech status = wiek >= 18 ? "dorosly" : "nieletni"
pokazl status    # dorosly

The ternary is right-associative, so it chains naturally without parentheses:

niech pkt = 75
niech ocena = pkt >= 90 ? "A" :
              pkt >= 70 ? "B" :
              pkt >= 50 ? "C" : "F"
pokazl ocena     # B

Only the selected branch is evaluated — side effects in the unchosen branch never run. This makes ternaries safe for guarded expressions like lista.dlg() > 0 ? lista[0] : nic.

Comments

# This is a single-line comment

/* This is a block comment.
   It can span multiple lines. */

niech x = 5    # comments can also follow code

Comments are stripped by the lexer before parsing — they have no effect on program behavior.

Control Flow

if / else / else if

The conditional construct uses jesli, albojesli, and albo:

niech x = 10

jesli x > 5 {
    pokazl "duze"
} albojesli x > 0 {
    pokazl "male"
} albo {
    pokazl "niedodatnie"
}

You can have any number of albojesli branches; albo is optional. The first branch whose condition is prawda runs; the rest are skipped.

For very short conditionals, the inline form with to reads naturally:

jesli x > 100 to pokazl "duzo"

The inline form executes a single statement after the condition. For multiple statements, use the block form.

The condition itself must be a boolean. AlexScript does not implicitly coerce strings, numbers, or arrays to truthy/falsy — only prawda, falsz, or (specially permitted) nic are accepted. Passing a string raises BladWykonania: Warunek musi byc boolem lub "nic".

Exiting a program: wyjscie

The wyjscie() function ends the program immediately. With no argument, the process exits with status code 0 (success):

pokazl "przed"
wyjscie()
pokazl "to się nie wykona"

Pass an integer literal to set the exit code — the convention is that 0 means success and non-zero means an error:

jesli !konfiguracja_ok() {
    pokazl "Krytyczny blad konfiguracji"
    wyjscie(1)
}

The argument to wyjscie must be an integer literal — you can’t pass an arbitrary expression or string message. If you need to print before exiting, do it on a separate line. Keep wyjscie for terminal failures: in normal flow, returning from a function or letting main fall off the end is more idiomatic.

while loop

The dopoki keyword runs a block as long as a condition holds:

niech k = 0

dopoki k < 5 {
    pokazl k
    k = k + 1
}

The condition is evaluated before each iteration. If it’s false on entry, the body never runs. As with jesli, the condition must be a boolean.

for loop (numeric)

The C-style numeric for loop has the form dla niech zmienna = start; koniec; krok { ... }:

dla niech k = 0; 10; 1 {
    pokazl k        # prints 0..9
}

The three parts are: initial value, end value (exclusive), and step. The variable counts up while it’s less than the end value. To count down, use a negative step:

dla niech k = 10; 0; -1 {
    pokazl k        # prints 10..1
}

The step part is optional and defaults to 1:

dla niech k = 0; 5 {
    pokazl k        # prints 0..4
}

All three parts of the header are full expressions, not just literals. The end value can be the length of an array, the result of a function call, anything that evaluates to an integer:

niech arr = [10, 20, 30, 40]

dla niech k = 0; arr.dlg(); 1 {
    pokazl arr[k]
}

Note: Because i is the logical-AND operator, it cannot be used as the loop counter name. The convention in AlexScript code is k, j, n, or idx.

for loop (collection)

To iterate over an array, use dla element w tablica { ... }:

niech owoce = ["jablko", "gruszka", "sliwka"]

dla owoc w owoce {
    pokazl owoc
}

To iterate over an object, use the two-variable form dla klucz, wartosc w obiekt { ... }:

niech osoba = { "imie": "Anna", "wiek": 30 }

dla klucz, wartosc w osoba {
    pokazl "#{klucz} = #{wartosc}"
}

A few important constraints to know:

Iteration is supported for arrays and objects only. You cannot iterate directly over a string with dla znak w "tekst" — that raises Moze iterowac tylko po tablicach. To process a string character by character, either index it with s.indeks(k) in a numeric loop, or split it first with s.rozdziel("").

For objects, the two-variable form is what you use — there is no widely-used single-variable iteration over object keys. If you only need the keys, call obj.klucze() first and iterate over the result.

Infinite loop

When you want to loop forever and exit explicitly from inside, use petla:

niech licznik = 0

petla {
    licznik = licznik + 1
    jesli licznik >= 10 to zakoncz
}

pokazl licznik    # 10

break and continue

Inside any loop, zakoncz exits the loop immediately and nastepny skips to the next iteration:

dla niech k = 0; 20; 1 {
    jesli k % 2 == 0 to nastepny     # skip even numbers
    jesli k > 10 to zakoncz          # stop after 10
    pokazl k                         # prints 1, 3, 5, 7, 9
}

Both keywords apply to the innermost loop only. They work in dla, dopoki, and petla — every loop construct.

Input and Output

You’ve already seen pokaz and pokazl for output. For input, AlexScript provides wczytaj:

niech imie = wczytaj("Jak masz na imie? ")
pokazl "Cześć, #{imie}!"

wczytaj displays its prompt argument (without a newline), reads a line from standard input, and returns it as a string. If you need a number, convert it explicitly with .liczba():

niech wiek_tekst = wczytaj("Twoj wiek: ")
niech wiek = wiek_tekst.liczba()

jesli wiek == nic {
    pokazl "To nie jest poprawna liczba"
} albojesli wiek >= 18 {
    pokazl "Witamy w klubie"
}

Two things to note about .liczba(). First, it always returns a float, even for input that looks integral — "42".liczba() is 42.0. Second, it returns nic (not zero, not an exception) for input that doesn’t parse as a number, so you can guard against bad input by checking for nic.

Strings

Strings are sequences of characters in double quotes. AlexScript strings are immutable: methods like duzymi() and wyczysc() return new strings rather than modifying the original.

String interpolation

Inside a string, the syntax #{expression} evaluates the expression and inserts its string representation. This is the most ergonomic way to build messages from variables:

niech imie = "Anna"
niech wiek = 30

pokazl "Cześć, #{imie}!"
pokazl "Mam #{wiek} lat."
pokazl "Za rok będę miał #{wiek + 1} lat."

The expression inside #{ ... } can be anything — arithmetic, method calls, ternary expressions, even nested function calls:

niech liczby = [1, 2, 3, 4, 5]

pokazl "Suma: #{liczby.suma()}"
pokazl "Średnia: #{liczby.srednia()}"
pokazl "Status: #{liczby.dlg() > 0 ? "ma elementy" : "puste"}"

To include a literal #{ in a string without triggering interpolation, escape the # with a backslash:

pokazl "literal: \#{nie_interpolowane}"

Interpolation works in any double-quoted string — anywhere AlexScript would accept a string literal. It composes naturally with all the other string operations.

Inspection

niech s = "Cześć świat"

pokazl s.dlg()             # 11
pokazl s.pusta()           # falsz
pokazl s.zawiera("świat")  # prawda
pokazl s.indeks(0)         # "C"   character at position
pokazl s.indeks(-1)        # "t"   negative index counts from end

Transformation

niech s = "  Hello World  "

pokazl s.duzymi()          # "  HELLO WORLD  "
pokazl s.malymi()          # "  hello world  "
pokazl s.wyczysc()         # "Hello World"     trim whitespace
pokazl s.zduzej()          # "  hello world  " capitalize first letter
pokazl s.odwroc()          # "  dlroW olleH  "
pokazl s.usun(" ")         # "HelloWorld"      remove characters

Slicing

niech s = "AlexScript"

pokazl s.wydziel(0, 4)     # "Alex"   (start, length)
pokazl s.wycinek(4, 9)     # "Script" (start, end inclusive)

Splitting and parsing

niech csv = "anna,jan,ewa"
pokazl csv.rozdziel(",")   # ["anna", "jan", "ewa"]

niech liczba = "42".liczba()
pokazl liczba              # 42.0   (always float)

niech zla = "abc".liczba()
pokazl zla                 # nic    (couldn't parse)

rozdziel also accepts a Wyrazenie (regular expression) for more flexible splitting — see Regular expressions.

Conversion

To convert any value to its string representation, call .napis():

pokazl 42.napis()           # "42"
pokazl 3.14.napis()         # "3.14"
pokazl prawda.napis()       # "prawda"
pokazl [1, 2].napis()       # "[1, 2]"

This is useful when you specifically need a string and want to be explicit about it. For building messages with embedded values, prefer string interpolation.

Arrays

Arrays are zero-indexed, can hold mixed types, and grow dynamically.

Creating and accessing

niech liczby = [10, 20, 30, 40, 50]
niech pusta = []

pokazl liczby[0]    # 10
pokazl liczby[2]    # 30

# Negative indices count from the end:
pokazl liczby[-1]   # 50
pokazl liczby[-2]   # 40

# Modify by index:
liczby[1] = 99
pokazl liczby       # [10, 99, 30, 40, 50]

Indexing past the end raises a BladZakresu exception.

Adding and removing elements

niech arr = [1, 2, 3]

arr.dodaj(4)              # add to end
arr << 5                  # same thing, shorter
pokazl arr                # [1, 2, 3, 4, 5]

arr.wstaw(0, 0)           # insert at index 0
pokazl arr                # [0, 1, 2, 3, 4, 5]

arr.usun(2)               # remove at index 2
pokazl arr                # [0, 1, 3, 4, 5]

arr.wyczysc()             # remove everything
pokazl arr                # []

<< on an array means append — the same operator that means left shift on integers. The interpreter dispatches based on the type of the left operand, so there’s no ambiguity.

Querying

niech arr = [10, 20, 30, 20, 10]

pokazl arr.dlg()              # 5
pokazl arr.pusta()            # falsz
pokazl arr.zawiera(20)        # prawda
pokazl arr.indeks(20)         # 1   (first occurrence)
pokazl arr.pierwszy()         # 10
pokazl arr.ostatni()          # 10

To count occurrences of a value, combine filtruj with dlg:

pokazl arr.filtruj(fn(x) { x == 20 }).dlg()    # 2

Slicing, joining, copying

niech arr = [10, 20, 30, 40, 50]

pokazl arr.wycinek(1, 3)      # [20, 30, 40]   inclusive on both ends
pokazl arr.kopiuj()           # [10, 20, 30, 40, 50]   shallow copy
pokazl arr.odwroc()           # [50, 40, 30, 20, 10]
pokazl arr.zlacz(", ")        # "10, 20, 30, 40, 50"
pokazl arr.polacz([60, 70])   # [10, 20, 30, 40, 50, 60, 70]

zlacz joins all elements into a single string with the given separator. polacz concatenates two arrays into a new array.

Numeric arrays

When all elements are numbers, additional methods become available:

niech liczby = [3, 1, 4, 1, 5, 9, 2, 6]

pokazl liczby.suma()      # 31
pokazl liczby.srednia()   # 3.875
pokazl liczby.min()       # 1
pokazl liczby.max()       # 9

Calling these on an array with non-numeric values raises a runtime error.

Higher-order methods

Arrays support a complete set of higher-order methods that take a function and apply it to each element. These are where AlexScript becomes really expressive.

mapuj

Applies a function to every element and returns a new array of results. The original array is not modified.

niech arr = [1, 2, 3, 4]
niech podwojone = arr.mapuj(fn(x) { x * 2 })
pokazl podwojone    # [2, 4, 6, 8]
pokazl arr          # [1, 2, 3, 4]   unchanged

If the callback accepts two parameters, the second receives the index:

niech arr = [10, 20, 30]
niech wynik = arr.mapuj(fn(el, idx) { el + idx })
pokazl wynik        # [10, 21, 32]

This dual-arity behavior — pass a 1-arg callback or a 2-arg callback — works for mapuj and kazdy. The interpreter checks how many parameters your callback declares and calls it accordingly.

filtruj

Returns a new array containing only the elements for which the callback returns prawda:

niech liczby = [1, 2, 3, 4, 5, 6, 7, 8]
niech parzyste = liczby.filtruj(fn(x) { x % 2 == 0 })
pokazl parzyste     # [2, 4, 6, 8]

redukuj

Combines all elements into a single value, starting from an explicit seed. The callback receives the running accumulator and the current element:

niech liczby = [1, 2, 3, 4, 5]
niech suma = liczby.redukuj(fn(acc, x) { acc + x }, 0)
pokazl suma         # 15

niech iloczyn = liczby.redukuj(fn(acc, x) { acc * x }, 1)
pokazl iloczyn      # 120

The seed (the second argument to redukuj) is required — there is no implicit default. This makes redukuj safe on empty arrays:

pokazl [].redukuj(fn(acc, x) { acc + x }, 0)    # 0

kazdy

Runs the callback for its side effects on each element. Returns nic. This is mapuj for cases where you don’t want a result array:

[1, 2, 3].kazdy(fn(x) {
    pokazl "element: #{x}"
})

znajdz

Returns the first element for which the callback returns prawda, or nic if none does:

niech ludzie = [
    {"imie": "Anna", "wiek": 25},
    {"imie": "Jan", "wiek": 40},
    {"imie": "Ewa", "wiek": 35}
]

niech starszy = ludzie.znajdz(fn(os) { os["wiek"] > 30 })
pokazl starszy["imie"]    # Jan

dowolny / wszystkie

Quantifier checks: dowolny returns prawda if the callback is prawda for at least one element, wszystkie returns prawda only if it’s prawda for every element.

niech liczby = [2, 4, 6, 8]

pokazl liczby.dowolny(fn(x) { x > 5 })       # prawda
pokazl liczby.wszystkie(fn(x) { x % 2 == 0 }) # prawda
pokazl liczby.wszystkie(fn(x) { x > 5 })     # falsz

The edge cases follow the standard mathematical convention: dowolny on an empty array returns falsz (no element satisfies anything), wszystkie on an empty array returns prawda (vacuously, every nonexistent element satisfies anything).

sortuj

Without arguments, sortuj returns a new array sorted in natural order — ascending for numbers, lexicographic for strings:

pokazl [3, 1, 4, 1, 5, 9, 2, 6].sortuj()
# [1, 1, 2, 3, 4, 5, 6, 9]

pokazl ["b", "a", "c"].sortuj()
# ["a", "b", "c"]

With a comparator function, it sorts according to the comparator. The comparator receives two elements and must return a number: negative if the first should come before the second, positive if after, zero if equal.

niech liczby = [3, 1, 4, 1, 5, 9, 2, 6]

# descending
niech malejaco = liczby.sortuj(fn(a, b) { b - a })
pokazl malejaco    # [9, 6, 5, 4, 3, 2, 1, 1]

# by string length
niech slowa = ["pies", "kot", "hipopotam", "as"]
niech wg_dlugosci = slowa.sortuj(fn(a, b) { a.dlg() - b.dlg() })
pokazl wg_dlugosci  # [as, kot, pies, hipopotam]

sortuj returns a new sorted array — the original is not modified.

Method chaining

The HOF methods compose well — chain them to build pipelines:

niech liczby = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

niech wynik = liczby
    .filtruj(fn(x) { x % 2 == 0 })
    .mapuj(fn(x) { x * x })
    .redukuj(fn(acc, x) { acc + x }, 0)

pokazl wynik    # 220   (4 + 16 + 36 + 64 + 100)

This style — express your intent as a series of transformations — is often clearer than the equivalent loop with intermediate variables.

Objects

Objects are key-value collections with string keys.

Creating and accessing

niech osoba = {
    "imie": "Anna",
    "wiek": 30,
    "miasto": "Warszawa"
}

# Access with bracket notation:
pokazl osoba["imie"]      # Anna

# Modify or add keys:
osoba["wiek"] = 31
osoba["email"] = "[email protected]"

# Remove keys:
osoba.usun("miasto")

Reading a missing key returns nic, not an error:

pokazl osoba["nieistniejacy"]    # nic

Object methods

niech obj = {"a": 1, "b": 2, "c": 3}

pokazl obj.dlg()             # 3
pokazl obj.pusty()           # falsz
pokazl obj.klucze()          # ["a", "b", "c"]
pokazl obj.wartosci()        # [1, 2, 3]
pokazl obj.ma_klucz("a")     # prawda
pokazl obj.ma_wartosc(2)     # prawda
pokazl obj.na_tablice()      # [["a", 1], ["b", 2], ["c", 3]]

To iterate over an object, use the two-variable dla loop:

dla klucz, wartosc w obj {
    pokazl "#{klucz} -> #{wartosc}"
}

Functions

Functions are declared with funkcja:

funkcja powitaj(imie) {
    pokazl "Cześć, #{imie}!"
}

powitaj("Anna")    # Cześć, Anna!

The body goes in { } braces. To return a value, use zwroc:

funkcja kwadrat(x) {
    zwroc x * x
}

pokazl kwadrat(7)    # 49

A function without an explicit zwroc returns nic. Note that bare zwroc (with no expression) is a syntax error — write zwroc nic if you want to explicitly return null.

Default parameters

Parameters can have default values that apply when the argument is omitted:

funkcja powitaj(imie, formalna = falsz) {
    jesli formalna {
        zwroc "Dzień dobry, #{imie}"
    }
    zwroc "Cześć, #{imie}"
}

pokazl powitaj("Anna")               # Cześć, Anna
pokazl powitaj("Anna", prawda)       # Dzień dobry, Anna

Defaults can be any expression. They’re evaluated each time the function is called with a missing argument.

Rest parameters

Prefix the last parameter with * to collect any remaining positional arguments into an array:

funkcja suma(*liczby) {
    niech wynik = 0
    dla n w liczby {
        wynik = wynik + n
    }
    zwroc wynik
}

pokazl suma()              # 0
pokazl suma(1, 2, 3)       # 6
pokazl suma(1, 2, 3, 4, 5) # 15

A rest parameter can come after regular parameters:

funkcja pierwszy_plus(pierwszy, *reszta) {
    niech wynik = pierwszy
    dla n w reszta {
        wynik = wynik + n
    }
    zwroc wynik
}

pokazl pierwszy_plus(10, 1, 2, 3)    # 16

Parameter declaration rules

Two rules govern how parameters can be combined:

Defaults must come after required parameters. funkcja test(a = 1, b, c) is invalid — once you’ve declared a parameter with a default, every parameter to its right must also have a default. The compile-time error reads Parametry bez wartosci domyslnych nie moga występowac po parametrach z wartosciami domyslnymi.

Rest parameters must be last. funkcja test(*a, b) is invalid. The rest parameter, if present, has to come at the end. The error reads …musi być ostatnim parametrem….

A correct full declaration looks like funkcja f(a, b, c = 10, d = 20, *reszta) — required, then defaulted, then optionally a rest.

Argument validation

If you call a function with too few arguments, AlexScript raises BladArgumentu:

funkcja suma(a, b) {
    zwroc a + b
}

# suma(5)
# BladArgumentu: Funkcja suma oczekiwala minimum 2 argumentów, otrzymała 1

The “minimum” count is the number of parameters without defaults. With a rest parameter, there is no maximum — extra arguments simply land in the rest array. Without a rest parameter, calling with too many arguments is also an error.

Nested functions

Functions can be defined inside other functions. The inner function is only visible within the enclosing function’s body:

funkcja zewnetrzna() {
    niech x = 10

    funkcja wewnetrzna() {
        pokazl x        # captures x from the enclosing scope
    }

    wewnetrzna()
}

zewnetrzna()
# wewnetrzna()       # BladNazwy — wewnetrzna is invisible here

Recursion

Functions can call themselves. AlexScript imposes a recursion-depth limit to protect against runaway calls — when exceeded, you get BladWykonania: zbyt glebokie zagniezdzenie stosu. You’ll always need a base case to terminate:

funkcja silnia(n) {
    jesli n <= 1 to zwroc 1
    zwroc n * silnia(n - 1)
}

pokazl silnia(5)    # 120
pokazl silnia(10)   # 3628800

For algorithms that would otherwise need very deep recursion, rewrite them using accumulator parameters or convert them to an explicit loop with a stack.

Anonymous functions and closures

Use fn(params) { body } to create a function value without giving it a name. These are commonly called lambdas or anonymous functions:

niech kwadrat = fn(x) { x * x }
pokazl kwadrat(5)    # 25

If the body is a single expression, the result of that expression is automatically returned — no zwroc needed:

niech podwoj = fn(x) { x * 2 }
pokazl podwoj(7)    # 14

For a multi-line body, use explicit zwroc:

niech znormalizuj = fn(s) {
    niech wyczyszczone = s.wyczysc()
    niech male = wyczyszczone.malymi()
    zwroc male
}

pokazl znormalizuj("  Hello  ")    # hello

Anonymous functions are most commonly passed as arguments to higher-order functions like mapuj and filtruj:

niech liczby = [1, 2, 3, 4, 5]
niech kwadraty = liczby.mapuj(fn(x) { x * x })
pokazl kwadraty    # [1, 4, 9, 16, 25]

You can also call an anonymous function inline (an “immediately invoked function expression”):

pokazl (fn(x) { x + 1 })(10)    # 11

Recursive anonymous functions

An anonymous function assigned to a variable can call itself by referring to the variable name. The function captures the variable along with everything else in its closure:

niech silnia = fn(n) {
    jesli n <= 1 to zwroc 1
    zwroc n * silnia(n - 1)
}

pokazl silnia(5)    # 120

This works because the body of fn is evaluated each time the function is called, by which point the variable silnia has already been bound.

Functions as values

Named functions are also first-class values. You can assign them to variables, pass them as arguments, store them in arrays and objects, and return them from other functions.

funkcja podwoj(x) { zwroc x * 2 }

niech alias = podwoj
pokazl alias(5)    # 10

Mixing them with arrays of functions, for example, is fine:

niech operacje = [
    fn(x) { x + 1 },
    fn(x) { x * 2 },
    fn(x) { x - 3 }
]

dla op w operacje {
    pokazl op(10)    # 11, 20, 7
}

Higher-order functions

A function that takes another function as an argument, or returns one, is called a higher-order function. They’re a powerful way to factor out repeated patterns.

funkcja zastosuj(f, wartosc) {
    zwroc f(wartosc)
}

pokazl zastosuj(fn(x) { x * 3 }, 7)              # 21
pokazl zastosuj(fn(s) { s.duzymi() }, "alex")    # ALEX

Returning a function from a function — the factory pattern — lets you build customized functions on demand:

funkcja mnoznik(n) {
    zwroc fn(x) { x * n }
}

niech podwoj = mnoznik(2)
niech potroj = mnoznik(3)

pokazl podwoj(10)    # 20
pokazl potroj(10)    # 30

The returned function “remembers” the value of n from the call that created it — this is a closure, covered next.

Closures

A closure is a function that captures variables from the scope where it was created. Captured variables stay alive as long as the closure does, even after the enclosing function has returned.

funkcja licznik() {
    niech n = 0

    zwroc fn() {
        n = n + 1
        zwroc n
    }
}

niech c = licznik()
pokazl c()    # 1
pokazl c()    # 2
pokazl c()    # 3

Each call to licznik() creates a fresh n, so two counters are independent:

niech c1 = licznik()
niech c2 = licznik()

pokazl c1()    # 1
pokazl c1()    # 2
pokazl c2()    # 1   independent state

Two specific patterns built on closures show up everywhere in real AlexScript code, so they’re worth naming explicitly.

Factory functions — returning a configured function instead of a value, so the caller can reuse it:

funkcja walidator_dlugosci(min, max) {
    zwroc fn(s) {
        zwroc s.dlg() >= min i s.dlg() <= max
    }
}

niech haslo_ok = walidator_dlugosci(8, 64)
niech imie_ok = walidator_dlugosci(2, 50)

pokazl haslo_ok("krotkie")             # falsz
pokazl haslo_ok("bezpieczne_haslo")    # prawda
pokazl imie_ok("Anna")                 # prawda

Stateful callbacks — passing a closure that maintains state across invocations, useful for accumulators, sequence generators, and event tracking:

funkcja akumulator() {
    niech suma = 0
    zwroc fn(x) {
        suma = suma + x
        zwroc suma
    }
}

niech akum = akumulator()
[1, 2, 3, 4, 5].kazdy(fn(x) { pokazl akum(x) })
# 1, 3, 6, 10, 15

Common functional patterns

A few patterns that come up often when working with first-class functions:

Function composition — combine two functions into one that runs them in sequence:

funkcja komponuj(f, g) {
    zwroc fn(x) { f(g(x)) }
}

niech dodaj1 = fn(x) { x + 1 }
niech razy2 = fn(x) { x * 2 }

niech pierwsze_dodaj_potem_razy = komponuj(razy2, dodaj1)
pokazl pierwsze_dodaj_potem_razy(4)    # 10

Pipeline — apply an array of functions left to right:

funkcja pipe(fns, wartosc) {
    niech wynik = wartosc
    dla f w fns {
        wynik = f(wynik)
    }
    zwroc wynik
}

niech kroki = [fn(x) { x + 1 }, fn(x) { x * 3 }, fn(x) { x - 2 }]
pokazl pipe(kroki, 4)    # 13

Decorator — wrap an existing function with extra behavior:

funkcja z_logowaniem(f) {
    zwroc fn(x) {
        pokazl "Wejście: #{x}"
        niech wynik = f(x)
        pokazl "Wyjście: #{wynik}"
        zwroc wynik
    }
}

niech podwoj = z_logowaniem(fn(x) { x * 2 })
podwoj(5)
# Wejście: 5
# Wyjście: 10

These aren’t built-in features — they’re patterns you build out of the same basic ingredients (fn, returning functions, passing them as arguments). Once you internalize them, a lot of code becomes much shorter.

Name Existence Check: istnieje

Sometimes you want to check whether a name is defined before using it — for optional configuration, plugins, or features. The istnieje() keyword returns prawda if a name refers to anything (variable, function, class, or module), and falsz otherwise. Crucially, it does so without raising an error for undefined names:

niech x = 5
pokazl istnieje(x)            # prawda
pokazl istnieje(brak)         # falsz   (no error)

niech y = nic
pokazl istnieje(y)            # prawda  (y exists, even though it holds nic)

istnieje recognizes every kind of named entity — not just variables:

funkcja powitaj() {}
klasa Test {}
modul Pomocniki {}

pokazl istnieje(powitaj)      # prawda
pokazl istnieje(Test)         # prawda
pokazl istnieje(Pomocniki)    # prawda

It respects lexical scope — a variable declared in an inner block is not visible from outside:

jesli prawda {
    niech wewnetrzna = 1
}
pokazl istnieje(wewnetrzna)    # falsz

The argument must be a single identifier — not an expression, not an object field access, not an array element. To check whether an object has a key, compare the value to nic or use obj.ma_klucz(...).

Imports

A program can be split across multiple .as files. To use code from another file, import it with import("path"):

# math_utils.as
funkcja kwadrat(x) { zwroc x * x }
funkcja szescian(x) { zwroc x * x * x }
# main.as
import("./math_utils.as")

pokazl kwadrat(5)     # 25
pokazl szescian(3)    # 27

Paths starting with ./ or ../ are resolved relative to the importing file. Paths without a leading dot resolve to AlexScript’s standard library — so import("json") loads the JSON library, import("plik") loads the file I/O library, and so on.

The .as extension is optional in import paths. Both import("./math_utils") and import("./math_utils.as") work the same way.

Each file is loaded at most once per program. Importing the same file from multiple places is safe — the second import is a no-op.

Transitive imports

If file A imports B, and B imports C, then everything in C is reachable from A. Definitions propagate up the chain. This is what lets you organize code into utility files imported by domain files imported by your main entry point — you only import the level you need to, and the rest is pulled in automatically.

Sibling imports and module merging

If two separately-imported files both open the same module, AlexScript merges them into one. Suppose you split a Zubr module across two files:

# a.as
modul Zubr {
    klasa A { funkcja konstruktor() {} }
}
# b.as
modul Zubr {
    klasa B { funkcja konstruktor() {} }
}
# main.as
import("./a.as")
import("./b.as")

niech a = Zubr::A.nowy()
niech b = Zubr::B.nowy()

After both imports, Zubr contains both A and B. The full merging rules are covered under Reopening modules.

Import error reporting

When a runtime error occurs in an imported file, AlexScript shows the chain of imports that led there, with line numbers — so you don’t have to guess which import indirectly triggered which file. A typical error looks like:

BladNazwy: Niezadeklarowany identyfikator nieznana_zmienna
  import './c.as' (b.as:1)
  import './b.as' (a.as:1)

The original error class (BladNazwy here) is preserved — you don’t get a generic “import failed” wrapper that hides the real problem. If the file itself doesn’t exist, you get BladImportu with the same import-chain trace.

Object-Oriented Programming

AlexScript supports a complete object-oriented programming model: classes with state and behavior, single inheritance, abstract classes, static and private methods, and polymorphism through method overriding. The keyword vocabulary is small — klasa, nowy, sam, super, statyczna, prywatne, abstrakcyjna — and once you’ve seen them you’ll recognize most of what a class can do.

Defining a class

A class is declared with klasa. Inside the class body, you define methods using funkcja:

klasa Osoba {
    funkcja konstruktor(imie, wiek) {
        niech @imie = imie
        niech @wiek = wiek
    }

    funkcja przedstaw_sie() {
        pokazl "Cześć, jestem #{@imie} i mam #{@wiek} lat."
    }
}

Two things to notice:

The konstruktor is the special method name for the initializer. It runs automatically when a new instance is created.

Names that start with @ are instance variables. They belong to the instance, not the method that introduces them, and persist for the lifetime of the object.

Creating instances

Use Klasa.nowy(...) to create an instance. The arguments are passed to the constructor:

niech anna = Osoba.nowy("Anna", 30)
anna.przedstaw_sie()    # Cześć, jestem Anna i mam 30 lat.

You can store the instance in a variable, pass it around, put it in a collection — instances are first-class values like everything else.

Instance variables

Instance variables (@imie, @wiek, etc.) are private to the instance. They’re accessed and modified from inside the class with the @ prefix:

klasa Licznik {
    funkcja konstruktor() {
        niech @wartosc = 0
    }

    funkcja zwieksz() {
        @wartosc = @wartosc + 1
    }

    funkcja wartosc() {
        zwroc @wartosc
    }
}

niech c = Licznik.nowy()
c.zwieksz()
c.zwieksz()
c.zwieksz()
pokazl c.wartosc()    # 3

Inside a method, you can both declare an instance variable with niech @nazwa = ... (typically in the constructor) and reassign an existing one with @nazwa = .... The same scoping rule applies as for local variables: bare assignment modifies the existing instance variable, while niech @nazwa = ... would shadow it within that block.

Instance variables are not directly accessible from outside the class — c.@wartosc is not valid syntax. To expose state, define accessor methods (often named the same as the variable, like wartosc() above). Trying to use @something outside a method raises BladWykonania: Nie można użyć zmiennej instancji poza kontekstem instancji.

If you read an instance variable that has never been assigned, you get nic — there are no “undeclared instance variable” errors.

Methods

Inside a method body, you can call other methods on the same instance simply by name:

klasa Kalkulator {
    funkcja konstruktor() {
        niech @historia = []
    }

    funkcja dodaj(a, b) {
        niech wynik = a + b
        zapisz_wynik(wynik)     # call sibling method
        zwroc wynik
    }

    funkcja zapisz_wynik(w) {
        @historia << w
    }

    funkcja historia() {
        zwroc @historia
    }
}

niech k = Kalkulator.nowy()
k.dodaj(2, 3)
k.dodaj(10, 4)
pokazl k.historia()    # [5, 14]

sam — the self reference

The keyword sam refers to the current instance from inside any of its methods. It’s the AlexScript equivalent of self (Python, Ruby) or this (Java, JavaScript). In the simplest case you don’t need it — calls to other methods on the same object work without any prefix — but it becomes essential in three situations: method chaining, passing the instance to other code, and making intent explicit.

Calling sibling methods explicitly:

klasa Kalkulator {
    funkcja dodaj(a, b) {
        niech wynik = a + b
        sam.zapisz_wynik(wynik)    # explicit self-call
        zwroc wynik
    }

    funkcja zapisz_wynik(w) { @historia << w }
}

sam.zapisz_wynik(wynik) and zapisz_wynik(wynik) are equivalent here. The implicit form is more common; the explicit form is occasionally useful for clarity.

Passing the instance to another object:

klasa Visitor {
    funkcja odwiedz(element) {
        zwroc "Odwiedzono: " + element.klasa()
    }
}

klasa Element {
    funkcja akceptuj(visitor) {
        zwroc visitor.odwiedz(sam)    # passes the current instance
    }
}

niech e = Element.nowy()
niech v = Visitor.nowy()
pokazl e.akceptuj(v)    # Odwiedzono: Element

Polymorphic introspection: when called from inside a parent class, sam.klasa() returns the actual (subclass) name, not the parent’s:

klasa Zwierze {
    funkcja jaki_typ() {
        zwroc sam.klasa()
    }
}

klasa Pies < Zwierze {}

niech p = Pies.nowy()
pokazl p.jaki_typ()    # "Pies", not "Zwierze"

What sam is not: you cannot use it as a variable name (niech sam = 42 is a syntax error), you cannot assign to it (sam = ... is a syntax error), and it has no meaning outside a method body (pokazl sam at the top level raises Nie można użyć 'sam' poza kontekstem instancji).

Method chaining and the Builder pattern

A method that ends with zwroc sam returns the current instance, which lets the next call hook directly onto the result. Chains of such calls give you a fluent interface — the Builder pattern in classical OOP terminology:

klasa Builder {
    funkcja konstruktor() {
        niech @x = 0
        niech @y = 0
    }

    funkcja ustaw_x(v) {
        @x = v
        zwroc sam
    }

    funkcja ustaw_y(v) {
        @y = v
        zwroc sam
    }

    funkcja suma() {
        zwroc @x + @y
    }
}

niech wynik = Builder.nowy().ustaw_x(10).ustaw_y(20).suma()
pokazl wynik    # 30

This pattern works for any kind of staged construction — pizza orders, database queries, HTTP request builders, configuration objects. The convention to follow: methods that mutate state and return sam are typically used in chains; methods that compute a final value (here suma) end the chain by returning the actual result.

Inheritance

A class can inherit from another class using the < operator:

klasa Zwierze {
    funkcja konstruktor(nazwa) {
        niech @nazwa = nazwa
    }

    funkcja odglos() {
        zwroc "..."
    }

    funkcja przedstaw() {
        zwroc "Jestem #{@nazwa} i robię #{sam.odglos()}"
    }
}

klasa Pies < Zwierze {
    funkcja odglos() {
        zwroc "Hau hau!"
    }
}

klasa Kot < Zwierze {
    funkcja odglos() {
        zwroc "Miau!"
    }
}

niech p = Pies.nowy("Burek")
niech k = Kot.nowy("Mruczek")

pokazl p.przedstaw()    # Jestem Burek i robię Hau hau!
pokazl k.przedstaw()    # Jestem Mruczek i robię Miau!

Pies inherits the constructor and the przedstaw method from Zwierze, but overrides odglos. When przedstaw calls sam.odglos(), it uses the most-specific version available — this is polymorphism in action.

AlexScript supports single inheritance only — a class can have at most one parent. For sharing behavior across multiple classes, use modules with dolacz (covered in Modules).

The inheritance chain can be of any depth: Pies < Zwierze, then Owczarek < Pies, and so on.

super

To call a parent’s method from inside an override, use super. The most common case is the constructor:

klasa Pojazd {
    funkcja konstruktor(marka, predkosc) {
        niech @marka = marka
        niech @predkosc = predkosc
    }
}

klasa Samochod < Pojazd {
    funkcja konstruktor(marka, predkosc, kolor) {
        super(marka, predkosc)      # call Pojazd's constructor
        niech @kolor = kolor
    }
}

niech s = Samochod.nowy("Toyota", 200, "czerwony")

Without arguments, super() calls the parent’s version of the current method:

klasa Bazowa {
    funkcja opis() {
        zwroc "obiekt"
    }
}

klasa Pochodna < Bazowa {
    funkcja opis() {
        zwroc super() + " ze wstawka"
    }
}

pokazl Pochodna.nowy().opis()    # "obiekt ze wstawka"

You can also call a specific named method on the parent with super.metoda(...):

klasa Pochodna < Bazowa {
    funkcja zlozona() {
        zwroc super.opis() + " (zmodyfikowany)"
    }
}

super works correctly through chains of any depth. In a hierarchy A → B → C → D, calling super(arg) from D’s constructor invokes C’s, which can in turn call super(arg) to reach B’s, all the way up to A. Each level can transform arguments before passing them up:

klasa A {
    funkcja konstruktor(s) { niech @s = s }
    funkcja s() { zwroc @s }
}
klasa B < A {
    funkcja konstruktor(s) { super(s + "-B") }
}
klasa C < B {
    funkcja konstruktor(s) { super(s + "-C") }
}

pokazl C.nowy("X").s()    # "X-C-B"

Calling super outside an instance context, or super.method in a class with no parent, both raise clear runtime errors.

Static methods and variables

Sometimes a method or value belongs to the class itself, not to any particular instance. Mark these with statyczna:

klasa Matematyka {
    statyczna niech PI = 3.14159
    statyczna niech E = 2.71828

    statyczna funkcja kwadrat(x) {
        zwroc x * x
    }

    statyczna funkcja pierwiastek(x) {
        zwroc x ** 0.5
    }
}

pokazl Matematyka.PI                # 3.14159
pokazl Matematyka.kwadrat(7)        # 49
pokazl Matematyka.pierwiastek(81)   # 9.0

Static methods and variables are inherited by subclasses just like instance methods.

The statyczna keyword is only valid inside a class body. Using it at the top level (statyczna niech X = 10) raises BladSkladni: Słowo kluczowe 'statyczna' może być używane tylko w ciele klasy.

Private methods

To hide implementation-detail methods that callers shouldn’t depend on, mark them as private. Place the prywatne keyword on a line by itself; every method declared after it is private:

klasa Kalkulator {
    funkcja konstruktor() {
        niech @wynik = 0
    }

    funkcja dodaj(a, b) {
        zwroc oblicz_sume(a, b)        # OK — internal call
    }

    prywatne

    funkcja oblicz_sume(a, b) {
        zwroc a + b
    }
}

niech k = Kalkulator.nowy()
pokazl k.dodaj(3, 4)         # 7
# k.oblicz_sume(3, 4)        # BladMetody — Próba wywołania prywatnej metody oblicz_sume

Private methods can be called from within the class and its subclasses, but not from outside. A subclass calling an inherited private method works fine — privacy is about external access, not class-internal access.

Abstract classes

An abstract class is one that cannot be instantiated — it exists only to be subclassed. Use it when you want to define a common interface or share partial behavior without committing to a concrete object. Mark a class abstract with abstrakcyjna before klasa:

abstrakcyjna klasa Figura {
    funkcja konstruktor() {}

    funkcja pole() {
        rzuc BladMetody.nowy("pole() musi byc zaimplementowane w klasie pochodnej")
    }

    funkcja opis() {
        zwroc "Figura o polu #{sam.pole()}"
    }
}

klasa Kwadrat < Figura {
    funkcja konstruktor(bok) {
        super()
        niech @bok = bok
    }

    funkcja pole() {
        zwroc @bok * @bok
    }
}

# Figura.nowy()        # Nie można utworzyć instancji klasy abstrakcyjnej Figura
niech k = Kwadrat.nowy(5)
pokazl k.opis()        # Figura o polu 25

The “abstract” mark in AlexScript is a runtime check — abstrakcyjna blocks Klasa.nowy(), but doesn’t enforce that subclasses implement specific methods. The convention to model “must override” methods is to write a default version that raises an error (as pole() does in the example above).

Reflection

Every class and every instance responds to a set of introspection methods that let you examine the program’s structure at runtime. The most commonly used ones:

klasa Pojazd {
    funkcja konstruktor(marka) {
        niech @marka = marka
    }

    funkcja jedz() {}
}

klasa Samochod < Pojazd {
    funkcja zatankuj() {}
}

# On a class:
pokazl Samochod.nazwa()         # "Samochod"
pokazl Samochod.typ()           # "klasa"
pokazl Samochod.rodzic()        # "Pojazd"
pokazl Samochod.metody()        # ["zatankuj", ...]   includes inherited
pokazl Samochod.przodkowie()    # ["Pojazd"]
pokazl Samochod.ma_metode("jedz")    # prawda

# On an instance:
niech s = Samochod.nowy("Toyota")
pokazl s.typ()                  # "instancja"
pokazl s.klasa()                # "Samochod"
pokazl s.czy_instancja("Pojazd")     # prawda  (inheritance counts)
pokazl s.zmienne_instancji()         # ["marka"]
pokazl s.czy_odpowiada("jedz")       # prawda  (inherited method)
pokazl s.identyczny(s)               # prawda

Reflection is most useful in three practical situations: writing a generic serializer or formatter that doesn’t know about specific classes, building debugging tools, and writing test helpers that check whether a class implements a contract. The full list of reflection methods on classes (metody_prywatne, metody_statyczne, info_metody, potomkowie, moduly, etc.) and instances (wartosc_zmiennej_instancji, kopia, napis, debug_info, etc.) is large — call obj.metody() on a value to discover what’s available.

A note worth knowing: built-in introspection methods like id, typ, klasa, and napis can be overridden by your own methods of the same name. If your Uzytkownik class defines funkcja id() { zwroc @id }, that’s what u.id() calls — the built-in version is replaced. This is intentional: your class is in charge of what its methods mean.

Modules

A module is a named container for related code. Modules in AlexScript serve two purposes:

Namespaces — group related classes, functions, and constants under a common name to avoid collisions.

Mixins — bundle methods that can be added to a class without inheritance.

A module is declared with modul:

modul Matematyka {
    niech PI = 3.14159

    funkcja kwadrat(x) { zwroc x * x }

    funkcja szescian(x) { zwroc x * x * x }
}

Accessing module members

Use the :: operator to access constants, functions, and classes inside a module from outside:

pokazl Matematyka::PI              # 3.14159
pokazl Matematyka::kwadrat(5)      # 25
pokazl Matematyka::szescian(3)     # 27

Inside a module, members can refer to each other directly without the :: prefix:

modul Matematyka {
    niech PI = 3.14159

    funkcja pole_kola(r) {
        zwroc PI * r * r        # PI is in the same module
    }
}

pokazl Matematyka::pole_kola(5)    # 78.53975

Module constants

Module-level niech declarations must use uppercase names — modules are not allowed to hold lowercase variables (the rationale: a module is a namespace, not an object with mutable state). Use functions if you need computed values.

modul Konfiguracja {
    niech MAX_LICZBA_POLACZEN = 100      # OK
    niech NAZWA_APLIKACJI = "MyApp"      # OK

    # niech licznik = 0                  # BladSkladni
}

Just like top-level constants, module constants cannot be reassigned after declaration.

Classes inside modules

You can put classes inside modules — this is the standard way to organize a larger program:

modul Geometria {
    klasa Punkt {
        funkcja konstruktor(x, y) {
            niech @x = x
            niech @y = y
        }

        funkcja x() { zwroc @x }
        funkcja y() { zwroc @y }
    }

    klasa Kolo {
        funkcja konstruktor(srodek, promien) {
            niech @srodek = srodek
            niech @promien = promien
        }
    }
}

niech p = Geometria::Punkt.nowy(3, 4)
niech k = Geometria::Kolo.nowy(p, 10)

Inside the module body, classes can reference each other directly: Kolo’s constructor doesn’t need to write Geometria::Punkt, just Punkt.

Dot vs. double-colon for class members

For static methods and static variables on a class inside a module, both . and :: work — they’re equivalent:

modul M {
    klasa K {
        statyczna niech WERSJA = 7

        statyczna funkcja info() { zwroc "K v#{WERSJA}" }
    }
}

pokazl M::K.WERSJA           # 7
pokazl M::K::WERSJA          # 7  (same)
pokazl M::K.info()           # K v7
pokazl M::K::info()          # K v7  (same)

For instances, you always use .M::K.nowy().metoda() — because .nowy() already returns an instance and instance method dispatch is dot-only.

Nested modules

Modules can contain other modules, and you can nest them as deep as your application requires:

modul MojaApp {
    modul Modele {
        klasa Uzytkownik {}
        klasa Post {}
    }

    modul Kontrolery {
        klasa UzytkownicyController {}
    }

    modul Pomocniki {
        funkcja sformatuj_date(d) { zwroc d.napis() }
    }
}

niech u = MojaApp::Modele::Uzytkownik.nowy()

Use a long path like A::B::C only when the structure genuinely warrants it — most programs are fine with one or two levels of nesting.

Mixins with dolacz

The other use of modules is to share methods across unrelated classes. A class can dolacz (include) a module, which copies the module’s methods into the class:

modul Identyfikator {
    funkcja id() {
        zwroc @id
    }

    funkcja id_string() {
        zwroc "ID:#{@id}"
    }
}

klasa Uzytkownik {
    dolacz Identyfikator

    funkcja konstruktor(id, imie) {
        niech @id = id
        niech @imie = imie
    }
}

klasa Post {
    dolacz Identyfikator

    funkcja konstruktor(id, tresc) {
        niech @id = id
        niech @tresc = tresc
    }
}

niech u = Uzytkownik.nowy(1, "Anna")
pokazl u.id()              # 1
pokazl u.id_string()       # ID:1

niech p = Post.nowy(42, "Treść")
pokazl p.id()              # 42

Mixed-in methods run with full access to the instance: they can read and modify @instance_variables, and sam inside a mixin method refers to the including class’s instance, not the module. So sam.klasa() from a mixin returns the class that included the module:

modul Identyfikowalny {
    funkcja kim_jestem() {
        zwroc "Jestem instancją: #{sam.klasa()}"
    }
}

klasa Osoba {
    dolacz Identyfikowalny
    funkcja konstruktor() {}
}

pokazl Osoba.nowy().kim_jestem()    # Jestem instancją: Osoba

A class can dolacz multiple modules. When two included modules define the same method name, the last one included wins. Methods defined directly on the class always take precedence over included methods:

modul A {
    funkcja hello() { zwroc "from A" }
}

modul B {
    funkcja hello() { zwroc "from B" }
}

klasa K {
    dolacz A
    dolacz B    # B comes second — its hello wins
}

pokazl K.nowy().hello()    # "from B"

When to inherit, when to mix in

A useful rule of thumb: inherit when the new class is a kind of the parent class (a Samochod is a Pojazd). Mix in a module when the class gains a capability that’s independent of its identity (a Uzytkownik can be identified — but so can a Post, and they’re not in the same hierarchy).

Reopening modules

Modules in AlexScript are open: you can declare the same module in multiple places, and each declaration adds to the same shared module. This is the standard way to split a large module across multiple files:

# in geom_core.as
modul Geometria {
    niech PI = 3.14159
}

# in geom_shapes.as
import("./geom_core.as")

modul Geometria {
    klasa Kolo {
        funkcja konstruktor(r) { niech @r = r }
        funkcja pole() { zwroc PI * @r * @r }
    }
}

Both declarations contribute to one Geometria module. The same merging rules apply within a single file.

The merge policy depends on the kind of member:

modul Operacje {
    funkcja dodaj(a, b) { zwroc a + b }
}

modul Operacje {
    funkcja odejmij(a, b) { zwroc a - b }
}

pokazl Operacje::dodaj(5, 3)     # 8
pokazl Operacje::odejmij(5, 3)   # 2

Class reopening is particularly powerful — you can add methods to an existing class from a different file, and even existing instances will see the new methods (method dispatch is dynamic):

modul M {
    klasa C {
        funkcja konstruktor() {}
        funkcja stara() { zwroc "stara" }
    }
}

niech c = M::C.nowy()    # instance created BEFORE reopen

modul M {
    klasa C {
        funkcja nowa() { zwroc "nowa" }
    }
}

pokazl c.stara()    # "stara"
pokazl c.nowa()     # "nowa"  — visible on the existing instance

You can also cross-reference between reopen blocks. Code in the second block of a module can use constants, functions, or even mixin modules defined in the first block:

modul App {
    modul Helpers {
        funkcja powitanie() { zwroc "witaj" }
    }
}

modul App {
    klasa Controller {
        dolacz Helpers          # ← uses Helpers from the previous block

        funkcja konstruktor() {}
        funkcja akcja() { zwroc powitanie() }
    }
}

pokazl App::Controller.nowy().akcja()    # "witaj"

Modules as values

Modules are first-class values — you can assign them to variables and inspect them with reflection methods:

niech m = Matematyka

pokazl m.typ()          # "modul"
pokazl m.nazwa()        # "Matematyka"
pokazl m.stale()        # ["PI"]
pokazl m.funkcje()      # ["kwadrat", "szescian", "pole_kola"]
pokazl m.klasy()        # []
pokazl m.podmoduly()    # []

This is mostly useful for tooling and metaprogramming. Day-to-day code rarely needs it.

Exceptions

When something goes wrong — a missing key, a network failure, an invalid argument — AlexScript raises an exception. You can catch exceptions and recover, propagate them up the call stack, or define your own to model domain-specific errors.

The exception hierarchy

All exceptions descend from WyjatekPodstawowy. The built-in classes are:

WyjatekPodstawowy
├── BladWykonania              # generic runtime error
│   ├── BladTypu               # type-related error
│   ├── BladZakresu            # out-of-range / index error
│   ├── BladMetody             # missing or invalid method
│   ├── BladNazwy              # undeclared identifier
│   ├── BladArgumentu          # wrong argument count or kind
│   └── BladDzieleniaPrzezZero
├── BladSkladni                # syntax error
├── BladImportu                # import / module loading error
└── BladLimituCzasu            # timeout error

These names are available without any import — they live in the global namespace from program start.

Catching interpreter errors

Errors that originate inside the interpreter — division by zero, out-of-range array access, undefined names, calling a method that doesn’t exist — are translated to the appropriate AlexScript exception class before they reach your program. You catch them by their AlexScript name, exactly like exceptions you raise yourself:

proba {
    niech arr = [1, 2, 3]
    pokazl arr[10]
} zlap (e : BladZakresu) {
    pokazl "Indeks poza zakresem: #{e["wiadomosc"]}"
}

A short reference of common interpreter-generated errors and what triggers them:

Class Typical cause
BladDzieleniaPrzezZero 10 / 0
BladZakresu arr[100] when arr.dlg() < 100
BladNazwy reading a variable that wasn’t declared
BladMetody calling a method that doesn’t exist on the value, or calling a private one from outside
BladTypu applying an operator to incompatible types, e.g. "abc" - 5
BladArgumentu calling a function with the wrong number of arguments
BladWykonania catch-all, including condition-type errors and stack overflow

You don’t have to memorize this — when in doubt, catch the broader BladWykonania (or even WyjatekPodstawowy) and read e["wiadomosc"] to see exactly what happened.

Raising exceptions: rzuc

Use rzuc to raise an exception. Two forms are accepted:

# String shorthand — wrapped in BladWykonania automatically:
rzuc "Coś poszło nie tak"

# Explicit instantiation — when you want a specific type:
rzuc BladTypu.nowy("Oczekiwano liczby")
rzuc BladDzieleniaPrzezZero.nowy("Mianownik nie moze byc zerem")

The constructor message defaults to "Błąd" if you omit it.

Catching: proba / zlap / wkoncu

The catching construct has three keywords:

proba {
    niech wynik = ryzykowna_operacja()
    pokazl wynik
} zlap (e) {
    pokazl "Złapano: #{e["wiadomosc"]}"
}

Inside the catch block, the catch variable (here e) is bound to an exception object — described below.

Typed catches

You can narrow a catch clause to a specific exception class (and its subclasses) by following the variable with : and a type name:

proba {
    rzuc BladTypu.nowy("zly typ")
} zlap (e : BladTypu) {
    pokazl "Błąd typu: #{e["wiadomosc"]}"
}

Multiple zlap clauses are matched top-to-bottom — the first matching one wins. Put more specific types first, more general ones last:

proba {
    operacja()
} zlap (e : BladDzieleniaPrzezZero) {
    pokazl "Dzielenie przez zero!"
} zlap (e : BladTypu) {
    pokazl "Błąd typu: #{e["wiadomosc"]}"
} zlap (e) {
    pokazl "Inny błąd: #{e["wiadomosc"]}"
}

A zlap (e) without a type matches anything — it works as a catch-all and should be placed last.

If no clause matches, the exception keeps propagating up the call stack — exactly as if there had been no proba at all.

Module-qualified exception types

When you define your own exception classes inside a module, the catch clause can reference them by their fully qualified path:

modul MojaApp {
    klasa BladDanych < WyjatekPodstawowy {
        funkcja konstruktor(k) { super(k) }
    }
}

proba {
    rzuc MojaApp::BladDanych.nowy("zła wartość")
} zlap (e : MojaApp::BladDanych) {
    pokazl "Błąd danych: #{e["wiadomosc"]}"
}

The same subclass-matching rule applies — zlap (e : MojaApp::BladDanych) matches the type itself and any of its subclasses.

Cleanup with wkoncu

The wkoncu block runs after the protected block whether the protected block completed normally, was caught by a zlap, or raised something none of the zlap clauses matched. It’s the right place for cleanup that must always happen:

proba {
    otworz_plik()
    przetworz()
} zlap (e) {
    pokazl "Błąd: #{e["wiadomosc"]}"
} wkoncu {
    zamknij_plik()
}

wkoncu is also valid without any zlap clauses — when you want guaranteed cleanup but no special error handling at this level:

proba {
    pokazl "robie cos"
} wkoncu {
    pokazl "sprzatam"
}

The exception object

The catch variable is a hash with the following keys:

proba {
    rzuc BladTypu.nowy("zly typ")
} zlap (e) {
    pokazl e["wiadomosc"]    # "zly typ"
    pokazl e["typ"]          # type name as string
    pokazl e["klasa"]        # "BladTypu"
    pokazl e["linia"]        # line number where raised
    pokazl e["instancja"]    # the original exception instance
    pokazl e["stos"]         # call stack at the time of throw
}

e["wiadomosc"] is what you’ll use most often. e["stos"] is useful for diagnostics — it’s an array of strings, one per stack frame, with the most recent frame first. e["instancja"] gives you the original exception instance, so any methods you defined on your custom exception class are callable through it.

Defining your own exceptions

Custom exceptions are ordinary classes — the only requirement is that they descend (directly or transitively) from WyjatekPodstawowy or one of its subclasses. The simplest case: an empty body that inherits the default constructor:

klasa MojWyjatek < WyjatekPodstawowy {}

rzuc MojWyjatek.nowy("Coś się zepsuło")

Building a hierarchy works exactly like ordinary inheritance, and zlap (e : ...) matches any subclass of the listed type:

klasa BladAplikacji < WyjatekPodstawowy {}
klasa BladDanych < BladAplikacji {}
klasa BladBazyDanych < BladDanych {}
klasa BladSieci < BladAplikacji {}

proba {
    operacja()
} zlap (e : BladBazyDanych) {
    # very specific
} zlap (e : BladDanych) {
    # any data error other than DB
} zlap (e : BladAplikacji) {
    # any application error
}

Custom exceptions can have their own state and methods. Override the constructor to accept extra information:

klasa BladHTTP < WyjatekPodstawowy {
    funkcja konstruktor(wiadomosc, kod) {
        super(wiadomosc)
        niech @kod = kod
    }

    funkcja kod() {
        zwroc @kod
    }
}

proba {
    rzuc BladHTTP.nowy("Not Found", 404)
} zlap (e : BladHTTP) {
    pokazl e["wiadomosc"]              # "Not Found"
    pokazl e["instancja"].kod()        # 404
}

A common pattern is to define your application’s exception hierarchy inside a module — it groups related errors and gives you fully-qualified names that won’t collide with anyone else’s:

modul Posel {
    klasa BladPosla < WyjatekPodstawowy {
        funkcja konstruktor(k) { super(k) }
    }
    klasa BladHttp < BladPosla {
        funkcja konstruktor(k) { super(k) }
    }
    klasa BladNieZnaleziono < BladHttp {
        funkcja konstruktor(k) { super(k) }
    }
}

proba {
    rzuc Posel::BladNieZnaleziono.nowy("404!")
} zlap (e : Posel::BladHttp) {
    pokazl "Błąd HTTP: #{e["wiadomosc"]}"
}

Re-raising and wrapping

A zlap block may itself contain rzuc. This is how you re-raise (possibly after wrapping) or how you turn a low-level failure into a domain-level one:

proba {
    polacz_z_baza()
} zlap (e) {
    rzuc BladBazyDanych.nowy("Nie udalo sie polaczyc: #{e["wiadomosc"]}")
}

The new exception travels up to the next enclosing proba (or out of the program if none).

Nested proba blocks compose naturally — an inner catch can either handle, transform-and-rethrow, or let the exception propagate to an outer catch:

proba {
    pokazl "Zewnętrzny try"
    proba {
        rzuc "Wewnętrzny wyjątek"
    } zlap (e) {
        pokazl "Wewnętrzny catch: #{e["wiadomosc"]}"
        rzuc "Zewnętrzny wyjątek"      # re-raise
    }
} zlap (e) {
    pokazl "Zewnętrzny catch: #{e["wiadomosc"]}"
}

Asynchronous Programming

AlexScript supports cooperative asynchronous programming through a Polish-flavored variant of the async/await model familiar from JavaScript and Python. Async lets you express concurrent operations — many things happening at once — without the complexity of threads and shared memory.

The key idea: an async function can suspend its execution at well-defined points (waiting for a timer, an HTTP response, a database query) and let other work make progress on the same thread. When the awaited thing is ready, the function resumes where it left off.

Under the hood, AlexScript uses Ruby fibers and a cooperative reactor with a Fiber Scheduler. All async work runs on a single thread; concurrency comes from yielding at suspension points, not from parallelism. Importantly, the scheduler also covers blocking native I/O — sleep, socket reads, and similar operations cooperatively yield instead of blocking the whole reactor.

asynchroniczna and czekaj

An asynchronous function is declared with asynchroniczna before funkcja (or fn for an async lambda):

asynchroniczna funkcja pobierz_dane() {
    czekaj uspij(100)
    zwroc "gotowe"
}

Calling an async function does not execute the body to completion. Instead, it returns an Obietnica (a “promise”) — an object representing “a value that will be available later” — and schedules the body to run cooperatively.

The czekaj keyword suspends the current async function until a promise settles, then returns its value:

asynchroniczna funkcja main() {
    niech wynik = czekaj pobierz_dane()
    pokazl wynik    # gotowe
}

czekaj is only valid inside an async function. The parser enforces this statically: using czekaj at the top level, in a synchronous function, or in a synchronous fn lambda is a BladSkladni caught at parse time, before your program even starts. The error tells you exactly what to do — wrap the call in an async function and use uruchom to enter the async world from sync code.

asynchroniczna itself must be followed by funkcja or fn. Writing asynchroniczna niech x = 1 is a syntax error.

uruchom — entry point

Top-level (synchronous) code cannot use czekaj directly. To run async work from the top level, use uruchom:

asynchroniczna funkcja main() {
    czekaj uspij(500)
    zwroc "po pol sekundzie"
}

pokazl uruchom(main)    # po pol sekundzie

uruchom blocks the calling thread until the given async function (or promise) settles, then returns its value. It accepts three forms:

Passing anything else — a number, a string, a sync function — raises an error along the lines of uruchom oczekuje obietnicy lub funkcji asynchronicznej.

A program typically has exactly one top-level uruchom call — your async main function — and everything else flows from there.

uspij — cooperative delay

uspij(ms) returns a promise that fulfills after ms milliseconds. Combine it with czekaj to pause an async function without blocking the whole program:

asynchroniczna funkcja powolne_powitanie() {
    pokazl "raz"
    czekaj uspij(1000)
    pokazl "dwa"
    czekaj uspij(1000)
    pokazl "trzy"
}

uruchom(powolne_powitanie)

Unlike a blocking sleep, uspij lets other async tasks run during the delay. Many async functions can uspij simultaneously and they’ll all wake up independently. uspij(0) is also valid — it yields cooperatively without an actual delay, useful when you want to give other tasks a chance to run.

Calling uspij with a non-number argument raises uspij oczekuje liczby.

Running tasks in parallel: uruchom_rownolegle

To run multiple async tasks concurrently, use uruchom_rownolegle. It takes a zero-argument function and returns a promise for its result:

asynchroniczna funkcja pobierz(id) {
    czekaj uspij(100)        # simulate network call
    zwroc id * 2
}

asynchroniczna funkcja main() {
    niech a = uruchom_rownolegle(fn() { czekaj pobierz(1) })
    niech b = uruchom_rownolegle(fn() { czekaj pobierz(2) })
    niech c = uruchom_rownolegle(fn() { czekaj pobierz(3) })

    # All three pobierz calls run concurrently.
    pokazl czekaj a    # 2
    pokazl czekaj b    # 4
    pokazl czekaj c    # 6
}

uruchom(main)

Without uruchom_rownolegle, three sequential czekaj pobierz(...) calls would take ~300ms total. With it, they overlap — total wall time is ~100ms.

The order in which parallel fibers complete depends on their work — fibers interleave at czekaj boundaries, so a faster one can finish before a slower one even though both started together:

asynchroniczna funkcja wolny() {
    czekaj uspij(60)
    pokazl "wolny"
}
asynchroniczna funkcja szybki() {
    czekaj uspij(20)
    pokazl "szybki"
}

asynchroniczna funkcja main() {
    niech k = uruchom_rownolegle(fn() { czekaj wolny() })
    niech s = uruchom_rownolegle(fn() { czekaj szybki() })
    czekaj k
    czekaj s
}

uruchom(main)
# Output:
# szybki
# wolny

A subtle point worth remembering: a section of synchronous code inside an async function runs without yielding — yielding only happens at czekaj. So three pokazl calls in a row will print in order; only czekaj gives other fibers a chance to interleave.

The Obietnica class

An Obietnica is a first-class value representing the eventual result of an async operation. It has three possible states:

A promise settles at most once. Once fulfilled or rejected, its state and value don’t change.

You can query a promise’s state directly without awaiting it:

asynchroniczna funkcja main() {
    niech p = pobierz_dane()       # promise, still pending
    pokazl p.stan()                 # "oczekuje"
    czekaj p
    pokazl p.stan()                 # "spelniona"
    pokazl p.wartosc()              # the fulfilled value
}

The instance methods on Obietnica:

Creating promises directly

For unit tests, simple async helpers, or when bridging to non-promise APIs, you can create promises in already-settled state:

niech ok = Obietnica.spelniona(42)
niech zle = Obietnica.odrzucona("blad")

pokazl ok.stan()      # "spelniona"
pokazl ok.wartosc()   # 42
pokazl zle.stan()     # "odrzucona"
pokazl zle.powod()    # "blad"

Awaiting a fulfilled promise returns its value immediately, without yielding. Awaiting a rejected promise raises the rejection reason as an exception, which you can catch with proba/zlap like any other error:

asynchroniczna funkcja main() {
    proba {
        czekaj Obietnica.odrzucona("problem")
    } zlap (e) {
        pokazl "zlapane: #{e["wiadomosc"]}"
    }
}

uruchom(main)

A handy bit of sugar: czekaj on a non-promise value returns that value unchanged. czekaj 42 is just 42. This means you don’t need to special-case “is this a promise or a plain value” in your own helpers — czekaj does the right thing either way.

The executor pattern: Obietnica.nowy

For more advanced cases — wrapping a callback-based API, deferring resolution to another fiber — use the executor form. Obietnica.nowy accepts a function that receives two callbacks: spelnij (resolve) and odrzuc (reject). The executor runs synchronously; calling either callback settles the promise:

asynchroniczna funkcja main() {
    niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
        spelnij(42)
    })
    zwroc czekaj p
}

pokazl uruchom(main)    # 42

The executor can defer resolution by spawning a parallel fiber:

asynchroniczna funkcja main() {
    niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
        uruchom_rownolegle(fn() {
            czekaj uspij(20)
            spelnij("po opóźnieniu")
        })
    })
    zwroc czekaj p
}

pokazl uruchom(main)    # po opóźnieniu

Two important behaviors of the executor pattern:

A promise settles at most once. After the first call to spelnij or odrzuc, subsequent calls are silently ignored:

niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
    spelnij("pierwszy")
    spelnij("drugi")        # no-op
    odrzuc("ignored")        # no-op
})
pokazl czekaj p    # "pierwszy"

Exceptions thrown from the executor body become rejections automatically — you don’t have to catch and call odrzuc yourself:

niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
    rzuc BladWykonania.nowy("wybuch w executorze")
})

proba {
    czekaj p
} zlap (e) {
    pokazl "zlapane: #{e["wiadomosc"]}"
}

Promise combinators

For coordinating multiple promises, the Obietnica class exposes three static combinators familiar from JavaScript’s Promise API.

Obietnica.wszystkie

Wait for all promises to fulfill, return their values in original order. Rejects fast if any input rejects.

asynchroniczna funkcja a() { czekaj uspij(30); zwroc "a" }
asynchroniczna funkcja b() { czekaj uspij(10); zwroc "b" }
asynchroniczna funkcja c() { czekaj uspij(20); zwroc "c" }

asynchroniczna funkcja main() {
    niech wyniki = czekaj Obietnica.wszystkie([a(), b(), c()])
    zwroc wyniki    # ["a", "b", "c"] — order preserved despite different timings
}

pokazl uruchom(main)

Empty array fulfills immediately with []. Non-promise values in the array are treated as already-fulfilled, so you can mix promises with literals:

niech wyniki = czekaj Obietnica.wszystkie([a(), "surowy", 42])
# ["a", "surowy", 42]

Obietnica.dowolna

Wait for the first promise to fulfill, return its value. The slower ones keep running but their results are discarded.

asynchroniczna funkcja wolny() {
    czekaj uspij(100)
    zwroc "wolny"
}
asynchroniczna funkcja szybki() {
    czekaj uspij(20)
    zwroc "szybki"
}

asynchroniczna funkcja main() {
    zwroc czekaj Obietnica.dowolna([wolny(), szybki()])
}

pokazl uruchom(main)    # "szybki"

Useful for race conditions like “fastest of multiple data sources” or “first-to-respond fallback”. If every promise in the array rejects, dowolna rejects too.

Obietnica.limit_czasu

Wrap a promise with a timeout. Returns a promise that fulfills with the original value if it settles in time, or rejects with a timeout error otherwise.

asynchroniczna funkcja powolne_zapytanie() {
    czekaj uspij(500)
    zwroc "wynik"
}

asynchroniczna funkcja main() {
    proba {
        niech wynik = czekaj Obietnica.limit_czasu(powolne_zapytanie(), 100)
        pokazl wynik
    } zlap (e) {
        pokazl "Przekroczony limit czasu"
    }
}

uruchom(main)

If the wrapped promise rejects on its own (faster than the timeout), the original rejection propagates — you don’t lose the error.

Error handling in async

A promise that ends in an exception is rejected. When you czekaj a rejected promise, the rejection is re-raised as an ordinary exception — caught with the same proba/zlap you’d use for synchronous code:

asynchroniczna funkcja moze_sie_zepsuc() {
    czekaj uspij(50)
    rzuc BladWykonania.nowy("cos poszlo nie tak")
}

asynchroniczna funkcja main() {
    proba {
        czekaj moze_sie_zepsuc()
    } zlap (e) {
        pokazl "zlapane: #{e["wiadomosc"]}"
    }
}

uruchom(main)

This is the reason async code reads almost identically to sync code — the only difference is the asynchroniczna/czekaj keywords. Control flow, error handling, and data manipulation all work the same way.

If an async function throws and nothing inside it catches the exception, the promise it returned ends in the rejected state. Awaiting it later re-raises the exception at the await point.

Unhandled rejections

If a promise is rejected and never awaited, AlexScript prints a warning to stderr at the end of the program. This catches a common bug — forgetting to czekaj a parallel task whose error you’d want to know about:

asynchroniczna funkcja main() {
    uruchom_rownolegle(fn() {
        rzuc BladWykonania.nowy("ignored error")
    })
    czekaj uspij(200)
    zwroc "done"
}

uruchom(main)
# stdout:  done
# stderr:  nieobsluzone odrzucenie: ignored error

Once you czekaj the promise (even just to observe it), the rejection is considered handled — even if you immediately catch and discard it. Inspecting .stan() does not count as handling; only czekaj does.

Async methods on classes

You can declare async methods inside a class:

klasa Klient {
    funkcja konstruktor(adres) {
        niech @adres = adres
    }

    asynchroniczna funkcja pobierz() {
        czekaj uspij(100)
        zwroc "dane z #{@adres}"
    }
}

asynchroniczna funkcja main() {
    niech k = Klient.nowy("api.example.com")
    pokazl czekaj k.pobierz()
}

uruchom(main)

sam works inside async methods just as in regular methods. Multiple async calls on the same instance share the instance’s state — instance variables persist across czekaj boundaries within the same instance:

klasa Akumulator {
    funkcja konstruktor() { niech @suma = 0 }

    asynchroniczna funkcja dodaj(x) {
        czekaj uspij(5)
        @suma = @suma + x
        zwroc @suma
    }
}

asynchroniczna funkcja main() {
    niech a = Akumulator.nowy()
    czekaj a.dodaj(10)
    czekaj a.dodaj(20)
    zwroc czekaj a.dodaj(30)
}

pokazl uruchom(main)    # 60

Async with native I/O

The cooperative scheduler covers blocking native operations — sleep, socket reads, file I/O. This means you can write what looks like ordinary blocking code, run multiple instances of it in parallel fibers, and they’ll cooperatively interleave instead of serializing:

import("socket")

asynchroniczna funkcja pobierz(port) {
    niech s = SocketTcp.nowy("127.0.0.1", port)
    niech dane = s.czytaj_linie()
    s.zamknij()
    zwroc dane
}

asynchroniczna funkcja main() {
    niech a = uruchom_rownolegle(fn() { czekaj pobierz(8080) })
    niech b = uruchom_rownolegle(fn() { czekaj pobierz(8080) })
    pokazl czekaj a
    pokazl czekaj b
}

uruchom(main)

Both czytaj_linie() calls block the fiber that issued them, but the scheduler keeps the reactor turning — so the second fiber can make progress while the first waits. This is what makes AlexScript usable for network servers and clients without threads or callbacks.

When to use async

Async is the right tool when you have I/O-bound work that would otherwise block — network requests, file operations, timers, waiting for input. Many independent I/O operations can run concurrently in the time one of them would take in sync code.

Async is not the right tool for CPU-bound work. Because all async tasks share one thread, a tight numeric loop will block every other async operation until it finishes. For CPU-heavy parallelism, you’d reach for actual threads — which AlexScript currently doesn’t expose to user code.

Mix async sparingly with sync code in the same program. The clearest pattern is: keep most code synchronous, and put async functions at the boundary where I/O happens. Have one top-level uruchom(main) that owns the whole async lifecycle.

Regular Expressions

For pattern matching on strings, AlexScript provides the Wyrazenie class — first-class regular expression objects. Construct a pattern once with Wyrazenie.nowy(wzor), then reuse it for matching, replacement, and splitting.

niech wz = Wyrazenie.nowy("[0-9]+")

pokazl wz.pasuje("rok 2026")              # prawda  (substring match)
pokazl wz.dopasuj("rok 2026").tekst()     # "2026"
pokazl wz.skanuj("ma 3 koty i 2 psy")     # ["3", "2"]
pokazl wz.zamien_wszystkie("a1 b2 c3", "#")    # "a# b# c#"

Flags can be passed as an optional second argument — "i" for case-insensitive, "m" for multiline, "x" for extended (whitespace-insensitive) mode:

niech wz = Wyrazenie.nowy("hello", "i")
pokazl wz.pasuje("HELLO")    # prawda

The dopasuj method returns a Dopasowanie object (or nic if no match) with information about the match:

niech wz = Wyrazenie.nowy("(\\w+)=(\\w+)")
niech d = wz.dopasuj("name=alex")

pokazl d.tekst()      # "name=alex"
pokazl d.grupa(1)     # "name"
pokazl d.grupa(2)     # "alex"
pokazl d.indeks()     # 0

Named capture groups are supported with (?<nazwa>...):

niech wz = Wyrazenie.nowy("(?<klucz>\\w+)=(?<wartosc>\\w+)")
niech d = wz.dopasuj("name=alex")
pokazl d.nazwana("klucz")     # "name"
pokazl d.nazwana("wartosc")   # "alex"

A Dopasowanie also exposes przed() and po() for the text before and after the match, grupy() for all capture groups as an array, nazwane() for all named captures as an object, and indeks_konca() for the end position.

When you build a pattern from user input or any untrusted source, escape it first to neutralize regex metacharacters:

niech termin = wczytaj("Wyszukaj: ")
niech wz = Wyrazenie.nowy(Wyrazenie.escapuj(termin), "i")

For convenience, three of the most common operations are also available directly on strings:

"123".pasuje(Wyrazenie.nowy("^[0-9]+$"))                     # prawda
"the 2026 year".dopasuj(Wyrazenie.nowy("[0-9]+")).tekst()    # "2026"
"a, b, c".rozdziel(Wyrazenie.nowy(",\\s*"))                  # ["a", "b", "c"]

For repeated matching, always construct the pattern once outside the loop. Patterns are compiled at construction time, and re-compiling on every iteration is a quiet performance trap.

Debugger

AlexScript ships with a built-in interactive debugger inspired by Ruby’s byebug. It lets you pause execution at any point, inspect and modify variables, step through code line by line, set breakpoints, watch values, and evaluate expressions in real time.

Activating the debugger

Place a debug() call anywhere in your code. When execution reaches it, the program pauses and drops into an interactive debugger REPL:

niech x = 10
niech y = 20
debug()
niech z = x + y
pokazl z

When the program runs, you’ll see something like:

⏺  debug() w test.as:3
     1 | niech x = 10
     2 | niech y = 20
  => 3 | debug()
     4 | niech z = x + y
     5 | pokazl z
debug>

The => marker indicates the current line. You’re now in the debugger and can issue commands.

You can also call debug() conditionally — jesli problem to debug() is a useful pattern for stopping only when something interesting happens.

When no debug() has been encountered, the debugger has zero performance impact on your program.

Execution control

Once paused, you control how execution resumes:

Inspecting state

Type any AlexScript expression at the prompt and it’s evaluated in the current scope:

debug> x
  => 10
debug> x + y
  => 30
debug> arr.dlg()
  => 5

To list all variables in the current scope grouped by category, use zmienne (or z):

debug> zmienne
  [Zmienne lokalne]
    x = 10
    y = 20
  [Zmienne instancji]  (Kalkulator)
    @wynik = 30

zmienne wszystkie walks the entire scope chain (current → parent → global) and shows variables at each level.

To see the call stack, use stos (or s):

debug> stos
  [Stos wywołań]
    0: Kalkulator#dodaj (kalkulator.as:15)
    1: funkcja oblicz (main.as:8)
    2: funkcja start (main.as:3)

To see the source code around the current line, use kod.

Modifying variables

You can modify existing variables right from the debugger:

debug> x
  => 10
debug> x = 42
debug> x
  => 42

The program continues with the modified value. (You can’t declare new variables with niech from the debugger — only modify existing ones.)

Breakpoints, watchpoints, logpoints

The debugger supports several kinds of breakpoint:

List active breakpoints with punkty. Remove them with usun N, usun_metode N, usun_sledz N, or usun_loguj N.

These features make the debugger more than a step-through tool — it’s effectively a runtime exploration environment for figuring out what your program is actually doing.

Standard Library Overview

AlexScript’s standard library is intentionally small but practical. Each library is loaded with import("name") (no path prefix). The libraries are:

A small example combining a few of them — read a JSON file, transform the data, write it back:

import("json")
import("plik")

niech dane = Json.parsuj_plik("./osoby.json")
niech dorosli = dane.filtruj(fn(o) { o["wiek"] >= 18 })
Json.generuj_plik("./dorosli.json", dorosli, prawda)

pokazl "Zapisano #{dorosli.dlg()} rekordow."

Each library has its own focused documentation — this overview is just a map of what’s available.

Where to Go from Here

You now have the full picture: variables and types, control flow, functions and closures, classes and modules, exceptions, async, regular expressions, and the debugger. The best way to internalize all of it is to build something — a small command-line tool, a JSON-driven script, a tiny HTTP server, a calculator, a parser. AlexScript’s standard library has enough for all of those.

A few suggestions for going deeper:

Use the REPL aggressively. Try the language interactively before committing things to a file — most language features can be explored in a single session, and the _ variable for the last result makes experimentation fast.

Read other people’s .as code. Patterns like the Zubr HTTP server (a router, parser, middleware stack, and connection handler all written in AlexScript) show how the language scales to non-trivial systems.

Use the debugger when you’re stuck. Stepping through code line by line is often the fastest way to understand what’s actually happening, and AlexScript’s debug() integration is a one-liner away.

When you find a behavior surprising, write a tiny test for it — a one-file .as program that demonstrates the case. Over time, this becomes your personal reference of “things I’ve learned the hard way”.

Welcome to AlexScript. Have fun.