Теперь перейдем к разработке высокоуровневого языка системного программирования. Для начала простые требования к языку:
- он должен быть достаточно низкоуровневым и позволять работать с памятью и прерываниями
- он должен минимизировать скрытый оверхед который добавляется к кодогенерации - все должно быть просто и прозрачно
- он должен позволять легко включать ассемблерные фрагменты непосредственно в программы написанные на нем
- он должен иметь LR(1) грамматику или близкую к ней позволяющую написать эффективный рекурсивный парсер
Так как у нас регистров достаточно много то логично не только использовать их для хранения локальных переменных, но и для передачи аргументов и возвратов значений. Из 16ти регистров 4 занято под служебные. Остается 12. Зафиксируем первые 4 из них для передачи аргументов и возврата результата. Да, я не оговорился - ничто не запрещает нам возвращать из функции более одного результата. Очень часто это очень удобно на практике. Функция обязана гарантировтаь целостность только регистров с 5 по 11й и может "портить" значения первых 4х регистров. Обычно это приводит к тому что первые четыре регистра используются для вычислений и хранения промежуточных результатов длинных вычислений. Если передается более 4х парамтеров то 5, 6 и далее передаются через стек. Количество возвращаемсых значений ограничено только метом в первых четырех регистрах.
Синтаксис должен по возможности исключать лишние символы для разделителей там где они не нужны и не вводить программиста в заблудение стилем написания кода (например "висячий else" в С). Так же в синтаксисе не должно быть всякой всячины которая раздражает в том же С. Например указателей на функцию с их зупутанным синтаксисом или стрелочки для взятия поля структуры по указателю, хотя и так понятно что это за указатель и на что он указывает.
Ну пора уже и хеллоу ворлд написать
Code: Select all
include "io.b16"
fn main() {
puts("Привет, CPU16!\n")
}
Очень похоже на C, но проще. Немного более сложный пример:
Code: Select all
fn main(argc: uint, argv: ptr ptr byte): int {
var i: uint = 1
while i < argc {
puts(argv[i]); putnl()
i++
}
return 0
}
Разделитель операторов ‘
;’ нужен, только когда в одной строке несколько операторов: конец строки сам завершает оператор. Обратная сторона: длинное выражение можно переносить только после знака операции, запятой или открывающей скобки. Если перенести перед минусом, получатся два оператора, и компилятор предупредит, что значение второго не используется.
Переменные описываются ключевым словом
var, тип пишется после имени. Если есть инициализатор, тип можно опустить, тогда он выводится. За неинициализированными переменными компилятор следит сам.
Функция может возвращать несколько результатов. Их получают множественным присваиванием, ненужный результат заменяют прочерком:
Code: Select all
fn divmod(a, b: uint): uint, uint { return a / b, a % b }
...
var q, r: uint
q, r = divmod(v, 10)
_, r = divmod(v, 10) // ненужный результат пропускается через _
В C для этого пришлось бы передавать указатели на выходные параметры. Здесь результаты просто возвращаются в регистрах R0–R3.
Типы: всё явно
Набор скалярных типов маленький и с точными размерами:
byte/
char (8 бит, без знака и со знаком),
uint/
int (16 бит),
ulong/
long (32 бита). Логического типа нет, как и в C: условие истинно, если значение не ноль. Указатель записывается словом
ptr и читается слева направо:
ptr ptr byte — «указатель на указатель на байт». Голый
ptr — аналог
void *.
Главное отличие от C в том, что неявный преобразований почти нет:
Code: Select all
var buf: [64]byte
var p: ptr byte = buf // массив сводится к указателю, как в C
var a = uint(p) // указатель -> число только явно
var n: uint = p // ошибка
var q: ptr = p // ok, нетипизированный указатель
var i: int = -1
var u: uint = 1
if i < u { } // ошибка: операнды разной знаковости
if i < int(u) { } // так можно
Приведение записывается как вызов функции:
uint(x),
ptr Node(x). Классическая ловушка C, когда
-1 < 1u ложно, здесь просто не компилируется.
Константы считаются точно
Литералы и
const нетипизированы. Арифметика над ними идёт без переполнения, а тип константа получает из контекста, причём с проверкой диапазона:
Code: Select all
const BUFSZ = 512
var b: byte = 300 // ошибка, а не тихое 44
var x = 40000 // тип uint: первый из int, uint, long, ulong, куда влезает
var big: ulong = 1 << 20 // 1048576, без сюрпризов
Приоритет битовых операций исправлен: они связывают сильнее сравнений.
Code: Select all
if flags & F_DIR != 0 { ... } // (flags & F_DIR) != 0, а не как в C
Цепочка
a < b < c — ошибка компиляции.
Управляющие конструкции
Условие пишется без скобок, тело всегда в фигурных скобках, так что проблемы висячего
else не возникает. Циклов два:
while и бесконечный
loop. Цикла
for нет. Есть
break,
continue и
goto.
Code: Select all
loop {
var c = getc()
if c == '\n' {
break
} else if c < ' ' {
continue
}
putc(c)
}
Функция с результатом должна его вернуть. Иначе это ошибка компиляции.
Структуры
Code: Select all
struct Node { next: ptr Node; val: int }
fn sum(p: ptr Node): int {
var s = 0
while p {
s += p.val // никакого ->: точка разыменовывает сама
p = p.next
}
return s
}
Структуры нельзя присваивать, сравнивать и передавать по значению, только через указатель. Так любая копия видна в тексте программы. Инициализаторы поддерживают имена полей:
Code: Select all
struct Key { scan: byte; name: ptr byte }
var keys: [2]Key = { { $48, "^" }, { .scan = $50, .name = "v" } }
Порядок объявлений верхнего уровня свободный: функцию можно вызвать выше по тексту, чем она определена, и прототипы для этого не нужны.
include подключает каждый файл один раз, так что guard‑макросы тоже не нужны (препроцессора нет вообще).
static ограничивает видимость имени файлом.
Указатели на функции
Синтаксис читается так же линейно, без спиральных деклараций C:
Code: Select all
fn twice(x: int): int { return x * 2 }
fn apply(f: ptr fn(int): int, x: int): int { return f(x) }
...
var r = apply(twice, 21)
32 бита на 16‑битной машине
long и
ulong поддерживают всю обычную арифметику. Сужение до 16 бит делается только явным приведением, расширение неявное:
Code: Select all
var size: ulong = 100000
var lo = uint(size) // младшее слово, бесплатно
var full = ulong(a) * ulong(b) // полное 32-битное произведение 16-битных
var w: uint = size // ошибка: сужение без приведения
Ближе к железу
Главное, ради чего язык затевался, — системное программирование без лишнего ассемблера. Порт или таблица векторов объявляются переменной по адресу, обработчик прерывания — отдельным видом функции:
Code: Select all
var VEC at $FF00: [16]ptr interrupt fn() // таблица обработчиков прерываний по абсолютному адресу $FF00
var ticks: uint = 0
interrupt fn on_tick() { ticks++ } // сохранение регистров и IRET делает компилятор
fn setup() {
atomic { // прерывания запрещены, по выходу IF восстанавливается
VEC[0] = on_tick
}
}
atomic корректно восстанавливает флаг прерываний при любом выходе из блока: через
return,
break или
goto. Типы тоже следят за порядком:
ptr interrupt fn() несовместим с
ptr fn(), поэтому обработчик нельзя случайно вызвать как функцию.
Отдельные команды процессора доступны как встроенные функции без вызова. Для всего остального есть вставки
asm { ... }.
Code: Select all
fn addpair(alo, ahi, blo, bhi: uint): uint, uint {
asm {
ADD R0, R2
ADC R1, R3
}
return alo, ahi
}
Кодогенерация очень прозрачна. Например
Code: Select all
fn sum3(a, b, c: int): int { return a + b + c }
порождает такой код
Итого
B16 — это C, из которого убрали препроцессор, неявные преобразования, путаницу с приоритетами и обязательные прототипы. Взамен добавили несколько результатов у функций, точные константы и средства для работы с железом прямо в языке:
at,
interrupt fn,
atomic.
В атаче введение в язык
Для того чтобы действительно убедиться что язык системный мы на нем напишем операционную систему с шеллом и утилитами командной строки, аналог нортон-командера, редактор текстов, компилятор с него самого, ассемблер и много чего другого

Так что продолжение следует