Вопрос или проблема
Я хочу сделать CAS на чистом C23. В качестве начала он будет работать в терминале, как любой CAS, где вы вводите данные, а он выдает результат.
Я искал в интернете способы реализации этого, и самое беспокоящее меня это: как использовать внутреннюю функцию? Вот два способа, которые я нашел:
Идея №1:
Согласно моей “наивной” идее, подтвержденной просмотром исходного кода gap, текст ввода можно использовать для создания .c файла, который мы вручную компилируем и затем выполняем. Например, с входными данными:
P := (2x+3)(x+4); P.simplify().solve();
Мы парсим это, затем создаем файл .c:
/* необходимые заголовки и глобальные переменные */
int main() {
return solve(simplify("(2x+3)(x+4)"));
} /* Я знаю, что это должно быть более сложно, но по крайней мере, это идея */
Компилируем, затем запускаем новый процесс с этим файлом и получаем результат через межпроцессное взаимодействие.
Я считаю этот процесс действительно избыточным и ненадежным, потому что он кажется очень подверженным ошибкам из-за различных компиляторов и их разных версий + процесса линковки (который заставляет библиотеки присутствовать в системе, таким образом CAS, который статически связанный, будет бесполезен), и создание нового процесса требует повторной реализации всех констант, создания специальных заголовков для компактного использования необходимых функций, что может быть также подвержено ошибкам, и создание новой инициализации программы для единственного результата кажется действительно неэффективным.
Идея №2:
Создать хэш-таблицу для функций и создать typedef для функций с n-аргументами. Пример:
typedef void (*GenericVoidFunc)();
typedef Obj (*FuncPtr_1Arg)(Obj arg1);
typedef Obj (*FuncPtr_2Arg)(Obj arg1, Obj args2);
...
/* Используя "Obj" здесь как непрозрачный тип */
typedef struct {
size_t number_of_arguments;
GenericVoidFunc p_func;
} PublicFunction;
PublicFunction table_of_functions[] = {
{ 1, (GenericVoidFunc) &solve }
};
Затем мы будем вызывать функцию вот так:
PublicFunction function = /* получить значение из таблицы*/;
switch (function.number_of_arguments) {
case 1:
Obj arg1 = /* получить значение ввода */;
Obj result = ((FuncPtr_1Arg) function.p_func)(arg1);
...
}
Это действительно неаккуратно, потому что я написал это на ходу, но я думаю, что этого достаточно, чтобы понять, как я это вижу.
Здесь нет потенциальных несовместимостей, как в предыдущем, и это работает в том же процессе, что и основная программа. Но, по моему мнению, это все равно избыточно, потому что это требует создания некой динамической типизации в C, что возможно (например, смотрите cello), но выходит за рамки моей программы.
В заключение, это то, что я в настоящее время знаю, и я, безусловно, недостаточно осведомлен и не имею много информации и ресурсов. Я не смог найти эффективный способ этого поиска и собрал все, что мог.
- Существует ли другой способ сделать это?
- По двум идеям, которые я показал, есть ли недопонимания, которые могут привести к неверным выводам?
- В случае, если мои выводы верны, существуют ли решения этих недостатков с похожими процессами?
Редактировать:
Я искренне не понимаю, как я мог бы быть более “сфокусированным”.
Я хочу написать CAS, который будет принимать ввод с командной строки, как sage. Я хочу знать, исходя из этого ввода, как я могу использовать функцию, которая определена в моем собственном коде. Например, если в моем коде есть функция solve()
, которую я сделал публичной, пользователь должен иметь возможность написать solve("...")
, и это внутренне вызовет мою функцию (с некоторым предварительным парсингом). Я показал 2 процесса, которые могли бы быть возможными как основа для размышлений, но любые советы по этому поводу будут признательны.
Ответ или решение
Создание REPL (Read-Eval-Print Loop) для компьютерной алгебраической системы (CAS) на чистом C — задача, требующая тщательного планирования и понимания. В данной статье мы рассмотрим концептуальные подходы, касающиеся реализации такого REPL, перечислим потенциальные проблемы и предложим решения. Давай рассмотрим это более детально.
1. Основные компоненты REPL
REPL состоит из четырёх основных компонентов:
- Чтение (Read): получение ввода от пользователя.
- Оценка (Eval): обработка введённого выражения и его выполнение.
- Вывод (Print): отображение результатов выполнения.
- Цикл (Loop): повторение процесса, чтобы обеспечить возможность ввода пользователем новых выражений.
2. Подходы к реализации
Подход 1: Динамическая компиляция
Как уже указано, можно создать .c
файл из пользовательского ввода, который затем компилируется и выполняется. Этот подход безопасен в плане типов, но имеет свои недостатки:
- Проблемы совместимости: Разные компиляторы могут работать по-разному. Вам придется учесть разницу в версиях и компиляторах.
- Задержки: Компиляция на лету требует времени, и пользователям придётся ждать, пока выполняется их код.
- Ресурсоёмкость: Каждый раз создаётся новый процесс, что может быть неэффективно.
Подход 2: Использование таблиц функций
В вашем втором подходе предполагается использование хеш-таблицы (или массива) для хранения функций. Это, безусловно, более эффективный способ, но требует некоторой динамической типизации, что является непростой задачей в языке C.
Ваша таблица функций будет выглядеть следующим образом:
typedef struct {
size_t number_of_arguments;
void (*p_func)(); // указатель на функцию
} PublicFunction;
PublicFunction function_registry[] = {
{ 1, (void (*)()) &solve },
// Добавьте другие функции
};
Вам нужно будет создать механизм для разбора входящей строки в аргументы, а также для вызова соответствующей функции из таблицы.
3. Парсинг пользовательского ввода
Парсинг выражений можно реализовать несколькими способами:
- Собственный парсер: Напишите парсер, который будет анализировать пользовательский ввод и определять, какая функция должна быть вызвана вместе с аргументами.
- Использование библиотеки: Можно воспользоваться уже существующими библиотеками (например,
Flex
иBison
) для генерации лексеров и парсеров.
4. Пример реализации REPL
Вот простой пример, который демонстрирует основные компоненты REPL на C:
#include <stdio.h>
#include <string.h>
typedef struct {
size_t number_of_arguments;
void (*p_func)();
} PublicFunction;
void solve(const char *input) {
printf("Solving: %s\n", input);
}
PublicFunction function_registry[] = {
{ 1, (void (*)()) &solve }
};
void process_input(const char *input) {
// Простейший парсер, извлечь имя функции и аргументы
char func_name[64];
sscanf(input, "%63s", func_name);
if (strcmp(func_name, "solve") == 0) {
char argument[256];
sscanf(input + strlen(func_name) + 1, "%255s", argument);
// Вызов функции
((void (*)(const char*))function_registry[0].p_func)(argument);
} else {
printf("Unknown function: %s\n", func_name);
}
}
int main() {
char input[512];
while (1) {
printf("Enter command: ");
fgets(input, sizeof(input), stdin);
input[strcspn(input, "\n")] = 0; // Убираем символ новой строки
process_input(input);
}
return 0;
}
5. Заключение
Разработка REPL для CAS в языке C — замечательная возможность создать мощный инструмент для математических вычислений. Хотя с этим процессом связаны определённые сложности, использование эффективных методов управления функциями и парсинга входных данных может значительно упростить задачу. Полученные навыки могут оказать invaluable в процессе дальнейшей разработки и оптимизации вашего проекта.