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

OSCHINA-MIRROR/valkmjolnir-Nasal-Interpreter

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

История разработки

Содержание

  • Парсер
    • v1.0
  • Абстрактное синтаксическое дерево
    • v1.2
    • v2.0
    • v3.0
    • v5.0
    • v11.0
  • Виртуальная машина байт-кода
    • v4.0
    • v5.0
    • v6.0
    • v6.5
    • v7.0
    • v8.0
    • v9.0
    • v10.0
  • Примечания к выпуску
    • v8.0
    • v11.0
    • v11.1

Парсер LL(1) парсер со специальной проверкой.

(var a,b,c)=[{b:nil},[1,2],func return 0;];
(a.b,b[0],c)=(1,2,3);

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

var f=func(x,y,z){return x+y+z}
(a,b,c)=(0,1,2);

Это будет распознано как это:

var f=func(x,y,z){return x+y+z}(a,b,c)
=(0,1,2);

и вызывает фатальную синтаксическую ошибку. И я попробовал эту программу в консоли flightgear nasal. Она также обнаружила, что это синтаксическая ошибка. Я думаю, что это серьёзный недостаток дизайна. Чтобы избежать этой синтаксической ошибки, измените программу следующим образом, просто добавьте точку с запятой:

var f=func(x,y,z){return x+y+z};
                               ^ здесь
(a,b,c)=(0,1,2);

Версия 1.0 парсера (последнее обновление 2019/10/14)

Первая полностью функциональная версия парсера. До версии 1.0 я много раз пытался создать правильный парсер. Наконец, я изучил LL(1) и LL(k) и написал парсер для математических формул в версии 0.16 (последнее обновление 2019/9/14). В версиях 0.17 (2019/9/15), 0.18 (2019/9/18), 0.19 (2019/10/1) я с удовольствием играл с парсером, а после этого написал версию 1.0. Этот проект начался 25 июля 2019 года.

Абстрактное синтаксическое дерево

Версия 1.2 ast (последнее обновление 2019/10/31)

Ast был завершён в этой версии.

Версия 2.0 ast (последнее обновление 2020/8/31)

Завершённый интерпретатор ast с незавершёнными библиотечными функциями.

Версия 3.0 ast (последнее обновление 2020/10/23)

Ast переработан и теперь его легче читать и поддерживать. Интерпретатор ast использует новые методы, поэтому он может выполнять коды более эффективно. Теперь вы можете добавлять свои собственные функции в качестве встроенных функций в этот интерпретатор! Я решил сохранить интерпретатор ast после выпуска v4.0. Потому что мне потребовалось много времени, чтобы подумать и написать...

Версия 5.0 ast (последнее обновление 2021/3/7)

Я передумал. AST-интерпретатор оставляет мне слишком много дел. Если я продолжу сохранять этот интерпретатор, мне будет сложнее сделать виртуальную машину байт-кода более эффективной.

Версия 11.0 ast (последняя)

Измените структуру ast. Теперь мы используем шаблон посетителя.

Виртуальная машина байт-кода

Версия 4.0 vm (последнее обновление 2020/12/17)

Я только что закончил первую версию интерпретатора байт-кода. Этот интерпретатор всё ещё находится на стадии тестирования. После этого теста я выпущу версию 4.0! Сейчас я пытаюсь найти скрытые ошибки в этом интерпретаторе. Надеюсь, вы сможете мне помочь! :) Вот пример кода байта ниже:

for(var i=0;i<4000000;i+=1);
.number 0
.number 4e+006
.number 1
.symbol i
0x00000000: pzero  0x00000000
0x00000001: loadg  0x00000000 (i)
0x00000002: callg  0x00000000 (i)
0x00000003: pnum   0x00000001 (4e+006)
0x00000004: less   0x00000000
0x00000005: jf     0x0000000b
0x00000006: pone   0x00000000
0x00000007: mcallg 0x00000000 (i)
0x00000008: addeq  0x00000000
0x00000009: pop    0x00000000
0x0000000a: jmp    0x00000002
0x0000000b: nop    0x00000000 **Версия 5.0 виртуальной машины (последнее обновление 2021/3/7)**

Я решил оптимизировать байт-код виртуальной машины в этой версии.

Потому что подсчёт от `0` до `4000000-1` занимает более 1,5 секунд. Это неэффективно!

**Обновление от 23 января 2021 года:** теперь подсчёт от `0` до `4000000-1` выполняется за 1,5 секунды.

**Версия 6.0 виртуальной машины (последнее обновление 2021/6/1)**

Используйте `loadg`/`loadl`/`callg`/`calll`/`mcallg`/`mcalll`, чтобы избежать ветвлений.

Удалите тип `vm_scop`.

Используйте константу `vm_num`, чтобы избегать частого создания и удаления объектов.

Измените сборщик мусора с подсчёта ссылок на пометки и очистку.

Операторы `vapp` и `newf` используют `.num` для уменьшения размера `exec_code`.

**Обновление от 3 апреля 2021 года**: теперь подсчёт от `0` до `4e6-1` выполняется за 0,8 секунды.

**Обновление от 19 апреля 2021 года**: теперь подсчёт от `0` до `4e6-1` выполняется за 0,4 секунды.

В этом обновлении я изменил глобальную и локальную область видимости с `unordered_map` на `vector`.

Поэтому генератор байт-кода сильно изменился.

```javascript
for(var i=0;i<4000000;i+=1);
.number 4e+006
0x00000000: intg   0x00000001
0x00000001: pzero  0x00000000
0x00000002: loadg  0x00000000
0x00000003: callg  0x00000000
0x00000004: pnum   0x00000000 (4e+006)
0x00000005: less   0x00000000
0x00000006: jf     0x0000000c
0x00000007: pone   0x00000000
0x00000008: mcallg 0x00000000
0x00000009: addeq  0x00000000
0x0000000a: pop    0x00000000
0x0000000b: jmp    0x00000003
0x0000000c: nop    0x00000000

Версия 6.5 виртуальной машины (последнее обновление 2021/6/24)

Обновление от 31 мая 2021 года:

Теперь сборщик мусора может правильно собирать мусор без повторной сборки, что может привести к фатальной ошибке.

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

Лучше использовать setsize и присваивание для получения большого массива, append в этом случае работает очень медленно.

Обновление от 3 июня 2021 года:

Исправлена ошибка, из-за которой сборщик мусора всё ещё повторно собирал мусор. На этот раз я использовал три состояния меток, чтобы убедиться, что мусор готов к сбору.

Изменил callf на callfv и callfh. И callfv извлекает аргументы из val_stack напрямую вместо использования vm_vec, что не очень эффективно.

Лучше используйте callfv, а не callfh, callfh будет извлекать vm_hash из стека и анализировать его, делая этот процесс медленным.

var f=func(x,y){return x+y;}
f(1024,2048);
.number 1024
.number 2048
.symbol x   
.symbol y
0x00000000: intg   0x00000001
0x00000001: newf   0x00000007
0x00000002: intl   0x00000003
0x00000003: offset 0x00000001
0x00000004: para   0x00000000 (x)
0x00000005: para   0x00000001 (y)
0x00000006: jmp    0x0000000b
0x00000007: calll  0x00000001
0x00000008: calll  0x00000002
0x00000009: add    0x00000000
0x0000000a: ret    0x00000000
0x0000000b: loadg  0x00000000
0x0000000c: callg  0x00000000
0x0000000d: pnum   0x00000000 (1024)
0x0000000e: pnum   0x00000001 (2048)
0x0000000f: callfv 0x00000002
0x00000010: pop    0x00000000
0x00000011: nop    0x00000000

Обновление от 21 июня 2021 года: Теперь сборщик мусора не будет собирать nullptr. И функция присваивания завершена, теперь разрешены следующие виды присваиваний:

var f=func()
{
    var _=[{_:0},{_:1}];
    return func(x)
    {
        return _[x];
    }
}
var m=f();
m(0)._=m(1)._=10;

[0,1,2][1:2][0]=0;

В старой версии парсер проверял это левое значение и сообщал, что эти виды левого значения не разрешены (плохое lvalue).

Но теперь это работает. И вы можете увидеть его использование, прочитав код выше. Чтобы убедиться, что это присваивание работает правильно, codegen сгенерирует байт-код с помощью codegen::call_gen(), а не codegen::mcall_gen(). А последний дочерний элемент ast будет сгенерирован с помощью codegen::mcall_gen(). Таким образом, байт-код теперь совершенно другой:

.number 10
.number 2
.symbol _
.symbol x
0x00000000: intg   0x00000002
0x00000001: newf   0x00000005
0x00000002: intl   0x00000002
0x00000003: offset 0x00000001
0x00000004: jmp    0x00000017
0x00000005: newh   0x00000000
0x00000006: pzero  0x00000000
0x00000007: happ   0x00000000 (_)
0x00000008: newh   0x00000000
0x00000009: pone   0x00000000
0x0000000a: happ   0x00000000 (_)
0x0000000b: newv   0x00000002
0x0000000c: loadl  0x00000001
0x0000000d: newf   0x00000012
0x0000000e: intl   0x00000003
0x0000000f:
``` В запросе представлен фрагмент программного кода, написанного на языке x86 Assembly. Это язык ассемблера для микропроцессоров семейства x86, который представляет собой машинно-ориентированный язык программирования низкого уровня.

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

**Перевод текста запроса:**

Как видно из представленного байт-кода, частота использования операндов mcall, mcallv и mcallh будет уменьшаться, а call, callv, callh, callfv и callfh — наоборот.

Из-за новой структуры mcall, стек addr_stack, используемый для хранения адреса памяти, удаляется из виртуальной машины (vm), и теперь vm использует nas_val* mem_addr для хранения адреса памяти. Это не вызовет фатальных ошибок, поскольку адрес памяти используется *немедленно* после его получения.

### Версия 7.0 виртуальной машины (последнее обновление от 2021/10/8)

Обновление от 26 июня 2021 года:

Диспетчеризация инструкций изменена с вызова-потоков на вычисляемый переход (с встроенной функцией). После изменения способа диспетчеризации инструкций в виртуальной машине произошли значительные улучшения. Теперь виртуальная машина может запускать тесты/bigloop и test/pi за 0,2 секунды! А тест/fib выполняется за 0,8 секунды на Linux. Вы можете увидеть данные о времени выполнения ниже, в разделе «Тестовые данные».

Эта версия использует расширение g++ «метки как значения», которое также поддерживается clang++. (Но я не знаю, поддерживает ли это MSVC).

Также произошло изменение в сборщике мусора (gc): глобальный std::vector был удалён, теперь все глобальные значения хранятся в стеке (от val_stack+0 до val_stack+intg-1).

Обновление от 29 июня 2021 года:

Добавлены инструкции, выполняющие константные значения: op_addc, op_subc, op_mulc, op_divc, op_lnkc, op_addeqc, op_subeqc, op_muleqc, op_diveqc, op_lnkeqc.

