Commit 01353d48 authored by Roman Alifanov's avatar Roman Alifanov

Add namespace/using/busing module system, rewrite cli.ct with action callbacks

- namespace X { } for symbol isolation (multi-file merge) - using X / using h = X / using X { a, b } (Vala-style) - busing for sourcing .sh scripts with named access - Remove legacy import system - Rewrite cli.ct: action callbacks, aliases, FloatFlag, auto help/version - Add cli examples: cli_demo, cli_flags, cli_subcommands, cli_categories - Fix parent class field initialization in child constructors - Fix var.field.method() dispatch for inherited array/dict fields - Recursive directory scan for content build/run/test - Full build-lib with metadata, directory support, --install - Meson custom_target to compile cli.sh during build - RPM spec: split content-lib-cli subpackage
parent 31733045
......@@ -797,15 +797,76 @@ on EXIT {
Поддерживаемые сигналы: `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGUSR1`, `SIGUSR2`, `EXIT`.
### Import
### Namespace
Пространства имён для изоляции символов. Один namespace может дополняться из разных файлов — компилятор мержит все блоки с одинаковым именем.
```
# utils/strings.ct
namespace utils {
func upper (s) {
return s.upper ()
}
}
# utils/format.ct
namespace utils {
func format (tpl, args...) {
# ...
}
class Formatter { ... }
}
```
Функции и классы внутри namespace получают префикс при компиляции:
- `utils.upper ()``utils__upper ()`
- `utils.Formatter ()``utils__Formatter ()`
### Using
Подключение символов namespace в текущий scope (стиль Vala).
**Формы:**
```
import "std/http"
import "std/json"
import "Logger"
import "MyLib.sh"
# Полный — всё из namespace доступно напрямую
using utils
greet ("Alice") # без префикса
Counter (5) # без префикса
# Квалифицированный доступ (всегда работает, using не нужен)
utils.greet ("Alice")
# Алиас — сокращение для namespace
using h = handlers
h.respond ("ok") # вместо handlers.respond ("ok")
# Селективный — только указанные символы напрямую
using cli { new_app, Command }
app = new_app ("myapp", "desc") # напрямую
cli.new_bool_flag ("v", "verbose") # остальное — квалифицированно
```
### Busing
Подключение чистых bash-библиотек (.sh файлов).
```
# С именем — доступ через имя (префикс отбрасывается при компиляции)
busing config = "/usr/bin/shell-config"
config.get_value ("key") # компилируется в: get_value "key"
config.set_option ("a", "b") # компилируется в: set_option "a" "b"
# Без имени — функции глобально доступны
busing "/usr/share/mylib/functions.sh"
format_text ("hello") # как обычная shell-команда
```
Компиляция:
- `busing "path"``source "path"`
- `busing name = "path"``source "path"` + регистрация имени
- `name.func ()``func ()` (префикс отбрасывается)
---
## Стандартная библиотека (stdlib)
......@@ -1326,6 +1387,7 @@ sudo meson install -C builddir
- `/usr/bin/content` — CLI
- `/usr/share/content/bootstrap/` — компилятор
- `/usr/share/content/lib/cli.ct` — стандартная библиотека
- `/usr/share/content/lib/cli.sh` — прекомпилированная CLI-библиотека
---
......@@ -1360,6 +1422,16 @@ content run . # Скомпилировать и запустить
Все файлы компилируются вместе. Функции и классы из одного файла доступны в других без явного импорта.
**Рекурсивный скан директорий:**
```bash
content build myapp/ # все .ct рекурсивно
content run myapp/ -- arg1 arg2 # запуск из директории
content test myapp/ # тесты из директории
```
Файлы с `namespace` ставятся первыми, `main.ct` — последним.
### run
```bash
......@@ -1373,7 +1445,17 @@ content run lib.ct main.ct -- arg1 arg2
### build-lib
```bash
content --build-lib MyLib.ct # -> MyLib.sh
content build-lib cli.ct # -> cli.sh (с метаданными)
content build-lib cli.ct -o /tmp/cli.sh # указать выходной файл
content build-lib lib/cli/ # собрать из директории
content build-lib cli.ct --install # установить в ~/.content/libs/
```
Метаданные в header .sh:
```bash
# content-lib: cli
# content-version: 1.0.0
# content-namespaces: cli
```
### test
......@@ -1405,23 +1487,20 @@ content build main.ct --warn-types # ошибки типов как пре
### Сборка
```bash
content --build-lib MyLib.ct
content build-lib MyLib.ct # -> MyLib.sh
content build-lib MyLib.ct -o /tmp/lib.sh # свой путь
content build-lib lib/mylib/ # из директории
content build-lib MyLib.ct --install # установить в ~/.content/libs/
```
Генерирует `MyLib.sh` с bash кодом и метаданными.
### Импорт
```
import "MyLib.sh"
import "Logger" # ищет в глобальных путях
```
Генерирует .sh с bash-кодом и метаданными (content-lib, content-version, content-namespaces).
### Глобальные пути
### Пути библиотек
```
/usr/lib/content/
~/.content/libs/
~/.content/libs/ # пользовательские библиотеки
/usr/lib/content/ # системные библиотеки
<pkgdatadir>/lib/ # установленные через meson
```
---
......
......@@ -22,6 +22,11 @@
- **Compile-time method validation** — catches unknown methods before runtime
- **Background processes**`async`/`await` for running background processes with coproc
- **Signal handling**`on SIGINT { }` for signal handlers (compiles to `trap`)
- **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
- **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
## Installation
......@@ -47,6 +52,7 @@ Installs:
- `/usr/bin/content` — CLI entry point
- `/usr/share/content/bootstrap/` — compiler (Python)
- `/usr/share/content/lib/cli.ct` — standard library
- `/usr/share/content/lib/cli.sh` — precompiled CLI library
## Quick Start
......@@ -140,6 +146,29 @@ logger.level = "WARN"
logger.log ("Changed level")
```
### Namespaces & Modules
```
namespace utils {
func greet (name) {
print ("Hello, {name}!")
}
}
using utils
greet ("Alice")
utils.greet ("Bob")
using h = handlers
h.respond ("ok")
using cli { new_app, Command }
busing config = "/usr/bin/shell-config"
config.get_value ("key")
```
### Pipe Operator
```
......@@ -368,6 +397,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` |
| **Modules** | `namespace X { }`, `using X`, `busing "path"` |
| **Signals** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
## CLI Commands
......@@ -378,6 +408,8 @@ content build main.ct # -> main.sh
content build main.ct -o app.sh # custom output
content build lib.ct main.ct -o app.sh # multi-file
content build . -o app.sh # all .ct in directory
content build myapp/ # recursive scan of directory
content run myapp/ -- arg1 arg2 # run from directory
# Run
content run main.ct
......@@ -387,7 +419,9 @@ content run main.ct -- arg1 arg2 # with arguments
content test main.ct # run @test functions
# Build library
content --build-lib MyLib.ct # -> MyLib.sh
content build-lib cli.ct # -> cli.sh (with metadata)
content build-lib lib/cli/ -o cli.sh # from directory
content build-lib cli.ct --install # install to ~/.content/libs/
# Lint
content build main.ct --lint # run ShellCheck
......@@ -575,7 +609,11 @@ tests/ # Test suite
├── test_decorators.py # Decorators, typing, @test, user decorators
├── test_awk.py # AWK functions (map/filter, sync, assert)
├── test_shell.py # Shell commands, pipes, mixed pipes
└── test_async.py # Background processes (async/await/on, pid)
├── test_async.py # Background processes (async/await/on, pid)
├── test_namespace.py # Namespace/using tests
├── test_busing.py # Bash library import tests
├── test_build_lib.py # Library build tests
└── test_autoscan.py # Auto-scan tests
examples/ # Example .ct programs
```
......
......@@ -22,6 +22,11 @@
- **Проверка методов при компиляции** — выявление несуществующих методов до запуска
- **Фоновые процессы**`async`/`await` для запуска фоновых процессов через coproc
- **Обработка сигналов**`on SIGINT { }` для обработчиков сигналов (компилируется в `trap`)
- **Пространства имён**`namespace X { }` для изоляции символов, поддержка нескольких файлов
- **Using**`using X` в стиле Vala для прямого доступа к символам namespace, алиасы и селективный импорт
- **Импорт bash-библиотек**`busing` для подключения .sh скриптов с именованным доступом
- **Авто-скан**`content build mydir/` рекурсивно находит все .ct файлы
- **Сборка библиотек**`content build-lib` с метаданными, поддержкой директорий, `--install`
- **Оптимизированный вывод** — без лишних subshell, инлайнинг методов
## Установка
......@@ -47,6 +52,7 @@ sudo meson install -C builddir
- `/usr/bin/content` — CLI точка входа
- `/usr/share/content/bootstrap/` — компилятор (Python)
- `/usr/share/content/lib/cli.ct` — стандартная библиотека
- `/usr/share/content/lib/cli.sh` — прекомпилированная CLI-библиотека
## Быстрый старт
......@@ -140,6 +146,29 @@ logger.level = "WARN"
logger.log ("Уровень изменён")
```
### Пространства имён и модули
```
namespace utils {
func greet (name) {
print ("Привет, {name}!")
}
}
using utils
greet ("Alice")
utils.greet ("Bob")
using h = handlers
h.respond ("ok")
using cli { new_app, Command }
busing config = "/usr/bin/shell-config"
config.get_value ("key")
```
### Pipe-оператор
```
......@@ -369,6 +398,7 @@ try {
| **Окружение** | `env.VAR` чтение, `env.VAR = value` установка — переменные окружения |
| **Процессы** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` |
| **Сигналы** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
| **Модули** | `namespace X { }`, `using X`, `busing "path"` |
## Команды CLI
......@@ -387,7 +417,13 @@ content run main.ct -- arg1 arg2 # с аргументами
content test main.ct # запуск @test функций
# Сборка библиотеки
content --build-lib MyLib.ct # -> MyLib.sh
content build-lib cli.ct # -> cli.sh (с метаданными)
content build-lib lib/cli/ -o cli.sh # из директории
content build-lib cli.ct --install # установить в ~/.content/libs/
# Директории
content build myapp/ # рекурсивный скан директории
content run myapp/ -- arg1 arg2 # запуск из директории
# Линтинг
content build main.ct --lint # запуск ShellCheck
......@@ -567,7 +603,11 @@ tests/ # Тестовый набор
├── test_decorators.py # Декораторы, типизация, @test
├── test_awk.py # AWK-функции
├── test_shell.py # Shell-команды, pipe
└── test_async.py # Фоновые процессы (async/await/on, pid)
├── test_async.py # Фоновые процессы (async/await/on, pid)
├── test_namespace.py # Тесты namespace/using
├── test_busing.py # Тесты импорта bash-библиотек
├── test_build_lib.py # Тесты сборки библиотек
└── test_autoscan.py # Тесты авто-скана
examples/ # Примеры .ct программ
```
......
......@@ -250,7 +250,23 @@ class DeferStmt (Statement):
@dataclass
class ImportStmt (Statement):
class NamespaceDecl (Statement):
name: str = ""
statements: List[Union[Statement, 'Declaration']] = field (default_factory=list)
location: Optional[SourceLocation] = None
@dataclass
class UsingStmt (Statement):
namespace: str = ""
alias: Optional[str] = None
names: Optional[List[str]] = None
location: Optional[SourceLocation] = None
@dataclass
class BusingStmt (Statement):
name: Optional[str] = None
path: str = ""
location: Optional[SourceLocation] = None
......
......@@ -19,6 +19,15 @@ ALL_KNOWN_METHODS = ARRAY_METHODS_ALL | DICT_METHODS_ALL | STRING_METHODS_ALL
class ClassMixin:
"""Mixin for class and method generation."""
def _is_method_used(self, cls_name, method_name):
if cls_name in self.used_methods and method_name in self.used_methods[cls_name]:
return True
if "__" in cls_name:
unprefixed = cls_name.split("__", 1)[1]
if unprefixed in self.used_methods and method_name in self.used_methods[unprefixed]:
return True
return False
def _get_field_info(self, field):
if isinstance(field, ClassField):
return field.name, field.type_annotation, field.default
......@@ -26,6 +35,21 @@ class ClassMixin:
return field[0], None, field[1] if len(field) > 1 else None
def generate_class(self, cls: ClassDecl):
if self.current_namespace:
effective_name = f"{self.current_namespace}__{cls.name}"
parent = cls.parent
if parent:
ns_parent = f"{self.current_namespace}__{parent}"
if ns_parent in self.classes:
parent = ns_parent
cls = ClassDecl(
name=effective_name,
parent=parent,
fields=cls.fields,
constructor=cls.constructor,
methods=cls.methods,
location=cls.location
)
self.current_class = cls.name
self.current_class_fields = set(self._get_field_info(f)[0] for f in cls.fields)
......@@ -59,6 +83,18 @@ class ClassMixin:
else:
self.class_field_types[(cls.name, field_name)] = "scalar"
if cls.parent and cls.parent in self.classes:
parent_chain = self._collect_parent_fields(cls.parent)
own_field_names = set(self._get_field_info(f)[0] for f in cls.fields)
for parent_name, parent_fields in parent_chain:
for field in parent_fields:
field_name, _, _ = self._get_field_info(field)
if field_name not in own_field_names:
parent_type = self.class_field_types.get((parent_name, field_name))
if parent_type:
self.class_field_types[(cls.name, field_name)] = parent_type
self.current_class_fields.add(field_name)
for method in cls.methods:
self._check_inlineable_method(cls, method)
......@@ -69,12 +105,8 @@ class ClassMixin:
self._generate_construct_method(cls)
for method in cls.methods:
if self.used_methods is not None:
method_used = (
cls.name in self.used_methods and
method.name in self.used_methods[cls.name]
)
if not method_used:
if self.used_methods is not None and not self.current_namespace:
if not self._is_method_used(cls.name, method.name):
continue
has_awk = any(dec.name == "awk" for dec in method.decorators)
......@@ -90,57 +122,78 @@ class ClassMixin:
own_method_names = {m.name for m in cls.methods}
for parent_method in parent_cls.methods:
if parent_method.name not in own_method_names:
if self.used_methods is not None:
method_used = (
cls.name in self.used_methods and
parent_method.name in self.used_methods[cls.name]
)
if not method_used:
if self.used_methods is not None and not self.current_namespace:
if not self._is_method_used(cls.name, parent_method.name):
continue
self._generate_inherited_method(cls, parent_cls, parent_method)
self.current_class = None
self.current_class_fields = set()
def _collect_parent_fields(self, cls_name):
"""Collect all fields from parent chain (top-down order)."""
fields = []
visited = set()
current = cls_name
while current and current in self.classes:
if current in visited:
break
visited.add(current)
parent_cls = self.classes[current]
fields.append((current, parent_cls.fields))
current = parent_cls.parent
fields.reverse()
return fields
def _generate_class_constructor(self, cls: ClassDecl):
"""Generate class factory function."""
self.emit(f"{cls.name} () {{")
with self.indented():
# Save instance immediately as nested constructors may overwrite __ct_last_instance
self.emit('local __ct_this_instance="__ct_inst_$RANDOM$RANDOM"')
self.emit('__ct_obj_class["$__ct_this_instance"]="{}"'.format(cls.name))
own_field_names = set(self._get_field_info(f)[0] for f in cls.fields)
if cls.parent and cls.parent in self.classes:
parent_chain = self._collect_parent_fields(cls.parent)
for _parent_name, parent_fields in parent_chain:
for field in parent_fields:
field_name, type_annotation, default_value = self._get_field_info(field)
if field_name in own_field_names:
continue
self._emit_field_init(field_name, type_annotation, default_value)
for field in cls.fields:
field_name, type_annotation, default_value = self._get_field_info(field)
if isinstance(default_value, ArrayLiteral):
elements = [self.generate_expr(e) for e in default_value.elements]
if elements:
arr_content = " ".join([f'"{e}"' for e in elements])
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=({arr_content})"')
else:
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=()"')
elif isinstance(default_value, DictLiteral):
self.emit(f'eval "declare -gA ${{__ct_this_instance}}_{field_name}=()"')
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="${{__ct_this_instance}}_{field_name}"')
elif default_value:
val = self.generate_expr(default_value)
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="{val}"')
else:
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]=""')
if cls.parent:
self.emit(f'# Inherit from {cls.parent}')
self._emit_field_init(field_name, type_annotation, default_value)
if cls.constructor:
self.emit("# Call constructor")
params_list = " ".join([f'"${{{i + 1}}}"' for i in range(len(cls.constructor.params))])
self.emit(f'__ct_class_{cls.name}_construct "$__ct_this_instance" {params_list}')
# Restore __ct_last_instance to this instance (after nested constructors may have changed it)
self.emit('__ct_last_instance="$__ct_this_instance"')
self.emit("}")
self.emit()
def _emit_field_init(self, field_name, type_annotation, default_value):
"""Emit field initialization in class factory function."""
if isinstance(default_value, ArrayLiteral):
elements = [self.generate_expr(e) for e in default_value.elements]
if elements:
arr_content = " ".join([f'"{e}"' for e in elements])
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=({arr_content})"')
else:
self.emit(f'declare -ga "${{__ct_this_instance}}_{field_name}=()"')
elif isinstance(default_value, DictLiteral):
self.emit(f'eval "declare -gA ${{__ct_this_instance}}_{field_name}=()"')
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="${{__ct_this_instance}}_{field_name}"')
elif default_value:
val = self.generate_expr(default_value)
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]="{val}"')
else:
self.emit(f'__CT_OBJ["$__ct_this_instance.{field_name}"]=""')
def _generate_class_metadata(self, cls: ClassDecl):
field_names = []
field_types = {}
......@@ -511,7 +564,9 @@ class ClassMixin:
return
name = func.name
if self.current_class:
if self.current_namespace and not self.current_class:
name = f"{self.current_namespace}__{func.name}"
elif self.current_class:
name = f"__ct_class_{self.current_class}_{func.name}"
self.emit(f"{name} () {{")
......
from typing import List, Dict, Optional, Set
from .ast_nodes import Program, ClassDecl, FunctionDecl, Assignment, Identifier
from .ast_nodes import Program, ClassDecl, FunctionDecl, Assignment, Identifier, NamespaceDecl, UsingStmt, BusingStmt
from .errors import ErrorCollector
from .stdlib import StdlibMixin
from .awk_codegen import AwkCodegenMixin
......@@ -62,6 +62,14 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.class_field_class: Dict[tuple, str] = {}
self.func_param_types: Dict[tuple, str] = {} # (func_name, param_name) -> "array"/"dict"
self.current_namespace: Optional[str] = None
self.namespaces: Dict[str, dict] = {}
self.busing_names: Dict[str, str] = {}
self.busing_sources: List[str] = []
self.using_direct: Set[str] = set()
self.using_aliases: Dict[str, str] = {}
self.using_selective: Dict[str, Set[str]] = {}
self.local_vars: Set[str] = set()
self.current_param_positions: Dict[str, int] = {} # param_name -> position (1-based)
self.global_vars: Set[str] = {
......@@ -172,7 +180,37 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
for program in programs:
for stmt in program.statements:
if isinstance(stmt, ClassDecl):
if isinstance(stmt, NamespaceDecl):
ns_name = stmt.name
if ns_name not in self.namespaces:
self.namespaces[ns_name] = {"functions": set(), "classes": set()}
for s in stmt.statements:
if isinstance(s, ClassDecl):
prefixed = f"{ns_name}__{s.name}"
self.classes[prefixed] = ClassDecl(
name=prefixed, parent=s.parent,
fields=s.fields, constructor=s.constructor,
methods=s.methods, location=s.location
)
self.namespaces[ns_name]["classes"].add(s.name)
elif isinstance(s, FunctionDecl):
prefixed = f"{ns_name}__{s.name}"
self.functions[prefixed] = s
self.namespaces[ns_name]["functions"].add(s.name)
elif isinstance(stmt, UsingStmt):
if stmt.alias:
self.using_aliases[stmt.alias] = stmt.namespace
elif stmt.names:
if stmt.namespace not in self.using_selective:
self.using_selective[stmt.namespace] = set()
self.using_selective[stmt.namespace].update(stmt.names)
else:
self.using_direct.add(stmt.namespace)
elif isinstance(stmt, BusingStmt):
self.busing_sources.append(stmt.path)
if stmt.name:
self.busing_names[stmt.name] = stmt.path
elif isinstance(stmt, ClassDecl):
self.classes[stmt.name] = stmt
elif isinstance(stmt, FunctionDecl):
self.functions[stmt.name] = stmt
......@@ -180,6 +218,23 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
if isinstance(stmt.target, Identifier):
self.global_vars.add(stmt.target.name)
for cls_name, cls_decl in list(self.classes.items()):
if cls_decl.parent and cls_decl.parent not in self.classes:
for ns in self.namespaces:
ns_parent = f"{ns}__{cls_decl.parent}"
if ns_parent in self.classes:
self.classes[cls_name] = ClassDecl(
name=cls_decl.name, parent=ns_parent,
fields=cls_decl.fields, constructor=cls_decl.constructor,
methods=cls_decl.methods, location=cls_decl.location
)
break
if self.busing_sources:
for path in self.busing_sources:
self.emit(f'source "{path}"')
self.emit()
for program in programs:
for stmt in program.statements:
self.generate_statement(stmt)
......@@ -189,6 +244,32 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
return "\n".join(self.output)
def _resolve_name(self, name: str) -> str:
if name in self.functions or name in self.classes:
return name
if self.current_namespace:
prefixed = f"{self.current_namespace}__{name}"
if prefixed in self.functions or prefixed in self.classes:
return prefixed
for ns in self.using_direct:
prefixed = f"{ns}__{name}"
if prefixed in self.functions or prefixed in self.classes:
return prefixed
for ns, names in self.using_selective.items():
if name in names:
prefixed = f"{ns}__{name}"
if prefixed in self.functions or prefixed in self.classes:
return prefixed
return name
def _resolve_qualified(self, ns_name: str, member: str) -> str:
if ns_name in self.namespaces:
return f"{ns_name}__{member}"
if ns_name in self.using_aliases:
real_ns = self.using_aliases[ns_name]
return f"{real_ns}__{member}"
return None
def _generate_test_runner(self):
"""Generate test runner that executes all @test functions."""
self.emit()
......
......@@ -8,7 +8,8 @@ from .ast_nodes import (
WhenBranch, TryStmt, ThrowStmt, DeferStmt, AwaitStmt, OnSignalStmt,
AsyncExpr, ReturnStmt, ArrayLiteral,
DictLiteral, IndexAccess, Lambda, MemberAccess, ThisExpr, Block,
BinaryOp, UnaryOp, WithStmt, Program, StringLiteral
BinaryOp, UnaryOp, WithStmt, Program, StringLiteral,
NamespaceDecl, BusingStmt
)
......@@ -40,6 +41,8 @@ class UsageAnalyzer:
self.current_method_name: str = None
self.method_calls: dict = {}
self.func_param_types: dict = {}
self.namespaces: dict = {}
self.busing_names: set = set()
def analyze(self, programs: list, test_mode: bool = False) -> set:
self.used = {'core'}
......@@ -52,6 +55,23 @@ class UsageAnalyzer:
self._collect_class_fields(stmt)
elif isinstance(stmt, FunctionDecl):
self.defined_functions[stmt.name] = stmt
elif isinstance(stmt, NamespaceDecl):
ns_name = stmt.name
if ns_name not in self.namespaces:
self.namespaces[ns_name] = {"classes": set(), "functions": set()}
for s in stmt.statements:
if isinstance(s, ClassDecl):
prefixed = f"{ns_name}__{s.name}"
self.defined_classes[prefixed] = s
self._collect_class_fields(s)
self.namespaces[ns_name]["classes"].add(s.name)
elif isinstance(s, FunctionDecl):
prefixed = f"{ns_name}__{s.name}"
self.defined_functions[prefixed] = s
self.namespaces[ns_name]["functions"].add(s.name)
elif isinstance(stmt, BusingStmt):
if stmt.name:
self.busing_names.add(stmt.name)
for program in programs:
for stmt in program.statements:
......@@ -157,6 +177,11 @@ class UsageAnalyzer:
return self.used_methods.get(class_name, set())
def _analyze_stmt(self, stmt):
if isinstance(stmt, NamespaceDecl):
for s in stmt.statements:
self._analyze_stmt(s)
return
if isinstance(stmt, ClassDecl):
self.has_classes = True
self.current_class_name = stmt.name
......@@ -415,6 +440,14 @@ class UsageAnalyzer:
elif isinstance(callee.object, Identifier):
ns = callee.object.name
method = callee.member
if ns in self.namespaces:
prefixed = f"{ns}__{method}"
if prefixed in self.defined_classes:
self.has_classes = True
self.used_classes.add(prefixed)
return
if ns in self.busing_names:
return
if ns in self.variable_types:
obj_class = self.variable_types[ns]
if obj_class not in self.used_methods:
......
......@@ -222,21 +222,23 @@ class DispatchMixin:
return
if isinstance(stmt.value, NewExpr):
resolved_class = self._resolve_name(stmt.value.class_name)
args_str = self._generate_call_args_str(stmt.value.arguments)
self.emit(f'{stmt.value.class_name} {args_str}')
self.emit(f'{resolved_class} {args_str}')
self.emit_var_assign(target, '$__ct_last_instance')
self.object_vars.add(target)
self.instance_vars[target] = stmt.value.class_name
self.instance_vars[target] = resolved_class
return
if isinstance(stmt.value, CallExpr) and isinstance(stmt.value.callee, Identifier):
callee_name = stmt.value.callee.name
if callee_name in self.classes:
resolved_callee = self._resolve_name(callee_name)
if resolved_callee in self.classes:
args_str = self._generate_call_args_str(stmt.value.arguments)
self.emit(f'{callee_name} {args_str}')
self.emit(f'{resolved_callee} {args_str}')
self.emit_var_assign(target, '$__ct_last_instance')
self.object_vars.add(target)
self.instance_vars[target] = callee_name
self.instance_vars[target] = resolved_callee
return
if isinstance(stmt.value, CallExpr) and isinstance(stmt.value.callee, MemberAccess):
......@@ -284,12 +286,13 @@ class DispatchMixin:
if isinstance(stmt.value, CallExpr) and isinstance(stmt.value.callee, Identifier):
func_name = stmt.value.callee.name
if func_name not in BUILTIN_FUNCS and func_name not in self.classes:
resolved_func = self._resolve_name(func_name)
if func_name not in BUILTIN_FUNCS and resolved_func not in self.classes:
args_str = self._generate_call_args_str(stmt.value.arguments)
if func_name in self.callback_vars:
self.emit_var_assign(target, f'$("${{{func_name}}}" {args_str})')
elif func_name in self.functions:
self.emit(f'{func_name} {args_str} >/dev/null')
elif resolved_func in self.functions:
self.emit(f'{resolved_func} {args_str} >/dev/null')
self.emit_var_assign(target, f'${RET_VAR}')
else:
self.emit_var_assign(target, f'$({func_name} {args_str})')
......@@ -396,11 +399,10 @@ class DispatchMixin:
self.emit(f'__ct_dst_{field}=("${{__ct_src_{field}[@]}}")')
return
# Handle class instantiation: this.field = SomeClass() or this.field = new SomeClass()
# Important: save instance immediately as nested constructors may overwrite __ct_last_instance
if isinstance(stmt.value, NewExpr):
resolved_class = self._resolve_name(stmt.value.class_name)
args_str = self._generate_call_args_str(stmt.value.arguments)
self.emit(f'{stmt.value.class_name} {args_str}')
self.emit(f'{resolved_class} {args_str}')
self.emit(f'local __ct_tmp_{field}="$__ct_last_instance"')
self.emit(f'__CT_OBJ["$this.{field}"]="$__ct_tmp_{field}"')
return
......@@ -517,6 +519,26 @@ class DispatchMixin:
var_name = callee.object.object.name
field_name = callee.object.member
method = callee.member
is_array_field = any(
self.class_field_types.get((cls, field_name)) == "array"
for cls in self.classes
)
is_dict_field = any(
self.class_field_types.get((cls, field_name)) == "dict"
for cls in self.classes
)
if is_array_field and method in ARR_METHODS:
arr_ref = f'"${{{var_name}}}_{field_name}"'
self.emit(f'{ARR_METHODS[method]} {arr_ref} {args_str} >/dev/null'.strip())
self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True
if is_dict_field and method in DICT_METHODS_MAP:
dict_ref = f'"${{{var_name}}}_{field_name}"'
self.emit(f'{DICT_METHODS_MAP[method]} {dict_ref} {args_str} >/dev/null'.strip())
self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True
field_ref = f'${{__CT_OBJ["${{{var_name}}}.{field_name}"]:-}}'
if method in STR_METHODS:
self.emit(f'{STR_METHODS[method]} "{field_ref}" {args_str} >/dev/null'.replace(' ', ' '))
......@@ -530,6 +552,24 @@ class DispatchMixin:
param_map = getattr(self, 'param_name_map', {})
mapped_name = param_map.get(obj_name, obj_name)
resolved = self._resolve_qualified(obj_name, method)
if resolved is not None:
if resolved in self.classes:
self.emit(f'{resolved} {args_str}')
self.emit_var_assign(target, '$__ct_last_instance')
self.object_vars.add(target)
self.instance_vars[target] = resolved
elif resolved in self.functions:
self.emit(f'{resolved} {args_str} >/dev/null')
self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
else:
self.emit_var_assign(target, f'$({resolved} {args_str})')
return True
if obj_name in self.busing_names:
self.emit_var_assign(target, f'$({method} {args_str})')
return True
if obj_name in BUILTIN_NAMESPACES:
self._validate_namespace_method(obj_name, method, location)
return False
......@@ -931,8 +971,9 @@ class DispatchMixin:
if isinstance(expr.callee, MemberAccess):
obj = expr.callee.object
if isinstance(obj, Identifier):
# Don't suppress if it's an object variable or in BUILTIN_NAMESPACES
if obj.name not in BUILTIN_NAMESPACES and obj.name not in self.object_vars:
if (obj.name not in BUILTIN_NAMESPACES and
obj.name not in self.object_vars and
obj.name not in getattr(self, 'busing_names', {})):
call_code = f'{call_code} >/dev/null'
# Don't add >/dev/null for ThisExpr or MemberAccess (this.field.method())
self.emit(call_code)
......@@ -1140,6 +1181,9 @@ class DispatchMixin:
elif name == "pid":
return '__ct_pid'
else:
resolved = self._resolve_name(name)
if resolved != name:
return f'{resolved} {args_str}'
if self._is_callback_var(name):
return f'"${{{name}}}" {args_str}'
return f'{name} {args_str}'
......@@ -1148,7 +1192,8 @@ class DispatchMixin:
"""Check if name is a variable holding a callback (function name)."""
if name in getattr(self, 'callback_vars', set()):
return True
if name in self.functions or name in self.classes:
resolved = self._resolve_name(name)
if resolved in self.functions or resolved in self.classes:
return False
if name in self.local_vars:
return True
......@@ -1161,9 +1206,10 @@ class DispatchMixin:
return True
if isinstance(expr, CallExpr) and isinstance(expr.callee, Identifier):
name = expr.callee.name
if (name not in self.functions and
resolved = self._resolve_name(name)
if (resolved not in self.functions and
name not in BUILTIN_FUNCS and
name not in self.classes and
resolved not in self.classes and
not self._is_callback_var(name)):
return True
return False
......@@ -1192,7 +1238,14 @@ class DispatchMixin:
return f'__ct_class_{self.current_class}_{method} "$this" {args_str}'
if isinstance(callee.object, Identifier):
result = self._generate_stdlib_call(callee.object.name, method, args, args_str, location)
obj_name = callee.object.name
resolved = self._resolve_qualified(obj_name, method)
if resolved is not None:
return f'{resolved} {args_str}'
if obj_name in self.busing_names:
return f'{method} {args_str}'
result = self._generate_stdlib_call(obj_name, method, args, args_str, location)
if result:
return result
......
......@@ -67,9 +67,10 @@ class ExprMixin:
return self.generate_lambda(expr)
if isinstance(expr, NewExpr):
resolved_class = self._resolve_name(expr.class_name)
args = [self.generate_expr(a) for a in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
return f'$({expr.class_name} {args_str}; echo "$__ct_last_instance")'
return f'$({resolved_class} {args_str}; echo "$__ct_last_instance")'
if isinstance(expr, AsyncExpr):
return self._generate_async_expr(expr)
......
......@@ -47,11 +47,23 @@ def compile_file (source_path: str, output_path: str = None) -> tuple[bool, str]
return compile_files ([source_path])
def _get_pkgdatadir () -> str:
"""Get package data directory (for meson-installed libs)."""
bootstrap_dir = os.path.dirname (os.path.abspath (__file__))
project_root = os.path.dirname (bootstrap_dir)
return os.path.join (project_root, "lib")
def compile_files (source_paths: list, type_check: bool = True, warn_types: bool = False) -> tuple[bool, str]:
"""Compile multiple .ct files to bash (Vala-style multi-file)."""
asts = []
parsed_files = set ()
for source_path in source_paths:
abs_path = os.path.abspath (source_path)
if abs_path in parsed_files:
continue
parsed_files.add (abs_path)
ast = parse_file (source_path)
if ast is None:
return False, ""
......@@ -68,41 +80,40 @@ def compile_files (source_paths: list, type_check: bool = True, warn_types: bool
def find_ct_files (directory: str = ".") -> list:
"""Find all .ct files in directory, sorted (libraries first, main.ct last)."""
"""Find all .ct files in directory recursively, sorted (namespace files first, main.ct last)."""
import glob
files = sorted (glob.glob (os.path.join (directory, "*.ct")))
files = sorted (glob.glob (os.path.join (directory, "**", "*.ct"), recursive=True))
files += [f for f in sorted (glob.glob (os.path.join (directory, "*.ct"))) if f not in files]
def is_library (path):
basename = os.path.basename (path)
if basename.startswith ("lib") or basename.startswith ("_"):
return True
def has_namespace (path):
try:
with open (path, 'r') as f:
content = f.read (2048)
return "class " in content
return "namespace " in content
except:
return False
main_files = [f for f in files if os.path.basename (f) == "main.ct"]
lib_files = [f for f in files if f not in main_files and is_library (f)]
other_files = [f for f in files if f not in main_files and f not in lib_files]
return lib_files + other_files + main_files
ns_files = [f for f in files if f not in main_files and has_namespace (f)]
other_files = [f for f in files if f not in main_files and f not in ns_files]
return ns_files + other_files + main_files
def cmd_build (args):
"""Build command - compile .ct to .sh (supports multiple files)"""
source_paths = args.sources
if source_paths == ["."]:
source_paths = find_ct_files (".")
if len (source_paths) == 1 and os.path.isdir (source_paths[0]):
directory = source_paths[0]
source_paths = find_ct_files (directory)
if not source_paths:
print ("Error: No .ct files found in current directory", file=sys.stderr)
return 1
for source_path in source_paths:
if not source_path.endswith (".ct"):
print (f"Error: Source file must have .ct extension: {source_path}", file=sys.stderr)
print (f"Error: No .ct files found in {directory}", file=sys.stderr)
return 1
else:
for source_path in source_paths:
if not source_path.endswith (".ct"):
print (f"Error: Source file must have .ct extension: {source_path}", file=sys.stderr)
return 1
if args.output:
output_path = args.output
......@@ -155,16 +166,17 @@ def cmd_run (args):
source_paths = all_args
script_args = []
if source_paths == ["."]:
source_paths = find_ct_files (".")
if len (source_paths) == 1 and os.path.isdir (source_paths[0]):
directory = source_paths[0]
source_paths = find_ct_files (directory)
if not source_paths:
print ("Error: No .ct files found in current directory", file=sys.stderr)
return 1
for source_path in source_paths:
if not source_path.endswith (".ct"):
print (f"Error: Source file must have .ct extension: {source_path}", file=sys.stderr)
print (f"Error: No .ct files found in {directory}", file=sys.stderr)
return 1
else:
for source_path in source_paths:
if not source_path.endswith (".ct"):
print (f"Error: Source file must have .ct extension: {source_path}", file=sys.stderr)
return 1
success, output = compile_files (source_paths)
if not success:
......@@ -203,16 +215,17 @@ def cmd_test (args):
"""Test command - run @test functions"""
source_paths = args.sources
if source_paths == ["."]:
source_paths = find_ct_files (".")
if len (source_paths) == 1 and os.path.isdir (source_paths[0]):
directory = source_paths[0]
source_paths = find_ct_files (directory)
if not source_paths:
print ("Error: No .ct files found in current directory", file=sys.stderr)
return 1
for source_path in source_paths:
if not source_path.endswith (".ct"):
print (f"Error: Source file must have .ct extension: {source_path}", file=sys.stderr)
print (f"Error: No .ct files found in {directory}", file=sys.stderr)
return 1
else:
for source_path in source_paths:
if not source_path.endswith (".ct"):
print (f"Error: Source file must have .ct extension: {source_path}", file=sys.stderr)
return 1
asts = []
for source_path in source_paths:
......@@ -263,37 +276,63 @@ def cmd_test (args):
def cmd_build_lib (args):
"""Build library command"""
source_path = args.source
if not source_path.endswith (".ct"):
print (f"Error: Source file must have .ct extension", file=sys.stderr)
return 1
output_path = source_path.replace (".ct", ".sh")
source = args.source
output_path = getattr (args, 'output', None)
install = getattr (args, 'install', False)
success, output = compile_file (source_path)
if not success:
return 1
if os.path.isdir (source):
source_paths = find_ct_files (source)
if not source_paths:
print (f"Error: No .ct files found in {source}", file=sys.stderr)
return 1
lib_name = os.path.basename (os.path.normpath (source))
if not output_path:
output_path = os.path.join (source, f"{lib_name}.sh")
else:
if not source.endswith (".ct"):
print (f"Error: Source file must have .ct extension", file=sys.stderr)
return 1
source_paths = [source]
lib_name = Path (source).stem
if not output_path:
output_path = source.replace (".ct", ".sh")
lib_name = Path (source_path).stem
metadata = f'''
asts = []
for source_path in source_paths:
ast = parse_file (source_path)
if ast is None:
return 1
asts.append (ast)
'''
footer = '''
codegen = CodeGenerator ()
code = codegen.generate_multi (asts)
'''
if codegen.errors.has_errors ():
codegen.errors.print_errors ()
return 1
output = metadata + output + footer
ns_list = " ".join (sorted (codegen.namespaces.keys ())) if codegen.namespaces else lib_name
metadata = f"# content-lib: {lib_name}\n# content-version: 1.0.0\n# content-namespaces: {ns_list}\n"
output = metadata + code
try:
with open (output_path, "w", encoding="utf-8") as f:
f.write (output)
os.chmod (output_path, 0o644)
print (f"Library built: {source_path} -> {output_path}")
src_desc = source if not os.path.isdir (source) else f"{source}/ ({len (source_paths)} files)"
print (f"Library built: {src_desc} -> {output_path}")
except Exception as e:
print (f"Error writing output: {e}", file=sys.stderr)
return 1
if install:
install_dir = os.path.expanduser ("~/.content/libs")
os.makedirs (install_dir, exist_ok=True)
install_path = os.path.join (install_dir, f"{lib_name}.sh")
import shutil
shutil.copy2 (output_path, install_path)
print (f"Installed: {install_path}")
return 0
......@@ -321,7 +360,9 @@ def main ():
run_parser.add_argument ("sources_and_args", nargs="+", help="Source files (.ct) [-- script args]")
lib_parser = subparsers.add_parser ("build-lib", help="Build a library")
lib_parser.add_argument ("source", help="Source file (.ct)")
lib_parser.add_argument ("source", help="Source file (.ct) or directory")
lib_parser.add_argument ("-o", "--output", help="Output file (.sh)")
lib_parser.add_argument ("--install", action="store_true", help="Install to ~/.content/libs/")
test_parser = subparsers.add_parser ("test", help="Run @test functions")
test_parser.add_argument ("sources", nargs="+", help="Source files (.ct)")
......
......@@ -2,13 +2,14 @@ from typing import List, Optional, Callable, Union
from .tokens import Token, TokenType
from .ast_nodes import (
SourceLocation, Program, Declaration, Statement, Decorator, FunctionDecl,
Parameter, ClassDecl, ClassField, ConstructorDecl, ImportStmt, Block, ReturnStmt,
Parameter, ClassDecl, ClassField, ConstructorDecl, Block, ReturnStmt,
BreakStmt, ContinueStmt, IfStmt, WhileStmt, ForStmt, ForeachStmt, WithStmt,
TryStmt, ThrowStmt, DeferStmt, AwaitStmt, OnSignalStmt, WhenStmt, WhenBranch, RangePattern,
ExpressionStmt, Assignment, IntegerLiteral, FloatLiteral, StringLiteral,
BoolLiteral, NilLiteral, ThisExpr, ArrayLiteral, DictLiteral, Identifier,
BinaryOp, UnaryOp, CallExpr, MemberAccess, IndexAccess, NewExpr, AsyncExpr, Lambda,
BaseCall, Expression, TypeAnnotation
BaseCall, Expression, TypeAnnotation,
NamespaceDecl, UsingStmt, BusingStmt
)
from .errors import CompileError, ErrorCollector
......@@ -145,8 +146,12 @@ class Parser:
return self.parse_function (decorators)
if self.check (TokenType.CLASS):
return self.parse_class ()
if self.check (TokenType.IMPORT):
return self.parse_import ()
if self.check (TokenType.NAMESPACE):
return self.parse_namespace ()
if self.check (TokenType.USING):
return self.parse_using ()
if self.check (TokenType.BUSING):
return self.parse_busing ()
if decorators:
self.error ("Decorators can only be applied to functions or methods")
......@@ -315,12 +320,57 @@ class Parser:
return ConstructorDecl (params=params, body=body, location=loc)
def parse_import (self) -> ImportStmt:
def parse_namespace (self) -> NamespaceDecl:
loc = self.location ()
self.expect (TokenType.IMPORT)
path = self.expect (TokenType.STRING, "Expected import path").value
return ImportStmt (path=path, location=loc)
self.expect (TokenType.NAMESPACE)
name = self.expect (TokenType.IDENTIFIER, "Expected namespace name").value
self.skip_newlines ()
self.expect (TokenType.LBRACE, "Expected '{' after namespace name")
self.skip_newlines ()
statements = []
while not self.check (TokenType.RBRACE) and not self.check (TokenType.EOF):
stmt = self.parse_declaration ()
if stmt:
statements.append (stmt)
self.skip_newlines ()
self.expect (TokenType.RBRACE, "Expected '}' to close namespace")
return NamespaceDecl (name=name, statements=statements, location=loc)
def parse_using (self) -> UsingStmt:
loc = self.location ()
self.expect (TokenType.USING)
name = self.expect (TokenType.IDENTIFIER, "Expected namespace name").value
alias = None
if self.match (TokenType.ASSIGN):
alias = name
name = self.expect (TokenType.IDENTIFIER, "Expected namespace name after '='").value
names = None
if self.match (TokenType.LBRACE):
names = []
while not self.check (TokenType.RBRACE) and not self.check (TokenType.EOF):
names.append (self.expect (TokenType.IDENTIFIER, "Expected symbol name").value)
if not self.match (TokenType.COMMA):
break
self.expect (TokenType.RBRACE, "Expected '}' after using names")
return UsingStmt (namespace=name, alias=alias, names=names, location=loc)
def parse_busing (self) -> BusingStmt:
loc = self.location ()
self.expect (TokenType.BUSING)
if self.check (TokenType.STRING):
path = self.advance ().value
return BusingStmt (name=None, path=path, location=loc)
name = self.expect (TokenType.IDENTIFIER, "Expected name or path").value
self.expect (TokenType.ASSIGN, "Expected '=' after busing name")
path = self.expect (TokenType.STRING, "Expected path string").value
return BusingStmt (name=name, path=path, location=loc)
def parse_statement (self) -> Optional[Statement]:
if self.check (TokenType.RETURN):
......
from .ast_nodes import (
FunctionDecl, ClassDecl, ImportStmt, Assignment, ExpressionStmt, IfStmt,
FunctionDecl, ClassDecl, Assignment, ExpressionStmt, IfStmt,
WhileStmt, ForStmt, ForeachStmt, WithStmt, TryStmt, ThrowStmt, DeferStmt,
AwaitStmt, OnSignalStmt, AsyncExpr,
WhenStmt, RangePattern, ReturnStmt, BreakStmt, ContinueStmt, Block,
CallExpr, Identifier, MemberAccess, ThisExpr, StringLiteral, NewExpr,
BinaryOp, DictLiteral, ArrayLiteral, WhenBranch
BinaryOp, DictLiteral, ArrayLiteral, WhenBranch,
NamespaceDecl, UsingStmt, BusingStmt
)
from .constants import RET_VAR, RET_ARR, COPROC_PREFIX
......@@ -16,12 +17,18 @@ class StmtMixin:
if isinstance(stmt, FunctionDecl):
self.generate_function(stmt)
elif isinstance(stmt, ClassDecl):
if self.used_classes is not None and stmt.name not in self.used_classes:
if self.current_namespace:
self.generate_class(stmt)
elif self.used_classes is not None and stmt.name not in self.used_classes:
self.emit(f"# DCE: skipped unused class {stmt.name}")
else:
self.generate_class(stmt)
elif isinstance(stmt, ImportStmt):
self.generate_import(stmt)
elif isinstance(stmt, NamespaceDecl):
self.generate_namespace(stmt)
elif isinstance(stmt, UsingStmt):
self.generate_using(stmt)
elif isinstance(stmt, BusingStmt):
self.generate_busing(stmt)
elif isinstance(stmt, Assignment):
self.generate_assignment(stmt)
elif isinstance(stmt, ExpressionStmt):
......@@ -58,22 +65,18 @@ class StmtMixin:
for s in stmt.statements:
self.generate_statement(s)
def generate_import(self, stmt: ImportStmt):
path = stmt.path
if path.startswith("std/"):
self.emit(f"# import {path} (stdlib - already included)")
elif path.endswith(".sh"):
self.emit(f'source "{path}"')
else:
self.emit(f'# import "{path}"')
self.emit(f'if [[ -f "$HOME/.content/libs/{path}.sh" ]]; then')
with self.indented():
self.emit(f'source "$HOME/.content/libs/{path}.sh"')
self.emit(f'elif [[ -f "/usr/lib/content/{path}.sh" ]]; then')
with self.indented():
self.emit(f'source "/usr/lib/content/{path}.sh"')
self.emit("fi")
self.emit()
def generate_namespace(self, stmt: NamespaceDecl):
old_ns = self.current_namespace
self.current_namespace = stmt.name
for s in stmt.statements:
self.generate_statement(s)
self.current_namespace = old_ns
def generate_using(self, stmt: UsingStmt):
pass
def generate_busing(self, stmt: BusingStmt):
pass
def generate_if(self, stmt: IfStmt):
if isinstance(stmt.condition, CallExpr) and isinstance(stmt.condition.callee, Identifier):
......@@ -488,9 +491,10 @@ class StmtMixin:
return
if isinstance(stmt.value, NewExpr):
resolved_class = self._resolve_name(stmt.value.class_name)
args = [self.generate_expr(arg) for arg in stmt.value.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{stmt.value.class_name} {args_str}')
self.emit(f'{resolved_class} {args_str}')
self.emit(f'{RET_VAR}="$__ct_last_instance"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
......@@ -613,25 +617,44 @@ class StmtMixin:
obj_name = expr.callee.object.name
method = expr.callee.member
args = [self.generate_expr(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
builtin_namespaces = {"fs", "http", "json", "logger", "regex", "args", "shell"}
resolved = self._resolve_qualified(obj_name, method)
if resolved is not None:
if resolved in self.classes:
self.emit(f'{resolved} {args_str}')
self.emit(f'{RET_VAR}="$__ct_last_instance"')
else:
self.emit(f'{resolved} {args_str} >/dev/null')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
if obj_name in self.busing_names:
self.emit_var_assign(RET_VAR, f'$({method} {args_str})')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
if obj_name not in builtin_namespaces:
result = self._dispatch_instance_method_return(obj_name, method, args)
if result:
return True
elif isinstance(expr.callee, Identifier):
func_name = expr.callee.name
if func_name in self.classes:
resolved = self._resolve_name(func_name)
if resolved in self.classes:
args = [self._generate_call_arg(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{func_name} {args_str}')
self.emit(f'{resolved} {args_str}')
self.emit(f'{RET_VAR}="$__ct_last_instance"')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
if func_name in self.functions:
if resolved in self.functions:
args = [self._generate_call_arg(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{func_name} {args_str} >/dev/null')
self.emit(f'{resolved} {args_str} >/dev/null')
self.emit(f'echo "${{{RET_VAR}}}"')
self.emit("return 0")
return True
......
......@@ -27,7 +27,6 @@ class TokenType (Enum):
WHILE = auto ()
BREAK = auto ()
CONTINUE = auto ()
IMPORT = auto ()
TRY = auto ()
EXCEPT = auto ()
FINALLY = auto ()
......@@ -40,6 +39,9 @@ class TokenType (Enum):
ASYNC = auto ()
AWAIT = auto ()
ON = auto ()
NAMESPACE = auto ()
USING = auto ()
BUSING = auto ()
PLUS = auto ()
MINUS = auto ()
......@@ -96,7 +98,6 @@ KEYWORDS = {
'while': TokenType.WHILE,
'break': TokenType.BREAK,
'continue': TokenType.CONTINUE,
'import': TokenType.IMPORT,
'try': TokenType.TRY,
'except': TokenType.EXCEPT,
'finally': TokenType.FINALLY,
......@@ -109,6 +110,9 @@ KEYWORDS = {
'async': TokenType.ASYNC,
'await': TokenType.AWAIT,
'on': TokenType.ON,
'namespace': TokenType.NAMESPACE,
'using': TokenType.USING,
'busing': TokenType.BUSING,
'true': TokenType.TRUE,
'false': TokenType.FALSE,
'nil': TokenType.NIL,
......
......@@ -30,6 +30,16 @@ AutoProv:no
ContenT is a DSL compiler that transforms .ct files into optimized Bash scripts.
The syntax is a hybrid of Python, Go and Vala.
%package -n %name-lib-cli
Summary: CLI library for ContenT (urfave/cli style)
Group: Development/Tools
Requires: %name = %EVR
%description -n %name-lib-cli
Precompiled CLI library for ContenT language.
Provides app creation, flag parsing, subcommands and auto-generated help
in the style of Go urfave/cli.
%prep
%setup
......@@ -45,9 +55,16 @@ The syntax is a hybrid of Python, Go and Vala.
%files
%_bindir/content
%_datadir/content/
%dir %_datadir/content/
%_datadir/content/bootstrap/
%doc README.md README_ru.md LANGUAGE_SPEC.md
%files -n %name-lib-cli
%dir %_datadir/content/lib/
%_datadir/content/lib/cli.sh
%_datadir/content/lib/cli.ct
%changelog
* Thu Feb 20 2026 Roman Alifanov <ximper@altlinux.org> 0.1.0-alt1
* Fri Feb 20 2026 Roman Alifanov <ximper@altlinux.org> 0.1.0-alt1
- initial build
using cli
app = new_app ("estrlist", "String list operations")
app.with_version ("2.0")
noop_cmd = new_command ("noop", "Do nothing")
app.add_command (noop_cmd)
strip_cmd = new_command ("strip", "Remove extra spaces")
strip_cmd.with_category ("Basic")
strip_cmd.with_action (cmd => {
print ("strip: {cmd.arg (0)}")
})
app.add_command (strip_cmd)
count_cmd = new_command ("count", "Count elements")
count_cmd.with_category ("Basic")
count_cmd.with_action (cmd => {
print ("count: {cmd.arg (0)}")
})
app.add_command (count_cmd)
union_cmd = new_command ("union", "Sort and deduplicate")
union_cmd.with_category ("Set Operations")
union_cmd.with_action (cmd => {
print ("union: {cmd.arg (0)}")
})
app.add_command (union_cmd)
inter_cmd = new_command ("intersection", "Common elements")
inter_cmd.with_category ("Set Operations")
inter_cmd.with_action (cmd => {
print ("intersection: {cmd.arg (0)}")
})
app.add_command (inter_cmd)
app.with_action (cmd => {
cmd.help ()
})
app.run ()
# main.ct - Example app using class-based cli.ct library
# Similar to urfave/cli v3 style
using cli
# Create app with built-in help and version flags
app = new_app ("myapp", "A sample CLI application")
app = new_app ("greet", "A simple CLI app")
app.with_version ("1.0.0")
# Add custom flags
name_flag = new_string_flag ("name", "World", "Name to greet")
name_flag.with_short ("n")
app.add_flag (name_flag)
lang_flag = new_string_flag ("lang", "english", "Language for the greeting")
lang_flag.with_short ("l")
app.add_flag (lang_flag)
count_flag = new_int_flag ("count", "1", "Number of times to greet")
count_flag.with_short ("c")
app.add_flag (count_flag)
loud_flag = new_bool_flag ("loud", "Use uppercase")
loud_flag.with_short ("l")
app.add_flag (loud_flag)
# Create commands
greet_cmd = new_command ("greet", "Greet someone")
app.add_command (greet_cmd)
calc_cmd = new_command ("calc", "Simple calculator")
calc_cmd.with_category ("math")
app.add_command (calc_cmd)
add_cmd = new_command ("add", "Add two numbers")
add_cmd.with_category ("math")
app.add_command (add_cmd)
# Run and get matched command
cmd = app.run ()
# Handle commands
when cmd {
"greet" {
name = app.string ("name")
count = app.int ("count")
loud = app.bool ("loud")
i = 0
while i < count {
if loud {
print ("HELLO, {name}!")
} else {
print ("Hello, {name}!")
}
i = i + 1
}
}
"calc" {
if calc_cmd.narg () < 3 {
print ("Usage: myapp calc <num1> <op> <num2>")
print ("Example: myapp calc 10 + 5")
} else {
# Args are in the subcommand
a = calc_cmd.arg (0)
op = calc_cmd.arg (1)
b = calc_cmd.arg (2)
when op {
"+" {
result = a + b
print ("{a} + {b} = {result}")
}
"-" {
result = a - b
print ("{a} - {b} = {result}")
}
"x" {
result = a * b
print ("{a} * {b} = {result}")
}
"/" {
result = a / b
print ("{a} / {b} = {result}")
}
else {
print ("Unknown operator: {op}")
}
}
}
app.with_action (cmd => {
name = "World"
if cmd.narg () > 0 {
name = cmd.arg (0)
}
"add" {
if add_cmd.narg () < 2 {
print ("Usage: myapp add <num1> <num2>")
} else {
a = add_cmd.arg (0)
b = add_cmd.arg (1)
result = a + b
print ("{a} + {b} = {result}")
}
lang = cmd.string ("lang")
if lang == "spanish" {
print ("Hola, {name}!")
} else if lang == "french" {
print ("Bonjour, {name}!")
} else {
print ("Hello, {name}!")
}
"" {
# No command - show help
app.help ()
}
else {
print ("Unknown command: {cmd}")
app.help ()
}
}
})
app.run ()
using cli
app = new_app ("flagdemo", "Demonstrates various flag types")
name_flag = new_string_flag ("name", "World", "Name to greet")
name_flag.with_short ("n")
app.add_flag (name_flag)
count_flag = new_int_flag ("count", "1", "Number of greetings")
count_flag.with_short ("c")
app.add_flag (count_flag)
loud_flag = new_bool_flag ("loud", "Use uppercase")
loud_flag.with_short ("l")
app.add_flag (loud_flag)
app.with_action (cmd => {
name = cmd.string ("name")
count = cmd.int ("count")
loud = cmd.bool ("loud")
i = 0
while i < count {
if loud {
print ("HELLO, {name}!")
} else {
print ("Hello, {name}!")
}
i = i + 1
}
})
app.run ()
using cli
app = new_app ("tasks", "A task management tool")
app.with_version ("0.1.0")
add_cmd = new_command ("add", "Add a task to the list")
add_cmd.with_alias ("a")
add_cmd.with_action (cmd => {
if cmd.narg () > 0 {
print ("added task: {cmd.arg (0)}")
} else {
print ("error: task name required")
}
})
app.add_command (add_cmd)
complete_cmd = new_command ("complete", "Complete a task on the list")
complete_cmd.with_alias ("c")
complete_cmd.with_action (cmd => {
if cmd.narg () > 0 {
print ("completed task: {cmd.arg (0)}")
} else {
print ("error: task name required")
}
})
app.add_command (complete_cmd)
tmpl_add = new_command ("add", "Add a new template")
tmpl_add.with_action (cmd => {
print ("new task template: {cmd.arg (0)}")
})
tmpl_remove = new_command ("remove", "Remove an existing template")
tmpl_remove.with_action (cmd => {
print ("removed task template: {cmd.arg (0)}")
})
template_cmd = new_command ("template", "Options for task templates")
template_cmd.with_alias ("t")
template_cmd.add_command (tmpl_add)
template_cmd.add_command (tmpl_remove)
app.add_command (template_cmd)
app.run ()
install_data('cli.ct',
install_dir: pkgdatadir / 'lib')
content_cli = meson.project_source_root() / 'content'
custom_target('cli-lib',
input: 'cli.ct',
output: 'cli.sh',
command: [py, content_cli, 'build-lib', '@INPUT@', '-o', '@OUTPUT@'],
install: true,
install_dir: pkgdatadir / 'lib',
)
import os
import tempfile
import shutil
from bootstrap.main import find_ct_files, compile_files
from helpers import run_ct
class TestFindCtFiles:
def test_recursive_finds_subdirs(self):
with tempfile.TemporaryDirectory() as d:
os.makedirs(os.path.join(d, "sub"))
with open(os.path.join(d, "main.ct"), "w") as f:
f.write('print("hello")\n')
with open(os.path.join(d, "sub", "utils.ct"), "w") as f:
f.write('namespace utils { func greet() { print("hi") } }\n')
files = find_ct_files(d)
basenames = [os.path.basename(f) for f in files]
assert "main.ct" in basenames
assert "utils.ct" in basenames
def test_namespace_files_first(self):
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, "main.ct"), "w") as f:
f.write('print("hello")\n')
with open(os.path.join(d, "helpers.ct"), "w") as f:
f.write('namespace helpers { func greet() { print("hi") } }\n')
files = find_ct_files(d)
basenames = [os.path.basename(f) for f in files]
assert basenames.index("helpers.ct") < basenames.index("main.ct")
def test_main_ct_last(self):
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, "main.ct"), "w") as f:
f.write('print("hello")\n')
with open(os.path.join(d, "app.ct"), "w") as f:
f.write('func run() { print("run") }\n')
with open(os.path.join(d, "config.ct"), "w") as f:
f.write('namespace config { func get() { return "val" } }\n')
files = find_ct_files(d)
basenames = [os.path.basename(f) for f in files]
assert basenames[-1] == "main.ct"
def test_empty_dir(self):
with tempfile.TemporaryDirectory() as d:
files = find_ct_files(d)
assert files == []
def test_deep_nesting(self):
with tempfile.TemporaryDirectory() as d:
deep = os.path.join(d, "a", "b", "c")
os.makedirs(deep)
with open(os.path.join(deep, "deep.ct"), "w") as f:
f.write('func deep() { print("deep") }\n')
files = find_ct_files(d)
assert len(files) == 1
assert "deep.ct" in os.path.basename(files[0])
class TestAutoscanIntegration:
def test_multifile_project_compiles(self):
with tempfile.TemporaryDirectory() as d:
os.makedirs(os.path.join(d, "utils"))
with open(os.path.join(d, "utils", "strings.ct"), "w") as f:
f.write('namespace utils {\n func greet(name) {\n print("Hello, {name}!")\n }\n}\n')
with open(os.path.join(d, "main.ct"), "w") as f:
f.write('using utils\ngreet("World")\n')
files = find_ct_files(d)
ok, output = compile_files(files)
assert ok
assert "utils__greet" in output
assert "Hello" in output
import os
import sys
import tempfile
import shutil
import subprocess
class TestBuildLib:
def test_build_lib_single_file(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".ct", delete=False) as f:
f.write('namespace mylib {\n func greet(name) {\n print("Hello, {name}!")\n }\n}\n')
ct_path = f.name
sh_path = ct_path.replace(".ct", ".sh")
try:
result = subprocess.run(
[sys.executable, "-m", "bootstrap.main", "build-lib", ct_path],
capture_output=True, text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
assert result.returncode == 0, f"stderr: {result.stderr}"
assert os.path.isfile(sh_path)
with open(sh_path) as f:
content = f.read()
assert "content-lib:" in content
assert "content-version:" in content
assert "content-namespaces:" in content
assert "mylib" in content
finally:
os.unlink(ct_path)
if os.path.exists(sh_path):
os.unlink(sh_path)
def test_build_lib_output_flag(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".ct", delete=False) as f:
f.write('func helper() { print("ok") }\n')
ct_path = f.name
with tempfile.NamedTemporaryFile(suffix=".sh", delete=False) as out:
out_path = out.name
try:
result = subprocess.run(
[sys.executable, "-m", "bootstrap.main", "build-lib", ct_path, "-o", out_path],
capture_output=True, text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
assert result.returncode == 0, f"stderr: {result.stderr}"
assert os.path.isfile(out_path)
with open(out_path) as f:
content = f.read()
assert "content-lib:" in content
finally:
os.unlink(ct_path)
if os.path.exists(out_path):
os.unlink(out_path)
default_sh = ct_path.replace(".ct", ".sh")
if os.path.exists(default_sh):
os.unlink(default_sh)
def test_build_lib_directory(self):
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, "funcs.ct"), "w") as f:
f.write('namespace mylib {\n func add(a, b) {\n return a + b\n }\n}\n')
out_path = os.path.join(d, "out.sh")
result = subprocess.run(
[sys.executable, "-m", "bootstrap.main", "build-lib", d, "-o", out_path],
capture_output=True, text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
assert result.returncode == 0, f"stderr: {result.stderr}"
assert os.path.isfile(out_path)
with open(out_path) as f:
content = f.read()
assert "content-lib:" in content
assert "mylib__add" in content
def test_build_lib_metadata_correct(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".ct", delete=False) as f:
f.write('namespace alpha {\n func a() { print("a") }\n}\nnamespace beta {\n func b() { print("b") }\n}\n')
ct_path = f.name
sh_path = ct_path.replace(".ct", ".sh")
try:
result = subprocess.run(
[sys.executable, "-m", "bootstrap.main", "build-lib", ct_path],
capture_output=True, text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
assert result.returncode == 0
with open(sh_path) as f:
lines = f.readlines()
metadata_lines = [l for l in lines[:5] if l.startswith("# content-")]
assert len(metadata_lines) >= 3
ns_line = [l for l in metadata_lines if "namespaces" in l][0]
assert "alpha" in ns_line
assert "beta" in ns_line
finally:
os.unlink(ct_path)
if os.path.exists(sh_path):
os.unlink(sh_path)
def test_build_lib_install(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".ct", delete=False) as f:
f.write('func installed() { print("installed") }\n')
ct_path = f.name
install_dir = os.path.expanduser("~/.content/libs")
lib_name = os.path.basename(ct_path).replace(".ct", "")
install_path = os.path.join(install_dir, f"{lib_name}.sh")
sh_path = ct_path.replace(".ct", ".sh")
try:
result = subprocess.run(
[sys.executable, "-m", "bootstrap.main", "build-lib", ct_path, "--install"],
capture_output=True, text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
assert result.returncode == 0, f"stderr: {result.stderr}"
assert os.path.isfile(install_path)
finally:
os.unlink(ct_path)
if os.path.exists(sh_path):
os.unlink(sh_path)
if os.path.exists(install_path):
os.unlink(install_path)
import pytest
import os
import tempfile
from helpers import run_ct, compile_ct
from bootstrap.lexer import Lexer
from bootstrap.parser import Parser
from bootstrap.codegen import CodeGenerator
from bootstrap.ast_nodes import BusingStmt
def compile_source(source):
lexer = Lexer(source)
tokens = lexer.tokenize()
parser = Parser(tokens)
ast = parser.parse()
gen = CodeGenerator()
return gen.generate(ast)
class TestBusingParser:
def test_busing_unnamed(self, parse):
ast = parse('busing "/usr/share/mylib/functions.sh"')
stmt = ast.statements[0]
assert isinstance(stmt, BusingStmt)
assert stmt.name is None
assert stmt.path == "/usr/share/mylib/functions.sh"
def test_busing_named(self, parse):
ast = parse('busing config = "/usr/bin/shell-config"')
stmt = ast.statements[0]
assert isinstance(stmt, BusingStmt)
assert stmt.name == "config"
assert stmt.path == "/usr/bin/shell-config"
class TestBusingCodegen:
def test_busing_unnamed_generates_source(self):
source = 'busing "/path/to/lib.sh"'
code = compile_source(source)
assert 'source "/path/to/lib.sh"' in code
def test_busing_named_generates_source(self):
source = 'busing config = "/path/to/config.sh"'
code = compile_source(source)
assert 'source "/path/to/config.sh"' in code
def test_busing_named_call(self):
source = '''
busing config = "/path/to/config.sh"
config.get_value("key")
'''
code = compile_source(source)
assert 'source "/path/to/config.sh"' in code
assert 'get_value "key"' in code
assert 'config__' not in code
def test_busing_named_assignment(self):
source = '''
busing config = "/path/to/config.sh"
result = config.get_value("key")
'''
code = compile_source(source)
assert 'source "/path/to/config.sh"' in code
assert 'get_value "key"' in code
def test_busing_no_dev_null(self):
source = '''
busing mylib = "/path/to/lib.sh"
mylib.do_something("arg")
'''
code = compile_source(source)
lines = code.split('\n')
call_lines = [l for l in lines if 'do_something' in l]
assert call_lines
assert '>/dev/null' not in call_lines[0]
def test_multiple_busing(self):
source = '''
busing "/usr/share/lib1.sh"
busing mylib = "/usr/share/lib2.sh"
'''
code = compile_source(source)
assert 'source "/usr/share/lib1.sh"' in code
assert 'source "/usr/share/lib2.sh"' in code
class TestBusingIntegration:
def test_busing_unnamed_runs(self):
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
f.write('#!/usr/bin/env bash\nmy_hello() { echo "hello from bash"; }\n')
sh_path = f.name
try:
source = f'''
busing "{sh_path}"
my_hello()
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "hello from bash" in out
finally:
os.unlink(sh_path)
def test_busing_named_runs(self):
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
f.write('#!/usr/bin/env bash\nget_greeting() { echo "hi $1"; }\n')
sh_path = f.name
try:
source = f'''
busing mylib = "{sh_path}"
mylib.get_greeting("World")
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "hi World" in out
finally:
os.unlink(sh_path)
def test_busing_named_assignment_runs(self):
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
f.write('#!/usr/bin/env bash\ncompute_sum() { echo $(($1 + $2)); }\n')
sh_path = f.name
try:
source = f'''
busing calc = "{sh_path}"
result = calc.compute_sum("3", "7")
print(result)
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "10" in out
finally:
os.unlink(sh_path)
......@@ -92,7 +92,6 @@ class TestLexerKeywords:
("while", TokenType.WHILE),
("break", TokenType.BREAK),
("continue", TokenType.CONTINUE),
("import", TokenType.IMPORT),
("try", TokenType.TRY),
("except", TokenType.EXCEPT),
("finally", TokenType.FINALLY),
......
import pytest
from helpers import run_ct, compile_ct
from bootstrap.lexer import Lexer
from bootstrap.parser import Parser
from bootstrap.codegen import CodeGenerator
from bootstrap.ast_nodes import NamespaceDecl, UsingStmt, FunctionDecl, ClassDecl
def compile_source(source):
lexer = Lexer(source)
tokens = lexer.tokenize()
parser = Parser(tokens)
ast = parser.parse()
gen = CodeGenerator()
return gen.generate(ast)
class TestNamespaceParser:
def test_namespace_parse(self, parse):
ast = parse('namespace utils {\n func greet() {\n print("hi")\n }\n}')
ns = ast.statements[0]
assert isinstance(ns, NamespaceDecl)
assert ns.name == "utils"
assert len(ns.statements) == 1
assert isinstance(ns.statements[0], FunctionDecl)
assert ns.statements[0].name == "greet"
def test_namespace_multiple_items(self, parse):
ast = parse('namespace utils {\n func a() { }\n func b() { }\n class C { }\n}')
ns = ast.statements[0]
assert isinstance(ns, NamespaceDecl)
assert len(ns.statements) == 3
def test_using_full(self, parse):
ast = parse('using utils')
stmt = ast.statements[0]
assert isinstance(stmt, UsingStmt)
assert stmt.namespace == "utils"
assert stmt.alias is None
assert stmt.names is None
def test_using_alias(self, parse):
ast = parse('using h = handlers')
stmt = ast.statements[0]
assert isinstance(stmt, UsingStmt)
assert stmt.namespace == "handlers"
assert stmt.alias == "h"
assert stmt.names is None
def test_using_selective(self, parse):
ast = parse('using cli { new_app, Command }')
stmt = ast.statements[0]
assert isinstance(stmt, UsingStmt)
assert stmt.namespace == "cli"
assert stmt.alias is None
assert stmt.names == ["new_app", "Command"]
class TestNamespaceCodegen:
def test_namespace_function_prefixed(self):
source = '''
namespace utils {
func greet(name) {
print("Hello, {name}!")
}
}
utils.greet("Alice")
'''
code = compile_source(source)
assert 'utils__greet ()' in code
assert 'utils__greet "Alice"' in code
def test_namespace_class_prefixed(self):
source = '''
namespace models {
class User {
name = ""
construct(n) {
this.name = n
}
}
}
u = models.User("Alice")
'''
code = compile_source(source)
assert 'models__User ()' in code
assert '__ct_class_models__User_construct' in code
def test_using_direct_access(self):
source = '''
namespace utils {
func greet(name) {
print("Hello, {name}!")
}
}
using utils
greet("Bob")
'''
code = compile_source(source)
assert 'utils__greet ()' in code
assert 'utils__greet "Bob"' in code
def test_using_alias(self):
source = '''
namespace handlers {
func respond(msg) {
print(msg)
}
}
using h = handlers
h.respond("ok")
'''
code = compile_source(source)
assert 'handlers__respond ()' in code
assert 'handlers__respond "ok"' in code
def test_using_selective(self):
source = '''
namespace utils {
func greet(name) {
print("Hello, {name}!")
}
func format(tpl) {
return tpl
}
}
using utils { greet }
greet("Charlie")
'''
code = compile_source(source)
assert 'utils__greet "Charlie"' in code
def test_qualified_access_always_works(self):
source = '''
namespace utils {
func helper() {
print("helper")
}
}
utils.helper()
'''
code = compile_source(source)
assert 'utils__helper' in code
def test_namespace_multiple_files_merge(self):
source1 = '''
namespace utils {
func upper(s) {
return s
}
}
'''
source2 = '''
namespace utils {
func lower(s) {
return s
}
}
using utils
upper("test")
lower("test")
'''
lexer1 = Lexer(source1)
tokens1 = lexer1.tokenize()
parser1 = Parser(tokens1)
ast1 = parser1.parse()
lexer2 = Lexer(source2)
tokens2 = lexer2.tokenize()
parser2 = Parser(tokens2)
ast2 = parser2.parse()
gen = CodeGenerator()
code = gen.generate_multi([ast1, ast2])
assert 'utils__upper ()' in code
assert 'utils__lower ()' in code
assert 'utils__upper "test"' in code
assert 'utils__lower "test"' in code
class TestNamespaceIntegration:
def test_namespace_function_runs(self):
source = '''
namespace utils {
func greet(name) {
print("Hello, {name}!")
}
}
utils.greet("World")
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "Hello, World!" in out
def test_using_direct_runs(self):
source = '''
namespace utils {
func greet(name) {
print("Hello, {name}!")
}
}
using utils
greet("Alice")
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "Hello, Alice!" in out
def test_using_alias_runs(self):
source = '''
namespace handlers {
func respond(msg) {
print(msg)
}
}
using h = handlers
h.respond("ok")
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "ok" in out
def test_namespace_class_runs(self):
source = '''
namespace models {
class User {
name = ""
construct(n) {
this.name = n
}
func greet() {
print("Hi, I am {this.name}")
}
}
}
u = models.User("Alice")
u.greet()
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "Hi, I am Alice" in out
def test_using_class_direct_runs(self):
source = '''
namespace models {
class User {
name = ""
construct(n) {
this.name = n
}
func get_name() {
return this.name
}
}
}
using models
u = User("Bob")
name = u.get_name()
print(name)
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "Bob" in out
def test_namespace_assignment_return(self):
source = '''
namespace math {
func double(x) {
return x * 2
}
}
result = math.double(5)
print(result)
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "10" in out
def test_using_selective_runs(self):
source = '''
namespace utils {
func greet(name) {
print("Hello, {name}!")
}
func farewell(name) {
print("Bye, {name}!")
}
}
using utils { greet }
greet("Test")
utils.farewell("Test")
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "Hello, Test!" in out
assert "Bye, Test!" in out
def test_multi_namespace(self):
source = '''
namespace utils {
func helper() {
print("from utils")
}
}
namespace models {
func helper() {
print("from models")
}
}
utils.helper()
models.helper()
'''
rc, out, err = run_ct(source)
assert rc == 0
assert "from utils" in out
assert "from models" in out
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