Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Интерпретатор Lua

Сборка и использование

cd build
cmake ..
cmake --build .

Будет создан файл luai в папке build/bin

luai [PATH]

Структура и ключевые отличия от стандарта Lua

  1. Парсинг (ANTLR lexer/parser + конструирование AST)
  2. Компиляция в байткод
  3. Подготовка интерпретатора (добавление и компиляция библиотек)
  4. Интерпретация

Непосредственно выполнением байткода занимаются Испольнители (в коде: Executioner), содержащие весь необходимый для одного потока стейт. Данная архитектура выбрана для возможности переключать исполняемый поток используя только один "реальный" (системный) поток.

Многопоточность реализована в кооперативном виде, то есть поток не уйдёт с исполнения, пока явно не передаст очередь следующему потоку. В случае Lua 5.5 передача исполнения происходит в данный случаях:

  • return ( RET в байткоде )
  • Вызов coroutine.resume
  • Вызов coroutine.yield
  • Завершение выполнения потока

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

Ниже представлен список отличий от официального стандарта Lua 5.5

Грамматика

Отсутствуют:

  • Вызов функций с синтаксисом таблиц function {...}

Компилятор

Не поддерживается:

  • Присваивание и объявление при количестве значений больше количества аргументов
    arg_1, ..., arg_N = exp_1, ..., exp_M при M > N
    Замечание: в самом интерпретаторе такого ограничения нет, и если какое-то из выражений кладёт на стек несколько аргументов, то с правой стороны может быть больше выражений, чем с левой.
    Например: status, result = pcall(f).
    Данное ограничение на этапе компиляции введено, тк любое выражение (кроме отдельных стандартных функций) после себя оставляет хотя бы одно значение на стеке (для функций, не возвращающий ничего: nil).

  • Присваивание выражению exp = exp
    Примеры:

    f(1) = 2
    f().x = 3
    

Замечание: это не касается динамической индексации.
То есть выражения table[get_key1()]...[get_keyN] = value являются валидными

  • Атрибуты, помимо const

  • goto и лэйблы

Интерпретатор

Список стандартных библиотек и функций:

require(filepath) -> Any

type(v) -> String
callable(v) -> Boolean  
setmetatable(t, mt) -> Table  
getmetatable(t) -> Table | Nil  

ipairs(t) -> (Iterator, Table, Int)  
pairs(t) -> (Iterator, Table, Any)  

iterator:next() -> (Any, Any) | Nil  

map(t, f) -> Table  
imap(t, f) -> Table  
io
file:close() -> Nil  
file:flush() -> Nil  
file:lines(...) -> Table  
file:read(...) -> String  
file:seek(whence, offset) -> Nil  
file:write(...) -> Int  
io.close(file) -> Nil  
io.flush() -> Nil  
io.input(file) -> File  
io.lines(filename, ...) -> String  
io.open(filename, mode) -> File  
io.output(file) -> File  
io.read(...) -> String  
io.type(obj) -> String  
io.write(...) -> Int  
io.print(...) -> Int  
coroutine
coroutine.close(co) -> Boolean  
coroutine.create(f) -> Thread  
coroutine.isyieldable(co) -> Boolean  
coroutine.ready(co) -> Boolean  
coroutine.resume(co, ...) -> Boolean, ...  
coroutine.running() -> Thread, Boolean  
coroutine.status(co) -> String  
coroutine.wrap(f) -> Function  
coroutine.yield(...) -> ...  
math
math.abs(x) -> Number  
math.acos(x) -> Number  
math.asin(x) -> Number  
math.atan(x) -> Number  
math.ceil(x) -> Integer  
math.cos(x) -> Number  
math.deg(x) -> Number  
math.exp(x) -> Number  
math.floor(x) -> Integer  
math.fmod(x, y) -> Number  
math.frexp(x) -> Number, Integer  
math.ldexp(m, e) -> Number  
math.log(x, base) -> Number  
math.max(x, ...) -> Number  
math.min(x, ...) -> Number  
math.modf(x) -> Number, Number  
math.rad(x) -> Number  
math.random(m, n) -> Number  
math.randomseed(x) -> nil  
math.sin(x) -> Number  
math.sqrt(x) -> Number  
math.tan(x) -> Number  
math.tointeger(x) -> Integer or nil  
math.type(x) -> String  
math.ult(m, n) -> Boolean  
string
string.byte(s, i, j) -> Integer, ...  
string.char(...) -> String  
string.find(s, pattern, init, plain) -> Integer, Integer, ...  
string.format(formatstring, ...) -> String  
string.len(s) -> Integer  
string.lower(s) -> String  
string.rep(s, n, sep) -> String  
string.reverse(s) -> String  
string.sub(s, i, j) -> String  
string.upper(s) -> String  
os
os.clock() -> Number
os.date(format, time) -> String | Table
os.difftime(t2, t1) -> Number
os.execute(command) -> Boolean, String, Integer
os.exit(code, close) -> (не возвращает)
os.getenv(varname) -> String | Nil
os.remove(filename) -> Boolean | (Nil, String, Integer)
os.rename(oldname, newname) -> Boolean | (Nil, String, Integer)
os.setlocale(locale, category) -> String | Nil
os.time(table) -> Integer
os.tmpname() -> String

dynlib

dynlib.open(path) -> DynHandle
dynlib.close(handle) -> Nil

dynhandle:close() -> Nil
dynhandle:get() -> Function

Не поддерживается:

  • Индексация таблиц с Nil, Table и Thread

  • Установка атрибутов на переменные

  • Перегрузка операторов:

    • Индексации
    • Сравнения (>, >=, <, <=)
    • Конкатенации
    • Арифметических операторов (+, -, *, /, //, %)
    • Унартных операторов (#, -, )

Особенности

  • Ошибки, не пойманные в pcall, будут останавливать выполнение всего интерпретатора, а не только потока, получившего ошибку.

  • Если внутри pcall создать корутину и она выбросит ошибку, то эта ошибка не будет поймана, т.к. она относится к другому потоку.

  • io.read не читает значения по аналогии с scanf - только возвращает всю прочитанную строку

  • require при первой загрузке модуля выполнит его и вернёт всё значения, которые были возвращены с return, но сохранит только первое, и вернёт только его при повторной загрузке модуля

    -- mod.lua
    return 1, 2, 3
    -- main.lua
    t1, t2, t3 = require("mod.lua") -- 1, 2, 3
    t1, t2, t3 = require("mod.lua") -- 1, nil, nil

Динамические библиотеки

Написание своих библиотек

При написании динамических библиотек, необходимо подключить заголовок связку. При этом в коде необходимо предоставить имплементацию функции void inner_prepare(), которая вызывается перед загрузкой библиотеки. Все экспортируемые функции должны зарегистрированы при подготовке с помощью функции void register_function(cxx_func func, const std::string& name).

#define DYNLIB_C_API_IMPL
#include "src/Interpreter/libs/dynlib/c_api.h"

std::vector< std::shared_ptr<Value> > my_dll_function (
    Executioner* exec,
    std::vector< std::shared_ptr<Value> > &args 
) {
    // ...
}

void inner_prepare() {
    register_function((cxx_func) &my_dll_function, "my_dll_function");
}

Для аллокации значений следует использовать не std::make_shared, а функции, представленные в HostAllocator_t host, поскольку иначе при выгрузке библиотеки код, относящийся к shared_ptr и значениям (деструкторы) уже не будет существовать, что вызовет неопределённое поведение.

Образец динамической библиотеки и её компиляции представлен в примерах.

Под капотом

При вызове dynlib.open после загрузки библиотеки:

  1. Устанавливается HostAllocator_t host через void set_allocator(HostAllocator_t host)
  2. Вызывается void prepare(), который перевызывает void inner_prepare() (перевызов необходим, тк сам prepare, как и все API функции, объявлен как extern "C" и позволяет писать только C код внутри)
  3. Вызывется void insert_registred_functions(void* interp), который перевызывается и вставляет в интерпретатор все названия объявленных функций
  4. Находится и сохраняется внутри DynHandle функция void* find_function(const char* name), возвращающая адресс зарегестрированной функции по её названию.

About

Lua 5.5 large subset interpreter

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages