1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/valkmjolnir-Nasal-Interpreter

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Это зеркальный репозиторий, синхронизируется ежедневно с исходного репозитория.
Клонировать/Скачать
tutorial.md 27 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 4 месяцев назад 7b70f75

Tutorial

Nasal легко учить. После прочтения этого туториала, примерно за 15 минут, вы сможете полноценно использовать nasal.

Содержание

Basic Type

none — это тип ошибки, используемый для прерывания выполнения. Этот тип не создаётся пользовательской программой.

nil — это нулевой тип. Как null.

var spc = nil;

num имеет 3 формата: dec, hex и oct. Используется IEEE754 double для хранения.

# этот язык использует '#' для написания заметок
var n = 2.71828;    # dec
var n = 2.147e16;   # dec
var n = 1e-10;      # dec
var n = 0xAA55;     # hex
var n = 0o170001;   # oct

# внимание: true и false также полезны в nasal сейчас
var n = true;       # на самом деле n теперь равен 1.0
var n = false;      # на самом деле n теперь равен 0.0

str имеет 3 формата. Третий используется для объявления символа.

var s = 'str';
var s = "another string";
var s = `c`;
# некоторые специальные символы разрешены в этом языке:
'\a'; '\b'; '\e'; '\f';
'\n'; '\r'; '\t'; '\v';
'\0'; '\\'; '\?'; '\'';
'\"';

vec имеет неограниченную длину и может хранить все типы значений.

var vec = [];
var vec = [0, nil, {}, [], func(){return 0}];
append(vec, 0, 1, 2);

hash — это хэш-карта (или как dict в python), которая хранит значения со строками/идентификаторами в качестве ключа.

var hash = {
    member1: nil,
    member2: "str",
    "member3": "имя участника также может быть строковой константой",
    funct: func() {
        return me.member2~me.member3;
    }
};

func — это функциональный тип (на самом деле это lambda).

var f = func(x, y, z) {
    return nil;
}
# функция может быть объявлена без параметров и `(`, `)`
var f = func {
    return 114514;
}
var f = func(x, y, z, deft = 1) {
    return x+y+z+deft;
}
var f = func(args...) {
    var sum = 0;
    foreach(var i; args) {
        sum += i;
    }
    return sum;
}

upval используется для хранения апвайлов, используется в vm, чтобы убедиться, что замыкание работает правильно.

ghost используется для хранения других сложных типов данных C/C++. Этот тип создаётся родной функцией nasal. Если хотите определить новый тип данных, посмотрите, как добавить родные функции, отредактировав код.

Операторы

Nasal имеет основные математические операторы + - * / и специальный оператор ~, который объединяет строки или векторы.

1+2-(1+3)*(2+4)/(16-9);
"str1"~"str2";
[0]~[1]; # должно быть [0, 1]

Для условных выражений операторы == != < > <= >= используются для сравнения двух значений. and or имеют ту же функцию, что и C/C++ && ||.

1+1 and (1<0 or 1>0);
1<=0 and 1>=0;
1==0 or 1!=0;

Унарные операторы - ! имеют ту же функцию, что и в C/C++.

-1;
!0;

Побитовые операторы ~ | & ^ имеют ту же функцию, что и в C/C++.

# эти операторы будут:
# 1. преобразовывать f64 в i32 (static_cast<int32_t>)
# 2. выполнять побитовую функцию

~0x80000000; # not 2147483647
0x8|0x1;     # or
0x1&0x2;     # and
0x8^0x1;     # xor

Операторы = += -= *= /= ~= ^= &= |= используются в выражениях присваивания.

a = b = c = d = 1;
a += 1;
a -= 1;
a *= 1;
a /= 1;
a ~= "string";

a ^= 0xff;
a &= 0xca;
a |= 0xba;

a = [0];
a ~= [1]; # должно быть [0, 1]

Оператор ?? используется для проверки того, является ли значение слева nil, и если нет, возвращает значение справа:


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

* *Оператор ?.* используется для получения значения элемента хеша, если значение слева не равно nil. Если значение слева равно nil или не является хешем, то это вызовет ошибку и завершение программы.

Пример использования оператора ?.:

var a = nil; print(a?.try_get); # nil

var a = {try_get: "congrats!"}; print(a?.try_get); # "congrats!"


* Определяются переменные:
    * одна переменная: `var a = 1;`;
    * несколько переменных из вектора: `var (a, b, c) = [0, 1, 2];`;
    * несколько переменных из кортежа: `var (a, b, c) = (0, 1, 2);`.

Примеры определения переменных:
```javascript
var a = 1;             # define single variable
var (a, b, c) = [0, 1, 2]; # define multiple variables from a vector
var (a, b, c) = (0, 1, 2); # define multiple variables from a tuple
  • В Nasal есть специальные глобальные символы:
    • globals — хеш-карта, включающая все глобальные символы и их значения;
    • arg — в глобальной области видимости arg — это аргументы командной строки, а в локальной области видимости — динамические аргументы этого вызова функции.

Например:

var a = 1;
println(globals); # will print {a:1}
# nasal a b c
println(arg); # will print ["a", "b", "c"]

func() {
    println(arg);
}(1, 2, 3);   # will print [1, 2, 3]
  • Для обмена значениями двух переменных часто используется следующее присваивание:
(a, b[0], c.d) = [0, 1, 2];
(a, b[1], c.e) = (0, 1, 2);
(a, b) = (b, a);
  • Условное выражение: в Nasal существует новое ключевое слово elsif, которое выполняет те же функции, что и else if.

Пример условного выражения:

if (1) {
    ;
} elsif (2) {
    ;
} else if (3) {
    ;
} else {
    ;
}
  • Циклы while и for похожи на циклы в языках C/C++.

Пример цикла while:

while(condition) {
    continue;
}

Пример цикла for:

for(var i = 0; i<10; i += 1) {
    break;
}

Nasal также имеет два вида циклов, которые перебирают элементы вектора:

  • Цикл forindex получает индекс вектора. Индекс будет равен от 0 до size(elem)-1.

Пример цикла forindex:

forindex(var i; elem) {
    print(elem[i]);
}
  • Цикл foreach получает элемент вектора. Элемент будет равен elem[0] до elem[size(elem)-1].

Пример цикла foreach:

foreach(var i; elem) {
    print(i);
}
  • Специальный синтаксис Subvec помогает пользователю создавать новый вектор, получая значения по одному индексу или значения по индексам в диапазоне от старого вектора. Если в скобках только один индекс, то мы получим значение напрямую. Использование индекса для поиска одного элемента в строке даст ascii-номер этого символа. Если вы хотите получить символ, используйте встроенную функцию chr().

Пример использования Subvec:

a[0];
a[-1, 1, 0:2, 0:, :3, :, nil:8, 3:nil, nil:nil];
"hello world"[0];
  • Специальные функции вызова:

Это не очень эффективно, потому что хеш-карты используют строку в качестве ключа для сравнения. Но если это действительно полезно, эффективность может быть не так важна...

Пример специального вызова функции:

f(x:0, y:nil, z:[]);
  • Лямбда-функции:

Также функции имеют такое использование:

Пример лямбда-функций:

func(x, y) {
    return x+y
}(0, 1);
func(x) {
    return 1/(1+math.exp(-x));
}(0.5);

Существует интересный тестовый файл y-combinator.nas, попробуйте его для развлечения:

var fib = func(f) {
    return f(f);
}(
    func(f) {
        return func(x) {
            if(x<2) return x;
            return f(f)(x-1)+f(f)(x-2);
        }
    }
);
  • Замыкание означает, что вы можете получить переменную, которая не находится в локальной области действия вызванной функции. Здесь результат равен 1:

Пример замыкания:

var f = func() {
    var a = 1;
    return func() {return a;};
}
print(f()());

Замыкания упрощают ООП.

Пример ООП с использованием замыканий:

var student = func(n, a) {
    var (name, age) = (n, a);
    return {
        print_info: func()  {println(name, ' ', age);},
        set_age:    func(a) {age = a;},
        get_age:    func()  {return age;},
        set_name:   func(n) {name = n;},
        get_name:   func()  {return name;}
    };
}
  • Черта:

Ещё один способ ООП — это черта. Когда хеш имеет член с именем parents и значение типа вектор, то при попытке найти член, которого нет в этом хеше, виртуальная машина будет искать член в parents. Если есть хеш, у которого есть этот член, вы получите значение члена. Используя этот механизм, мы можем реализовать ООП. Перевод текста с английского на русский язык:

Подобным образом, результатом будет «114514»:

var trait = {
    get: func {return me.val;},
    set: func(x) {me.val = x;}
};

var class = {
    new: func() {
        return {
            val: nil,
            parents: [trait]
        };
    }
};
var a = class.new();
a.set(114514);
println(a.get());

Первая виртуальная машина не может найти член set в хэше a, но в a.parents есть хэш trait, который имеет член set, поэтому мы получаем set. Переменная me указывает на хэш a, поэтому мы меняем a.val. И get имеет тот же процесс.

И мы должны напомнить вам, что если вы сделаете это:

var trait = {
    get: func {return me.val;},
    set: func(x) {me.val = x;}
};

var class = {
    new: func() {
        return {
            val: nil,
            parents: [trait]
        };
    }
};
var a = class.new();
var b = class.new();
a.set(114);
b.set(514);
println(a.get());
println(b.get());

var c = a.get;
var d = b.get;

println(c());
println(c());
println(d());
println(d());

Вы получите такой результат сейчас:

114
514
514
514

Потому что a.get установит me=a в trait.get. Затем b.get сделает me=b. Так что фактически c тоже является b.get после выполнения var d=b.get. Если вы хотите использовать этот трюк для повышения эффективности программы, вы должны знать этот специальный механизм.

Импорт нескольких файлов/модулей

Подробнее см. в namespace.md

Собственные функции и импорт модулей

Эта часть показывает, как мы добавляем собственные функции в этом интерпретаторе. Если вас интересует эта часть, это может вам помочь. И...

ОСТОРОЖНО: Если вы хотите добавить свои собственные функции без изменения исходного кода, см. раздел module после этой части.

Если вы действительно хотите изменить исходный код, проверьте встроенные функции в lib.nas и посмотрите пример ниже.

Определение:

// вы также можете использовать макрос для определения одного.
var builtin_print(context*, gc*);

Затем завершите эту функцию, используя C++:

var builtin_print(context* ctx, gc* ngc) {
    // находим значение с индексом, начинающимся с 1
    // потому что local[0] зарезервирован для значения 'me'
    for(auto& i : ctx->localr[1].vec().elems) {
        std::cout << i;
    }
    std::cout << std::flush;
    // генерируем возвращаемое значение,
    // используем ngc::alloc(type) для создания нового значения
    // или используем зарезервированные ссылки nil/one/zero
    return nil;
}

При запуске встроенной функции alloc будет выполняться более одного раза, это может вызвать маркировку-очистку в gc::alloc. Полученное ранее значение будет собрано, но всё ещё используется во встроенной функции, это вызовет фатальную ошибку.

Поэтому используйте gc::temp во встроенных функциях для временного хранения управляемого gc значения, которое вы хотите вернуть позже. Вот так:

var builtin_keys(context* ctx, gc* ngc) {
    auto hash = ctx->localr[1];
    if (hash.type!=vm_hash && hash.type!=vm_map) {
        return nas_err("keys", "\"hash\" must be hash");
    }
    // используем gc.temp для хранения управляемого значением gc, чтобы избежать очистки
    auto res = ngc->temp = ngc->alloc(vm_vec);
    auto& vec = res.vec().elems;
    if (hash.type==vm_hash) {
        for(const auto& iter : hash.hash().elems) {
            vec.push_back(ngc->newstr(iter.first));
        }
    } else {
        for(const auto& iter : hash.map().mapper) {
            vec.push_back(ngc->newstr(iter.first));
        }
    }
    ngc->temp = nil;
    return res;
}

После этого зарегистрируйте имя встроенной функции (в nasal) и указатель функции в этой таблице:

nasal_builtin_table builtin[] = {
    {"__print", builtin_print},
    {nullptr,  nullptr}
};

Наконец, оберните __print в носовой файл:

var print = func(elems...) {
    return __print(elems);
};

На самом деле аргументы, которые использует __print, не являются необходимыми. Так что писать это так тоже правильно:

var print = func(elems...) {
    return __print;
};

Если вы не обернёте встроенную функцию в обычную носовую функцию, эта собственная функция может вызвать segmentation fault при поиске аргументов.

Используйте import("filename.nas") для получения носового файла, включая... Функции в Nasal: использование модулей C++

Если вы хотите использовать встроенные функции, то можете это сделать. Также есть ещё один способ импорта назальных файлов, оба способа имеют одинаковую функцию:

use dirname.dirname.filename;
import("./dirname/dirname/filename.nas");

Модули C++ (для разработчиков библиотек)

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

Функции, используемые для загрузки динамических библиотек, добавляются в std/dylib.nas:

var dlopen = func(libname) {
    ...
}

var dlclose = func(lib) {
    ...
}

var dlcall = func(ptr, args...) {
    ...
}

var limitcall = func(arg_size = 0) {
    ...
}

Как видите, эти функции используются для загрузки динамических библиотек в среду выполнения назального кода и их выполнения. Давайте посмотрим, как они работают.

Сначала напишите файл C++, который вы хотите преобразовать в динамическую библиотеку, используя fib.cpp в качестве примера (примеры кодов находятся в папке ./module):

// добавьте заголовочный файл nasal.h, чтобы получить API
#include "nasal.h"
double fibonaci(double x) {
    if (x<=2) {
        return x;
    }
    return fibonaci(x-1)+fibonaci(x-2);
}
// пример списка параметров функций модуля
var fib(var* args, usize size, gc* ngc) {
    if (!size) {
        return nas_err("fib", "не хватает аргументов");
    }
    // аргументы генерируются в vm_vec: args
    // получите значения из вектора, которые должны быть использованы здесь
    var num = args[0];
    // если вы хотите, чтобы ваша функция была более безопасной, попробуйте это
    // nas_err выведет информацию об ошибке на экран
    // и вернёт vm_null для прерывания выполнения
    if(num.type!=vm_num) {
        return nas_err("extern_fib", "\"num\" должно быть числом");
    }
    // хорошо, вы должны знать, что vm_num теперь не управляется сборщиком мусора
    // если хотите вернуть объект gc, используйте ngc->alloc(type)
    // использование gc такое же, как добавление встроенной функции
    return var::num(fibonaci(num.tonum()));
}

// затем поместите имя функции и адрес в эту таблицу
// убедитесь, что конец таблицы равен {nullptr,nullptr}
module_func_info func_tbl[] = {
    {"fib", fib},
    {nullptr, nullptr}
};

// необходимо написать эту функцию, она поможет назальному коду
// получить указатель на функцию по имени
// причина использования этого способа получения указателя на функцию
// заключается в том, что `var` имеет конструкторы, которые несовместимы с C
// поэтому "extern "C" var fib" может вызывать предупреждения компиляции
NASAL_EXTERN module_func_info* get() {
    return func_tbl;
}

Затем скомпилируйте этот fib.cpp в динамическую библиотеку.

Linux (*.so):

clang++ -c -O3 fib.cpp -fPIC -o fib.o

clang++ -shared -o libfib.so fib.o

Mac (*.so & *.dylib): аналогично Linux.

Windows (*.dll):

g++ -c -O3 fib.cpp -fPIC -o fib.o

g++ -shared -o libfib.dll fib.o

Затем мы пишем тестовый назальный файл для запуска этой функции fib:

use std.dylib;
var dlhandle = dylib.dlopen("libfib");
var fib = dlhandle.fib;
for(var i = 1; i<30; i += 1)
    println(dylib.dlcall(fib, i));
dylib.dlclose(dlhandle.lib);

Функция dylib.dlopen используется для загрузки динамической библиотеки и получения адреса функции.

Функция dylib.dlcall используется для вызова функции, первый аргумент — это адрес функции, убедитесь, что этот аргумент является vm_obj и type=obj_extern.

Функция dylib.dlclose используется для выгрузки библиотеки, в момент вызова функции все адреса функций, полученные из неё, становятся недействительными.

Функция dylib.limitcall используется для получения функции dlcall, которая имеет ограниченный размер параметра, эта функция может повысить производительность вашего кода, поскольку она не использует vm_vec для хранения аргументов, а вместо этого использует локальную область видимости для их хранения, так что это может избежать частого сбора мусора. И приведённый выше код также можно записать следующим образом:

use std.dylib;
var dlhandle = dylib.dlopen("libfib");
var fib = dlhandle.fib;
var invoke = dylib.limitcall(1); # это означает, что вызываемая функция имеет только один параметр
for(var i = 1; i<30; i += 1)
    println(invoke(fib, i));
dylib.dlclose(dlhandle.lib);

Если у вас всё получилось, поздравляем!

./nasal a.nas
1
2 
3 
5 
8 
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
``` ## Ghost Type (для разработчиков lib)

Теперь довольно просто создать нового ghost самостоятельно.
Посмотрите на пример ниже:

```c++
const auto ghost_for_test = "ghost_for_test";

// объявляем деструктор для типа ghost
void ghost_for_test_destructor(void* ptr) {
    std::cout << "ghost_for_test::destructor (0x";
    std::cout << std::hex << reinterpret_cast<u64>(ptr) << std::dec << ") {\n";
    delete static_cast<u32*>(ptr);
    std::cout << "    delete 0x" << std::hex;
    std::cout << reinterpret_cast<u64>(ptr) << std::dec << ";\n";
    std::cout << "}\n";
}

var create_new_ghost(var* args, usize size, gc* ngc) {
    var res = ngc->alloc(vm_obj);
    // создаём тип ghost
    res.ghost().set(ghost_for_test, ghost_for_test_destructor, new u32);
    return res;
}

var set_new_ghost(var* args, usize size, gc* ngc) {
    var res = args[0];
    if (!res.object_check(ghost_for_test)) {
        std::cout << "set_new_ghost: not ghost for test type.\n";
        return nil;
    }
    f64 num = args[1].num();
    *(reinterpret_cast<u32*>(res.ghost().pointer)) = static_cast<u32>(num);
    std::cout << "set_new_ghost: successfully set ghost = " << num << "\n";
    return nil;
}

var print_new_ghost(var* args, usize size, gc* ngc) {
    var res = args[0];
    // проверяем тип ghost по имени типа
    if (!res.object_check(ghost_for_test)) {
        std::cout << "print_new_ghost: not ghost for test type.\n";
        return nil;
    }
    std::cout << "print_new_ghost: " << res.ghost() << " result = "
              << *((u32*)res.ghost().pointer) << "\n";
    return nil;
}

Мы используем эту функцию для создания нового типа ghost:

void nas_ghost::set(const std::string&, nasal::nas_ghost::destructor, void*);

const std::string& — это имя типа ghost.

nasal::nas_ghost::destructor — указатель деструктора типа ghost.

void* — указатель экземпляра типа ghost.

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

bool var::object_check(const std::string&);

Параметр — это имя типа ghost.

Комментарий ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://gitlife.ru/oschina-mirror/valkmjolnir-Nasal-Interpreter.git
git@gitlife.ru:oschina-mirror/valkmjolnir-Nasal-Interpreter.git
oschina-mirror
valkmjolnir-Nasal-Interpreter
valkmjolnir-Nasal-Interpreter
master