Теперь байт-код теста/bigloop.nas выглядит так:
```x86asm
.number 4e+006
.number 1
0x00000000: intg   0x00000001
0x00000001: pzero  0x00000000
0x00000002: loadg  0x00000000
0x00000003: callg  0x00000000
0x00000004: pnum   0x00000000 (4000000)
0x00000005: less   0x00000000
0x00000006: jf     0x0000000b
0x00000007: mcallg 0x00000000
0x00000008: addeqc 0x00000001 (1)
0x00000009: pop    0x00000000
0x0000000a: jmp    0x00000003
0x0000000b: nop    0x00000000

И этот тестовый файл выполняется за 0,1 секунды после этого обновления. Большинство вычислений ускорено.

Кроме того, назначение байт-кода сильно изменилось. Теперь первый идентификатор, вызываемый при назначении, будет использовать op_load для назначения вместо op_meq, op_pop.

var (a,b)=(1,2);
a=b=0;
.number 2
0x00000000: intg   0x00000002
0x00000001: pone   0x00000000
0x00000002: loadg  0x00000000
0x00000003: pnum   0x00000000 (2)
0x00000004: loadg  0x00000001
0x00000005: pzero  0x00000000
0x00000006: mcallg 0x00000001
0x00000007: meq    0x00000000 (b=2 use meq,pop->a)
0x00000008: loadg  0x00000000 (a=b use loadg)
0x00000009: nop    0x00000000

Версия 8.0 виртуальной машины (последнее обновление от 2022/2/12)

Обновление от 8 октября 2021 года:

В этой версии vm_nil и vm_num теперь не управляются сборщиком мусора (gc), что снижает использование gc::alloc и повышает эффективность выполнения.

Добавлен новый тип значений: vm_obj. Этот тип зарезервирован для пользователей, чтобы они могли определять свои собственные типы значений. Соответствующий API будет добавлен в будущем.

Полностью функциональная замыкание: добавлены новые операнды, которые получают и устанавливают upvalues. Удалён старый... Operand op_offset.

Обновление от 13 октября 2021 года:

Формат вывода информации о байт-кодах изменяется следующим образом:

0x000002f2: newf   0x2f6
0x000002f3: intl   0x2
0x000002f4: para   0x3e ("x")
0x000002f5: jmp    0x309
0x000002f6: calll  0x1
0x000002f7: lessc  0x0 (2)
0x000002f8: jf     0x2fb
0x000002f9: calll  0x1
0x000002fa: ret
0x000002fb: upval  0x0[0x1]
0x000002fc: upval  0x0[0x1]
0x000002fd: callfv 0x1
0x000002fe: calll  0x1
0x000002ff: subc   0x1d (1)
0x00000300: callfv 0x1
0x00000301: upval  0x0[0x1]
0x00000302: upval  0x0[0x1]
0x00000303: callfv 0x1
0x00000304: calll  0x1
0x00000305: subc   0x0 (2)
0x00000306: callfv 0x1
0x00000307: add
0x00000308: ret
0x00000309: ret
0x0000030a: callfv 0x1
0x0000030b: loadg  0x32

Обновление от 22 января 2022 года:

Удалите op_pone и op_pzero. Они не имеют смысла и будут заменены на op_pnum.

Версия 9.0 виртуальной машины (последнее обновление от 18 мая 2022 года):

Обновление от 12 февраля 2022 года:

Локальные значения теперь хранятся в стеке. Поэтому вызов функций будет быстрее, чем раньше. Потому что в версии 8.0 при вызове функции новый vm_vec выделяется сборщиком мусора, это заставляет сборщик мусора выполнять маркировку и очистку слишком много раз и занимает довольно много времени. В тестовом файле test/bf.nas требуется слишком много времени для тестирования файла, потому что этот файл имеет слишком много вызовов функций (см. данные теста ниже в таблице «Версия 8.0 (R9-5900HX ubuntu-WSL 2022/1/23)»).

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

Обновление от 27 марта 2022 года:

В обновлениях этого месяца мы меняем значение upvalue с vm_vec на vm_upval, специальный объект, управляемый сборщиком мусора, который имеет почти ту же структуру, что и объект upvalue в другом языке программирования Lua.

Сегодня мы изменяем формат вывода байт-кода. Новый формат вывода выглядит как objdump:

  0x0000029b:       0a 00 00 00 00        newh

func <0x29c>:
  0x0000029c:       0b 00 00 02 a0        newf    0x2a0
  0x0000029d:       02 00 00 00 02        intl    0x2
  0x0000029e:       0d 00 00 00 66        para    0x66 ("libname")
  0x0000029f:       32 00 00 02 a2        jmp     0x2a2
  0x000002a0:       40 00 00 00 42        callb   0x42 <__dlopen@0x41dc40>
  0x000002a1:       4a 00 00 00 00        ret
<0x29c>;

  0x000002a2:       0c 00 00 00 67        happ    0x67 ("dlopen")

func <0x2a3>:
  0x000002a3:       0b 00 00 02 a8        newf    0x2a8
  0x000002a4:       02 00 00 00 03        intl    0x3
  0x000002a5:       0d 00 00 00 68        para    0x68 ("lib")
  0x000002a6:       0d 00 00 00 69        para    0x69 ("sym")
  0x000002a7:       32 00 00 02 aa        jmp     0x2aa
  0x000002a8:       40 00 00 00 43        callb   0x43 <__dlsym@0x41df00>
  0x000002a9:       4a 00 00 00 00        ret
<0x2a3>;

  0x000002aa:       0c 00 00 00 6a        happ    0x6a ("dlsym")

Версия 10.0 виртуальной машины (последнее обновление от 16 августа 2022 года):

Обновление от 19 мая 2022 года:

Теперь мы добавляем сопрограмму в эту среду выполнения:

var coroutine={
    create: func(function){return __cocreate;},
    resume: func(co)      {return __coresume;},
    yield:  func(args...) {return __coyield; },
    status: func(co)      {return __costatus;},
    running:func()        {return __corun;   }
};

coroutine.create используется для создания нового объекта сопрограммы с использованием функции. Но эта сопрограмма не запустится немедленно.

coroutine.resume используется для продолжения работы сопрограммы.

coroutine.yield используется для прерывания работы сопрограммы и выдачи некоторых значений. Эти значения будут приняты и возвращены coroutine.resume. А coroutine.yield сам по себе возвращает vm_nil в функции сопрограммы.

coroutine.status используется для просмотра статуса сопрограммы. Существует 3 типа статуса: suspended означает ожидание запуска,running означает запуск,dead означает завершение работы.

coroutine.running используется, чтобы определить, работает ли сейчас сопрограмма.

Внимание: сопрограмма не должна создаваться или работать внутри другой сопрограммы. Резюме и результаты работы здесь:

Когда вызывается op_callb, кадр стека выглядит следующим образом:

+----------------------+(main stack)
| old pc(vm_ret)       | <- top[0]
+----------------------+
| old localr(vm_addr)  | <- top[-1]
+----------------------+
| old upvalr(vm_upval) | <- top[-2]
+----------------------+
| local scope(var)     |
| ...                  |
+----------------------+ <- local pointer stored in localr
| old funcr(vm_func)   | <- old function stored in funcr
+----------------------+

В процессе выполнения op_callb следующий шаг кадра стека:

+----------------------+(main stack)
| nil(vm_nil)          | <- push nil
+----------------------+
| old pc(vm_ret)       |
+----------------------+
| old localr(vm_addr)  |
+----------------------+
| old upvalr(vm_upval) |
+----------------------+
| local scope(var)     |
| ...                  |
+----------------------+ <- local pointer stored in localr
| old funcr(vm_func)   | <- old function stored в funcr
+----------------------+

Затем мы вызываем resume, эта функция изменит стек. Как мы видим, в стеке сопрограммы уже есть некоторые значения, но если мы впервые войдём в него, вершина стека будет vm_ret, а возвращаемый pc равен 0.

Поэтому для безопасного выполнения при первом вызове сопрограммы resume вернёт gc.top[0]. op_callb сделает top[0]=resume(), поэтому значение не изменится.

+----------------------+(coroutine stack)
| pc:0(vm_ret)         | <- теперь gc.top[0]
+----------------------+

При вызове yield функция будет делать следующее. И мы обнаруживаем, что op_callb поместил nil на вершину. Но куда отправляется возвращённый local[1]?

+----------------------+(сопрограммный стек)
| nil(vm_nil)          | <- push nil
+----------------------+
| старый pc(vm_ret)     |
+----------------------+
| старый localr(vm_addr)|
+----------------------+
| старый upvalr(vm_upval)|
+----------------------+
| локальная область(var)|
| ...                   |
+----------------------+ <- локальный указатель, хранящийся в localr
| старая функция(vm_func)| <- старая функция, хранящаяся в funcr
+----------------------+

Когда builtin_coyield завершает работу, стек устанавливается в основной стек, и возвращённый local[1], фактически, устанавливается на вершину основного стека с помощью op_callb:

+----------------------+(основной стек)
| возвращаемое значение(var)|
+----------------------+
| старый pc(vm_ret)      |
+----------------------+
| старый localr(vm_addr) |
+----------------------+
| старый upvalr(vm_upval) |
+----------------------+
| локальная область(var) |
| ...                    |
+----------------------+ <- локальный указатель, хранящийся в localr
| старая функция(vm_func)| <- старая функция, хранящаяся в funcr
+----------------------+

Таким образом, основной прогресс воспринимает значение на вершине как возвращённое значение resume. Но на самом деле возвращённое значение resume установлено в кадре стека сопрограммы. Итак, мы делаем вывод:

resume (main->coroutine) возвращает coroutine.top[0]. coroutine.top[0] = coroutine.top[0];
yield  (coroutine->main) возвращает вектор. main.top[0]      = vector;

Release Notes

версия 8.0 release

Я сделал большую ошибку в v8.0 выпуске:

в nasal_dbg.h:215: auto canary=gc.stack+STACK_MAX_DEPTH-1;

это вызовет некорректную ошибку stackoverflow. пожалуйста, измените его на:

canary=gc.stack+STACK_MAX_DEPTH-1;

Если вы не измените эту строку, только отладчик будет работать неправильно. эта ошибка исправлена в v9.0.

Ещё одна ошибка заключается в том, что в nasal_err.h:class nasal_err мы должны добавить конструктор для этого класса:

    nasal_err(): error(0) {}

Эта ошибка исправлена в v9.0. Поэтому мы рекомендуем не использовать v8.0.

version 11.0 release

  1. Используйте C++ std=c++17.

  2. Измените структуру ast, используя шаблон посетителя.

  3. Новый формат информации о структуре дампа ast.

  4. Измените способ экспорта модулей, разделите библиотеку на разные модули. Символы, начинающиеся с _, не будут экспортироваться.

  5. Замените stl на std.

  6. Добавьте интерпретатор REPL.

  7. Улучшите структуру виртуальной машины, разделите глобальный стек символов (хранит значения глобальных символов) и стек значений (используется в процессе).

Опубликовать ( 0 )

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

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