Форум программистов, компьютерный форум CyberForum.ru

Пишем свой интерпретатор языка BASIC - C++

Восстановить пароль Регистрация
 
 
Рейтинг: Рейтинг темы: голосов - 1509, средняя оценка - 4.80
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
20.06.2009, 20:03     Пишем свой интерпретатор языка BASIC #1
*****************
Благодаря форуму и Evg в частности интерпретатор развивается, потихоньку превращаясь в простенький интерпретатор QBASIC.
Некоторые из самых старых версий сохранились в теме и ссылки на них будут добавлены в это сообщение,а также ссылки на другие темы,связанные с этой.

Репозиторий с проектом находится тут, там же есть возможность в браузере посмотреть историю ревизий (английский в логах весьма примитивен,комментарии и рекомендации можете писать в личку),а также скачать самый последний архив репозитория в формате .tar.gz
Если кто-то пользуется Subversion,скачать исходники можно так:
Код
svn co https://basin.svn.sourceforge.net/svnroot/basin basin
Эти темы возникли в результате моих вопросов по ходу написания:
Технический приём для формирования согласованных данных
Makefile: как с использованием gcc строить автоматические зависимости от .h файлов?
Вопрос по svn (Subversion)
Создание системы тестирования ПО.
Вопрос про разные реализации бэйсиков
[C/C++] Можно ли выразить порядковый номер элемента массива через индексы?
[C++] Какие флаги указать линкеру для компиляции программы?
Как можно определить переменную в файле configure.in,чтобы её можно было использовать в Makefile?
Странный SIGSEGV, или что зависит от порядка написания интерфейса класса
[C++]Можно ли как-то указать в Makefile,чтобы часть файлов компилировал компилятор C?
Альтернативная версия интерпретатора от Evg на C
Это простая реализация разбора выражений, написанная Evg на C:
Представление выражения в двоичном дереве
*****************
Первое сообщение:
*****************
Задание(Страуструп,из книги,по готовому коду): Введите программу калькулятора и заставьте её работать.Например,при вводе
C++
1
2
r = 2.5
area = pi*r*r
Программа калькулятора выведет:
C++
1
2
2.5
19.635
Получили такой код:
LexicalAnalyzer.h
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// LexicalAnalyzer.h
#ifndef LEXICALANALYZER_H_INCLUDED
#define LEXICALANALYZER_H_INCLUDED
 
#include <cctype>
#include <string>
#include <map>
#include <iostream>
 
enum Token_value {
    NAME,       NUMBER,      END,
    PLUS = '+', MINUS = '-', MUL = '*', DIV = '/',
    PRINT = ';',ASSIGN = '=',LP = '(',  RP = ')'
};
extern Token_value curr_tok;
extern std::map<std::string,double>table;
extern int no_of_errors;
 
Token_value get_token();
 
double expr(bool);
double term (bool);
double prim (bool);
int error(const std::string&);
 
#endif // LEXICALANALYZER_H_INCLUDED

LexicalAnalyzer.cpp
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// LexicalAnalyzer.cpp
#include "LexicalAnalyzer.h"
 
 
std::map<std::string,double>table;
Token_value curr_tok=PRINT;
 
double expr (bool get)
{
    double left = term(get);
 
    for (;;)
        switch (curr_tok) {
            case PLUS:
                 left += term(true);
            break;
            case MINUS:
                 left-= term(true);
            break;
            default:
                 return left;
        }
}
 
double term (bool get)
{
    double left = prim (get);
 
    for (;;)
        switch (curr_tok) {
            case MUL:
                 left*=prim(true);
            break;
            case DIV:
                 if (double d = prim (true)) {
                     left /= prim (true);
                     break;
                 }
                 return error("Деление на ноль");
            default:
                 return left;
        }
}
 
double number_value;
std::string string_value;
 
double prim (bool get)
{
    if (get) get_token();
    switch (curr_tok){
        case NUMBER:{
            double& v = number_value;
            get_token();
            return v;
        }
        case NAME:{
            double& v = table[string_value];
            if (get_token()==ASSIGN) v = expr(true);
            return v;
        }
        case MINUS:
            return -prim(true);
        case LP:{
            double e = expr(true);
            if (curr_tok!=RP) return error("Ожидалась )");
            get_token();
            return e;
        }
        default:
            return error("Ожидалось первичное выражение");
    }
}
 
Token_value get_token()
{
    char ch = 0;
 
    do {
        if (!std::cin.get(ch)) return curr_tok = END;
    } while (ch!='\n'&&isspace(ch));
 
    switch (ch) {
        case 0:
             return curr_tok = END;
        case ';':case '\n':
             return curr_tok = PRINT;
        case '*':case'/':case '+':case '-':case '(':case ')':case '=':
             return Token_value(ch);
        case '0':case '1':case '2':case '3':case '4' :
        case '5':case '6':case '7':case '8':case '9':case '.':
             std::cin.putback(ch);
             std::cin>>number_value;
             return curr_tok=NUMBER;
        default:
             if (isalpha(ch)) {
                 string_value = ch;
                 while (std::cin.get(ch)&&isalnum(ch)) string_value.push_back(ch);
                 std::cin.putback(ch);
                 return curr_tok = NAME;
             }
             error ("Неправильная лексема");
             return curr_tok = PRINT;
    }
}
int no_of_errors=0;
int error (const std::string& s)
{
    no_of_errors++;
    std::cerr<<"Ошибка: "<<s<<'\n';
    return no_of_errors;
}

main.cpp
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.cpp
#include "LexicalAnalyzer.h"
 
 
int main()
{
    table["pi"]=3.1415926535897932385;
    table["e"]=2.7182818284590452354;
    while (std::cin) {
        get_token();
        if (curr_tok==END) break;
        if (curr_tok==PRINT) continue;
        std::cout<<expr(false)<<'\n';
    }
    return no_of_errors;
}

Анализатор-то работает,но конечное значение не вычисляется.Более того,если вводим
C++
1
a = 3 + 6
,то получаем "a", равное первому элементу в выражении,то есть 3.В чём логическая ошибка данной программы?С этими каскадными вызовами она слегка запутана.Уверен,что кто-то уже делал это задание.

Добавлено через 2 часа 5 минут 30 секунд
Пришлось решать влоб с дебаггером.У Страуструпа опечатка (или намеренная ошибка,что более вероятно ) Вот в этом куске кода в функции get_token():
C++
1
2
        case '*':case'/':case '+':case '-':case '(':case ')':case '=':
             return Token_value(ch);
Нехватает смены значения curr_tok,что и приводит к ошибочной работе.
C++
1
2
        case '*':case'/':case '+':case '-':case '(':case ')':case '=':
             return curr_tok=Token_value(ch);
Теперь всё пашет,всем спасибо,вопрос можно считать закрытым,но есть вопрос поважнее: В функциях prim и term возвращается int при ошибке,но ведь они имеют тип double,как вообще это работает?Происходит неявное преобразование типа,так?Мне интересно,почему Страуструп прибег к такому способу,это распространённая практика?

Добавлено через 16 минут 19 секунд
И ещё опечатка была
C++
1
2
3
                 if (double d = prim (true)) {
                     left /= d;// было left /= prim (true)
                     break;
Лучшие ответы (1)
После регистрации реклама в сообщениях будет скрыта и будут доступны все возможности форума.
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
14.07.2009, 20:15     Пишем свой интерпретатор языка BASIC #61
Из файла value_class подключается syntax_parser.h (класс Value ещё не определён). Из syntax_parser.h подключается value_class.h, на этот раз впустую, т.к. макрос CONST_CLASS_H_INCLUDED взведён (кстати, макрос переименуй), класс Value по прежнему неопределён, затем подключается variable_class.h, из которого опять вхолостую подключается value_class.h. Далее идёт использование класса, но он неопределён (неизвестен его размер).

Если у тебя проблемы с этими делами, то можно поступать просто. Создаёшь файл common.h, и делаешь в нём все нужные инклюды в нужном порядке. Из каждого модуля *.cpp подключаешь common.h и больше ничего. В файлах *.h вообще ничего не подключаешь.
После регистрации реклама в сообщениях будет скрыта и будут доступны все возможности форума.
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
15.07.2009, 17:32  [ТС]     Пишем свой интерпретатор языка BASIC #62
Я попробовал всякие комбинации,но пока не смог сделать так,чтобы всё лежало по порядку и без ошибок,а суть ошибки ясна.Я пока просто положил реализацию функций,возвращающих Variable в variable_class.cpp,но описание сделал в заголовках syntax_parser.h,чтобы было видно,что они относятся к модулю syntax_parser. Придётся пока это дело прокомментировать,и оставить так,потому как я не знаю,как иначе разрешить данный конфликт
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
15.07.2009, 17:48     Пишем свой интерпретатор языка BASIC #63
Просто всё долно быть аккуратно разложено по файлам. У тебя я наблюдал много бардака, но я про него ничего не пишу, ибо со временем сам поймёшь. А порядок примерно такой (что должно подключать в каком порядке):

1. debug: самый ниский уровень, в котором должны быть просто описаны флаги и, возможно, какие-то базовые общеиспользуемые функции. Не подключает ничего
2. parser (лексический парсер): тоже низкий уровень. Работает напрямую с файлом (который получает параметром) и отдаёт "наверх" значения. Функции собственной отладочной печати должен содержать в себе (а не в модуле debugger). Не подключает ничего
3. value: низкий уровень. Из себя ничего не вызывает, а наоборот, его вызывают "сверху". Аналогично отладочные печати должны быть внутри. Не подключает ничего
4. variable: более высокий уровень. Содержит внутри себя value, а потому подключает value. Должен содержать свои отладочные печати
5. syntax_parser: более высокий уровень. Использует parser, value и variable, а потому подключает эти три компоненты. Как таковых отладочных печатей нет (т.к. по сути дела не имеет своих структур данных), есть только трассирующие печати (типа "зашли в правило Expr" и т.п.)
6. driver: верхний управляющий уровень. Напрямую вызывает только syntax_parser, во всех других компонентах должен вызывать только инициализацию и завершение (если это сложно реализовывать через конструкторы и деструкторы). Передача именеи файла в parser не совсем верно, имя файла должно передаться в syntax_parser, а оттуда уже в parser

Просто всё это на бумажке в виде блоков надо аккуратно рисовать, чтобы более понятно стало
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
21.07.2009, 03:11  [ТС]     Пишем свой интерпретатор языка BASIC #64
Ура! Прога работает Теперь можно сделать такой исходник:
PureBasic
1
2
3
4
5
6
7
8
9
DIM string c,int x,int b,int a;
 
LET x=1;
LET b=3;
LET a=b+x;
 
INPUT "Введите значение c: ",c;
 
PRINT c," ",x," ",b," ",a;

Многое,конечно,ещё нужно доделать,но даже от этого я чуть не прыгал Добавил имена типов,для последующего контроля,правда,у меня не получилось запихать значение строки в union в классе Value,постоянно выбивало в segfault в Си-функции strcat,я так и не разобрался,почему,поэтому пока сделал обычную строку,которую придётся таскать с собой,пока не переделаю.
Скобки даже пока не сделал,увлёкся.Ещё пришлось сделать доступ к параметрам экземпляра Value внутри класса Variable,так уж увидел решение для INPUT.Добавил строковые константы,но пока прибавление float к ним проблематично,нужно ещё написать функцию перевода c_itof.Строковые константы возможны для печати,для облегчения пользовательского интерфейса,а также для вывода вспомогательных сообщений в инструкции INPUT.
Буду теперь думать,как сделать инструкцию IF,в принципе,есть идея,что IF внутри себя должна запускать функцию с остальными инструкциями,чтобы реализовать независимый блок.Надо ещё подумать над тем,как лучше реализовать условие..
Вообще,программа разрастается потихоньку,парсер разросся уже не на шутку,но,с другой стороны,расширять её пока не так трудно.Код выкладывать в просмотре не имеет смысла,так как уже слишком громоздкий.
Контроля ошибок пока нет,но будет.
1) Реальная проблема,с которой я столкнулся(это,по идее и есть вопрос)- это как освобождать всю ту память,которая выделяется динамически?А там её выделяется уйма.Я пока не разбираюсь в теме,и вот не знаю,что там реально происходит,походу дикая утечка памяти,и я не знаю,как это вообще решается в данном случае.
Вот исходник:

P.S. Имена типов в исходнике должны быть маленькими буквами,просто движок сайта поменял на большие
Вложения
Тип файла: rar Interpreter.rar (8.5 Кб, 187 просмотров)
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
03.08.2009, 21:22     Пишем свой интерпретатор языка BASIC #65
Собственно, поздравляю. Я тебе уже писал, что со временем сам сможешь сделать всё, что надо и как надо. Но когда самостоятельно смог расширить до нужной функциональности - значит базовый принцип понял, что очень даже радует

По поводу динамической памяти - вопрос так с ходу осилить не могу. Полностью ковыряться в исходнике пока морально не готов

