Commit 08862c2c authored by Roman Alifanov's avatar Roman Alifanov

Add plugin system: runtime loading of namespace libraries

- New stdlib module `plugin` with 7 methods: load_dir, load, list, has, call, each, each_if - Plugins are namespaces compiled via build-lib, loaded at runtime via source - Dynamic dispatch: plugin.call(name, func) -> ${name}__${func} - Empty function bodies now emit `:` (bash no-op) instead of invalid empty block - 21 new tests in test_plugin.py
parent af265565
......@@ -1216,6 +1216,78 @@ count = args.count ()
arg = args.get (0)
```
### plugin (система плагинов)
Модуль для runtime-загрузки namespace-библиотек (.sh файлов, собранных через `build-lib`).
Плагин — это namespace, скомпилированный в .sh библиотеку. Функции плагина вызываются динамически по имени.
```
# Загрузить все плагины из директории
plugin.load_dir ("/usr/share/myapp/plugins")
# Загрузить один плагин
plugin.load ("/path/to/plugin.sh")
# Список загруженных плагинов (через пробел)
names = plugin.list ()
# Проверить наличие функции у плагина
if plugin.has ("myplugin", "init") {
plugin.call ("myplugin", "init")
}
# Вызвать функцию плагина
result = plugin.call ("myplugin", "get_value")
plugin.call ("myplugin", "process", "arg1", "arg2")
# Вызвать функцию на всех плагинах (пропускает если нет)
plugin.each ("apply", "dark-theme")
# Вызвать функцию на плагинах, где check возвращает true
plugin.each_if ("is_available", "apply", "dark-theme")
```
**Создание плагина:**
```
# my_plugin.ct
namespace my_plugin {
func greet (name: string) {
print ("Hello from plugin, {name}!")
}
}
```
```bash
# Сборка плагина
content build-lib my_plugin.ct -o plugins/my_plugin.sh
```
**Использование:**
```
plugin.load_dir ("./plugins")
foreach name in plugin.list ().split (" ") {
print ("loaded: {name}")
}
plugin.each ("greet", "World")
```
**Методы:**
| Метод | Описание |
|-------|----------|
| `plugin.load_dir (path)` | Загрузить все .sh плагины из директории |
| `plugin.load (path)` | Загрузить один .sh плагин |
| `plugin.list ()` | Имена загруженных плагинов (через пробел) |
| `plugin.has (name, func)` | Есть ли функция у плагина |
| `plugin.call (name, func, args...)` | Вызвать функцию конкретного плагина |
| `plugin.each (func, args...)` | Вызвать функцию на всех плагинах |
| `plugin.each_if (check, func, args...)` | Вызвать func где check возвращает true |
### env (переменные окружения)
```
......@@ -1585,6 +1657,7 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list
- `shell``capture`, `exec`, `source`
- `time``ms`, `now`
- `math``abs`, `add`, `div`, `max`, `min`, `mod`, `mul`, `sub`
- `plugin``call`, `each`, `each_if`, `has`, `list`, `load`, `load_dir`
---
......@@ -25,6 +25,7 @@
- **Namespaces**`namespace X { }` for symbol isolation, multi-file support
- **Using** — Vala-style `using X` to bring namespace symbols into scope, alias and selective forms
- **Bash library import**`busing` for sourcing .sh scripts with optional named access
- **Plugin system**`plugin.load_dir/load/list/has/call/each/each_if` for runtime plugin loading
- **Auto-scan**`content build mydir/` recursively finds all .ct files
- **Library build**`content build-lib` with metadata, directory support, `--install`
- **Optimized output** - no unnecessary subshells, inlined methods
......@@ -403,6 +404,7 @@ try {
| **Logger** | `logger.info/warn/error/debug` |
| **Env** | `env.VAR` read, `env.VAR = value` set — environment variables |
| **Process** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` |
| **Plugin** | `plugin.load_dir()`, `plugin.load()`, `plugin.list()`, `plugin.has()`, `plugin.call()`, `plugin.each()`, `plugin.each_if()` |
| **Modules** | `namespace X { }`, `using X`, `busing "path"` |
| **Signals** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
......@@ -549,6 +551,53 @@ FAIL failing test (1ms)
2 of 3 tests passed
```
## Plugin System
Load namespace libraries (`.sh` files built with `build-lib`) at runtime:
```
# Create a plugin
# my_plugin.ct
namespace my_plugin {
func greet (name: string) {
print ("Hello from plugin, {name}!")
}
}
```
```bash
# Build it
content build-lib my_plugin.ct -o plugins/my_plugin.sh
```
```
# Load and use plugins
plugin.load_dir ("./plugins")
foreach name in plugin.list ().split (" ") {
print ("loaded: {name}")
}
plugin.each ("greet", "World")
if plugin.has ("my_plugin", "greet") {
plugin.call ("my_plugin", "greet", "Alice")
}
# Call only on plugins where check returns true
plugin.each_if ("is_available", "apply", "dark-theme")
```
| Method | Description |
|--------|-------------|
| `plugin.load_dir (path)` | Load all .sh plugins from directory |
| `plugin.load (path)` | Load a single .sh plugin |
| `plugin.list ()` | Space-separated names of loaded plugins |
| `plugin.has (name, func)` | Check if plugin has a function |
| `plugin.call (name, func, args...)` | Call a specific plugin's function |
| `plugin.each (func, args...)` | Call function on all plugins |
| `plugin.each_if (check, func, args...)` | Call func where check returns true |
## Examples
### Telegram Echo Bot
......
......@@ -25,6 +25,7 @@
- **Пространства имён**`namespace X { }` для изоляции символов, поддержка нескольких файлов
- **Using**`using X` в стиле Vala для прямого доступа к символам namespace, алиасы и селективный импорт
- **Импорт bash-библиотек**`busing` для подключения .sh скриптов с именованным доступом
- **Система плагинов**`plugin.load_dir/load/list/has/call/each/each_if` для загрузки плагинов в рантайме
- **Авто-скан**`content build mydir/` рекурсивно находит все .ct файлы
- **Сборка библиотек**`content build-lib` с метаданными, поддержкой директорий, `--install`
- **Оптимизированный вывод** — без лишних subshell, инлайнинг методов
......@@ -403,6 +404,7 @@ try {
| **Логгер** | `logger.info/warn/error/debug` |
| **Окружение** | `env.VAR` чтение, `env.VAR = value` установка — переменные окружения |
| **Процессы** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` |
| **Плагины** | `plugin.load_dir()`, `plugin.load()`, `plugin.list()`, `plugin.has()`, `plugin.call()`, `plugin.each()`, `plugin.each_if()` |
| **Сигналы** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
| **Модули** | `namespace X { }`, `using X`, `busing "path"` |
......@@ -543,6 +545,53 @@ FAIL падающий тест (1ms)
2 of 3 tests passed
```
## Система плагинов
Загрузка namespace-библиотек (`.sh` файлов, собранных через `build-lib`) в рантайме:
```
# Создание плагина
# my_plugin.ct
namespace my_plugin {
func greet (name: string) {
print ("Привет из плагина, {name}!")
}
}
```
```bash
# Сборка
content build-lib my_plugin.ct -o plugins/my_plugin.sh
```
```
# Загрузка и использование
plugin.load_dir ("./plugins")
foreach name in plugin.list ().split (" ") {
print ("загружен: {name}")
}
plugin.each ("greet", "Мир")
if plugin.has ("my_plugin", "greet") {
plugin.call ("my_plugin", "greet", "Alice")
}
# Вызов только на плагинах, где проверка возвращает true
plugin.each_if ("is_available", "apply", "dark-theme")
```
| Метод | Описание |
|-------|----------|
| `plugin.load_dir (path)` | Загрузить все .sh плагины из директории |
| `plugin.load (path)` | Загрузить один .sh плагин |
| `plugin.list ()` | Имена загруженных плагинов (через пробел) |
| `plugin.has (name, func)` | Есть ли функция у плагина |
| `plugin.call (name, func, args...)` | Вызвать функцию конкретного плагина |
| `plugin.each (func, args...)` | Вызвать функцию на всех плагинах |
| `plugin.each_if (check, func, args...)` | Вызвать func где check возвращает true |
## Примеры
### Telegram эхо-бот
......
......@@ -227,10 +227,12 @@ def _emit_function_body(
elif ptype == 'dict':
ctx.param_dict_vars.add(param.name)
if fn.body:
if fn.body and fn.body.stmts:
ctx.in_function = True
emit_block(fn.body, ctx)
ctx.in_function = False
elif not fn.params:
ctx.emit(':')
ctx.array_vars.clear()
ctx.array_vars.update(saved_array_vars)
......
......@@ -77,6 +77,7 @@ _NS_PREFIX: dict[str, str] = {
'json': JSON_PREFIX,
'math': MATH_PREFIX,
'regex': REGEX_PREFIX,
'plugin': '__ct_plugin_',
}
# Builtin CT function names → bash function names
......@@ -723,6 +724,7 @@ _NS_METHOD_PREFIX: dict[str, str] = {
'time': '__ct_time_',
'args': '__ct_args_',
'shell': '__ct_shell_',
'plugin': '__ct_plugin_',
}
......
......@@ -59,6 +59,8 @@ def emit_stdlib(out: list[str], used_categories: set[str], indent: str = '') ->
_emit_test(em)
if 'misc' in cats:
_emit_busing_misc(em)
if 'plugin' in cats:
_emit_plugin(em)
em.line('# === End Standard Library ===')
em.blank()
......@@ -440,3 +442,97 @@ def _emit_test(em: _Emitter) -> None:
def _emit_busing_misc(em: _Emitter) -> None:
pass
def _emit_plugin(em: _Emitter) -> None:
em.line('declare -ga __CT_PLUGINS=()')
em.blank()
em.line('__ct_plugin_load_dir () {')
em.indent()
em.line('local dir="$1"')
em.line('for f in "$dir"/*.sh; do')
em.indent()
em.line('[[ -f "$f" ]] || continue')
em.line('__ct_plugin_load "$f"')
em.dedent()
em.line('done')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_load () {')
em.indent()
em.line('local f="$1"')
em.line('source "$f"')
em.line('local __ns_line')
em.line("__ns_line=$(sed -n 's/^# content-namespaces: *//p' \"$f\" | head -1)")
em.line('local __old_ifs="$IFS"; IFS=","')
em.line('local __ns')
em.line('for __ns in $__ns_line; do')
em.indent()
em.line('__ns="${__ns## }"; __ns="${__ns%% }"')
em.line('[[ -n "$__ns" ]] && __CT_PLUGINS+=("$__ns")')
em.dedent()
em.line('done')
em.line('IFS="$__old_ifs"')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_list () {')
em.indent()
em.line('__CT_RET="${__CT_PLUGINS[*]}"')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_has () {')
em.indent()
em.line('declare -f "${1}__${2}" &>/dev/null && __CT_RET=true || __CT_RET=false')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_call () {')
em.indent()
em.line('local __pn="$1" __fn="$2"; shift 2')
em.line('"${__pn}__${__fn}" "$@"')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_each () {')
em.indent()
em.line('local __fn="$1"; shift')
em.line('local __p')
em.line('for __p in "${__CT_PLUGINS[@]}"; do')
em.indent()
em.line('if declare -f "${__p}__${__fn}" &>/dev/null; then "${__p}__${__fn}" "$@"; fi')
em.dedent()
em.line('done')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_each_if () {')
em.indent()
em.line('local __check="$1" __fn="$2"; shift 2')
em.line('local __p')
em.line('for __p in "${__CT_PLUGINS[@]}"; do')
em.indent()
em.line('if declare -f "${__p}__${__check}" &>/dev/null; then')
em.indent()
em.line('"${__p}__${__check}"')
em.line('if [[ "$__CT_RET" == "true" ]]; then')
em.indent()
em.line('"${__p}__${__fn}" "$@"')
em.dedent()
em.line('fi')
em.dedent()
em.line('fi')
em.dedent()
em.line('done')
em.dedent()
em.line('}')
em.blank()
......@@ -14,6 +14,7 @@ from .args import ArgsMethods
from .core import CoreFunctions, AwkBuiltinFunctions
from .reflect import ReflectMethods
from .process_handle import ProcessHandleMethods
from .plugin import PluginMethods
STRING_METHODS = collect_methods(StringMethods)
ARRAY_METHODS = collect_methods(ArrayMethods)
......@@ -31,6 +32,7 @@ CORE_FUNCTIONS = collect_methods(CoreFunctions)
AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions)
REFLECT_METHODS = collect_methods(ReflectMethods)
PROCESS_HANDLE_METHODS = collect_methods(ProcessHandleMethods)
PLUGIN_METHODS = collect_methods(PluginMethods)
NAMESPACE_REGISTRY = {
"fs": FS_METHODS,
......@@ -42,6 +44,7 @@ NAMESPACE_REGISTRY = {
"time": TIME_METHODS,
"math": MATH_METHODS,
"reflect": REFLECT_METHODS,
"plugin": PLUGIN_METHODS,
"shell": {"exec", "capture", "source"},
}
......
from .base import Method
class PluginMethods:
load_dir = Method(
name="load_dir",
bash_func="__ct_plugin_load_dir",
min_args=1, max_args=1,
)
load = Method(
name="load",
bash_func="__ct_plugin_load",
min_args=1, max_args=1,
)
list = Method(
name="list",
bash_func="__ct_plugin_list",
min_args=0, max_args=0,
)
has = Method(
name="has",
bash_func="__ct_plugin_has",
min_args=2, max_args=2,
)
call = Method(
name="call",
bash_func="__ct_plugin_call",
min_args=2, max_args=None,
)
each = Method(
name="each",
bash_func="__ct_plugin_each",
min_args=1, max_args=None,
)
each_if = Method(
name="each_if",
bash_func="__ct_plugin_each_if",
min_args=2, max_args=None,
)
......@@ -49,6 +49,7 @@ _NS_CATEGORIES: dict[str, str] = {
'math': 'math',
'time': 'time',
'args': 'args',
'plugin': 'plugin',
}
......
......@@ -43,13 +43,13 @@ BUILTIN_FUNCS = frozenset({
'print', 'exit', 'len', 'range', 'random', 'random_range', 'pid',
'assert', 'assert_eq',
'http', 'fs', 'json', 'logger', 'reflect', 'regex', 'math',
'time', 'args', 'env', 'shell',
'time', 'args', 'env', 'shell', 'plugin',
})
# Names that are namespace objects (not functions), accessed via dot
STDLIB_NAMESPACES = frozenset({
'http', 'fs', 'json', 'logger', 'reflect', 'regex', 'math',
'time', 'args', 'env', 'shell',
'time', 'args', 'env', 'shell', 'plugin',
})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment