Бесконечно более портативная альтернатива языку программирования C.

Для тех из вас, кто помнит «бесплатный», Oak - это, по сути, более надежная и высокая версия этого проекта. Цель Oak - быть как можно более высоким уровнем на фронте, но максимально маленький и низкий уровень в бэкэнде.
Я недавно выпускник школ и первокурсник в колледже в поисках работы. Если вам нравятся мои проекты, подумайте о поддержке меня, купив мне кофе!
Ключом к безумной переносимости Oak является невероятно компактная реализация бэкэнд. Код для бэкэнда Oak может быть выражен в менее 100 линиях C. Такая небольшая реализация возможна только из -за крошечного набора инструкций промежуточного представления. IR Oak состоит только из 17 различных инструкций . Это наравне с Brainfuck!
Бэкэнд из дуба функционирует очень просто. Каждая инструкция работает на ленте памяти . Эта лента, по сути, является статическим массивом поплавков с двойной задачей.
let x : num = 5.25 ; ... let p : & num = & x ; `beginning of heap`
| | |
v v v
[ 0 , 0 , 0 , 5.25 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 3 , 0 , 0 , 0 , 0 , 0 , 0 , ... ]
^
|
`current location of the stack pointer`Когда переменная определяется в функции, ей дается статическое положение относительно текущего базового указателя виртуальной машины. Таким образом, когда вызывается функция, пространство для переменных функции выделяется в стеке, а базовый указатель увеличивается для использования этого нового пространства. Затем компилятор просто заменяет переменную на его адрес, добавленный в смещение базового указателя в остальной части кода!
Кроме того, лента памяти функционирует как стек и куча . После места для всех переменных программы присваивается память, используемая для стека. Стек растет и сжимается с данными по всей программе: например, когда суммируются два числа, они выпадают из стека и заменяются результатом. Точно так же куча растет и сокращается на протяжении всей программы. Куча, однако, используется для динамически распределенных данных: информация с следами памяти , неизвестным во время компиляции .
Теперь, когда вы понимаете, как работает Backend Oak, вот полный набор инструкций!
| Инструкция | Побочный эффект |
|---|---|
push(n: f64); | Нажмите номер в стек. |
add(); | Выберите два числа со стека и вытолкните их сумму. |
subtract(); | Выпейте два числа из стека. Вычтите первое из второго и нажмите результат. |
multiply(); | Заберите два числа из стека и вытолкните их продукт. |
divide(); | Выпейте два числа из стека. Разделите второе на первое и нажмите результат. |
sign(); | Выберите номер из стека. Если он больше или равен нулю, нажмите 1 , в противном случае нажмите -1 . |
allocate(); | Выберите номер из стека и верните указатель на это количество свободных ячеек на куче. |
free(); | Выберите номер из стека и перейдите туда, где это число указывает на память. Выберите еще один номер из стека и освободите, что многие ячейки в этом месте в памяти. |
store(size: i32); | Выберите номер из стека и перейдите туда, где это число указывает на память. Затем числа size поп -размеры от стека. Храните эти цифры в обратном порядке в этом месте в памяти. |
load(size: i32); | Выберите номер из стека и перейдите туда, где это число указывает на память. Затем нажмите номер size последовательных ячеек памяти в стек. |
call(fn: i32); | Вызовите функцию, определенную пользователем с помощью идентификатора назначенного компилятора. |
call_foreign_fn(name: String); | Вызовите внешнюю функцию по его названию в источнике. |
begin_while(); | Начните с цикла. Для каждой итерации вытащите номер из стека. Если число не равна нулю, продолжайте петлю. |
end_while(); | Отметьте конец цикла. |
load_base_ptr(); | Загрузите базовый указатель установленной кадры стека, который всегда меньше или равен указателю стека. Переменные хранятся относительно базового указателя для каждой функции. Таким образом, функция, которая определяет x: num и y: num , x может храниться в base_ptr + 1 , а y может храниться в base_ptr + 2 . Это позволяет функциям сохранять переменные в памяти динамически и по мере необходимости, а не использовать статические местоположения памяти. |
establish_stack_frame(arg_size: i32, local_scope_size: i32); | Отключите arg_size Количество ячеек из стека и сохраните их. Затем вызовите load_base_ptr , чтобы возобновить родительскую кадре стека, когда эта функция заканчивается. Протолкните количество local_scope_size Количество нулей в стек, чтобы освободить место для переменных функции. Наконец, оттолкните сохраненные ячейки аргументации обратно в стек, так как они были первоначально упорядочены. |
end_stack_frame(return_size: i32, local_scope_size: i32); | Отключите return_size Количество ячеек из стека и сохраните их. Затем POP local_scope_size Количество ячеек со стека, чтобы отказаться от памяти кадра стека. Выберите значение из стека и сохраните его в базовом указателе, чтобы возобновить каркас родительского стека. Наконец, нажмите сохраненные ячейки обратного значения обратно в стек, так как они были заказаны. |
Используя только эти инструкции, Oak может реализовать еще более высокие абстракции, чем C, может предложить C !!! Это может звучать не так много, но это очень мощно для языка такого маленького.
Синтаксис дуба сильно вдохновлен языком программирования Rust.
Функции объявляются с помощью ключевого слова fn и синтаксически идентичны функциям ржавчины, за исключением семантики return . Кроме того, пользовательские типы и константы объявляются с помощью ключевых слов type и const соответственно.
Подобно внешним атрибутам Rust, Oak представляет множество флагов времени компиляции. Некоторые из них продемонстрированы ниже вместе с другими особенностями дуба.

Так как именно работает дубовый компилятор?
Сгладить структуры в их функции
putnumln(*bday.day) становится putnumln(*Date::day(&bday)) . Это довольно простой процесс.Рассчитайте размер типа каждой операции
// `3` is the size of the structure on the stack
fn Date :: new ( month : 1 , day : 1 , year : 1 ) -> 3 {
month ; day ; year
}
// self is a pointer to an item of size `3`
fn Date :: day ( self : & 3 ) -> & 1 { self + 1 }
fn main ( ) -> 0 {
let bday : 3 = Date :: new ( 5 , 14 , 2002 ) ;
}Статически вычислить след в памяти программы
Преобразовать выражения дуба и заявления в эквивалентные ИК -инструкции
Есть много разных обстоятельств, когда метод вызов действителен. Методы всегда принимают указатель на структуру в качестве аргумента . Тем не менее, объект, который вызывает метод, не требуется, чтобы быть указателем . Например, следующий код действителен: let bday: Date = Date::new(); bday.print(); Полем Переменная bday не является указателем, но метод .print() все еще может использоваться. Вот почему.
Когда компилятор видит вызов сплюснутого метода, ему необходимо найти способ преобразовать «выражение экземпляра» в указатель. Для переменных это легко: просто добавьте ссылку! Например, выражения, которые уже являются указателями, это еще проще: ничего не делайте! Для любого другого вида выражения, однако, это немного более словесное. Компилятор прокраивается в скрытой переменной, чтобы сохранить выражение, а затем собирает метод снова, используя переменную в качестве выражения экземпляра. Довольно круто, верно?
Соберите ИК -инструкции для цели
Target . Если вы реализуете каждую из инструкций IR для вашего языка, используя Target черту, то Oak может автоматически компилировать вплоть до вашего нового языка программирования или сборочного языка! Да, это так же просто, как кажется! Чтобы позволить пользователям читать документацию библиотек и файлов без доступа к Интернету, Oak предоставляет doc Subcommand. Это позволяет авторам добавлять атрибуты документации в свой код, чтобы помочь другим пользователям понять свой код или API без необходимости просеивать источник и прочитать комментарии.
Вот какой -то пример кода.
# [ std ]
# [ header ( "This file tests Oak's doc subcommand." ) ]
# [ doc ( "This constant is a constant." ) ]
const CONSTANT = 3 ;
// No doc attribute
const TEST = CONSTANT + 5 ;
# [ doc ( "This structure represents a given date in time.
A Date object has three members:
|Member|Value|
|-|-|
|`month: num` | The month component of the date |
|`day: num` | The day component of the date |
|`year: num` | The year component of the date |" ) ]
struct Date {
let month : num , day : num , year : num ;
# [ doc ( "The constructor used to create a date." ) ]
fn new ( month : num , day : num , year : num ) -> Date {
return [ month , day , year ] ;
}
# [ doc ( "Print the date object to STDOUT" ) ]
fn print ( self : & Date ) {
putnum ( self ->month ) ; putchar ( '/' ) ;
putnum ( self ->day ) ; putchar ( '/' ) ;
putnumln ( self ->year ) ;
}
}
# [ doc ( "This function takes a number `n` and returns `n * n`, or `n` squared." ) ]
fn square ( n : num ) -> num {
return n * n
}
fn main ( ) {
let d = Date :: new ( 5 , 14 , 2002 ) ;
d . print ( ) ;
} И вот пример использования подкоманда doc для печати форматированной документации на терминал.

Чтобы получить текущую сборку разработки, клонируйте репозиторий и установите его.
git clone https://github.com/adam-mcdaniel/oakc
cd oakc
cargo install -f --path . Чтобы получить текущую сборку выпуска, установите с crates.io.
# Also works for updating oakc
cargo install -f oakcЗатем дубовые файлы могут быть скомпилированы с бинарным материалом OAKC.
oak c examples/hello_world.ok -c
main.exeC Backend - любой компилятор GCC, который поддерживает C99
Go Backend - Golang 1.14 Компилятор
Backend TypeScript - TypeScript 3.9 Компилятор