Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Язык Glyph — справочник

Статус: v2.1.0. Документ описывает реально работающий синтаксис (проверено на examples/ и тестах). Незадокументированные конструкции не считаются частью языка.

1. Структура файла

Файл — последовательность объявлений верхнего уровня:

@module examples.demo        // имя модуля верхнего уровня
@use other.module;            // импорт другого модуля

@const SCALE: Float64 = 1.5;

@struct Point { x: Float64, y: Float64 }

@enum Op { Plus, Minus, MulDiv(Float64, Float64) }

@fn twice(x: Int64) -> Int64 { return x * 2; }

@test @fn test_twice() -> Void { assert_eq(twice(2), 4, "2 * 2 == 4"); }

Атрибуты (пишутся через @):

АтрибутПрименяется кНазначение
@moduleфайлимя модуля
@useфайлимпорт модуля
@fnфункцияобъявление функции
@structструктураобъявление структуры
@enumперечислениеобъявление перечисления
@constконстантаименованная константа
@pubfn/struct/enumпубличная видимость (для импортируемых модулей)
@testfnтестовая функция (см. docs/testing.md)

Комментарии: // строка, /* блок */.

2. Типы

ТипC-представлениеПримечание
Int64int64_tцелые
UInt64uint64_tбеззнаковые
Float64doubleчисла с плавающей точкой
Boolinttrue/false
Stringchar*строки
Byteschar*сырые байты
List<T>GlyphList (refcounted fat-struct)литерал [..], диапазон .., .len(), .append(), .free(), срезы, for x in xs, ++, == для POD
Option<T>GlyphBox* (в C — void*)коробка {tag, data}; Some(x)/None
Result<T, E>GlyphBox* (в C — void*)коробка {tag, data}; Ok(x)/Err(e)
&TT*ссылка на значение (для перечислений в match)
MyStruct, MyEnumstruct/tagged unionпользовательские типы

Result / Option с данными

Result<Ok, E> и Option<T> строятся вариантами и сопоставляются в match. Payload копируется в кучу (коробка { tag, data }); тег 1 — Ok/Some, 0 — Err/None:

@fn divide(a: Int64, b: Int64) -> Result<Int64, String> {
    if b == 0 {
        return Result::Err("division by zero");
    }
    return Result::Ok(a / b);
}

@fn main() {
    let q: Int64 = match divide(10, 2) {
        | Ok(v) => v,
        | Err(e) => -1
    };
    print_int(q);

    let parsed: Option<Float64> = parse_float("2.5");
    let f: Float64 = match parsed {
        | Some(v) => v,
        | None => 0.0
    };
    print_float(f);
}

Замечания:

  • Option::None/Result::Ok(x) без контекста дают неизвестный параметр (Type::Void); в let он унифицируется с объявленным типом (let n: Option<Int64> = Option::None)
  • parse_int/parse_float/to_int/to_float возвращают коробку Option (Some/None) и пригодны для match

Обобщённые функции

Параметры типа объявляются в угловых скобках после имени функции и могут использоваться в сигнатуре и теле, в том числе вложенно (List<T>, Option<T>, Result<T, E>). Компилятор мономорфизирует каждую инстанциацию в отдельную C-функцию (identity_i64, first_str_i64):

@fn identity<T>(x: T) -> T {
    return x;
}

@fn first<T, K>(a: T, b: K) -> T {
    return a;
}

@fn main() {
    let a: Int64 = identity(5);          // T = Int64 из аргумента
    let s: String = first("hi", 42);     // T = String, K = Int64
    print_int(a);
}

Вывод типов:

  • по типам аргументов вызова (включая вложенные: List<Int64> даёт T = Int64);
  • из аннотации let для параметров, встречающихся только в возвращаемом типе (let r: Result<String, Int64> = ok_wrap("fine") выводит E = Int64).

Ограничения v2.1: обобщаются только функции (структуры/enum/impl — конкретные); явные аргументы типов (f::<Int64>) не поддерживаются; вызов generic-функции внутри generic-тела требует конкретных типов (иначе — ошибка Cannot infer type arguments); тип [T; N] описан, но литерал всегда порождает List<T>, поэтому массивы неконструируемы.

Литералы:

42         // Int64
498_351_8   // разделители разрядов
3.14       // Float64
true false // Bool
"hello"    // String
0xFF       // hex

3. Переменные и константы

let x: Int64 = 42;            // неизменяемая
let mut acc: Float64 = 0.0;   // изменяемая
let y = 42;                   // untyped let: тип выводится (Int64, String, List<T>, ...)
let r = 0..5;                 // List<Int64]
let s = "a" ++ "b";           // String

x = 10;        // ❌ ошибка типов: x неизменяемая
acc = acc + 1; // ✅

@const PI: Float64 = 3.141592653589793;
@const MAX_RETRIES: Int64 = 3;

Untyped let выводит тип из значения (литералы, List, Range, конкатенация, вызовы). Неизвестный тип — ошибка кодгена (cannot infer type of untyped let), а не мусор в C.

Константы компилируются в static const-выражения и могут ссылаться на другие константы и литералы.

4. Функции

@fn add(a: Int64, b: Int64) -> Int64 { return a + b; }

@fn log_message(msg: String) -> Void { println(msg); }

@fn default() -> Int64 { 0 }   // тело-выражение
@fn make() -> Float64 { return 2.0; }
  • Параметры типизированы; тип возврата указывается через ->, Void — без значения.
  • Аргументы передаются по значению; доступны ссылки &T.
  • Неквалифицированные вызовы внутри собственного модуля работают как есть.
  • @fn async (и @fn async методы в @impl) возвращают ленивый хендл — см. раздел «Конкурентность» ниже. Async-generic функции не поддерживаются.

5. Операторы

Арифметика: + - * / %.

Сравнения: == != < > <= >=. Логика: && || !.

Конкатенация строк: ++ — "Hello, " ++ "World!". При конкатенации с числами используйте конвертацию: "x=" ++ int_to_string(x).

Приведение типов as: let f: Float64 = 42 as Float64;.

Ссылка &: let r: &Shape = &circle; (используется для match по значению без копирования).

6. Управляющий поток

if / else

if x > 0 {
    print("positive");
} else if x < 0 {
    print("negative");
} else {
    print("zero");
}

if is also an expression: when both branches evaluate to a value of the same scalar type (Int64, UInt64, Float64, Bool), the whole if/else has that type and can be assigned. An integer literal branch promotes to a sibling’s Float64/UInt64 type. Heap-typed branches (String, List<T>, structs) are not supported as if expression values.

let hi: Int64 = if coins > 100 { 2; } else { 1; };
let scale: Float64 = if fast { 1.5; } else { 1; };

while

let i: Int64 = 0;
while i < 5 {
    i = i + 1;
}

loop / break / continue

let i: Int64 = 0;
loop {
    i = i + 1;
    if i >= 100 { break; }
}

for .. in (целочисленный диапазон)

let sum: Int64 = 0;
for i in 0..5 {        // 0..5 — не включая 5 → сумма 10
    sum = sum + i;
}
for j in 1..=5 {       // 1..=5 — включительно
    sum = sum + j;
}

match

match value {
    | 1 => print("one")
    | 2 => print("two")
    | _ => print("other")
}

match status {
    | HttpStatus::Ok => "OK"
    | HttpStatus::NotFound => "Not Found"
    | _ => "Unknown"
}

match shape {
    | Shape::Circle(r) => 3.14159 * r * r
    | Shape::Rectangle(w, h) => w * h
    | Shape::Point => 0.0
}

Паттерны: литералы, Enum::Variant, Enum::Variant(bindings), _ (wildcard). Body arms — выражения (в т.ч. вызовы, завершившиеся ;).

guard

@fn divide(a: Float64, b: Float64) -> DivisionResult {
    #guard(b != 0.0) else {
        return DivisionResult::DivisionByZero;
    };
    return DivisionResult::Success(a / b);
}

#guard(условие) else { ... }; — если условие ложно, выполняется блок else и текущая функция возвращается; после else обязательна ;.

7. Строки и списки

let greeting: String = "Hello, " ++ "Glyph!";
let n: Int64 = len(greeting);                // 13
let sub: String = substring(greeting, 0, 5); // "Hello"
let has: Bool = contains(greeting, "Glyph"); // true
let upper: String = to_upper(greeting);      // "HELLO, GLYPH!"
let words: String = split("a,b,c", ",");     // CSV-фрагмент
let num: Option<Int64> = parse_int("42");    // Option<Int64>
let s: String = int_to_string(42);

let arr: List<Int64> = [10, 20, 30];          // литерал списка
print_int(arr[1]);                            // 20 — индексация
let points: List<P> = [P { x: 1.0, y: 2.0 }];
print_float(points[0].y);                     // 2.0 — индексация структур
let range: List<Int64> = 0..10;   // [0, 1, ..., 9]
print_int(range.len());           // 10 — длина
range.append(10);                 // рост буфера
let both: List<Int64> = arr ++ range;  // конкатенация списков
for x in arr {                    // итерация по списку
    print_int(x);
}

Списки — fat-struct {data, len, elem_size, refs} поверх heap-буфера: литералы и диапазоны выделяют память, append растягивает буфер, индексация arr[i] возвращает элемент T. Копирование списка (let b = a, передача в функцию) считается ref-count’ом: буфер жив, пока жива хотя бы одна копия, поэтому a.free(); b[0] безопасно. append при нескольких владельцах делает copy-on-write (отдельный буфер для этого владельца; append внутри функции не виден снаружи). Тип элемента литерала выводится по первому элементу. let без аннотации тоже выводит тип (let xs = [1, 2] даёт List<Int64>).

Срезы xs[a..b] / xs[a..=b] возвращают новый List<T> (копия диапазона, границы клампятся). Списки POD-скаляров (Int64, UInt64, Float64, Bool) сравниваются по значению (==/!= через len + memcmp); остальные — громкая ошибка кодгена. xs.free() отпускает один ref и обнуляет список (буфер остаётся, пока есть другие копии).

7.1. Map

Map<String, V> — chained-хэшмап (FNV-1a, ключ всегда String), значения копируются по elem_size(V). Литерал #{ "key": value, ... } строит map; тип значения выводится по первой паре (пустой — Map<String, Void>).

@fn main() -> Void {
    let m: Map<String, Int64> = #{ "alice": 90, "bob": 75 };
    print_int(m["alice"]);          // чтение; отсутствующий ключ — abort
    m["carol"] = 60;                // запись через индекс
    m.put("bob", 80);               // запись методом
    let got: Option<Int64> = m.get("dave");   // Option вместо abort
    let v: Int64 = match got {
        | Some(x) => x
        | None => 0
    };
    drop(got);                      // Option-бокс требует drop(box)
    print_int(m.len());

    for k in m {                    // итерация даёт ключи (String)
        print(k);
    }

    m.free();                       // освободить хранилище
}

Внимание: m["k"] по отсутствующему ключу аварийно завершает программу — для проверяемого доступа используйте m.get(k) (возвращает Option<V>; Some-бокс освобождается через drop(box)). Итерация for k in m проходит по bucket-цепям (порядок = хэшу, не порядку вставки) и выдаёт ключи; значения читаются через m[k].

7.2. Статическая защита от use-after-free

Компилятор отслеживает в потоке операторов вызовы v.free() для List и Map и отклоняет последующее обращение к v до переприсваивания:

let xs: List<Int64> = [1, 2, 3];
xs.free();
xs.append(4);        // ОШИБКА: use after free: 'xs'
xs.free();           // ОШИБКА: double free: 'xs'

Легальный путь — переприсвоить переменную после free() (слот «оживает»):

xs.free();
xs = [4, 5];         // переприсваивание сбрасывает флаг
xs.append(6);

Ограничения анализатора: он statement-flow и пофункциональный — не отслеживает алиасы (let y = xs; xs.free(); y.len() не ловится), а free() внутри ветки match трактуется как выполненный и после match. Покрываются чтение, индексация, итерация, передача в аргументы и повторный .free().

8. Структуры и методы

@struct Point {
    x: Float64,
    y: Float64,
}

let p: Point = Point { x: 0.0, y: 0.0 };
let dx: Float64 = p2.x - p1.x;

Методы через @impl

@impl <Type> { @fn ... } объявляет методы типа. Ресивер передаётся первым параметром, вызов obj.method(a, b) превращается в Type_method(obj, a, b).

@impl Point {
    @fn norm(p: Point) -> Float64 {
        return sqrt(p.x * p.x + p.y * p.y);
    }
    @fn scaled(p: Point, k: Float64) -> Point {
        return Point { x: p.x * k, y: p.y * k };
    }
}

let p: Point = Point { x: 3.0, y: 4.0 };
let n: Float64 = p.norm();          // → Point_norm(p)
let q: Point = p.scaled(2.0);       // → Point_scaled(p, 2.0)