По поводу IF'а. Надо понимать, что при работе IF'а у тебя может быть пропущен либо один (THEN), либо другой (ELSE) фрагмент кода. Более того, есть ещё циклы, где какой-то код придётся исполнять по нескольку раз. А ещё есть GOTO, где осуществляется переход на произвольный участок кода (который может быть как до, так и после оператора GOTO). И это мы с тобой ещё пока отложили в далёкий ящик реализацию функций (процедур). Если интересно подумать самому и есть соображения - выкладывай. Если нет - тогда расскажу как это можно сделать
.::.DIMA.::.
142 / 142 / 4
Регистрация: 26.10.2008
Сообщений: 782
04.08.2009, 02:57     Пишем свой интерпретатор языка BASIC #66
А как называется книга из которой этот пример? Судя по коду она не для начинающих.
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
04.08.2009, 03:45  [ТС]     Пишем свой интерпретатор языка BASIC #67
Я уже заждался Надеюсь,отпуск прошёл нормально.
1) Ну во-первых,хочу сказать,что уже многое изменилось в программе,я понял некоторые из ошибок,некоторые ещё не исправил,но займусь и этим.Одна из них - это насчёт неявного приведения типов.Я успел немного проштудировать материал по C,и узнал,что приведение выполняется всегда к большему типу(в смысле диапазона значений),учавствующему в выражении.Может,я не понял то,что говорил ты,но я тогда сделал не правильно.То есть если в выражении участвуют два операнда,к примеру, и первый из них int,а второй double,то приводить нужно не к первому встречному,а к большему,чтобы избежать излишних переполнений.Это я взял себе на заметку,но пока ещё не исправил.Были ещё недочёты,и ещё кучу я заметил,но пока оставил в покое.Сделал единую типизацию для функций,переменных и констант.Имеется ввиду не со стороны парсера,а просто общее перечисление типов для всех,используемое уже в синтаксическом анализаторе:
C++
1
2
3
4
5
6
   enum Types {
       INT,         // Тип константы,переменной,
       FLOAT,    // или функции
       STRING,
       VOID
   };
2) Мне удалось добавить инструкцию IF ... ELSE,но на тот момент,когда я это делал,у меня были трудности с концом инструкции,и я добавил FI. Мне просто показалось,что так удобнее,и это просто служебное слово,означающее завершение IF.Разумеется,ненужный блок пропускается.
3) Также мне удалось добавить цикл WHILE с проверкой условия.
4) Сами проверки условия я ещё не доработал,и пока в выражении может быть только один оператор сравнения,то есть могут учавствовать только два выражения по обеим сторонам.Пока я оставил так,просто нужно сделать подобие обычных операций,с таким же спуском как и везде.
5) В настоящий момент я как раз делаю функции,это будет выглядеть примерно так:
PureBasic
1
2
3
4
5
6
7
8
9
FUNC someFuncName(int x,double y) 
BEGIN
  # function body ;
  # Само тело функции по задумке считывается 
  # в буфер,который является членом класса,
  # и после вызова функции на месте идёт подстановка
  # имён и значений параметров,после чего возвращается значение,если оно есть.
END;
#program body
Проблемы:
1)я всё ещё не разобрался с выделением динамической памяти,и как её освобождать.Пока тема сыровата.
2) Есть такая специфическая проблема: например если внутри инструкции IF сделать цикл WHILE,то если написать
C++
1
2
IF (выражение)
{WHILE ...
,то при возврате в исходную позицию цикла программа (парсер) попадает как раз на символ '\0',и при этом возникает ошибка,сути которой я пока не понял,поэтому пока есть ограничение на такую запись,т.е. не следует писать WHILE в одну строчку с чем-либо слитно,необходимо ставить пробел или перевод строки.
3) Логика внутри функций if и while довольно запутанная,что отношу к недостатку,вообще код мне кажется довольно "грязным".Сейчас делаю функцию объявлений для процедур,и сегодня не побрезговал конструкциями типа :
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
        // In this function i have used operator goto,but in very local places
        // (label is close to "goto" that pointing on that label).
        StmtKwFUNC_label1:;
 
        switch (parser_GetToken())
        {
            case TOKEN_TYPE_FLOAT:  function->SetFunc_Type (FLOAT);
            break;
 
            case TOKEN_TYPE_INT:    function->SetFunc_Type (INT);
            break;
 
            case TOKEN_TYPE_STRING: function->SetFunc_Type (STRING);
            break;
 
            case TOKEN_TYPE_VOID:   function->SetFunc_Type (VOID);
            break;
 
            case TOKEN_EOL: goto StmtKwFUNC_label1;// This is only local,
                                                                                  // in-function jump to
                                                                                  // skip EOL's
 
            default: error(UNEXPECTED_SYMBOL,"near "+ ::parser_CurTokenStr);
        }
В принципе,эквивалентно можно было организовать цикл,но просто мне было по душе сделать так,тем более я строго следовал порядку,чтобы не запутать программу.Метки очень близко находятся к вызывающим их goto.И хотя не рекомендуют использовать,я всё-таки попробовал,мне показалось это логичным.
Ну вот,вроде все новости пока.Код прилагаю,но с учётом,что функция syntax_parserStmtKwFUNC () должна быть закомментирована,так как ещё в процессе(синтаксические ошибки и пр.).
P.S. Только сейчас обнаружил,что ELSE перестала работать .Видимо,пока правил и менял,поломал ELSE ...ну ладно,разберёмся.Вот такой,к примеру можно сделать исходник:
PureBasic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#
# Это небольшая программа
# на языке,подражающем
# языку BASIC.
#
#
 
DIM int a,int b,int c;
 
LET a = 10;
LET b = 9;
 
IF (b>6)
{
    INPUT "Введите b: ",b;
    WHILE (b<a)
    {
        IF (b<a) 
        {
            PRINT a;
        }
        FI
        PRINT b," ";
        PRINT " ",a," ";
    }
}
FI
Ещё важно,я уже тут привык использовать тег BASIC,но он меняет названия типов int на INT,так что нужно исправить,если что.А,ещё добавил простенькую поддержку параметра командной строки.
Вложения
Тип файла: rar Interpreter.rar (14.3 Кб, 129 просмотров)
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
04.08.2009, 03:46  [ТС]     Пишем свой интерпретатор языка BASIC #68
Цитата Сообщение от qwert Посмотреть сообщение
А как называется книга из которой этот пример? Судя по коду она не для начинающих.
Этот пример уже не из книги,а совместный продукт Evg и #pragma Но тот код,что был в самом начале,это из книги "Б.Страуструп.Язык программирования С++.Специальное издание."
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
04.08.2009, 17:44     Пишем свой интерпретатор языка BASIC #69
Сам понимаешь, после отпуска с ходу сложно что-то сделать полезное для человечество. Некоторое время должно пройти в традиционно российском виде спорта - борьбе с ленью

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

По поводу неявного приведения типа. Приведение к более широкому типу - я в первый раз такое слышу. По стандарту, как мне казалось, всегда второй операнд приводится к типу первого. Т.е. int+double эквивалентно "int+(int)double". Но на всякий случай уточню

Про GOTO я не зря разговор затеял. Потому как "хорошее" решение повлечёт за собой серьёзную переделку программы. Принцип останется тот же, но начинка немного поменяется. При этом ситуация "рабочая". Т.е. в реальной жизни с реальными коммерческими продуктами с похожей ситуацией сталкиваются часто

Как у тебя реализованы IF'ы? То, что ты ввёл FI, даёт мне повод заподозрить, что одну из альтернатив ты просто пропускаешь без разбора. А потому, если в пропущенной альтернативе есть синтаксическая ошибка, то ты её в этот момент не поймаешь. А поймаешь только тогда, когда исполнение пойдёт по этой альтернативе. Сие означает, что твоя программа, содержащая синтаксическую ошибку, может довольно долго работать (к примеру, несколько дней), но потом сломаться из-за досадной опечатки. Понятно, что для тренировочной программы это мелочь, но вообще проблема серьёзная.

Добавлено через 6 минут 49 секунд
Цитата Сообщение от qwert Посмотреть сообщение
А как называется книга из которой этот пример? Судя по коду она не для начинающих.
Попробую немного более расширенно ответить.

Изначально #pragma делал просто задачу из книги, в которой простенький разбор выражений с печатью результата. В одной из других тем, я спросил его, насколько серьёзно он собирается заниматься программированием. Он ответил, что серьёзно. А потому данную задачу я предложил ему "чуть-чуть" расширить, а потом это "чуть-чуть" вылилось в довольно серьёзную задачу.

В книжках этого ты нигде не найдёшь (разве что готовое решение с краткими пояснениями). Ну и большая практическая польза была в том, что pragma в данном случае прошёл по пути, когда делается неправильное решение, потом переделывается на более правильное. При этом весь процесс работы он ведёт самостоятельно, я лишь делаю небольшие теоретические выкладки (которых тоже, к сожалению, в книжке скорее всего не встретишь). К тому же параллельно зацепляется очень много моментов по технике программирования: задача из нескольких модуле, правильная организация структур данных да и просто программирование на Си++. Всё это делается "под конкретного человека", потому как многие идеи и технические решения pragm'ы остаются за кадром. С моей стороны вся теоретическая часть в теме присутсвует. Так что если есть интерес, попробуй прочти всю тему с начала и повторить всё это безобразие на практике

Добавлено через 1 час 52 минуты 30 секунд
На текущий момент мне видится следующий порядок действий (именно в таком порядке):
  1. Учимся работать с svn. Если ты ещё не перешёл на хранение исходников под управлением svn.
  2. Делаем тестовый пакет с автоматическим запуском и проверкой. Ты уже успел заметить, что после некоторых твоих правок у тебя протухла работа ELSE. Чтобы такого не происходило в дальнейшем, нужно ставить на контроль функциональность твоего интепретатора. Если пояснить на пальцах, то, например, имеем несколько тестовых исходников на бейсике, в которых делается некая контрольная печать, по которой можно сделать вывод о правильной работоспособности той или иной конструкции. Есть эталонная печать. Тестирование заключается в запуске всех тестов и сравнении выдачи с эталонной. Догадываюсь, что этим заниматься лениво, особенно сейчас, но без этого ты не сможешь удерживать стабильность работы интерпретатора
  3. Обсуждаем дальнейшую внутреннюю структуру интерпретатора. Сейчас у тебя наконец появилось общее понимание того, как работает интерпретатор, а потому уже можно приступать к обсуждению. Проблема сам понимаешь какая: построение работы таким образом, чтобы наиболее просто работать с НЕпоследовательной интерпретацией
  4. Реализация базы интерпретатора. Т.е. строим все управляющие конструкции: IF, FOR, GOTO, WHILE, функции. После чего нужно окинуть критическим взглядом реализованное и начать наводить порядок. Потому как дальнейшее развитие поддержки языка будет, в основном, лишь в части добавления новых операторов, базовая часть при этом, скорее всего, останется в неизменном виде (если опять не возникнет желания навести порядок). В том числе будем разбираться и с динамической памятью (в чём заключается проблема, я пока не понял)

Более далёкая перспектива
  1. Допиливание конструкций, внимательное изучение языка (бейсика) с целью привести наш интерпретатор как можно ближе к стандартным бэйсикам. В том числе и можно решать, как у нас быдет язык чувствовать регистр букв (т.е. "IF" и "if" будет одно и тоже или нет и т.п.)
  2. Перенос интерпретатора на другую платформу (например, windows). Итоговое написание интерпретатора в виде кросс-платформенной программы. Идеальным вариантом будет поддержка в интерпретаторе графики: под виндой и линухом это строится на совершенно разных принципах
  3. Реализация IDE для интерпретатора (по типу всяких билдеров)
  4. Реализация отладчика. ХЗ, есть ли отладчики у интерпретаторов, но в теории вроде бы это ничему не противоречит. Если делать только под текстовый режим, то 100% это реализуемо и не особенно сложно

Добавлено через 6 часов 58 минут 9 секунд
отом могу забыть. Ещё в линуксовом разделе заведи отдельную тему по Makefile. Покажу, как с использованием gcc строить автоматические зависимости от .h файлов (чтобы постоянно Makefile не хачить при изменении структуры include'ов)

Добавлено через 13 минут 5 секунд
Немного по стилю написания

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void syntax_parserStmtKwLET()
{
...
    if (parser_GetToken() == TOKEN_IDENT)
    {
        int i = FindNameOverVector (::parser_CurTokenStr,defined_vars);
 
        if (i >= 0)
        {
            if (parser_GetToken() == TOKEN_DELIM_EQUAL)
            {
                defined_vars.at(i) = syntax_parserBinaryExpr ();
                defined_vars.at(i).SetInitialization(true);
            }
            else error(WRONG_LET_STATEMENT);
        }
        else error(UNDEFINED_SYMBOL, ::parser_CurTokenStr);
    }
    else error(WRONG_LET_STATEMENT);
}
Такой код тяжело читать, потому что интересующий нас фрагмент находится глубоко во вложении. При этом образное мышление у тебя линейное: сначала ДОЛЖНО быть имя переменной, затем ДОЛЖЕН быть знак "равно" затем ДОЛЖНО быть выражение

Этот же кусок можно написать с проинвертированными условиями. При этом он будет выглядет линейно. А все нелинейные вызовы error - это "плохие" случаи, которые нас не интересуют. При таком написании получается, что мы идём по прямой и отметаем в сторону ошибки. А небольшой комментарий позволяет быстро и ненапряжно оценить код

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Разбор конструкции LET Ident "=" Expr
// Слово LET разобрано в месте вызова
void syntax_parserStmtKwLET()
{
...
    // Ident
    if (parser_GetToken() != TOKEN_IDENT)
      error(WRONG_LET_STATEMENT)
 
    // Поиск переменной по имени
    int i = FindNameOverVector (::parser_CurTokenStr,defined_vars);
 
    if (i < 0)
      error(UNDEFINED_SYMBOL, ::parser_CurTokenStr);
 
    // "="
    if (parser_GetToken() != TOKEN_DELIM_EQUAL)
      error(WRONG_LET_STATEMENT);
 
    // Expr
    defined_vars.at(i) = syntax_parserBinaryExpr ();
    defined_vars.at(i).SetInitialization(true);
}
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
05.08.2009, 09:03  [ТС]     Пишем свой интерпретатор языка BASIC #70
Цитата Сообщение от Evg Посмотреть сообщение
Как у тебя реализованы IF'ы? То, что ты ввёл FI, даёт мне повод заподозрить, что одну из альтернатив ты просто пропускаешь без разбора. А потому, если в пропущенной альтернативе есть синтаксическая ошибка, то ты её в этот момент не поймаешь. А поймаешь только тогда, когда исполнение пойдёт по этой альтернативе. Сие означает, что твоя программа, содержащая синтаксическую ошибку, может довольно долго работать (к примеру, несколько дней), но потом сломаться из-за досадной опечатки. Понятно, что для тренировочной программы это мелочь, но вообще проблема серьёзная.
Да,это я заметил и мне это тоже очень не нравится.Вывод-интерпретатор должен только проверять синтаксис,но не выполнять саму программу,а переносить её в некое промежуточное состояние,уже после которого программа выполняется.Я поспрашивал людей,кто писал примеры компиляторов,говорят,что сначала интерпретация,потом программа транслируется в байт-код,а уже потом выполняется.Но как это сделать,я понятия не имею.То есть получается,что весь подход изначально был неверный? Интерпретатор,получается,должен только проверять синтаксис и выдавать ошибки,параллельно с написанием промежуточного кода,и после того,как пройдётся по всему коду,передавать управление следующему блоку?
Цитата Сообщение от Evg Посмотреть сообщение
Добавлено через 1 час 52 минуты 30 секунд
На текущий момент мне видится следующий порядок действий (именно в таком порядке):
  1. Учимся работать с svn. Если ты ещё не перешёл на хранение исходников под управлением svn.
  2. Делаем тестовый пакет с автоматическим запуском и проверкой. Ты уже успел заметить, что после некоторых твоих правок у тебя протухла работа ELSE. Чтобы такого не происходило в дальнейшем, нужно ставить на контроль функциональность твоего интепретатора. Если пояснить на пальцах, то, например, имеем несколько тестовых исходников на бейсике, в которых делается некая контрольная печать, по которой можно сделать вывод о правильной работоспособности той или иной конструкции. Есть эталонная печать. Тестирование заключается в запуске всех тестов и сравнении выдачи с эталонной. Догадываюсь, что этим заниматься лениво, особенно сейчас, но без этого ты не сможешь удерживать стабильность работы интерпретатора
  3. Обсуждаем дальнейшую внутреннюю структуру интерпретатора. Сейчас у тебя наконец появилось общее понимание того, как работает интерпретатор, а потому уже можно приступать к обсуждению. Проблема сам понимаешь какая: построение работы таким образом, чтобы наиболее просто работать с НЕпоследовательной интерпретацией
  4. Реализация базы интерпретатора. Т.е. строим все управляющие конструкции: IF, FOR, GOTO, WHILE, функции. После чего нужно окинуть критическим взглядом реализованное и начать наводить порядок. Потому как дальнейшее развитие поддержки языка будет, в основном, лишь в части добавления новых операторов, базовая часть при этом, скорее всего, останется в неизменном виде (если опять не возникнет желания навести порядок). В том числе будем разбираться и с динамической памятью (в чём заключается проблема, я пока не понял)
С svn я уже работаю с момента создания мной темы про эту систему.Сделал репозиторий на локальной машине,и обращаюсь не как к серверу,а просто через file:/// .Со скриптами придётся почитать маны,я умею делать простейшие скрипты для загрузки,но что-то более сложное пока не пробовал.
ELSE я починил,дело было не в самой инструкции,а в неправильном алгоритме пропуска фигурных скобок в WHILE. Поэтому я сделал ключевое слово LOOP.Но вообще,со всем этим пропуском без проверки реальная проблема.

Добавлено через 15 минут 16 секунд
Насчёт Makefile я сделал тему http://www.cyberforum.ru/cpp-linux/thread46096.html надеюсь,никто не огорчится, в гугле ведь тоже много чего есть на эту тему,но то,что ты говоришь,довольно специфично..
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
05.08.2009, 09:40     Пишем свой интерпретатор языка BASIC #71
Цитата Сообщение от #pragma Посмотреть сообщение
Да,это я заметил и мне это тоже очень не нравится.Вывод-интерпретатор должен только проверять синтаксис,но не выполнять саму программу,а переносить её в некое промежуточное состояние,уже после которого программа выполняется.
Да ты уже и сам всё знаешь

Цитата Сообщение от #pragma Посмотреть сообщение
То есть получается,что весь подход изначально был неверный?
Подход был верный, но была неверная реализация. Я уже писал о том, что намеренно тебя веду по тому пути, что потом будем переделывать. Причин для этого несколько (о некоторых я уже писал):
  • Для написания некоторой начальной стадии компилятора тебе уже приходится одновременно решать несколько проблем: изучать технику работы Си++, учиться строить многомодульное приложение, одновременно реализовывать новые для себя вещи - значение, переменная, лексический анализатор, синтаксический парсер. Дополнительный большой геморрой в виде промежуточного представления сльно бы усложнил задачу
  • Изначально у тебя не было понимания того, как ведётся синтаксический разбор. Я мог бы тебе сразу предложить работать с промежуточным представлением, но это было бы как по книжке - тупо делаешь так, как написано и не задумываешься о том, а почему же надо по другому. Сейчас ты это ощутил на своей шкуре, а потому у тебя уже есть собственное понимание того, когда интерпретатор может работать непосредственно, а когда нужно промежуточное представление
  • Хотелось показать живой пример, когда программа должна менять структуру, а общая (или бОльшая часть) схема работы не меняется. Сейчас это именно тот случай. Синтаксический разбор на самом деле остаётся точно таким же по схеме, просто меняется начинка

Цитата Сообщение от #pragma Посмотреть сообщение
Интерпретатор,получается,должен только проверять синтаксис и выдавать ошибки,параллельно с написанием промежуточного кода,и после того,как пройдётся по всему коду,передавать управление следующему блоку?
Интерпретатор на первом проходе делает полный синтаксический разбор и строит отображение исходника в некое промежуточное представление, которое затем в удобном виде интерпретируется (а в далёком будущем ещё и возможна пошаговая отладка)

Цитата Сообщение от #pragma Посмотреть сообщение
Со скриптами придётся почитать маны,я умею делать простейшие скрипты для загрузки,но что-то более сложное пока не пробовал.
Прежде чем заняться переделками интерпретатора, предлагаю наладить систему тестирования. По крайней мере будет какой-то механизм, позволяющий быстро оценитьнадёжность работы интерпретатора. Ну и заодно появится небольшая тестовая база. В процессе написания тестов ты уже будешь понимать, как эти тесты будут парситься, а потому параллельно в голове начнёт устаканиваться дальнейшая структура.

Чтобы было понятно, вкратце схематично опишу, что из себя приблизительно представляет промежуточное представление

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Промежуточное представление statement'а
struct Statement
{
  // Всё провязано в список для последовательного обхода и интерпретации
  Statement *next;
 
  // Тип statement'а: LET, PRINT и т.п.
  StatementKind_t kind;
 
  union
  {
    // Описание представления оператора LET
    struct
    {
      Variable *var;  // Левая часть
      Expression *expr;  // Правая часть
    } let;
 
    // Описание представления оператора IF
    struct
    {
      Expression *cond_expr;  // Условие (условное выражение)
      Statement *true_list; // Список операторов по ветке THEN
      Statement *false_list; // Список операторов по ветке ELSE
    } if;
 
    ...
  }
}
Если примерно понял, что это. Сейчас я всё представил так, что выглядеть это будет по сути в виде некоторого дерева. Линейный участок - это линейный список операторов. Для всех IF, WHILE, FOR появляется ответвеление, куда лепится новый список операторов, который фактически находится внутри фигурных скобок. Для каждой процедуры будем иметь свой список операторов

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

Работу скриптов так же лучше обсудить в отдельной теме
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
09.08.2009, 17:10  [ТС]     Пишем свой интерпретатор языка BASIC #72
В-общем я сделал темку насчёт скриптов тестирования,начал искать - очень много информации,это новый язык по сути.
Мне,наконец,удалось заполнить буфер для функции,я долго бился об непонятную проблему,да и вообще разбирался с буферизацией.Проблема вообще странная.У меня был такой исходник:
PureBasic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
DIM int a,int b,int c;
 
LET a = 6;
LET b = 11;
 
FUNC int aaa(int c,int d)
BEGIN
 WHILE (0<3)
 {
    IF (0<1) 
    {
       PRINT a;
    }
    ELSE
    {
       PRINT "Hello";
    }
    FI
    PRINT " uu";
    PRINT "uu ";
  }
  LOOP
END
 
IF (b>1)
{
#   INPUT "Введите b: ",b;
    WHILE (b<3)
    {
        IF (b<1) 
        {
            PRINT a;
        }
        ELSE
        {
           PRINT "Hello";
        }
        FI
        PRINT b," ";
        PRINT " ",a," ";
    }
    LOOP
}
ELSE
{
    PRINT b;
}
FI
И вот если было убрать секцию с функцией,то всё работало,а так программа вылетала в самом неожиданном месте-в первой инструкции IF при проверке условия(сразу после функции).Вся эта лажа происходила в функции поиска по вектору:
C++
1
2
3
4
5
6
7
8
9
   int FindNameOverVector (const string str,vector<class Variable>vec)
   {
       for (vector<class Variable>::iterator i = vec.begin();
                                             i < vec.end();
                                           ++i               )
       if ((*i).GetVarName()  == str)
          return i-vec.begin();
       return -1;
   }
Я попробовал добавить & :
C++
1
2
3
4
5
6
7
8
9
   int FindNameOverVector (const string str,vector<class Variable>&vec)
   {
       for (vector<class Variable>::iterator i = vec.begin();
                                             i < vec.end();
                                           ++i               )
       if ((*i).GetVarName()  == str)
          return i-vec.begin();
       return -1;
   }
Заработало! Почему? Меня мучает этот вопрос,как вообще кусок кода с буферизацией связан с проверками условий???Там всё ведь нормально вроде-пишется буфер,после чего указатель в потоке находится после END,и только тогда управление передаётся далее. У меня только одна мысь на этот счёт-программа на грани пропасти,и всё это связано с проблемами динамической памяти,и там по ходу было переполнение стека,или ещё какая-то бяка.Векторы наводят ужас.Вот поэтому я подумал,что хватит пока биться головой об стену,нужно сделать тестирующий скрипт. И таких вот ньюансов по ходу написания была целая куча,я уже все не перепомню,я просто как-то исправлял их,но многие возникающие вопросы так и утонули..
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
09.08.2009, 17:31     Пишем свой интерпретатор языка BASIC #73
По поводу &. Я плохо разбираюсь в си++, но проблема, возможно, в следующем. Без & ты весь вектор передаешь по значению, т.е. формируется копия. При создании копии вектора вызывается конструктор копии для каждого элемента. Может где-то в конструкторе Variable тащится выделение памяти, которая затем не освобождается?

А вообще, глядя на подобные коды во мне всё больше крепнет уверенность, что какие-то вещи всё-таки надо писать на Си. В том плане, что реально используется компилятор Си++, но при этом код выглядит как код на Си. Ибо все эти stl'вские конструкции в данном месте вносят больше проблем, чем пользы. Либо это нужно как-то правильно переписывать, но я, не имея опыта работы на Си++, не могу сказать как именно

В любом случае исходники интерпретатора надо будет причёсывать

Добавлено через 1 минуту 37 секунд
Цитата Сообщение от #pragma Посмотреть сообщение
И таких вот ньюансов по ходу написания была целая куча,я уже все не перепомню,я просто как-то исправлял их,но многие возникающие вопросы так и утонули..
Когда-то и у меня было то же самое Програма в какойто момент времени начинала превращаться в большой набор всяческих затычек. А поскольку никаких комментариев не было, то через 3 дня уже ничерта невозможно было понять, как это работает (и работает ли)
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
09.08.2009, 18:01  [ТС]     Пишем свой интерпретатор языка BASIC #74
Ну вот ещё не хватало,чтобы мой код кого-то отверг от С++ С++ со своей STL мне кажется весьма удобным средством,для самых ленивых и для профанов.Но,конечно,порой тонкости весьма сложны.
Если ты заметил,в моём коде ни разу(!) не используется оператор delete.А new стоит на каждом шагу.Вот это и требует изменения,только вот я пока не знаю,как.Может имеет смысл сделать некоторые переменные static(те что не попадают в вектор),и тогда можно будет освобождать память в следующем узле.Но я не уверен,можно ли так сделать.А вот с переменными,которые попадают в вектор,довольно интересная ситуация.Мы выделяем память,указатель на эту память помирает,но сама память попадает под управление контейнера,то есть к ней возможен доступ через контейнер,и может быть,это как раз случай,когда delete не нужен?
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
10.08.2009, 14:00     Пишем свой интерпретатор языка BASIC #75
Цитата Сообщение от #pragma Посмотреть сообщение
С++ со своей STL мне кажется весьма удобным средством,для самых ленивых и для профанов
Возможно. Ну у меня очень Сишное мышление. Привык к тому, что программа должна иметь свой простенький менеджер памяти, который затачивается под нужды твоей программы и работает так, как тебе нужно ("хочешь - морожное, хочешь - пирожное"), а не так, как работает new (который в общем случае хрен знает как внутри себя устроен)

Цитата Сообщение от #pragma Посмотреть сообщение
Если ты заметил,в моём коде ни разу(!) не используется оператор delete.А new стоит на каждом шагу
Я на детали в твоём коде внимания не обращал, потому как самая важная для тебя задача - это понять как вообще работает интерпретатор. Поэтому я сморел только общую схему работы и общий принцип, не вдаваясь в детали. Соственно, теперь понимаю, про какую динамическую память ты говорил ранее. И дейстиветльно, это надо всё чинить. Но сначала система тестирования, а потом промежуточное представление и динамическая память

Цитата Сообщение от #pragma Посмотреть сообщение
А вот с переменными,которые попадают в вектор,довольно интересная ситуация.Мы выделяем память,указатель на эту память помирает,но сама память попадает под управление контейнера,то есть к ней возможен доступ через контейнер,и может быть,это как раз случай,когда delete не нужен?
С ходу на пальцах затрудняюсь ответить на вопрос. Надо смотреть конкретную реализацию и конкретные вопросы

Добавлено через 18 часов 37 минут 50 секунд
Для истории:
тема про svn: Вопрос по svn (Subversion)
тема про тестирование: Создание системы тестирования ПО.
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
13.08.2009, 21:51  [ТС]     Пишем свой интерпретатор языка BASIC #76
Я тут немного подзастрял на функциях.Система тестирования ещё в процессе.
Я сделал определение функции,и поскольку мало смыслю в работе с буферизацией(попробовал и так и так,но в итоге решил,что проще отказаться от этого варианта),я подумал будет проще просто запоминать место в коде,где начинается тело функции,а после разбора тела функции и получения результата возвращаться назад в то же место в коде,где были раньше.Что-то вроде того.Написал всё,поменял функцию,отвечающую за rValue в выражениях,чтобы сделать возможным вызов функций в выражениях.Дошёл до реализации разбора самого тела внутри экземпляра класса.И вот тут встал такой вопрос: привызове функции ведь нужно использовать локальные переменные,поэтому получается,что нельзя использовать syntax_parser,ведь он работает с глобальными переменными программы,или тогда придётся отказаться от идеи локальных переменных,но тогда будет не так интересно,или,как второй вариант,писать отдельный парсер специально для функций,так,получается?Иначе как объяснить парсеру,что он работает внутри тела функции и все переменные находятся внутри экземпляра класса,который его вызвал?
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
14.08.2009, 09:53     Пишем свой интерпретатор языка BASIC #77
Насколько я понял, вызов функции ты хочешь инерпретировать прямо из файла. Но при таком варианте задолбаешься с этим работать и вскоре интерпретатор превратится в болото. Полезно было бы это попробовать, дабы поиметь негативный опыт, но тут слишком много времени уйдёт в пустую, а последствия разгребать будет сложно

Тут надо работать через промежуточное представление. Хотя всё равно будет возникать вопрос о заведении переменной - в глобальной части или в локальной. Вообще, об этом я уже писал (пост 37, предпоследний абзац). Если перевести на текущее положение дел, то фактически тебе надо будет иметь две таблицы переменных (два вектора, или как у тебя называется). Локалы записываешь в локальную таблицу,глобалы в глобальную. При поиске переменной по имени сначала проверяешь в локальной таблице (если ты внутри процедуры), а потом в глобальной. При завершении работы процедуры локальная таблица удаляется

Если делать промежуточное представление, тот это место немного меняется. У тебя появляются три вещи. Таблицы переменных непосредственно, которые будут жить на протяжении времени жизни всей программы. А из промежуточного представления будут торчать обращения в одну из таблиц переменных. Второе - таблица символьных имён (о ней чуть ниже). И третье - таблица текущих значений: у тебя же процедура может рекурсивно саму себя вызывать, а у каждой активации процедуры своё значение переменных (об этом тоже ниже)

В итоге будем иметь что-то типа того в качестве промежуточного представления. Я пишу в своих упрощённых терминах, а дальше ты корячься со своими Си++'ными векторами и т.п.

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Описание переменной
struct Variable
{
  // Условно провязываю в список все переменные текущего контекста.
  // Т.е. глобальные переменные будут в одном списке, локальные переменные
  // процедуры - в другом (в каждой процедуре будет свой список)
  variable *next;
 
  // Имя переменной. Для исполнения промежуточного представление не нужно.
  // Нужно только для отладки и, вероятно, для выдачи ошибок
  char *name;
 
  // Дальше тип, может что-то ещё нужно
 
  // Текущее значение переменной (об этом ниже)
  Value *value;
}
 
// Описание процедуры
struct Procedure
{
  // Процедуры тоже условно провязываем в список
  Procedure *next;
 
  // Аналогично, для исполнения представления имя уже неважно, только
  // для отладки и выдачи ошибок
  char *name;
 
  // Информация о типе процедуры, ещё какая-то информация
 
  // Список параметров процедуры
  Variable *params;
 
  // Список локальных переменных процедуры
  Variable *locals;
 
  // Список операторов
  Statement *statement;
}
 
// Описание всей программы
struct Program
{
  // Список глобальных переменных
  Variable *globals;
 
  // Список процедур
  Procedure *procedures;
 
  // Список операторов
  Stetement *statement;
 
  // Может быть, что-то ещё
}
По поводу таблицы имён. На первом проходе парсера ты читаешь исходник и строишь промежуточное представление. При этом в процессе работы ты будешь строить конструкции Variable промежуточного представления. И будет возникать вопрос, как одно сдругим связывать по имени. Для этого нужно будет иметь таблицу имён, которая есть по сути набор пар { символьное имя, ссылка struct Variable* }. При этом пока ты работаешь в глобальной части, ты работаешь только с глобальной таблицей (т.е. таблица, в которой прописаны соотвествия для глобальных имён), а когда начинаешь парсить внутренности процедуры, то заводишь вторую дополнительную таблицу для имён локалов процедуры (включая параметры). Когда встречаешь имя - сначала проверяешь в таблице локалов, если там нет - в таблице глобалов. После того, как по имени нашёл соотвествующую struct Variable*, ссылку на эту переменную втыкаешь в представление. После того, как закончил парсить процедуру, таблицу локалов очищаешь. Понятно, что при таком раскладе процедура внутри процедуры не будет строиться. Да и большинство современных языков такое не позволяют, а потому думаю, что такое ограничение будет нормальным

Теперь таблица значений. В чём-то смысл похож на таблицу имён, только таблица о сути хранит набор всех значений (struct Value) для переменных текущего контекста. Одна таблица будет хранить набор значений для глобалов и будет жить всегда. В глобальной переменной будет торчать ссылка на struct Value, которая за всё время работы не поменяется (имею ввиду, что из переменной всегда будет торчать ссылка на одну и ту же запись Value). При входе в процедуру в момент ИНТЕРПРЕТАЦИИ (не парсенья) заводится новая таблица значений (каждое значение прописывается как Неинициализированное), а в представлении обходятся все локалы процедуры и поля value замыкаются на соответсвующую запись в твблице значений. При выходе из процедуры таблица значений удаляется (а ссылки на Value внутри локальных переменных на всякий случай устанваливается в NULL). Таким образом мы сможем рекурсивно вызывать процедуру? при каждой активации будет заводиться новая таблица значений (которая по большому счёту выполняет роль окна в стеке, как если бы это был код из-под компилятора). Тут есть тонкий момент: при рекурсивном возврате нам нужно уметь восстанавливать правильные связки "переменная-значение". Т.е. в момент интерпретации операции CALL нужно каким-то образом сохранить эти привязки переменной к значению, а после возврата - восстановить.Смысл этого понятен - в представлении (как и в тексте программы), локальная переменная присуствует в количестве одной штуки, но в момент исполнения их может быть несколько (из-за того, что может быть несколько активаций процедуры)

После всего написанного - ты ещё горишь желанием связываться с процедурами (и локальными объектами в них)? Потому как есть простые версии бейсика, где процедур как таковых нет. Формально они есть - за счёт оператора GOSUB (уход на подпрограмму), но при этом нет понятия локльной переменной, а потому работа ведётся через глобальные. Для использования это неудобно, т.к. не является полноценной процедурой, но очень и очень сильно упрощает интерпретатор. Именно поэтому я тебе изначально предлагал не связываться с процедурами. Не потому что это трудно по своей сути, а потому что для начинающего слишком большой объём работ, который тяжело выполнить аккуратно
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
14.08.2009, 13:23  [ТС]     Пишем свой интерпретатор языка BASIC #78
Тут еще есть одна неувязка.Я почитал,что ты писал про промежуточное представление,а также последний пост,и подумал,а ведь промежуточное представление это не меньший геморрой,ведь внутри некоторых стэйтментов могут быть вложенные инструкции,а сколько их,заранее не известно.. Есть такая идея - просто модифицировать синтаксический парсер так,чтобы он проходил исходник 2 раза,используя некое логическое значение.Просто нужно хорошенько подумать,какие операции делать в первом проходе(то есть создавать ли переменные,ведь нужно проверять их на инициализацию и т.д.).Или просто сделать какие-то исключения только для пропускаемых блоков,и проходить по блоку в любом случае,просто затем освобождать переменные и не делать печати и т.д.Это бы позволило сократить объём работы,и продвигаться в чём-то ещё(например интерпретация различных вариантов ошибок и т.д.).Или ты считаешь,что без промежуточного представления невозможно двигаться дальше,и переделка начинки необходима ?
Evg
Эксперт С++Автор FAQ
 Аватар для Evg
16828 / 5249 / 321
Регистрация: 30.03.2009
Сообщений: 14,139
Записей в блоге: 26
14.08.2009, 13:43     Пишем свой интерпретатор языка BASIC #79
Цитата Сообщение от #pragma Посмотреть сообщение
ведь внутри некоторых стэйтментов могут быть вложенные инструкции,а сколько их,заранее не известно..
Например?

Цитата Сообщение от #pragma Посмотреть сообщение
Есть такая идея - просто модифицировать синтаксический парсер так,чтобы он проходил исходник 2 раза,используя некое логическое значение
Конечно же, можно делать и так. Но ведь при этом проблемы с локальными переменными и обеспечением нормальной работы рекурсивных процедур никуда не денутся.

Цитата Сообщение от #pragma Посмотреть сообщение
Или ты считаешь,что без промежуточного представления невозможно двигаться дальше,и переделка начинки необходима ?
Нет, так я не считаю. Мне просто кажется, что при поддержке нормальных процедурных механизмов работать с промежуточным представлением удобнее. Точно я этого знать не могу, так же как и ты, ибо интерпретаторов не писал Но пока мне кажется именно так. К тому же в таком варианте гораздо проще приделать встроенный отладчик (понятно, что довод про отладчик не стОит учитывать при выборе метода, ибо хз когда до него доберёмся, если доберёмся вообще)

Сейчас как бы не получилось так, что ты замахнулся слишком высоко, а потом это реализоватьне получится. Вариант с двумя проходами по файлу вот ещё чем хорош. Поскольку нет промежуточного представления, то внутренние структуры данных не будут претерпевать сильных изменений, в случае если делать язык без процедур, а потом добавить процедуры. В идеале меняться не должо ничего, только добавляться
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
14.08.2009, 14:18     Пишем свой интерпретатор языка BASIC
Еще ссылки по теме:

Пишем свой чекер C++
Не удается откомпилировать интерпретатор М-языка C++
Пишем свой класс, спецификатор доступа protected C++

Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
#pragma
Временно недоступен
 Аватар для #pragma
952 / 223 / 6
Регистрация: 12.04.2009
Сообщений: 921
14.08.2009, 14:18  [ТС]     Пишем свой интерпретатор языка BASIC #80
Цитата Сообщение от Evg Посмотреть сообщение
Например?
Ну например
PureBasic
1
2
3
4
IF () {
     IF (){
          IF(){
#   Неизвестное количество раз
Цитата Сообщение от Evg Посмотреть сообщение
Сейчас как бы не получилось так, что ты замахнулся слишком высоко, а потом это реализоватьне получится. Вариант с двумя проходами по файлу вот ещё чем хорош. Поскольку нет промежуточного представления, то внутренние структуры данных не будут претерпевать сильных изменений, в случае если делать язык без процедур, а потом добавить процедуры. В идеале меняться не должо ничего, только добавляться
Я уже понял,что процедуры пока слишком сложно(то есть много работы,просто неохота долго работать над чем-то,что не знаешь,будет ли работать вообще,и требует переделки программы).Есть такое поверье-фирмы хайтек падают не при создании первой версии,а при переходе на вторую,иногда получается,что переделывать слишком много .Так что я за парсинг два раза,просто пока отказываемся от процедур.Я пока стараюсь немного причесать код,кое-где подправил условия,как ты предложил,так действительно лучше(проинвертированные условия),также добавил работу с препроцессором,что немного уменьшило выходной файл.Ещё я поубавил отступы,код выглядит поплотнее,но не знаю,я только надеюсь,что это не ухудшило читабельность,мне то трудно судить,я его уже знаю от и до ..
Yandex
Объявления
14.08.2009, 14:18     Пишем свой интерпретатор языка BASIC
Закрытая тема Создать тему
Опции темы

Текущее время: 17:38. Часовой пояс GMT +3.
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2016, vBulletin Solutions, Inc.
Рейтинг@Mail.ru