Skip to content

Грамматическая спецификация ​

Настоящий документ определяет грамматическую спецификацию языка программирования YaoXiang, включая лексическую структуру, правила грамматики и приоритет операторов.


Глава 1: Лексическая структура ​

1.1 Исходный файл ​

Исходный файл YaoXiang должен использовать кодировку UTF-8. Исходные файлы обычно имеют расширение .yx.

1.2 Классификация лексических единиц ​

КатегорияОписаниеПример
ИдентификаторНачинается с буквы или подчёркиванияx, _private, my_var
Ключевое словоЯзыковые предопределённые зарезервированные словаType, pub, use
ЛитералФиксированное значение42, "hello", true
ОператорСимвол операции+, -, *, /
РазделительСинтаксический разделитель(, ), {, }, ,

1.3 Ключевые слова ​

YaoXiang определяет очень небольшое количество ключевых слов:

pub    use    spawn
ref    mut    if     else
else   match  while  for    return
break  continue as     in     unsafe

Эти ключевые слова имеют специальное значение в любом контексте и не могут использоваться в качестве идентификаторов.

1.4 Зарезервированные слова ​

«Зарезервированные слова» YaoXiang делятся на три уровня, распознаваемых парсером (parser) и проверщиком типов (type checker) на разных этапах:

1.4.1 Литеральные зарезервированные слова ​

Идентификаторы литералов, имеющие собственные токены в парсере и не могущие использоваться как обычные идентификаторы:

ИдентификаторПринадлежит типуОписание
Type—Ключевое слово мета-типа
trueBoolЛогическая истина
falseBoolЛогическая ложь
voidVoidЛитерал Void (значение Unit). Строчное void — литерал значения; прописное Void — имя типа (см. §1.4.3).

1.4.2 Конструкторы выражений ​

Следующие конструкторы распознаются парсером в контексте pattern matching и выражений:

КонструкторПринадлежит типуОписание
some(T)OptionКонструктор value variant Option
ok(T)ResultУспешный variant Result
err(E)ResultВариант ошибки Result

1.4.3 Имена встроенных типов ​

Следующие имена типов предварительно зарегистрированы проверщиком типов и могут использоваться в позиции типа без импорта. Парсер рассматривает их как обычные идентификаторы — не зарезервированные слова, могут быть затенены локальными связываниями (не рекомендуется).

Имя типаЛогическое соответствиеОписание
Void⊤ (истина/Unit)Тип-произведение с нулём полей, ровно один обитатель (литерал void, см. §1.4.1)
Never⊥ (ложь/пустой тип)Тип с нулём вариантов, ноль обитателей. Никакое выражение не может породить значение Never. Never <: T выполняется для всех T (принцип взрыва).
Int—Знаковое целое число
Float—Число с плавающей точкой
Bool—Логическое значение: true / false
Char—Символ Unicode
String—Строка

1.5 Идентификаторы ​

Идентификатор начинается с буквы или подчёркивания, последующие символы могут быть буквами, цифрами или подчёркиванием. Идентификаторы чувствительны к регистру.

Специальные идентификаторы:

  • _ используется как заглушка, означающая игнорирование значения
  • Идентификаторы, начинающиеся с подчёркивания, обозначают закрытые члены

1.6 Литералы ​

1.6.1 Целые числа ​

Decimal     ::= [0-9][0-9_]*
Octal       ::= 0o[0-7][0-7_]*
Hex         ::= 0x[0-9a-fA-F][0-9a-fA-F_]*
Binary      ::= 0b[01][01_]*

1.6.2 Числа с плавающей точкой ​

Float       ::= [0-9][0-9_]* '.' [0-9][0-9_]* ([eE][+-]?[0-9][0-9_]*)?

1.6.3 Строки ​