Правила:

  • первый параметр метода должен иметь тип (или &-ссылку на тип) Type — тот же, что и у объекта вызова;
  • остальные параметры — обычные аргументы; args.len() == params.len() - 1;
  • тело метода обращается к полям ресивера через параметр (p.x), без self;
  • методы других модулей не префиксуются (§10), имена методов — Type_method; методы нельзя импортировать как функции.

9. Перечисления

@enum Color { Red, Green, Blue }              // unit-варианты

@enum Shape {
    Circle(Float64),                          // вариант с данными
    Rectangle(Float64, Float64),
    Point,
}

@enum HttpStatus {                            // дискриминанты
    Ok = 200,
    NotFound = 404,
}

Конструкция: Shape::Circle(5.0), HttpStatus::Ok, Color::Red.

Сопоставление:

match s {
    | Shape::Circle(r) => 3.14159 * r * r
    | Shape::Rectangle(w, h) => w * h
    | Shape::Point => 0.0
}

Правило разбора ::: отрезот патh с заглавной буквы — конструктор перечисления (Shape::Circle(...)); со строчной — квалифицированный вызов функции модуля (math::add(...)).

10. Модули

// math.glyph  —— модуль
@module my.math
@pub @fn add(a: Int64, b: Int64) -> Int64 { return a + b; }

// main.glyph —— entry
@use my.math;
@fn main() -> Void {
    let sum: Int64 = my.math::add(1, 2);
}
  • Entry-файл (тот, что передаётся в glyphc run/compile) не префиксуется.
  • Имена импортированных модулей префиксуются (add → my_math_add); ссылки на собственные функции внутри модуля переписываются автоматически.
  • @pub — публичность; по умолчанию символы приватные, но в v1.0 маркировка не является жёстким ограничением.

11. Тесты и ассерты

См. docs/testing.md.

12. Конкурентность

Вызов @fn async не выполняет тело, а возвращает ленивый хендл Async<T>. spawn h; ставит задачу в общую FIFO-очередь пула воркеров (идемпотентно); пул — это M:N (M задач на N потоках), N = число ядер, не больше 8, задаётся через переменную окружения GLYPH_WORKERS=K. h await ждёт результат: выполняет синхронно, если хендл ещё не запущен; иначе ждёт завершения на condvar. Ждущий поток «помогает» пулу — пока цель не готова, он разбирает очередь и исполняет другие задачи (work-sharing), поэтому вложенные await внутри async-функций не блокируют пул намертво. Повторный await возвращает кэшированное значение:

@fn async fetch(url: String) -> String {
    return url;
}

@fn main() {
    let h: Async<String> = fetch("http://x");
    spawn h;
    println(h await);
}

List/Map через границу воркера передаются по значению-структуре: трамплин кладёт struct в ячейку, а каждый await делает её глубинную копию (GlyphList/GlyphMap memcpy-данные + свежий refcount). Поэтому хендл хранит приватный экземпляр результата: мутация и free() полученного по await значения безопасны и портят только эту копию; повторный await снова даёт исходное значение. (Ячейка результата хендла не освобождается — тот же класс утечки, что и у Int64/String: рантайм без GC.)

Типизированные каналы создаются конструктором Channel<T>(capacity): ёмкость 0 — rendezvous (прямая передача без буфера), > 0 — буфер. send(v) блокирует при полном буфере и возвращает false, если канал закрыт; recv() блокирует при пустом буфере и возвращает None, когда канал закрыт и пуст; close() будит всех ожидающих:

@fn async produce(ch: Channel<Int64>) -> Int64 {
    ch.send(1);
    ch.send(2);
    ch.close();
    return 2;
}

@fn main() {
    let ch: Channel<Int64> = Channel<Int64>(4);
    spawn produce(ch);
    let m: Option<Int64> = ch.recv();
    let v: Int64 = match m {
        | Some(x) => x,
        | None => -1
    };
    print_int(v);
}

select: ожидание первого готового события

select { ... } ждёт одно из нескольких событий и выполняет соответствующий блок. Рука — это либо | имя: Тип <- канал.recv(), либо | имя: Тип <- хендл await, либо | timeout(мс), либо | default. Срабатывает первый по порядку источник, у которого данные уже готовы; для recv из буферизированного канала и для await заведение задачи в пул происходит автоматически. timeout(мс) срабатывает, если за мс миллисекунд ни одно событие не стало готовым; default — если прямо сейчас готовых нет. timeout и default нельзя указывать одновременно (в каждом select), timeout-рук тоже не может быть несколько. Логика ожидания — пулинг с шагом ~1 мс, поэтому таймауты честные, а пул воркеров продолжает работать:

@fn async worker(ch: Channel<Int64>) -> Int64 {
    ch.send(42);
    ch.close();
    return 42;
}

@fn main() {
    let ch: Channel<Int64> = Channel<Int64>(1);
    let h: Async<Int64> = worker(ch);
    select {
        | v: Int64 <- ch.recv() => print_int(v),
        | v: Int64 <- h await => print_int(v),
        | timeout(100) => print_int(-1)
    }
}

recv из закрытого и опустошённого канала — готов немедленно и возвращает нулевое значение типа (аналог None).

Ограничения: рантайм без GC (хендлы, каналы и списки не освобождаются); Option/Result-боксы: временные (результат вызова прямо в match) освобождаются автоматически, именованные — через drop(box); рука select должна быть именно recv() или await (произвольные выражения, включая recv_timeout, не допускаются); потоки не вытесняются на уровне задачи — долгий вычислительный корень задачи занимает воркера целиком (через вложенные await воркер переключается на другие задачи); async-generic функции запрещены; send(&ref) запрещён типом (адрес стека нельзя передавать между потоками). Значения List/Map в канале передаются копией структуры с разделяемым буфером (COW, как String): после send отправитель не должен переиспользовать или освобождать значение — владелец приёмник.

13. Стандартная библиотека

Библиотека доступна без @use; сигнатуры объявлены в компиляторе, тела — в stdlib/.

print(msg: String) -> Void
println(msg: String) -> Void
eprintln(msg: String) -> Void
print_int(value: Int64) -> Void
print_float(value: Float64) -> Void
print_bool(value: Bool) -> Void
read_line() -> String

sqrt(x: Float64) -> Float64
pow(base: Float64, exp: Float64) -> Float64
abs(x: Float64) -> Float64
floor(x: Float64) -> Int64
ceil(x: Float64) -> Int64
round(x: Float64) -> Int64
min(a: Float64, b: Float64) -> Float64
max(a: Float64, b: Float64) -> Float64
clamp(value: Float64, low: Float64, high: Float64) -> Float64
log(x: Float64) -> Float64      log2(x: Float64) -> Float64
log10(x: Float64) -> Float64    sin(x: Float64) -> Float64
cos(x: Float64) -> Float64      tan(x: Float64) -> Float64

len(s: String) -> Int64
substring(s: String, start: Int64, end: Int64) -> String
contains(s: String, sub: String) -> Bool
starts_with(s: String, prefix: String) -> Bool
ends_with(s: String, suffix: String) -> Bool
replace(s: String, from: String, to: String) -> String
split(s: String, delimiter: String) -> String
trim(s: String) -> String
to_upper(s: String) -> String    to_lower(s: String) -> String
char_at(s: String, index: Int64) -> String
parse_int(s: String) -> Option<Int64>
parse_float(s: String) -> Option<Float64>
int_to_string(value: Int64) -> String
float_to_string(value: Float64) -> String
bool_to_string(value: Bool) -> String

to_int(s: String) -> Option<Int64>
to_float(s: String) -> Option<Float64>
to_bool(s: String) -> Option<Bool>

file_read(path: String) -> String
file_write(path: String, content: String) -> Bool
file_append(path: String, content: String) -> Bool
file_exists(path: String) -> Bool
create_dir(path: String) -> Bool
remove_file(path: String) -> Bool
list_dir(path: String) -> String
file_copy(src: String, dst: String) -> Bool
file_rename(old: String, new: String) -> Bool

alloc(size: Int64) -> Bytes
free(ptr: Bytes) -> Void
memcpy(dst: Bytes, src: Bytes, size: Int64) -> Void
memset(dst: Bytes, value: Int64, size: Int64) -> Void

assert(condition: Bool, msg: String) -> Void
assert_eq(a: Int64|UInt64|Float64|Bool|String, b: ..., msg: String) -> Void
assert_ne(a: ..., b: ..., msg: String) -> Void
assert_true(condition: Bool, msg: String) -> Void
assert_false(condition: Bool, msg: String) -> Void