Commit fc7a945f authored by Roman Alifanov's avatar Roman Alifanov

Add async/await background processes, on signal handlers, pid()

parent 12c3a0f1
......@@ -752,6 +752,51 @@ func processData () {
}
```
### Фоновые процессы (async/await)
Запуск фоновых процессов. Компилируется в bash `coproc` (именованные, bash 4.3+).
```
# async — запуск фонового процесса
proc = async yad ("--progress", "--auto-close")
# Методы процесс-хэндла
proc.write ("50") # отправить данные в stdin процесса
proc.read () # прочитать строку из stdout процесса
proc.close () # закрыть stdin (EOF)
proc.kill () # убить процесс
proc.wait () # ждать завершения
proc.pid # PID процесса
# await — ожидание завершения
await proc
```
Контекстный менеджер с автоматическим cleanup:
```
with proc in async cmd () {
proc.write ("data")
} # auto-close + await
```
### Обработка сигналов (on)
Регистрация обработчиков сигналов. Компилируется в bash `trap`.
```
on SIGINT {
print ("прервано")
exit (1)
}
on EXIT {
print ("завершение")
}
```
Поддерживаемые сигналы: `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGUSR1`, `SIGUSR2`, `EXIT`.
### Import
```
......@@ -774,6 +819,12 @@ print ("Hello, {name}!")
print (data)
```
### pid ()
```
mypid = pid () # текущий PID процесса ($$)
```
### http
```
......@@ -1409,6 +1460,7 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list
- **Словарей**`del`, `get`, `has`, `keys`, `set`
- **Строк**`charAt`, `contains`, `ends`, `index`, `len`, `lower`, `replace`, `split`, `starts`, `substr`, `trim`, `upper`, `urlencode`
- **Файловых дескрипторов**`close`, `read`, `readline`, `write`, `writeln`
- **Процесс-хэндлов**`close`, `kill`, `read`, `wait`, `write`
- **Stdlib namespaces**:
- `fs``append`, `exists`, `list`, `mkdir`, `open`, `read`, `remove`, `write`
- `http``delete`, `get`, `post`, `put`
......
......@@ -20,6 +20,8 @@
- **String interpolation**`"Hello, {name}!"`
- **@awk functions** — compile to AWK for ~300x speedup on string/numeric operations
- **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`)
- **Optimized output** - no unnecessary subshells, inlined methods
## Installation
......@@ -261,6 +263,33 @@ with f in fs.open ("/tmp/test.txt") {
} # f.close() called automatically
```
### Background Processes (async/await)
```
# Launch background process
proc = async cat ()
proc.write ("hello")
proc.close ()
await proc
# Access process PID
proc = async sleep ("10")
print ("PID: {proc.pid}")
proc.kill ()
await proc
# Automatic cleanup with 'with'
with proc in async cmd () {
proc.write ("data")
} # auto-close + await
# Signal handlers
on SIGINT {
print ("interrupted")
exit (1)
}
```
### Control Flow
```
......@@ -306,7 +335,7 @@ try {
| Module | Functions |
|--------|-----------|
| **I/O** | `print()`, `exit()` |
| **I/O** | `print()`, `exit()`, `pid()` |
| **HTTP** | `http.get/post/put/delete` |
| **Filesystem** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **File handles** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
......@@ -323,6 +352,8 @@ try {
| **Args** | `args.count/get` |
| **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` |
| **Signals** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
## CLI Commands
......@@ -502,7 +533,7 @@ bootstrap/ # Bootstrap compiler (Python)
│ ├── string.py # String methods
│ ├── array.py # Array methods
│ ├── dict.py # Dict methods
│ └── ... # http, fs, json, logger, math, time, etc.
│ └── ... # http, fs, json, logger, math, time, process_handle, etc.
├── dce.py # Dead code elimination
├── codegen.py # Main Bash code generator (mixin coordinator)
├── expr_codegen.py # Expression generation (mixin)
......@@ -528,7 +559,8 @@ tests/ # Test suite
├── test_stdlib.py # Standard library (env, json, fs, with)
├── 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_shell.py # Shell commands, pipes, mixed pipes
└── test_async.py # Background processes (async/await/on, pid)
examples/ # Example .ct programs
```
......
......@@ -20,6 +20,8 @@
- **Строковая интерполяция**`"Привет, {name}!"`
- **@awk функции** — компиляция в AWK для ускорения ~300x на строковых/числовых операциях
- **Проверка методов при компиляции** — выявление несуществующих методов до запуска
- **Фоновые процессы**`async`/`await` для запуска фоновых процессов через coproc
- **Обработка сигналов**`on SIGINT { }` для обработчиков сигналов (компилируется в `trap`)
- **Оптимизированный вывод** — без лишних subshell, инлайнинг методов
## Установка
......@@ -261,6 +263,33 @@ with f in fs.open ("/tmp/test.txt") {
} # f.close() вызывается автоматически
```
### Фоновые процессы (async/await)
```
# Запуск фонового процесса
proc = async cat ()
proc.write ("hello")
proc.close ()
await proc
# Доступ к PID процесса
proc = async sleep ("10")
print ("PID: {proc.pid}")
proc.kill ()
await proc
# Автоматический cleanup через 'with'
with proc in async cmd () {
proc.write ("data")
} # auto-close + await
# Обработчики сигналов
on SIGINT {
print ("прервано")
exit (1)
}
```
### Условия и циклы
```
......@@ -306,7 +335,7 @@ try {
| Модуль | Функции |
|--------|---------|
| **Ввод/вывод** | `print()`, `exit()` |
| **Ввод/вывод** | `print()`, `exit()`, `pid()` |
| **HTTP** | `http.get/post/put/delete` |
| **Файловая система** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **Файловые дескрипторы** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
......@@ -323,6 +352,8 @@ try {
| **Аргументы** | `args.count/get` |
| **Логгер** | `logger.info/warn/error/debug` |
| **Окружение** | `env.VAR` чтение, `env.VAR = value` установка — переменные окружения |
| **Процессы** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` |
| **Сигналы** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
## Команды CLI
......@@ -494,7 +525,7 @@ bootstrap/ # Bootstrap-компилятор (Python)
│ ├── string.py # Строковые методы
│ ├── array.py # Методы массивов
│ ├── dict.py # Методы словарей
│ └── ... # http, fs, json, logger, math, time, etc.
│ └── ... # http, fs, json, logger, math, time, process_handle, etc.
├── dce.py # Устранение мёртвого кода
├── codegen.py # Основной генератор Bash-кода (координатор миксинов)
├── expr_codegen.py # Генерация выражений (миксин)
......@@ -520,7 +551,8 @@ tests/ # Тестовый набор
├── test_stdlib.py # Стандартная библиотека (env, json, fs, with)
├── test_decorators.py # Декораторы, типизация, @test
├── test_awk.py # AWK-функции
└── test_shell.py # Shell-команды, pipe
├── test_shell.py # Shell-команды, pipe
└── test_async.py # Фоновые процессы (async/await/on, pid)
examples/ # Примеры .ct программ
```
......
......@@ -141,6 +141,11 @@ class NewExpr (Expression):
location: Optional[SourceLocation] = None
@dataclass
class AsyncExpr (Expression):
expression: Optional[Expression] = None
location: Optional[SourceLocation] = None
@dataclass
class Statement (ASTNode):
......@@ -251,6 +256,19 @@ class ImportStmt (Statement):
@dataclass
class AwaitStmt (Statement):
expression: Optional[Expression] = None
location: Optional[SourceLocation] = None
@dataclass
class OnSignalStmt (Statement):
signal: str = ""
body: Optional['Block'] = None
location: Optional[SourceLocation] = None
@dataclass
class RangePattern (Expression):
"""Range pattern for when branches: 1..10"""
start: Optional[Expression] = None
......
......@@ -52,6 +52,9 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.dict_vars: Set[str] = set()
self.object_vars: Set[str] = set()
self.file_handle_vars: Set[str] = set()
self.process_handle_vars: Set[str] = set()
self.process_handle_map: Dict[str, str] = {}
self.coproc_counter = 0
self.nameref_vars: Set[str] = set() # vars that are namerefs to arrays/dicts
self.callback_vars: Set[str] = set() # vars that hold function names (callbacks)
self.instance_vars: Dict[str, str] = {} # var_name -> class_name
......
......@@ -18,3 +18,5 @@ FS_FUNC_PREFIX = "__ct_fs_"
JSON_FUNC_PREFIX = "__ct_json_"
REGEX_FUNC_PREFIX = "__ct_regex_"
MATH_FUNC_PREFIX = "__ct_math_"
COPROC_PREFIX = "__ct_cp"
......@@ -5,7 +5,8 @@ import re
from .ast_nodes import (
ClassDecl, NewExpr, CallExpr, Identifier, FunctionDecl, Assignment,
ExpressionStmt, IfStmt, ForStmt, ForeachStmt, WhileStmt, WhenStmt,
WhenBranch, TryStmt, ThrowStmt, DeferStmt, ReturnStmt, ArrayLiteral,
WhenBranch, TryStmt, ThrowStmt, DeferStmt, AwaitStmt, OnSignalStmt,
AsyncExpr, ReturnStmt, ArrayLiteral,
DictLiteral, IndexAccess, Lambda, MemberAccess, ThisExpr, Block,
BinaryOp, UnaryOp, WithStmt, Program, StringLiteral
)
......@@ -16,7 +17,7 @@ class UsageAnalyzer:
CATEGORIES = {
'core', 'object', 'http', 'fs', 'json', 'logger', 'string',
'array', 'dict', 'regex', 'math', 'time', 'awk', 'exception',
'args', 'misc',
'args', 'misc', 'async',
}
ARRAY_RETURNING_METHODS = {'keys', 'split', 'slice'}
......@@ -259,6 +260,14 @@ class UsageAnalyzer:
self.used.add('exception')
self._analyze_expr(stmt.expression)
elif isinstance(stmt, AwaitStmt):
self.used.add('async')
self._analyze_expr(stmt.expression)
elif isinstance(stmt, OnSignalStmt):
self.used.add('async')
self._analyze_body(stmt.body)
elif isinstance(stmt, ReturnStmt):
if stmt.value:
self._analyze_expr(stmt.value)
......@@ -313,6 +322,10 @@ class UsageAnalyzer:
elif isinstance(expr, Lambda):
self._analyze_body(expr.body)
elif isinstance(expr, AsyncExpr):
self.used.add('async')
self._analyze_expr(expr.expression)
elif isinstance(expr, NewExpr):
self.has_classes = True
self.used_classes.add(expr.class_name)
......
from .ast_nodes import (
CallExpr, MemberAccess, Identifier, ThisExpr, Assignment, ArrayLiteral,
DictLiteral, NewExpr, Lambda, ExpressionStmt, BaseCall, ReturnStmt,
DictLiteral, NewExpr, AsyncExpr, Lambda, ExpressionStmt, BaseCall, ReturnStmt,
StringLiteral, BinaryOp, IndexAccess, TypeAnnotation, IntegerLiteral,
FloatLiteral, BoolLiteral
)
from .methods import (
STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS,
PROCESS_HANDLE_METHODS,
NAMESPACE_METHODS, BUILTIN_NAMESPACES, BUILTIN_FUNCS, get_method_names
)
from .constants import RET_VAR, RET_ARR
from .constants import RET_VAR, RET_ARR, COPROC_PREFIX
ARR_METHODS = {name: m.bash_func for name, m in ARRAY_METHODS.items()}
STR_METHODS = {name: m.bash_func for name, m in STRING_METHODS.items()}
......@@ -206,6 +207,10 @@ class DispatchMixin:
column=stmt.location.column if stmt.location else 0
)
if isinstance(stmt.value, AsyncExpr):
self._generate_async_assignment(stmt, target)
return
if isinstance(stmt.value, BinaryOp) and stmt.value.operator == "|":
self._generate_pipe_assignment(stmt, target)
return
......@@ -590,6 +595,13 @@ class DispatchMixin:
else:
self._validate_type_method("file_handle", method, location)
if obj_name in self.process_handle_vars:
if method in PROCESS_HANDLE_METHODS:
self._generate_process_method(obj_name, method, args, target)
return True
else:
self._validate_type_method("process_handle", method, location)
if obj_name in self.object_vars:
ret_type = self._get_method_return_type(obj_name, method)
obj = self.generate_expr(callee.object)
......@@ -732,6 +744,43 @@ class DispatchMixin:
self.emit(f'declare -gA {target}=()')
self.dict_vars.add(target)
def _generate_async_assignment(self, stmt: Assignment, target: str):
self.coproc_counter += 1
cp_name = f'{COPROC_PREFIX}{self.coproc_counter}'
inner = stmt.value.expression
if isinstance(inner, CallExpr) and self._is_shell_command(inner):
cmd_str = self._extract_shell_command_str(inner)
elif isinstance(inner, CallExpr) and isinstance(inner.callee, Identifier):
func_name = inner.callee.name
args_str = self._generate_call_args_str(inner.arguments)
cmd_str = f'exec 3>&1; {func_name} {args_str}'.strip()
else:
cmd_str = self.generate_expr(inner)
self.emit(f'coproc {cp_name} {{ {cmd_str}; }}')
self.emit(f'{cp_name}_wr=${{{cp_name}[1]}}')
self.emit(f'{cp_name}_rd=${{{cp_name}[0]}}')
self.process_handle_vars.add(target)
self.process_handle_map[target] = cp_name
def _generate_process_method(self, var_name: str, method: str, args: list, target: str = None):
cp = self.process_handle_map[var_name]
if method == "write":
data = args[0] if args else ""
self.emit(f'echo "{data}" >&${cp}_wr')
elif method == "read":
self.emit(f'read -r {RET_VAR} <&${cp}_rd')
self.emit(f'echo "${RET_VAR}"')
if target:
self.emit_var_assign(target, f'${RET_VAR}')
elif method == "close":
self.emit(f'exec {{{cp}_wr}}>&-')
elif method == "kill":
self.emit(f'kill ${cp}_PID 2>/dev/null || true')
elif method == "wait":
self.emit(f'wait ${cp}_PID 2>/dev/null || true')
def _generate_pipe_assignment(self, stmt: Assignment, target: str):
"""Generate pipe expression assignment."""
elements = self._collect_pipe_chain(stmt.value)
......@@ -1023,6 +1072,14 @@ class DispatchMixin:
self._validate_type_method("file_handle", method, location)
return False
if var_name in self.process_handle_vars:
if method in PROCESS_HANDLE_METHODS:
self._generate_process_method(var_name, method, args)
return True
else:
self._validate_type_method("process_handle", method, location)
return False
if method in STR_METHODS:
func_name = STR_METHODS[method]
args_str = " ".join([f'"{a}"' for a in args])
......@@ -1080,6 +1137,8 @@ class DispatchMixin:
return f'__ct_assert "{args[0]}"'
elif name == "assert_eq":
return f'__ct_assert_eq {args_str}'
elif name == "pid":
return '__ct_pid'
else:
if self._is_callback_var(name):
return f'"${{{name}}}" {args_str}'
......@@ -1175,6 +1234,22 @@ class DispatchMixin:
else:
self._validate_type_method("file_handle", method, location)
if var_name and var_name in self.process_handle_vars:
if method in PROCESS_HANDLE_METHODS:
cp = self.process_handle_map[var_name]
if method == "read":
return f'read -r {RET_VAR} <&${cp}_rd && echo "${RET_VAR}"'
elif method == "write":
return f'echo {args_str} >&${cp}_wr'
elif method == "close":
return f'exec {{{cp}_wr}}>&-'
elif method == "kill":
return f'kill ${cp}_PID 2>/dev/null || true'
elif method == "wait":
return f'wait ${cp}_PID 2>/dev/null || true'
else:
self._validate_type_method("process_handle", method, location)
if method in STR_METHODS:
return f'{STR_METHODS[method]} "{obj}" {args_str}'.strip()
......
......@@ -2,8 +2,8 @@ import re
from .ast_nodes import (
Expression, IntegerLiteral, FloatLiteral, StringLiteral, BoolLiteral,
NilLiteral, Identifier, ThisExpr, ArrayLiteral, DictLiteral, BinaryOp,
UnaryOp, CallExpr, MemberAccess, IndexAccess, Lambda, NewExpr, BaseCall,
Block, ReturnStmt
UnaryOp, CallExpr, MemberAccess, IndexAccess, Lambda, NewExpr, AsyncExpr,
BaseCall, Block, ReturnStmt
)
......@@ -71,6 +71,9 @@ class ExprMixin:
args_str = " ".join([f'"{a}"' for a in args])
return f'$({expr.class_name} {args_str}; echo "$__ct_last_instance")'
if isinstance(expr, AsyncExpr):
return self._generate_async_expr(expr)
if isinstance(expr, BaseCall):
args = [self.generate_expr(a) for a in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
......@@ -109,8 +112,12 @@ class ExprMixin:
parts = content.split('.', 1)
if parts[0] == 'this':
return f'\\$${{__CT_OBJ["$this.{parts[1]}"]}}'
else:
return f'\\$${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
ph_vars = getattr(self, 'process_handle_vars', set())
ph_map = getattr(self, 'process_handle_map', {})
if parts[0] in ph_vars and parts[1] == 'pid':
cp = ph_map[parts[0]]
return f'\\$${cp}_PID'
return f'\\$${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
return f'\\$${{{content}}}'
def replace_interpolation(match):
......@@ -128,8 +135,12 @@ class ExprMixin:
parts = content.split('.', 1)
if parts[0] == 'this':
return f'${{__CT_OBJ["$this.{parts[1]}"]}}'
else:
return f'${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
ph_vars = getattr(self, 'process_handle_vars', set())
ph_map = getattr(self, 'process_handle_map', {})
if parts[0] in ph_vars and parts[1] == 'pid':
cp = ph_map[parts[0]]
return f'${cp}_PID'
return f'${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
return f'${{{content}}}'
value = value.replace('\x00DOLLAR\x00{', '\x01ESCAPED_DOLLAR_BRACE\x01')
......@@ -261,12 +272,21 @@ class ExprMixin:
paren_idx = content.find('(')
func_name = content[:paren_idx].strip()
args_part = content[paren_idx + 1:-1].strip()
interp_builtin_map = {
'print': '__ct_print', 'len': '__ct_str_len', 'exit': '__ct_exit',
'range': '__ct_range', 'is_number': '__ct_is_number',
'is_empty': '__ct_is_empty', 'pid': '__ct_pid',
'random': '__ct_random', 'random_range': '__ct_random_range',
}
bash_name = interp_builtin_map.get(func_name, func_name)
if args_part:
args_list = self._parse_args_with_parens(args_part)
args_bash = ' '.join([self._convert_arg_to_bash(a) for a in args_list])
return f'$({func_name} {args_bash})'
return f'$({bash_name} {args_bash})'
else:
return f'$({func_name})'
return f'$({bash_name})'
def _parse_args_with_parens(self, args_str: str) -> list:
"""Parse comma-separated args, respecting nested parentheses."""
......@@ -433,12 +453,36 @@ class ExprMixin:
return None
def _generate_async_expr(self, expr: AsyncExpr) -> str:
from .constants import COPROC_PREFIX
self.coproc_counter += 1
cp_name = f'{COPROC_PREFIX}{self.coproc_counter}'
inner = expr.expression
if isinstance(inner, CallExpr) and self._is_shell_command(inner):
cmd_str = self._extract_shell_command_str(inner)
elif isinstance(inner, CallExpr) and isinstance(inner.callee, Identifier):
func_name = inner.callee.name
args_str = self._generate_call_args_str(inner.arguments)
cmd_str = f'exec 3>&1; {func_name} {args_str}'.strip()
else:
cmd_str = self.generate_expr(inner)
self.emit(f'coproc {cp_name} {{ {cmd_str}; }}')
self.emit(f'{cp_name}_wr=${{{cp_name}[1]}}')
self.emit(f'{cp_name}_rd=${{{cp_name}[0]}}')
return ""
def _generate_member_access(self, expr: MemberAccess) -> str:
if isinstance(expr.object, Identifier) and expr.object.name == "env":
return f'${{{expr.member}}}'
if isinstance(expr.object, Identifier):
obj_name = expr.object.name
if obj_name in getattr(self, 'process_handle_vars', set()) and expr.member == "pid":
cp = self.process_handle_map[obj_name]
return f'${cp}_PID'
if isinstance(expr.object, Identifier):
obj_name = expr.object.name
param_map = getattr(self, 'param_name_map', {})
mapped_name = param_map.get(obj_name, obj_name)
if mapped_name in getattr(self, 'dict_vars', set()):
......
......@@ -13,6 +13,7 @@ from .time import TimeMethods
from .args import ArgsMethods
from .core import CoreFunctions, AwkBuiltinFunctions
from .reflect import ReflectMethods
from .process_handle import ProcessHandleMethods
STRING_METHODS = collect_methods(StringMethods)
ARRAY_METHODS = collect_methods(ArrayMethods)
......@@ -29,6 +30,7 @@ ARGS_METHODS = collect_methods(ArgsMethods)
CORE_FUNCTIONS = collect_methods(CoreFunctions)
AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions)
REFLECT_METHODS = collect_methods(ReflectMethods)
PROCESS_HANDLE_METHODS = collect_methods(ProcessHandleMethods)
NAMESPACE_REGISTRY = {
"fs": FS_METHODS,
......@@ -56,6 +58,7 @@ def get_method(type_name: str, method_name: str):
"array": ARRAY_METHODS,
"dict": DICT_METHODS,
"file_handle": FILE_HANDLE_METHODS,
"process_handle": PROCESS_HANDLE_METHODS,
}
return registry.get(type_name, {}).get(method_name)
......@@ -66,6 +69,7 @@ def get_method_names(type_name: str) -> set:
"array": ARRAY_METHODS,
"dict": DICT_METHODS,
"file_handle": FILE_HANDLE_METHODS,
"process_handle": PROCESS_HANDLE_METHODS,
}
methods = registry.get(type_name, {})
return set(methods.keys())
......
......@@ -56,6 +56,11 @@ class CoreFunctions:
awk_builtin=lambda a: f"int({a[0]} + rand() * ({a[1]} - {a[0]} + 1))",
min_args=2, max_args=2,
)
pid = Method(
name="pid",
bash_func="__ct_pid",
bash_impl='echo "$$"',
)
class AwkBuiltinFunctions:
......
from .base import Method
class ProcessHandleMethods:
write = Method(
name="write",
bash_func="__ct_ph_write",
bash_impl=None,
min_args=1, max_args=1,
)
read = Method(
name="read",
bash_func="__ct_ph_read",
bash_impl=None,
)
close = Method(
name="close",
bash_func="__ct_ph_close",
bash_impl=None,
)
kill = Method(
name="kill",
bash_func="__ct_ph_kill",
bash_impl=None,
)
wait = Method(
name="wait",
bash_func="__ct_ph_wait",
bash_impl=None,
)
......@@ -4,10 +4,10 @@ from .ast_nodes import (
SourceLocation, Program, Declaration, Statement, Decorator, FunctionDecl,
Parameter, ClassDecl, ClassField, ConstructorDecl, ImportStmt, Block, ReturnStmt,
BreakStmt, ContinueStmt, IfStmt, WhileStmt, ForStmt, ForeachStmt, WithStmt,
TryStmt, ThrowStmt, DeferStmt, WhenStmt, WhenBranch, RangePattern,
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, Lambda,
BinaryOp, UnaryOp, CallExpr, MemberAccess, IndexAccess, NewExpr, AsyncExpr, Lambda,
BaseCall, Expression, TypeAnnotation
)
from .errors import CompileError, ErrorCollector
......@@ -53,6 +53,18 @@ class Parser:
self.error (msg)
return self.current ()
KEYWORD_AS_IDENT = {TokenType.ON, TokenType.ASYNC, TokenType.AWAIT}
def expect_name (self, message: str = "Expected identifier") -> Token:
if self.check (TokenType.IDENTIFIER):
return self.advance ()
if self.current ().type in self.KEYWORD_AS_IDENT:
tok = self.advance ()
tok.value = tok.value or tok.type.name.lower ()
return tok
self.error (message)
return self.current ()
def error (self, message: str, token: Token = None):
token = token or self.current ()
self.errors.add_error (
......@@ -149,7 +161,7 @@ class Parser:
obj = None
if self.match (TokenType.DOT):
obj = name
name = self.expect (TokenType.IDENTIFIER, "Expected method name after '.'").value
name = self.expect_name ("Expected method name after '.'").value
arguments = []
if self.match (TokenType.LPAREN):
......@@ -188,7 +200,7 @@ class Parser:
def parse_function (self, decorators: List[Decorator] = None) -> FunctionDecl:
loc = self.location ()
self.expect (TokenType.FUNC)
name = self.expect (TokenType.IDENTIFIER, "Expected function name").value
name = self.expect_name ("Expected function name").value
self.expect (TokenType.LPAREN, "Expected '(' after function name")
params = self.parse_parameters ()
......@@ -333,6 +345,10 @@ class Parser:
return self.parse_throw ()
if self.check (TokenType.DEFER):
return self.parse_defer ()
if self.check (TokenType.AWAIT):
return self.parse_await ()
if self.check (TokenType.ON):
return self.parse_on_signal ()
if self.check (TokenType.WHEN):
return self.parse_when ()
if self.check (TokenType.LBRACE):
......@@ -528,6 +544,20 @@ class Parser:
expression = self.parse_expression ()
return DeferStmt (expression=expression, location=loc)
def parse_await (self) -> AwaitStmt:
loc = self.location ()
self.expect (TokenType.AWAIT)
expr = self.parse_expression ()
return AwaitStmt (expression=expr, location=loc)
def parse_on_signal (self) -> OnSignalStmt:
loc = self.location ()
self.expect (TokenType.ON)
signal = self.expect (TokenType.IDENTIFIER, "Expected signal name").value
self.skip_newlines ()
body = self.parse_block ()
return OnSignalStmt (signal=signal, body=body, location=loc)
def parse_when (self) -> WhenStmt:
loc = self.location ()
self.expect (TokenType.WHEN)
......@@ -635,6 +665,12 @@ class Parser:
operand = self.parse_unary ()
return UnaryOp (operator=operator.value, operand=operand, location=loc)
if self.check (TokenType.ASYNC):
loc = self.location ()
self.advance ()
expr = self.parse_unary ()
return AsyncExpr (expression=expr, location=loc)
if self.check (TokenType.NEW):
loc = self.location ()
self.advance () # consume 'new'
......@@ -660,7 +696,7 @@ class Parser:
expr = CallExpr (callee=expr, arguments=args, location=expr.location)
elif self.match (TokenType.DOT):
member = self.expect (TokenType.IDENTIFIER, "Expected member name").value
member = self.expect_name ("Expected member name").value
expr = MemberAccess (object=expr, member=member, location=expr.location)
elif self.match (TokenType.LBRACKET):
......
......@@ -98,6 +98,8 @@ class StdlibMixin:
self.emit(f"{CORE_FUNCTIONS['len'].bash_func} () {{ {CORE_FUNCTIONS['len'].bash_impl}; }}")
self.emit()
self.emit(f"{CORE_FUNCTIONS['pid'].bash_func} () {{ {CORE_FUNCTIONS['pid'].bash_impl}; }}")
self.emit()
def _emit_http(self):
"""HTTP functions from HTTP_METHODS."""
......
from .ast_nodes import (
FunctionDecl, ClassDecl, ImportStmt, 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
)
from .constants import RET_VAR, RET_ARR
from .constants import RET_VAR, RET_ARR, COPROC_PREFIX
class StmtMixin:
......@@ -41,6 +42,10 @@ class StmtMixin:
self.generate_throw(stmt)
elif isinstance(stmt, DeferStmt):
self.generate_defer(stmt)
elif isinstance(stmt, AwaitStmt):
self.generate_await(stmt)
elif isinstance(stmt, OnSignalStmt):
self.generate_on_signal(stmt)
elif isinstance(stmt, WhenStmt):
self.generate_when(stmt)
elif isinstance(stmt, ReturnStmt):
......@@ -318,8 +323,29 @@ class StmtMixin:
self.emit("# with statement")
local_kw = "local " if self.in_function else ""
with_async = {}
for i, (var, resource) in enumerate(zip(stmt.variables, stmt.resources)):
if isinstance(resource, AsyncExpr):
self.coproc_counter += 1
cp_name = f'{COPROC_PREFIX}{self.coproc_counter}'
inner = resource.expression
if isinstance(inner, CallExpr) and self._is_shell_command(inner):
cmd_str = self._extract_shell_command_str(inner)
elif isinstance(inner, CallExpr) and isinstance(inner.callee, Identifier):
func_name = inner.callee.name
args_str = self._generate_call_args_str(inner.arguments)
cmd_str = f'exec 3>&1; {func_name} {args_str}'.strip()
else:
cmd_str = self.generate_expr(inner)
self.emit(f'coproc {cp_name} {{ {cmd_str}; }}')
self.emit(f'{cp_name}_wr=${{{cp_name}[1]}}')
self.emit(f'{cp_name}_rd=${{{cp_name}[0]}}')
self.process_handle_vars.add(var)
self.process_handle_map[var] = cp_name
with_async[i] = cp_name
continue
if isinstance(resource, CallExpr) and isinstance(resource.callee, MemberAccess):
if resource.callee.member == "open":
self.file_handle_vars.add(var)
......@@ -339,7 +365,12 @@ class StmtMixin:
self.emit("# with cleanup")
for i in range(len(stmt.variables) - 1, -1, -1):
self.emit(f'__ct_fh___exit__ "$__ct_with_{i}"')
if i in with_async:
cp = with_async[i]
self.emit(f'exec {{{cp}_wr}}>&- 2>/dev/null || true')
self.emit(f'wait ${cp}_PID 2>/dev/null || true')
else:
self.emit(f'__ct_fh___exit__ "$__ct_with_{i}"')
def generate_try(self, stmt: TryStmt):
self.emit("# try/except block")
......@@ -414,6 +445,30 @@ class StmtMixin:
expr = self.generate_expr(stmt.expression)
self.deferred_calls.append(expr)
def generate_await(self, stmt: AwaitStmt):
if isinstance(stmt.expression, Identifier):
var = stmt.expression.name
if var in self.process_handle_vars:
cp = self.process_handle_map[var]
self.emit(f'wait ${cp}_PID 2>/dev/null || true')
return
expr = self.generate_expr(stmt.expression)
self.emit(f'wait "{expr}" 2>/dev/null || true')
def generate_on_signal(self, stmt: OnSignalStmt):
signal_map = {
"SIGINT": "INT", "SIGTERM": "TERM", "SIGHUP": "HUP",
"SIGUSR1": "USR1", "SIGUSR2": "USR2", "EXIT": "EXIT",
}
sig = signal_map.get(stmt.signal, stmt.signal)
handler = f'__ct_on_{sig.lower()}'
self.emit(f'{handler} () {{')
with self.indented():
for s in stmt.body.statements:
self.generate_statement(s)
self.emit('}')
self.emit(f'trap {handler} {sig}')
def generate_return(self, stmt: ReturnStmt):
if self.deferred_calls:
self.emit("# Deferred calls before return")
......
......@@ -37,6 +37,9 @@ class TokenType (Enum):
WHEN = auto ()
WITH = auto ()
NEW = auto ()
ASYNC = auto ()
AWAIT = auto ()
ON = auto ()
PLUS = auto ()
MINUS = auto ()
......@@ -103,6 +106,9 @@ KEYWORDS = {
'when': TokenType.WHEN,
'with': TokenType.WITH,
'new': TokenType.NEW,
'async': TokenType.ASYNC,
'await': TokenType.AWAIT,
'on': TokenType.ON,
'true': TokenType.TRUE,
'false': TokenType.FALSE,
'nil': TokenType.NIL,
......
from helpers import run_ct, compile_ct
class TestAsyncBasic:
def test_async_cat_write_close_await(self):
code, stdout, _ = run_ct('''
proc = async cat ()
proc.write ("hello from coproc")
proc.close ()
await proc
print ("done")
''')
assert code == 0
assert "done" in stdout
def test_async_pid(self):
code, stdout, _ = run_ct('''
proc = async sleep ("10")
mypid = proc.pid
print ("pid:{mypid}")
proc.kill ()
await proc
''')
assert code == 0
assert "pid:" in stdout
pid_val = stdout.strip().split("pid:")[1]
assert pid_val.isdigit()
def test_async_pid_interpolation(self):
code, stdout, _ = run_ct('''
proc = async sleep ("10")
print ("PID={proc.pid}")
proc.kill ()
await proc
''')
assert code == 0
assert "PID=" in stdout
pid_val = stdout.strip().split("PID=")[1]
assert pid_val.isdigit()
def test_async_kill(self):
code, stdout, _ = run_ct('''
proc = async sleep ("60")
proc.kill ()
await proc
print ("killed")
''')
assert code == 0
assert "killed" in stdout
def test_async_read(self):
code, stdout, _ = run_ct('''
proc = async echo ("hello output")
line = proc.read ()
print ("got:{line}")
await proc
''')
assert code == 0
assert "got:hello output" in stdout
class TestAwaitStatement:
def test_await_process(self):
code, stdout, _ = run_ct('''
proc = async echo ("test")
await proc
print ("waited")
''')
assert code == 0
assert "waited" in stdout
class TestOnSignal:
def test_on_exit(self):
code, stdout, _ = run_ct('''
on EXIT {
print ("exiting")
}
print ("running")
''')
assert code == 0
assert "running" in stdout
assert "exiting" in stdout
def test_on_signal_compile(self):
code, output, _ = compile_ct('''
on SIGTERM {
print ("terminated")
exit (1)
}
print ("ok")
''')
assert code == 0
assert "trap __ct_on_term TERM" in output
assert "__ct_on_term ()" in output
class TestPidBuiltin:
def test_pid_returns_number(self):
code, stdout, _ = run_ct('''
mypid = pid ()
print ("{mypid}")
''')
assert code == 0
assert stdout.strip().isdigit()
def test_pid_in_interpolation(self):
code, stdout, _ = run_ct('''
print ("PID={pid ()}")
''')
assert code == 0
pid_val = stdout.strip().split("PID=")[1]
assert pid_val.isdigit()
class TestCompileAsync:
def test_compile_coproc(self):
code, output, _ = compile_ct('''
proc = async cat ()
proc.write ("data")
proc.close ()
await proc
''')
assert code == 0
assert "coproc __ct_cp1" in output
assert "__ct_cp1_wr=${__ct_cp1[1]}" in output
assert "__ct_cp1_rd=${__ct_cp1[0]}" in output
assert 'echo "data" >&$__ct_cp1_wr' in output
assert "exec {__ct_cp1_wr}>&-" in output
assert "wait $__ct_cp1_PID" in output
def test_compile_on_signal(self):
code, output, _ = compile_ct('''
on SIGINT {
print ("interrupted")
}
''')
assert code == 0
assert "__ct_on_int () {" in output
assert "trap __ct_on_int INT" in output
def test_compile_pid(self):
code, output, _ = compile_ct('''
x = pid ()
print ("{x}")
''')
assert code == 0
assert "__ct_pid" in output
def test_compile_multiple_coprocs(self):
code, output, _ = compile_ct('''
p1 = async cat ()
p2 = async cat ()
p1.close ()
p2.close ()
await p1
await p2
''')
assert code == 0
assert "coproc __ct_cp1" in output
assert "coproc __ct_cp2" in output
assert "wait $__ct_cp1_PID" in output
assert "wait $__ct_cp2_PID" in output
class TestKeywordAsIdentifier:
def test_on_as_method_name(self):
code, stdout, _ = run_ct('''
class Bus {
func on (event) {
print ("event:{event}")
}
}
b = new Bus ()
b.on ("click")
''')
assert code == 0
assert "event:click" in stdout
def test_on_as_decorator_method(self):
code, output, _ = compile_ct('''
class EventBus {
listeners = {}
func on (event, handler) {
this.listeners.set (event, handler)
}
func emit (event, data) {
if this.listeners.has (event) {
handler = this.listeners.get (event)
handler (data)
}
}
}
bus = new EventBus ()
@bus.on ("click")
func on_click (data) {
print ("Clicked: {data}")
}
bus.emit ("click", "button1")
''')
assert code == 0
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