String      ::= '"' ([^"\\] | EscapeSequence)* '"'
Escape      ::= '\\' ([nrt'"\\] | UnicodeEscape)
Unicode     ::= 'u' '{' HexDigit+ '}'

1.6.4 Коллекции ​

List        ::= '[' Expr (',' Expr)* ']'
Dict        ::= '{' String ':' Expr (',' String ':' Expr)* '}'
Array       ::= '[' Expr (',' Expr)* ']'   // при аннотации целевого типа как Array(T, N) литерал становится массивом фиксированной длины

Литерал словаря требует как минимум одну пару ключ-значение: {} не является пустым словарём — это пустой блок (значение Void, см. §2.9). Для пустого словаря используйте конструктор dict.new():

yaoxiang
empty = dict.new()                  // ✅ пустой словарь
d = { "a": 1 }                       // ✅ литерал словаря
wrong = {}                           // ❌ это не словарь, а пустой блок (Void)

Критерий — содержимое: грамматика Dict требует как минимум один String ':' Expr; {} не имеет содержимого, на которое можно опереться, поэтому берётся нулевая форма блочной структуры. Непустые формы самоописываются содержимым ({ "k": v } имеет пару ключ-значение → словарь) — это того же происхождения, что и f = { 5 }, являющееся значением Int, а не функцией: тип определяется содержимым.

Set не имеет литеральной грамматики и представления в runtime — тип множества в планировании, при появлении потребности будет дополнен по образцу Dict (std.set + HeapValue::Set). Куда попадают литералы List/Dict, определяется аннотацией контекстного типа: голые литералы и аннотация List(T) попадают в растущие списки; аннотация Array(T, N), действующая непосредственно на литерал, помещает в массив фиксированной длины. Неявное преобразование List→Array запрещено.

Семантика литерала Array:

  • Число элементов должно быть равно N, иначе ошибка компиляции E1002; пустой литерал с ненулевым N также отвергается
  • Тип каждого элемента должен быть совместим с T, иначе ошибка компиляции E1002
  • Грамматическая форма N: только целочисленный литерал (может быть отрицательным) или имя константы; составные выражения (такие как 2+1) отвергаются на этапе парсинга
  • Когда N является символьной константой (параметр const функции, как в Array(Int, n)), проверка числа откладывается до этапа уточнения типа
  • Вложенные литералы массива v1 (Array(Array(Int,2),2) = [[1,2],[3,4]]) отвергаются на этапе компиляции, требуется явное послойное конструирование; рекурсивное попадание отложено до следующих версий

1.6.5 List comprehension ​

ListComp    ::= '[' Expr 'for' Identifier 'in' Expr (',' Expr)* ('if' Expr)? ']'

Ужесточение поведения (запись миграции): грамматика переменной итерации по существу является 'for' Identifier 'in', но в старой реализации pattern проходил полный парсинг pratt — после того как 'in' был зарегистрирован как инфиксный оператор, x поглощал бы in items как выражение membership. После исправления pattern, не являющийся идентификатором, просто завершается ошибкой парсинга, больше не делая fallback на _ (тихое проглатывание ошибки), как в старой реализации. Область влияния: записи вида [x for (a, b) in pairs], которые раньше могли быть распарсены, теперь приводят к ошибке — эта форма никогда не имела определённого поведения (переменная всегда была _), направление ужесточения верное, затрат на семантическую миграцию нет.

1.6.6 Проверка принадлежности ​

Membership  ::= Expr 'in' Expr

in — это бинарный реляционный оператор, возвращающий Bool — true при попадании, false при непопадании, без ошибки. Разделение семантики: [] — это утверждение существования и извлечение значения (при неудаче — ошибка), in — это запрос о существовании (непопадание — нормальный false). Покрытие правого операнда: List / Array / Dict(набор ключей) / Tuple / String(подстрока) / Range(интервал). in — это предикат первого класса Хоара, на этапе уточнения типа служит базой для доказуемых утверждений compile-time. (Set исключён из списка правых операндов — Set не имеет представления в runtime, см. §1.6.4)

1.7 Комментарии ​

// однострочный комментарий

/* многострочный комментарий
   может занимать несколько строк */

1.8 Правила отступов ​

Код должен использовать отступ в 4 пробела, использование символов табуляции запрещено. Это обязательное синтаксическое правило.


Глава 2: Правила грамматики ​

2.1 Классификация выражений ​

Expr        ::= Literal
              | Identifier
              | FnCall
              | MemberAccess
              | IndexAccess
              | UnaryOp
              | BinaryOp
              | TypeCast
              | RangeExpr
              | ErrorPropagate
              | RefExpr
              | IfExpr
              | MatchExpr
              | Block
              | Lambda

2.2 Приоритет операторов ​

ПриоритетОператорыАссоциативность
1() [] . ?Слева направо
2asСлева направо
3Унарный префикс ! - +Справа налево
4* / %Слева направо
5+ -Слева направо
6..Слева направо
7<< >>Слева направо
8& | ^Слева направо
9== != < > <= >=Слева направо
10and orСлева направо
11if...elseСправа налево
12= += -= *= /=Справа налево

Унарные префиксные операторы (! - +) тесно связываются: уступают только вызовам и доступу к членам, выше всех бинарных операторов. Поэтому !a == b ≡ (!a) == b (семантика в стиле Zig); ! — чистая унарная операция, не участвующая в потоке управления с коротким замыканием, ортогональна ключевым словам and/or (короткое замыкание) (RFC-010).

Сила связывания Range: .. — сила (6, 7) — левый 6 ниже сложения (7), правый 7 поглощает сложение, но не поглощает .. того же уровня. Сравнение до и после изменения:

ВыражениеДо изменения (уровень 1, правая ассоциативность)После изменения ((6,7), левая ассоциативность)
x in 1..10x in 1..10 (правый операнд in уровня 4, .. уровня 1 не может поглотить, фактически не парсится)x in (1..10) — интервал целиком как правый операнд in
0..n+2(0..n)+2 (ловушка правой ассоциативности: верхняя граница съедается, цикл for напрямую E3004)0..(n+2) — верхняя граница является арифметическим выражением
a == b..ca == (b..c) (уровень .. 1 < уровня == 3, естественно целиком)a == (b..c) — семантика не изменилась, .. всё ещё выше уровня сравнения
1..2*3(1..2)*31..(2*3) — верхняя граница является арифметическим выражением
a..b..ca..(b..c) (правоассоциативная цепочка, бессмысленный Range в Range)(a..b)..c — форма step (c — шаг)

Итог: составная верхняя граница for i in 0..n+2 переходит от «успешный парсинг, но E3004» к «сразу доступно»; x in 1..10 переходит от «не парсится» к «проверка интервала»; a..b..c переходит от «бессмысленной вложенности» к «компоненту шага». Уровень 6 попадает между + (уровень 5) и << (уровень 7), математическая конвенция: интервал — это тесно связываемая конструкция, верхняя граница естественно является полным арифметическим выражением.

2.3 Вызов функции ​

FnCall      ::= Expr '(' ArgList? ')'
ArgList     ::= Expr (',' Expr)* (',' NamedArg)* | NamedArg (',' NamedArg)*
NamedArg    ::= Identifier '=' Expr

Именованные аргументы используют форму name = value (RFC-010 §определение функций, RFC-011 §форма конструктора). Позиционные аргументы должны идти перед именованными; параметры, заданные по порядку, могут располагаться в именованных в любом порядке:

yaoxiang
add: (a: Int, b: Int) -> Int = a + b

add(3, 5)          // 位置式
add(a = 3, b = 5)  // 命名式
add(b = 5, a = 3)  // 顺序任意
add(3, b = 5)      // 混用,位置在前

Неправильное имя именованного аргумента даёт ошибку E1014, одновременное указание одного формального параметра позиционно и именованно — E1015, несоответствие числа — E1010 (RFC-013).

2.4 Доступ к членам ​

MemberAccess::= Expr '.' Identifier

2.5 Доступ по индексу ​

IndexAccess ::= Expr '[' Expr ']'

Три уровня семантики (RFC-011b): a[i] разветвляется по типу a — ① встроенные контейнеры (List/Vec/Array/Dict/Tuple) идут по нативной инструкции индексации (быстрый путь); ② пользовательские типы, реализующие интерфейс Index, диспетчеризируются к их методу index (инстанцирование Index(Grid, Int, Float) внутри тела типа + метод Grid.index); ③ привязка позиции f[0] из RFC-004 существует только в объявлениях привязки, не идёт по данной грамматике. Ключи многомерного индекса a[0, 1] упаковываются в кортеж. Другие типы, не реализующие Index, отвергаются на уровне типов.

2.6 Преобразование типа ​

TypeCast    ::= Expr 'as' TypeExpr

2.7 Условное выражение ​

IfExpr      ::= 'if' Expr Block ('else' 'if' Expr Block)* ('else' Block)?

2.8 Сопоставление с образцом ​

MatchExpr   ::= 'match' Expr '{' MatchArm+ '}'
MatchArm    ::= Pattern ('|' Pattern)* ('if' Expr)? '=>' Expr ','
Pattern     ::= Literal
              | Identifier
              | Wildcard
              | StructPattern
              | TuplePattern
              | EnumPattern
              | OrPattern

2.9 Блочное выражение ​

Block       ::= '{' Stmt* Expr? '}'

Правила завершения инструкций: разделение между инструкциями и поведение переноса строк (явное разделение ;, завершение переносом строки, исключения для продолжения строки, отсутствие объединения для (/[ в начале строки) определены в RFC-038.

2.9.1 Три формы { ​

{ в позиции выражения имеет ровно три интерпретации, определяемые содержимым за один проход:

ФормаЗаписьТипПример
Пустой блок{}Voidx: Void = {}
Литерал словаря{ "k": v, ... }Dict(K, V)d = { "a": 1 }
Блок{ Stmt* Expr? }Тип хвостового выраженияy = { 1 + 1 }

Порядок определения:

  1. {} (без содержимого) → пустой блок, значение Void
  2. Первый элемент — пара ключ-значение String ':' Expr → литерал словаря
  3. Остальное → блок, значение задаётся хвостовым выражением

Почему {} не является пустым словарём: «пустота» пустого словаря не может быть самоописательной (это может быть как Dict(K, V), так и пустой блок), а грамматика словаря §1.6.4 требует как минимум одной пары ключ-значение. При отсутствии содержимого, на которое можно опереться, берётся нулевая форма блочной структуры: это согласуется с unsafe {} / spawn {}, не вводя особых случаев. Для пустого словаря используйте dict.new().

Почему функциям нужна аннотация: f = { stmt } — это значение (тип хвостового выражения), а не функция. Чтобы определить функцию, укажите аннотацию Fn: f: () -> Int = { 5 }. Это тот же принцип, что и для словаря: тип определяется содержимым, а не наличием или отсутствием аннотации. (Это правило см. в RFC-010a, приложение D.)

Единая семантика: значение всех блоков {} задаётся хвостовым выражением, return — это нелокальный выход типа Never.

Тип блокаВыход значенияПустой блок {}
Обычный {}Хвостовое выражениеVoid
unsafe {}Хвостовое выражениеVoid
spawn {}Хвостовое выражениеVoid

Основные принципы (подробности см. в RFC-010a):

  • Значение блока = хвостовое выражение (последнее выражение), единственный выход, без исключений
  • Если в конце стоит инструкция присваивания, значение блока — Void; если нужно Void, явно напишите Void
  • return выходит за ближайшую границу функции (проходит сквозь любой блок, не «возвращается блоку»), тип Never; Never <: T выполняется для любого типа (принцип взрыва), поэтому может находиться в любой позиции возвращаемого типа
  • Форма выражения = expr непосредственно даёт значение
yaoxiang
// 普通 {} 块:尾表达式给出值
result = {
    x = compute()
    x                // 块的值
}

// unsafe {} 块:尾表达式给出类型定义
SqliteDb = unsafe {
    SqliteDb: Type = {
        handle: *Void
    }
    SqliteDb         // 块的值
}

// spawn {} 块:尾表达式给出结果
(a, b) = spawn {
    result1 = fetch("url1"),
    result2 = fetch("url2")
    (result1, result2)   // 块的值
}

// return:穿透块,退出函数
f: (n: Int) -> Int = {
    if n < 0 {
        return 0     // 穿出 if 与函数体,退出函数
    }
    n * 2            // 尾表达式
}

name = { ... } — это функция или значение блока? ​

name = { ... } может быть как определением функции (RFC-007 «простейшая форма с пустыми параметрами»), так и связыванием значения блока. Решение по принципу аннотация в приоритете, по умолчанию функция (RFC-010a, приложение D):

СлучайРезультатПример
Значение — Lambda (=>)Функцияf = () => 5 → f() = 5
Аннотация — функциональный типФункцияf: () -> Int = { 5 } → f() = 5
Аннотация — не функциональный типЗначение блокаx: Int = { 5 } → x = 5
Без аннотацииФункцияf = { 5 } → f() = 5

Аннотация есть тип: x: Int = ... объявляет, что x — это Int, значит { ... } вычисляется в Int; f: () -> Int = ... объявляет, что f — это функция, значит { ... } — это тело функции.

Чтобы { ... } вычислялся на месте, просто напишите целевой тип (новый синтаксис не нужен):

yaoxiang
// 块值:立即求值
x: Int = {
    y = 5
    y            // x = 5
}

// 函数:无注解默认
f = { 5 }        // f() = 5

Вложенные функциональные типы: каррирование или возврат функции? ​

Вложенный функциональный тип справа от -> имеет два прочтения, различаемых скобками (RFC-004):

ЗаписьЗначениеВызов
(a: Int) -> (b: Int) -> IntКаррированиеf(1)(2)
(a: Int) -> ((b: Int) -> Int)Возврат функцииg(1) даёт функцию

Основание: аннотация есть тип. g: (a: Int) -> ((b: Int) -> Int) объявляет, что g(1) : (b: Int) -> Int, поэтому g(1) должен быть той функцией, а не «следующей порцией параметров».

yaoxiang
// 柯里化:两段参数逐层给
add: (a: Int) -> (b: Int) -> Int = { a + b }
add(1)(2)        // → 3

// 返回函数:外层一段参数,返回的才是函数
adder: (n: Int) -> ((x: Int) -> Int) = (x) => x + n
adder(10)(5)     // → 15
h = adder(10)    // 返回的函数可存变量、可传递

Вложенный Fn без скобок всегда является каррированием (включая форму с параметрами типа из RFC-011):

yaoxiang
identity: (T: Type) -> (x: T) -> T = (x) => x   // 柯里化
identity(5)      // → 5

Проверка типов: скобки объявляют тип возврата, тело должно порождать этот тип. f: () -> (() -> Int) = { 7 } — ошибка — ожидается () -> Int, фактически получено Int (E1002).

2.10 Лямбда-выражение ​

Lambda      ::= '(' ParamList? ')' '=>' Expr
            |  '(' ParamList? ')' '=>' Block

2.11 Оператор распространения ошибки ​

ErrorPropagate ::= Expr '?'

Оператор ? является постфиксным оператором, того же приоритета, что и .. Для типа Result(T, E):

  • При Ok(v) извлекает значение v и продолжает выполнение
  • При Err(e) распространяет ошибку вверх (return Err(e))
yaoxiang
process: (data: Data) -> Result(Data, Error) = {
    validated = validate(data)?     // 成功时提取值,失败时向上传播
    transform(validated)
}

2.12 Выражение диапазона ​

RangeExpr   ::= Expr '..' Expr ('..' Expr)?

.. создаёт значение диапазона (Range — значение первого класса, не синтаксический сахар).

yaoxiang
for i in 0..10 { print(i) }
slice = array[0..5]

// Range 是值:绑定、传递、成员判断
r = 1..10
assert.assert(5 in r, "membership")
for i in r { print(i) }

// step 形态(第三分量,默认 1)
for i in 0..10..2 { print(i) }  // 0, 2, 4, 6, 8
for i in 10..0..(-2) { print(i) }  // 10, 8, 6, 4, 2

Семантика step: в a..b..c c — это шаг. c = 0 как литерал отвергается на этапе компиляции; динамический c проверяется в runtime (семейство E6001; после внедрения системы ошибок повышается до Result). c < 0 допустимо, направление интервала инвертируется по знаку (10..0..(-2) убывает).

2.13 Выражение ref ​

RefExpr     ::= 'ref' Expr

ref создаёт разделяемое владение. Компилятор автоматически выбирает Rc (однозадачный) или Arc (межзадачный), пользователю не нужно заботиться о деталях реализации.

yaoxiang
data = ref heavy_data
spawn { use(data) }   // 跨任务:编译器自动选 Arc

2.14 Выражение unsafe ​

UnsafeExpr  ::= 'unsafe' Block

Блок unsafe используется для определения непрозрачных типов и операций с сырыми указателями. Используйте return, чтобы вернуть определение типа в вышестоящую область видимости.

Семантика:

  • В unsafe {} можно определять типы и оперировать сырыми указателями
  • Возвращённый тип доступен за пределами unsafe {}
  • Доступ к полям типа требует прав unsafe
yaoxiang
// 在 unsafe 块中定义不透明类型
SqliteDb = unsafe {
    SqliteDb: Type = {
        handle: *Void  // 裸指针
    }
    return SqliteDb
}

// SqliteDb 在 unsafe 块外可用
db = sqlite3_open("test.db")

2.15 Область видимости ​

Основные правила:

  • Каждый блок {} создаёт область видимости
  • Внутренняя область видимости может обращаться к переменным внешней
  • Внешняя область видимости не может обращаться к переменным внутренней
  • Объявление переменных следует принципу «присваивание в первую очередь»
yaoxiang
// 块作用域
{
    x = 10
    // x 在此作用域内可见
}
// x 在此作用域外不可见

// 函数作用域
add: (a: Int, b: Int) -> Int = {
    result = a + b
    return result
}
// result 在函数外不可见

Объявление и затенение переменных:

  • x = value: поиск x по цепочке областей видимости наружу, при нахождении — присваивание, иначе — новое объявление
  • mut x = value: явное новое объявление изменяемой переменной, запрещено совпадение имён с внешней
  • В одной области видимости любое имя может быть объявлено только один раз

Подробное определение: полные правила области видимости, механизмы объявления и затенения переменных см. в Спецификации системы модулей.


Глава 3: Инструкции ​

3.1 Классификация инструкций ​

Stmt        ::= LetStmt
              | ExprStmt
              | ReturnStmt
              | BreakStmt
              | ContinueStmt
              | IfStmt
              | MatchStmt
              | WhileStmt
              | ForStmt
              | SpawnStmt

3.2 Объявление переменной ​

LetStmt     ::= ('mut')? Identifier (':' TypeExpr)? '=' Expr

3.3 Инструкция return ​

ReturnStmt  ::= 'return' Expr?

Семантика: return — это нелокальный выход, выходящий за ближайшую границу функции (проходит сквозь любые блоки — включая if / while / for / match / голые блоки / spawn / unsafe), передавая значение вызывающему. Он не «возвращается блоку».

Тип: return e : Never (при e : T). Never <: T' выполняется для любого T' (принцип взрыва, см. type system §2.2), поэтому return может находиться в любой позиции возвращаемого типа без дополнительных правил.

Связь с вычислением блока: значение блока всегда равно хвостовому выражению (см. §2.9). { return n } как блок имеет значение n типа Never; одновременно return выполняет роль выхода из функции. Оба утверждения истинны одновременно, они сосуществуют благодаря принципу взрыва.

return вместе с хвостовым выражением обеспечивают «ранний возврат» без специальных правил для return относительно функции — см. RFC-010a.

yaoxiang
factorial: (n: Int) -> Int = {
    if n <= 1 {
        return 1          // 穿出 if,退出函数(类型 Never)
    }
    n * factorial(n - 1)  // 尾表达式 = 块的值
}

3.4 Инструкция break ​

BreakStmt   ::= 'break'

Семантика: немедленно завершает самый внутренний цикл while/for, в котором находится, передавая управление коду после тела этого цикла.

  • Выход только из ближайшего слоя: break всегда действует на самый внутренний цикл, его содержащий. В вложенных циклах, когда нужно выйти сразу из нескольких слоёв, вынесите внутренний цикл в функцию и используйте return, или используйте флаг (break/continue без меток; если в будущем будут введены метки циклов, они пройдут процедуру RFC по синтаксису со стороны объявления цикла, совместно с многоточечным дизайном канала доказательств)
  • Только внутри цикла: break может находиться только в теле цикла while/for (включая вложенные блоки/if/match в теле); появление вне цикла даёт ошибку компиляции (E1102 'break' outside of a loop)
  • Не влияет на доказательство завершимости: break не участвует в аргументации завершимости — он не предоставляет меру и не образует шага убывания меры; обязательство завершимости цикла не зависит от break и инициируется уточнением типа (см. type-system §8.4)
  • Семантика заимствования: ребро потока управления break участвует в структурном разрыве обратного BFS-анализа живости RFC-009a (выпрыгнувшие итерации не участвуют в выводе живости обратных рёбер)
yaoxiang
mut i = 0
while i < 10 {
    i = i + 1
    if i == 3 {
        break              // 控制流转到循环之后,i == 3
    }
}

// 嵌套循环:break 只出内层
while j < 3 {
    while k < 10 {
        if k == 2 { break }    // 只终止内层循环
    }
    j = j + 1                  // 每轮外层迭代都会执行到这里
}

3.5 Инструкция continue ​

ContinueStmt::= 'continue'

Семантика: пропускает оставшиеся инструкции текущей итерации, сразу переходя к следующему раунду самого внутреннего цикла, в котором находится, — для while происходит перевычисление условия, для for — взятие следующего элемента.

  • Действует только на ближайший слой: то же, что и break, без меток
  • Только внутри цикла: появление вне цикла даёт ошибку компиляции (E1102)
yaoxiang
mut sum = 0
mut n = 0
while n < 5 {
    n = n + 1
    if n == 3 {
        continue           // 跳过下面的累加,n == 3 不计入
    }
    sum = sum + n
}
// sum == 12(1 + 2 + 4 + 5)

3.6 Инструкция if ​

IfStmt      ::= 'if' Expr Block ('else' 'if' Expr Block)* ('else' Block)?

3.7 Инструкция match ​

MatchStmt   ::= 'match' Expr '{' MatchArm+ '}'

3.8 Инструкция while ​

WhileStmt   ::= 'while' Expr Block

3.9 Инструкция for ​

ForStmt     ::= 'for' 'mut'? Identifier 'in' Expr Block

3.9.1 Семантика: каждая итерация — это связывание нового значения ​

Семантика цикла for в YaoXiang отличается от таковой в традиционных языках: каждая итерация — это связывание нового значения, а не изменение одной и той же переменной.

yaoxiang
// 示例:for i in 1..5
for i in 1..5 {
    print(i)
}

Процесс выполнения:

ИтерацияПоведение переменной цикла
1-яСоздаётся новое связывание i = 1, выполняется тело цикла, печатается 1
2-яСоздаётся новое связывание i = 2 (предыдущее связывание уничтожено), выполняется тело цикла, печатается 2
3-яСоздаётся новое связывание i = 3, выполняется тело цикла, печатается 3
4-яСоздаётся новое связывание i = 4, выполняется тело цикла, печатается 4
ЗавершениеТело цикла заканчивается, связывание уничтожается

Ключевой момент: после завершения каждой итерации созданное в ней связывание уничтожается. Следующая итерация — это совершенно новое связывание, никак не связанное со связыванием предыдущей итерации.

3.9.2 Разница между for и for mut ​

СинтаксисИзменяемость переменной циклаПояснение
for i in 1..5НеизменяемаяВ теле цикла нельзя изменить связывание
for mut i in 1..5ИзменяемаяВ теле цикла можно изменить связывание
yaoxiang
// 合法:每次迭代绑定新值,不需要修改
for i in 1..5 {
    print(i)  // 读取 i 的值
}

// 错误:不可变绑定,不能修改
for i in 1..5 {
    i = i + 1  // 错误:不能修改不可变绑定
}

// 合法:使用 for mut 允许修改绑定
for mut i in 1..5 {
    i = i + 1  // 允许修改
}

3.9.3 Проверка затенения ​

YaoXiang запрещает затенение переменных. Переменная цикла for не может иметь то же имя, что и переменная во внешней области видимости:

yaoxiang
// 错误:i 已经在外部声明
i = 10
for i in 1..5 {
    print(i)
}

// 正确:使用不同的变量名
i = 10
for j in 1..5 {
    print(j)
}

Это правило применяется ко всем блокам кода, подробности см. в 4.3 Правила затенения.

3.9.4 Сравнение с другими языками ​

ЯзыкСемантика переменной цикла for
YaoXiangКаждая итерация — новое связывание
RustИзменение одной и той же переменной (требуется mut)
PythonИзменение одной и той же переменной (mut не требуется)
C/C++Изменение одной и той же переменной (требуется указатель или ссылка)

Обоснование дизайна: YaoXiang использует семантику связывания, потому что:

  1. Более соответствует естественной семантике. В естественном языке «для каждого элемента x в коллекции» означает, что каждый x — это отдельная сущность. for i in 1..5 в YaoXiang читается как «для каждого i от 1 до 5», каждое i в итерации — это совершенно новое связывание, что согласуется с интуитивным пониманием человека.

  2. Избежание случайных изменений. Семантика неизменяемого связывания по умолчанию означает, что переменная цикла не может быть случайно изменена внутри тела. Не нужно беспокоиться о том, что где-то в сложном теле цикла случайно написано i = ..., приводящее к трудноотслеживаемому багу.

  3. Высокопроизводительное решение на расстоянии вытянутой руки. Когда действительно необходимо повторно использовать переменную между итерациями (например, аккумулятор, кеш), используйте for mut, чтобы переключиться на режим изменяемого связывания. Это яснее, чем скрытое разделяемое состояние, — намерение выражается явно через синтаксис, а не прячется в поведении runtime.

3.10 Инструкция spawn ​

SpawnBlock  ::= '(' Pattern (',' Pattern)* ')' '=' 'spawn' '{' Expr (',' Expr)* '}'
SpawnFor    ::= Identifier '=' 'spawn' 'for' 'mut'? Identifier 'in' Expr '{' Expr '}'
SpawnStmt   ::= SpawnBlock | SpawnFor

spawn блок: явное объявление области параллелизма, выражения внутри блока выполняются параллельно.

yaoxiang
(result_a, result_b) = spawn {
    parse(fetch("url1")),
    parse(fetch("url2"))
}

spawn цикл: цикл с параллелизмом по данным.

yaoxiang
results = spawn for item in items {
    process(item)
}

spawn блок захватывает переменные внешней области видимости (RFC-024 §2.3, семантика захвата значения):

  • Тело блока ссылается на внешнюю переменную = захват значения через Move: значение снапшотится в окружение замыкания в точке создания spawn, тело блока читает через env (LoadUpvalue)
  • Примитивы (Int/Float/Bool/Char) — копирование значения, внешняя переменная не затрагивается
  • Типы-дескрипторы (Struct/String/List и т.д.) — снапшот = копирование дескриптора, базовый объект разделяется; Embedded runtime (по умолчанию) с одним потоком и одной кучей, дескриптор действителен
  • Для разделения между несколькими задачами требуется явный ref (§2.13, компилятор автоматически выбирает Rc/Arc)
  • return в блоке, ссылающийся на внешнюю переменную, также захватывается
yaoxiang
t1 = 1 + 1
t2 = 2 + 2
result = spawn {
    return t1 + t2    // t1/t2 值捕获,result == 6
}

3.11 Точка входа в программу и инструкции верхнего уровня ​

В глазах компилятора исходный файл имеет две роли, определяемые наличием yaoxiang.toml:

РольОпределениеТело программы
ScriptОдин файл запускается напрямую, без yaoxiang.tomlИнструкции верхнего уровня (выполняются в порядке записи); main — обычное связывание
BinСуществует yaoxiang.tomlФункция main; исполняемые инструкции верхнего уровня запрещены

Script: инструкции верхнего уровня — это программа ​

При отсутствии manifest файл является «скриптом», инструкции верхнего уровня выполняются в порядке записи:

yaoxiang
use std.io
io.println("hello")          // 直接执行
x: Int = { 42 }              // 顶层绑定:运行时初始化
io.println(x)                // 42

В этом режиме main не является особым — это просто обычное связывание. Чтобы main выполнился, нужно явно вызвать:

yaoxiang
use std.io
main: () -> Void = { io.println("only runs if called") }

main()                       // ← 必须写这一行

Почему main не вызывается автоматически? Инструкции верхнего уровня уже являются телом программы. Если бы ещё и неявно вызывался main, сценарий с явным main() выполнялся бы дважды. Эти два правила не могут сосуществовать, поэтому в режиме Script есть только одна точка выполнения — «инструкции верхнего уровня».

Bin: main — точка входа ​

При наличии manifest файл является «исполняемым целевым объектом», в этом случае:

  • main должен быть определён, и он должен быть функцией (сигнатура должна допускать вызов без аргументов)
  • Исполняемые инструкции верхнего уровня запрещены — тело программы и есть main
yaoxiang
main: () -> Void = {
    print("hello")
}

Отсутствие main или не-функция main являются ошибками компиляции (первое: без main все функции недостижимы; второе: связывание значения не будет вызвано).

Файлы библиотек: файлы, которые use-ятся другими файлами, или файлы, на которые указывает [lib].path / [exports], не требуют main — они не являются точкой входа.

Инициализация связываний верхнего уровня ​

Значения инициализации связываний верхнего уровня вычисляются в runtime, не требуя быть константами compile-time:

yaoxiang
answer: Int = { 42 }              // 块值
inc: (Int) -> Int = (x) => x + 1
computed: Int = inc(41)           // 函数调用

Инициализация выполняется в порядке зависимостей, независимо от порядка записи:

yaoxiang
derived: Int = base * 3           // 引用后声明的 base
base: Int = 7                     // 先初始化(拓扑排序)

Циклическая зависимость — ошибка компиляции (будут перечислены имена в цикле):

yaoxiang
a: Int = b + 1
b: Int = a + 1                    // 错误:a → b → a

Обоснование дизайна: RFC-029f (модель роли файла), RFC-010a, приложение D (решение о блочном связывании).


Приложение: Краткий справочник по синтаксису ​

A.1 Управление потоком ​

if Expr Block (else if Expr Block)* (else Block)?
match Expr { MatchArm+ }
while Expr Block
for 'mut'? Identifier 'in' Expr Block
break | continue          // 仅循环体内(§3.4 / §3.5)

A.2 Обработка ошибок ​

Expr '?'              // 错误传播(Result 类型)

A.3 Синтаксис match ​

match value {
    pattern1 => expr1,
    pattern2 if guard => expr2,
    _ => default_expr,
}