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 () { ...@@ -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 ### Import
``` ```
...@@ -774,6 +819,12 @@ print ("Hello, {name}!") ...@@ -774,6 +819,12 @@ print ("Hello, {name}!")
print (data) print (data)
``` ```
### pid ()
```
mypid = pid () # текущий PID процесса ($$)
```
### http ### http
``` ```
...@@ -1409,6 +1460,7 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list ...@@ -1409,6 +1460,7 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list
- **Словарей**`del`, `get`, `has`, `keys`, `set` - **Словарей**`del`, `get`, `has`, `keys`, `set`
- **Строк**`charAt`, `contains`, `ends`, `index`, `len`, `lower`, `replace`, `split`, `starts`, `substr`, `trim`, `upper`, `urlencode` - **Строк**`charAt`, `contains`, `ends`, `index`, `len`, `lower`, `replace`, `split`, `starts`, `substr`, `trim`, `upper`, `urlencode`
- **Файловых дескрипторов**`close`, `read`, `readline`, `write`, `writeln` - **Файловых дескрипторов**`close`, `read`, `readline`, `write`, `writeln`
- **Процесс-хэндлов**`close`, `kill`, `read`, `wait`, `write`
- **Stdlib namespaces**: - **Stdlib namespaces**:
- `fs``append`, `exists`, `list`, `mkdir`, `open`, `read`, `remove`, `write` - `fs``append`, `exists`, `list`, `mkdir`, `open`, `read`, `remove`, `write`
- `http``delete`, `get`, `post`, `put` - `http``delete`, `get`, `post`, `put`
......
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
- **String interpolation**`"Hello, {name}!"` - **String interpolation**`"Hello, {name}!"`
- **@awk functions** — compile to AWK for ~300x speedup on string/numeric operations - **@awk functions** — compile to AWK for ~300x speedup on string/numeric operations
- **Compile-time method validation** — catches unknown methods before runtime - **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 - **Optimized output** - no unnecessary subshells, inlined methods
## Installation ## Installation
...@@ -261,6 +263,33 @@ with f in fs.open ("/tmp/test.txt") { ...@@ -261,6 +263,33 @@ with f in fs.open ("/tmp/test.txt") {
} # f.close() called automatically } # 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 ### Control Flow
``` ```
...@@ -306,7 +335,7 @@ try { ...@@ -306,7 +335,7 @@ try {
| Module | Functions | | Module | Functions |
|--------|-----------| |--------|-----------|
| **I/O** | `print()`, `exit()` | | **I/O** | `print()`, `exit()`, `pid()` |
| **HTTP** | `http.get/post/put/delete` | | **HTTP** | `http.get/post/put/delete` |
| **Filesystem** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` | | **Filesystem** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **File handles** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` | | **File handles** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
...@@ -323,6 +352,8 @@ try { ...@@ -323,6 +352,8 @@ try {
| **Args** | `args.count/get` | | **Args** | `args.count/get` |
| **Logger** | `logger.info/warn/error/debug` | | **Logger** | `logger.info/warn/error/debug` |
| **Env** | `env.VAR` read, `env.VAR = value` set — environment variables | | **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 ## CLI Commands
...@@ -502,7 +533,7 @@ bootstrap/ # Bootstrap compiler (Python) ...@@ -502,7 +533,7 @@ bootstrap/ # Bootstrap compiler (Python)
│ ├── string.py # String methods │ ├── string.py # String methods
│ ├── array.py # Array methods │ ├── array.py # Array methods
│ ├── dict.py # Dict 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 ├── dce.py # Dead code elimination
├── codegen.py # Main Bash code generator (mixin coordinator) ├── codegen.py # Main Bash code generator (mixin coordinator)
├── expr_codegen.py # Expression generation (mixin) ├── expr_codegen.py # Expression generation (mixin)
...@@ -528,7 +559,8 @@ tests/ # Test suite ...@@ -528,7 +559,8 @@ tests/ # Test suite
├── test_stdlib.py # Standard library (env, json, fs, with) ├── test_stdlib.py # Standard library (env, json, fs, with)
├── test_decorators.py # Decorators, typing, @test, user decorators ├── test_decorators.py # Decorators, typing, @test, user decorators
├── test_awk.py # AWK functions (map/filter, sync, assert) ├── 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 examples/ # Example .ct programs
``` ```
......
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
- **Строковая интерполяция**`"Привет, {name}!"` - **Строковая интерполяция**`"Привет, {name}!"`
- **@awk функции** — компиляция в AWK для ускорения ~300x на строковых/числовых операциях - **@awk функции** — компиляция в AWK для ускорения ~300x на строковых/числовых операциях
- **Проверка методов при компиляции** — выявление несуществующих методов до запуска - **Проверка методов при компиляции** — выявление несуществующих методов до запуска
- **Фоновые процессы**`async`/`await` для запуска фоновых процессов через coproc
- **Обработка сигналов**`on SIGINT { }` для обработчиков сигналов (компилируется в `trap`)
- **Оптимизированный вывод** — без лишних subshell, инлайнинг методов - **Оптимизированный вывод** — без лишних subshell, инлайнинг методов
## Установка ## Установка
...@@ -261,6 +263,33 @@ with f in fs.open ("/tmp/test.txt") { ...@@ -261,6 +263,33 @@ with f in fs.open ("/tmp/test.txt") {
} # f.close() вызывается автоматически } # 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 { ...@@ -306,7 +335,7 @@ try {
| Модуль | Функции | | Модуль | Функции |
|--------|---------| |--------|---------|
| **Ввод/вывод** | `print()`, `exit()` | | **Ввод/вывод** | `print()`, `exit()`, `pid()` |
| **HTTP** | `http.get/post/put/delete` | | **HTTP** | `http.get/post/put/delete` |
| **Файловая система** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` | | **Файловая система** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **Файловые дескрипторы** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` | | **Файловые дескрипторы** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
...@@ -323,6 +352,8 @@ try { ...@@ -323,6 +352,8 @@ try {
| **Аргументы** | `args.count/get` | | **Аргументы** | `args.count/get` |
| **Логгер** | `logger.info/warn/error/debug` | | **Логгер** | `logger.info/warn/error/debug` |
| **Окружение** | `env.VAR` чтение, `env.VAR = value` установка — переменные окружения | | **Окружение** | `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 ## Команды CLI
...@@ -494,7 +525,7 @@ bootstrap/ # Bootstrap-компилятор (Python) ...@@ -494,7 +525,7 @@ bootstrap/ # Bootstrap-компилятор (Python)
│ ├── string.py # Строковые методы │ ├── string.py # Строковые методы
│ ├── array.py # Методы массивов │ ├── array.py # Методы массивов
│ ├── dict.py # Методы словарей │ ├── dict.py # Методы словарей
│ └── ... # http, fs, json, logger, math, time, etc. │ └── ... # http, fs, json, logger, math, time, process_handle, etc.
├── dce.py # Устранение мёртвого кода ├── dce.py # Устранение мёртвого кода
├── codegen.py # Основной генератор Bash-кода (координатор миксинов) ├── codegen.py # Основной генератор Bash-кода (координатор миксинов)
├── expr_codegen.py # Генерация выражений (миксин) ├── expr_codegen.py # Генерация выражений (миксин)
...@@ -520,7 +551,8 @@ tests/ # Тестовый набор ...@@ -520,7 +551,8 @@ tests/ # Тестовый набор
├── test_stdlib.py # Стандартная библиотека (env, json, fs, with) ├── test_stdlib.py # Стандартная библиотека (env, json, fs, with)
├── test_decorators.py # Декораторы, типизация, @test ├── test_decorators.py # Декораторы, типизация, @test
├── test_awk.py # AWK-функции ├── test_awk.py # AWK-функции
└── test_shell.py # Shell-команды, pipe ├── test_shell.py # Shell-команды, pipe
└── test_async.py # Фоновые процессы (async/await/on, pid)
examples/ # Примеры .ct программ examples/ # Примеры .ct программ
``` ```
......
...@@ -141,6 +141,11 @@ class NewExpr (Expression): ...@@ -141,6 +141,11 @@ class NewExpr (Expression):
location: Optional[SourceLocation] = None location: Optional[SourceLocation] = None
@dataclass
class AsyncExpr (Expression):
expression: Optional[Expression] = None
location: Optional[SourceLocation] = None
@dataclass @dataclass
class Statement (ASTNode): class Statement (ASTNode):
...@@ -251,6 +256,19 @@ class ImportStmt (Statement): ...@@ -251,6 +256,19 @@ class ImportStmt (Statement):
@dataclass @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): class RangePattern (Expression):
"""Range pattern for when branches: 1..10""" """Range pattern for when branches: 1..10"""
start: Optional[Expression] = None start: Optional[Expression] = None
......
...@@ -52,6 +52,9 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin, ...@@ -52,6 +52,9 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.dict_vars: Set[str] = set() self.dict_vars: Set[str] = set()
self.object_vars: Set[str] = set() self.object_vars: Set[str] = set()
self.file_handle_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.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.callback_vars: Set[str] = set() # vars that hold function names (callbacks)
self.instance_vars: Dict[str, str] = {} # var_name -> class_name self.instance_vars: Dict[str, str] = {} # var_name -> class_name
......
...@@ -18,3 +18,5 @@ FS_FUNC_PREFIX = "__ct_fs_" ...@@ -18,3 +18,5 @@ FS_FUNC_PREFIX = "__ct_fs_"
JSON_FUNC_PREFIX = "__ct_json_" JSON_FUNC_PREFIX = "__ct_json_"
REGEX_FUNC_PREFIX = "__ct_regex_" REGEX_FUNC_PREFIX = "__ct_regex_"
MATH_FUNC_PREFIX = "__ct_math_" MATH_FUNC_PREFIX = "__ct_math_"
COPROC_PREFIX = "__ct_cp"
...@@ -5,7 +5,8 @@ import re ...@@ -5,7 +5,8 @@ import re
from .ast_nodes import ( from .ast_nodes import (
ClassDecl, NewExpr, CallExpr, Identifier, FunctionDecl, Assignment, ClassDecl, NewExpr, CallExpr, Identifier, FunctionDecl, Assignment,
ExpressionStmt, IfStmt, ForStmt, ForeachStmt, WhileStmt, WhenStmt, 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, DictLiteral, IndexAccess, Lambda, MemberAccess, ThisExpr, Block,
BinaryOp, UnaryOp, WithStmt, Program, StringLiteral BinaryOp, UnaryOp, WithStmt, Program, StringLiteral
) )
...@@ -16,7 +17,7 @@ class UsageAnalyzer: ...@@ -16,7 +17,7 @@ class UsageAnalyzer:
CATEGORIES = { CATEGORIES = {
'core', 'object', 'http', 'fs', 'json', 'logger', 'string', 'core', 'object', 'http', 'fs', 'json', 'logger', 'string',
'array', 'dict', 'regex', 'math', 'time', 'awk', 'exception', 'array', 'dict', 'regex', 'math', 'time', 'awk', 'exception',
'args', 'misc', 'args', 'misc', 'async',
} }
ARRAY_RETURNING_METHODS = {'keys', 'split', 'slice'} ARRAY_RETURNING_METHODS = {'keys', 'split', 'slice'}
...@@ -259,6 +260,14 @@ class UsageAnalyzer: ...@@ -259,6 +260,14 @@ class UsageAnalyzer:
self.used.add('exception') self.used.add('exception')
self._analyze_expr(stmt.expression) 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): elif isinstance(stmt, ReturnStmt):
if stmt.value: if stmt.value:
self._analyze_expr(stmt.value) self._analyze_expr(stmt.value)
...@@ -313,6 +322,10 @@ class UsageAnalyzer: ...@@ -313,6 +322,10 @@ class UsageAnalyzer:
elif isinstance(expr, Lambda): elif isinstance(expr, Lambda):
self._analyze_body(expr.body) self._analyze_body(expr.body)
elif isinstance(expr, AsyncExpr):
self.used.add('async')
self._analyze_expr(expr.expression)
elif isinstance(expr, NewExpr): elif isinstance(expr, NewExpr):
self.has_classes = True self.has_classes = True
self.used_classes.add(expr.class_name) self.used_classes.add(expr.class_name)
......
from .ast_nodes import ( from .ast_nodes import (
CallExpr, MemberAccess, Identifier, ThisExpr, Assignment, ArrayLiteral, CallExpr, MemberAccess, Identifier, ThisExpr, Assignment, ArrayLiteral,
DictLiteral, NewExpr, Lambda, ExpressionStmt, BaseCall, ReturnStmt, DictLiteral, NewExpr, AsyncExpr, Lambda, ExpressionStmt, BaseCall, ReturnStmt,
StringLiteral, BinaryOp, IndexAccess, TypeAnnotation, IntegerLiteral, StringLiteral, BinaryOp, IndexAccess, TypeAnnotation, IntegerLiteral,
FloatLiteral, BoolLiteral FloatLiteral, BoolLiteral
) )
from .methods import ( from .methods import (
STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS, STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS,
PROCESS_HANDLE_METHODS,
NAMESPACE_METHODS, BUILTIN_NAMESPACES, BUILTIN_FUNCS, get_method_names 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()} 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()} STR_METHODS = {name: m.bash_func for name, m in STRING_METHODS.items()}
...@@ -206,6 +207,10 @@ class DispatchMixin: ...@@ -206,6 +207,10 @@ class DispatchMixin:
column=stmt.location.column if stmt.location else 0 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 == "|": if isinstance(stmt.value, BinaryOp) and stmt.value.operator == "|":
self._generate_pipe_assignment(stmt, target) self._generate_pipe_assignment(stmt, target)
return return
...@@ -590,6 +595,13 @@ class DispatchMixin: ...@@ -590,6 +595,13 @@ class DispatchMixin:
else: else:
self._validate_type_method("file_handle", method, location) 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: if obj_name in self.object_vars:
ret_type = self._get_method_return_type(obj_name, method) ret_type = self._get_method_return_type(obj_name, method)
obj = self.generate_expr(callee.object) obj = self.generate_expr(callee.object)
...@@ -732,6 +744,43 @@ class DispatchMixin: ...@@ -732,6 +744,43 @@ class DispatchMixin:
self.emit(f'declare -gA {target}=()') self.emit(f'declare -gA {target}=()')
self.dict_vars.add(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): def _generate_pipe_assignment(self, stmt: Assignment, target: str):
"""Generate pipe expression assignment.""" """Generate pipe expression assignment."""
elements = self._collect_pipe_chain(stmt.value) elements = self._collect_pipe_chain(stmt.value)
...@@ -1023,6 +1072,14 @@ class DispatchMixin: ...@@ -1023,6 +1072,14 @@ class DispatchMixin:
self._validate_type_method("file_handle", method, location) self._validate_type_method("file_handle", method, location)
return False 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: if method in STR_METHODS:
func_name = STR_METHODS[method] func_name = STR_METHODS[method]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
...@@ -1080,6 +1137,8 @@ class DispatchMixin: ...@@ -1080,6 +1137,8 @@ class DispatchMixin:
return f'__ct_assert "{args[0]}"' return f'__ct_assert "{args[0]}"'
elif name == "assert_eq": elif name == "assert_eq":
return f'__ct_assert_eq {args_str}' return f'__ct_assert_eq {args_str}'
elif name == "pid":
return '__ct_pid'
else: else:
if self._is_callback_var(name): if self._is_callback_var(name):
return f'"${{{name}}}" {args_str}' return f'"${{{name}}}" {args_str}'
...@@ -1175,6 +1234,22 @@ class DispatchMixin: ...@@ -1175,6 +1234,22 @@ class DispatchMixin:
else: else:
self._validate_type_method("file_handle", method, location) 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: if method in STR_METHODS:
return f'{STR_METHODS[method]} "{obj}" {args_str}'.strip() return f'{STR_METHODS[method]} "{obj}" {args_str}'.strip()
......
...@@ -2,8 +2,8 @@ import re ...@@ -2,8 +2,8 @@ import re
from .ast_nodes import ( from .ast_nodes import (
Expression, IntegerLiteral, FloatLiteral, StringLiteral, BoolLiteral, Expression, IntegerLiteral, FloatLiteral, StringLiteral, BoolLiteral,
NilLiteral, Identifier, ThisExpr, ArrayLiteral, DictLiteral, BinaryOp, NilLiteral, Identifier, ThisExpr, ArrayLiteral, DictLiteral, BinaryOp,
UnaryOp, CallExpr, MemberAccess, IndexAccess, Lambda, NewExpr, BaseCall, UnaryOp, CallExpr, MemberAccess, IndexAccess, Lambda, NewExpr, AsyncExpr,
Block, ReturnStmt BaseCall, Block, ReturnStmt
) )
...@@ -71,6 +71,9 @@ class ExprMixin: ...@@ -71,6 +71,9 @@ class ExprMixin:
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
return f'$({expr.class_name} {args_str}; echo "$__ct_last_instance")' 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): if isinstance(expr, BaseCall):
args = [self.generate_expr(a) for a in expr.arguments] args = [self.generate_expr(a) for a in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args]) args_str = " ".join([f'"{a}"' for a in args])
...@@ -109,7 +112,11 @@ class ExprMixin: ...@@ -109,7 +112,11 @@ class ExprMixin:
parts = content.split('.', 1) parts = content.split('.', 1)
if parts[0] == 'this': if parts[0] == 'this':
return f'\\$${{__CT_OBJ["$this.{parts[1]}"]}}' return f'\\$${{__CT_OBJ["$this.{parts[1]}"]}}'
else: 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'\\$${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
return f'\\$${{{content}}}' return f'\\$${{{content}}}'
...@@ -128,7 +135,11 @@ class ExprMixin: ...@@ -128,7 +135,11 @@ class ExprMixin:
parts = content.split('.', 1) parts = content.split('.', 1)
if parts[0] == 'this': if parts[0] == 'this':
return f'${{__CT_OBJ["$this.{parts[1]}"]}}' return f'${{__CT_OBJ["$this.{parts[1]}"]}}'
else: 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'${{__CT_OBJ["${{{parts[0]}}}.{parts[1]}"]:-}}'
return f'${{{content}}}' return f'${{{content}}}'
...@@ -261,12 +272,21 @@ class ExprMixin: ...@@ -261,12 +272,21 @@ class ExprMixin:
paren_idx = content.find('(') paren_idx = content.find('(')
func_name = content[:paren_idx].strip() func_name = content[:paren_idx].strip()
args_part = content[paren_idx + 1:-1].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: if args_part:
args_list = self._parse_args_with_parens(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]) 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: else:
return f'$({func_name})' return f'$({bash_name})'
def _parse_args_with_parens(self, args_str: str) -> list: def _parse_args_with_parens(self, args_str: str) -> list:
"""Parse comma-separated args, respecting nested parentheses.""" """Parse comma-separated args, respecting nested parentheses."""
...@@ -433,12 +453,36 @@ class ExprMixin: ...@@ -433,12 +453,36 @@ class ExprMixin:
return None 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: def _generate_member_access(self, expr: MemberAccess) -> str:
if isinstance(expr.object, Identifier) and expr.object.name == "env": if isinstance(expr.object, Identifier) and expr.object.name == "env":
return f'${{{expr.member}}}' return f'${{{expr.member}}}'
if isinstance(expr.object, Identifier): if isinstance(expr.object, Identifier):
obj_name = expr.object.name 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', {}) param_map = getattr(self, 'param_name_map', {})
mapped_name = param_map.get(obj_name, obj_name) mapped_name = param_map.get(obj_name, obj_name)
if mapped_name in getattr(self, 'dict_vars', set()): if mapped_name in getattr(self, 'dict_vars', set()):
......
...@@ -13,6 +13,7 @@ from .time import TimeMethods ...@@ -13,6 +13,7 @@ from .time import TimeMethods
from .args import ArgsMethods from .args import ArgsMethods
from .core import CoreFunctions, AwkBuiltinFunctions from .core import CoreFunctions, AwkBuiltinFunctions
from .reflect import ReflectMethods from .reflect import ReflectMethods
from .process_handle import ProcessHandleMethods
STRING_METHODS = collect_methods(StringMethods) STRING_METHODS = collect_methods(StringMethods)
ARRAY_METHODS = collect_methods(ArrayMethods) ARRAY_METHODS = collect_methods(ArrayMethods)
...@@ -29,6 +30,7 @@ ARGS_METHODS = collect_methods(ArgsMethods) ...@@ -29,6 +30,7 @@ ARGS_METHODS = collect_methods(ArgsMethods)
CORE_FUNCTIONS = collect_methods(CoreFunctions) CORE_FUNCTIONS = collect_methods(CoreFunctions)
AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions) AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions)
REFLECT_METHODS = collect_methods(ReflectMethods) REFLECT_METHODS = collect_methods(ReflectMethods)
PROCESS_HANDLE_METHODS = collect_methods(ProcessHandleMethods)
NAMESPACE_REGISTRY = { NAMESPACE_REGISTRY = {
"fs": FS_METHODS, "fs": FS_METHODS,
...@@ -56,6 +58,7 @@ def get_method(type_name: str, method_name: str): ...@@ -56,6 +58,7 @@ def get_method(type_name: str, method_name: str):
"array": ARRAY_METHODS, "array": ARRAY_METHODS,
"dict": DICT_METHODS, "dict": DICT_METHODS,
"file_handle": FILE_HANDLE_METHODS, "file_handle": FILE_HANDLE_METHODS,
"process_handle": PROCESS_HANDLE_METHODS,
} }
return registry.get(type_name, {}).get(method_name) return registry.get(type_name, {}).get(method_name)
...@@ -66,6 +69,7 @@ def get_method_names(type_name: str) -> set: ...@@ -66,6 +69,7 @@ def get_method_names(type_name: str) -> set:
"array": ARRAY_METHODS, "array": ARRAY_METHODS,
"dict": DICT_METHODS, "dict": DICT_METHODS,
"file_handle": FILE_HANDLE_METHODS, "file_handle": FILE_HANDLE_METHODS,
"process_handle": PROCESS_HANDLE_METHODS,
} }
methods = registry.get(type_name, {}) methods = registry.get(type_name, {})
return set(methods.keys()) return set(methods.keys())
......
...@@ -56,6 +56,11 @@ class CoreFunctions: ...@@ -56,6 +56,11 @@ class CoreFunctions:
awk_builtin=lambda a: f"int({a[0]} + rand() * ({a[1]} - {a[0]} + 1))", awk_builtin=lambda a: f"int({a[0]} + rand() * ({a[1]} - {a[0]} + 1))",
min_args=2, max_args=2, min_args=2, max_args=2,
) )
pid = Method(
name="pid",
bash_func="__ct_pid",
bash_impl='echo "$$"',
)
class AwkBuiltinFunctions: 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 ( ...@@ -4,10 +4,10 @@ from .ast_nodes import (
SourceLocation, Program, Declaration, Statement, Decorator, FunctionDecl, SourceLocation, Program, Declaration, Statement, Decorator, FunctionDecl,
Parameter, ClassDecl, ClassField, ConstructorDecl, ImportStmt, Block, ReturnStmt, Parameter, ClassDecl, ClassField, ConstructorDecl, ImportStmt, Block, ReturnStmt,
BreakStmt, ContinueStmt, IfStmt, WhileStmt, ForStmt, ForeachStmt, WithStmt, 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, ExpressionStmt, Assignment, IntegerLiteral, FloatLiteral, StringLiteral,
BoolLiteral, NilLiteral, ThisExpr, ArrayLiteral, DictLiteral, Identifier, BoolLiteral, NilLiteral, ThisExpr, ArrayLiteral, DictLiteral, Identifier,
BinaryOp, UnaryOp, CallExpr, MemberAccess, IndexAccess, NewExpr, Lambda, BinaryOp, UnaryOp, CallExpr, MemberAccess, IndexAccess, NewExpr, AsyncExpr, Lambda,
BaseCall, Expression, TypeAnnotation BaseCall, Expression, TypeAnnotation
) )
from .errors import CompileError, ErrorCollector from .errors import CompileError, ErrorCollector
...@@ -53,6 +53,18 @@ class Parser: ...@@ -53,6 +53,18 @@ class Parser:
self.error (msg) self.error (msg)
return self.current () 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): def error (self, message: str, token: Token = None):
token = token or self.current () token = token or self.current ()
self.errors.add_error ( self.errors.add_error (
...@@ -149,7 +161,7 @@ class Parser: ...@@ -149,7 +161,7 @@ class Parser:
obj = None obj = None
if self.match (TokenType.DOT): if self.match (TokenType.DOT):
obj = name obj = name
name = self.expect (TokenType.IDENTIFIER, "Expected method name after '.'").value name = self.expect_name ("Expected method name after '.'").value
arguments = [] arguments = []
if self.match (TokenType.LPAREN): if self.match (TokenType.LPAREN):
...@@ -188,7 +200,7 @@ class Parser: ...@@ -188,7 +200,7 @@ class Parser:
def parse_function (self, decorators: List[Decorator] = None) -> FunctionDecl: def parse_function (self, decorators: List[Decorator] = None) -> FunctionDecl:
loc = self.location () loc = self.location ()
self.expect (TokenType.FUNC) 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") self.expect (TokenType.LPAREN, "Expected '(' after function name")
params = self.parse_parameters () params = self.parse_parameters ()
...@@ -333,6 +345,10 @@ class Parser: ...@@ -333,6 +345,10 @@ class Parser:
return self.parse_throw () return self.parse_throw ()
if self.check (TokenType.DEFER): if self.check (TokenType.DEFER):
return self.parse_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): if self.check (TokenType.WHEN):
return self.parse_when () return self.parse_when ()
if self.check (TokenType.LBRACE): if self.check (TokenType.LBRACE):
...@@ -528,6 +544,20 @@ class Parser: ...@@ -528,6 +544,20 @@ class Parser:
expression = self.parse_expression () expression = self.parse_expression ()
return DeferStmt (expression=expression, location=loc) 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: def parse_when (self) -> WhenStmt:
loc = self.location () loc = self.location ()
self.expect (TokenType.WHEN) self.expect (TokenType.WHEN)
...@@ -635,6 +665,12 @@ class Parser: ...@@ -635,6 +665,12 @@ class Parser:
operand = self.parse_unary () operand = self.parse_unary ()
return UnaryOp (operator=operator.value, operand=operand, location=loc) 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): if self.check (TokenType.NEW):
loc = self.location () loc = self.location ()
self.advance () # consume 'new' self.advance () # consume 'new'
...@@ -660,7 +696,7 @@ class Parser: ...@@ -660,7 +696,7 @@ class Parser:
expr = CallExpr (callee=expr, arguments=args, location=expr.location) expr = CallExpr (callee=expr, arguments=args, location=expr.location)
elif self.match (TokenType.DOT): 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) expr = MemberAccess (object=expr, member=member, location=expr.location)
elif self.match (TokenType.LBRACKET): elif self.match (TokenType.LBRACKET):
......
...@@ -98,6 +98,8 @@ class StdlibMixin: ...@@ -98,6 +98,8 @@ class StdlibMixin:
self.emit(f"{CORE_FUNCTIONS['len'].bash_func} () {{ {CORE_FUNCTIONS['len'].bash_impl}; }}") self.emit(f"{CORE_FUNCTIONS['len'].bash_func} () {{ {CORE_FUNCTIONS['len'].bash_impl}; }}")
self.emit() self.emit()
self.emit(f"{CORE_FUNCTIONS['pid'].bash_func} () {{ {CORE_FUNCTIONS['pid'].bash_impl}; }}")
self.emit()
def _emit_http(self): def _emit_http(self):
"""HTTP functions from HTTP_METHODS.""" """HTTP functions from HTTP_METHODS."""
......
from .ast_nodes import ( from .ast_nodes import (
FunctionDecl, ClassDecl, ImportStmt, Assignment, ExpressionStmt, IfStmt, FunctionDecl, ClassDecl, ImportStmt, Assignment, ExpressionStmt, IfStmt,
WhileStmt, ForStmt, ForeachStmt, WithStmt, TryStmt, ThrowStmt, DeferStmt, WhileStmt, ForStmt, ForeachStmt, WithStmt, TryStmt, ThrowStmt, DeferStmt,
AwaitStmt, OnSignalStmt, AsyncExpr,
WhenStmt, RangePattern, ReturnStmt, BreakStmt, ContinueStmt, Block, WhenStmt, RangePattern, ReturnStmt, BreakStmt, ContinueStmt, Block,
CallExpr, Identifier, MemberAccess, ThisExpr, StringLiteral, NewExpr, CallExpr, Identifier, MemberAccess, ThisExpr, StringLiteral, NewExpr,
BinaryOp, DictLiteral, ArrayLiteral, WhenBranch BinaryOp, DictLiteral, ArrayLiteral, WhenBranch
) )
from .constants import RET_VAR, RET_ARR from .constants import RET_VAR, RET_ARR, COPROC_PREFIX
class StmtMixin: class StmtMixin:
...@@ -41,6 +42,10 @@ class StmtMixin: ...@@ -41,6 +42,10 @@ class StmtMixin:
self.generate_throw(stmt) self.generate_throw(stmt)
elif isinstance(stmt, DeferStmt): elif isinstance(stmt, DeferStmt):
self.generate_defer(stmt) 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): elif isinstance(stmt, WhenStmt):
self.generate_when(stmt) self.generate_when(stmt)
elif isinstance(stmt, ReturnStmt): elif isinstance(stmt, ReturnStmt):
...@@ -318,8 +323,29 @@ class StmtMixin: ...@@ -318,8 +323,29 @@ class StmtMixin:
self.emit("# with statement") self.emit("# with statement")
local_kw = "local " if self.in_function else "" local_kw = "local " if self.in_function else ""
with_async = {}
for i, (var, resource) in enumerate(zip(stmt.variables, stmt.resources)): 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 isinstance(resource, CallExpr) and isinstance(resource.callee, MemberAccess):
if resource.callee.member == "open": if resource.callee.member == "open":
self.file_handle_vars.add(var) self.file_handle_vars.add(var)
...@@ -339,6 +365,11 @@ class StmtMixin: ...@@ -339,6 +365,11 @@ class StmtMixin:
self.emit("# with cleanup") self.emit("# with cleanup")
for i in range(len(stmt.variables) - 1, -1, -1): for i in range(len(stmt.variables) - 1, -1, -1):
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}"') self.emit(f'__ct_fh___exit__ "$__ct_with_{i}"')
def generate_try(self, stmt: TryStmt): def generate_try(self, stmt: TryStmt):
...@@ -414,6 +445,30 @@ class StmtMixin: ...@@ -414,6 +445,30 @@ class StmtMixin:
expr = self.generate_expr(stmt.expression) expr = self.generate_expr(stmt.expression)
self.deferred_calls.append(expr) 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): def generate_return(self, stmt: ReturnStmt):
if self.deferred_calls: if self.deferred_calls:
self.emit("# Deferred calls before return") self.emit("# Deferred calls before return")
......
...@@ -37,6 +37,9 @@ class TokenType (Enum): ...@@ -37,6 +37,9 @@ class TokenType (Enum):
WHEN = auto () WHEN = auto ()
WITH = auto () WITH = auto ()
NEW = auto () NEW = auto ()
ASYNC = auto ()
AWAIT = auto ()
ON = auto ()
PLUS = auto () PLUS = auto ()
MINUS = auto () MINUS = auto ()
...@@ -103,6 +106,9 @@ KEYWORDS = { ...@@ -103,6 +106,9 @@ KEYWORDS = {
'when': TokenType.WHEN, 'when': TokenType.WHEN,
'with': TokenType.WITH, 'with': TokenType.WITH,
'new': TokenType.NEW, 'new': TokenType.NEW,
'async': TokenType.ASYNC,
'await': TokenType.AWAIT,
'on': TokenType.ON,
'true': TokenType.TRUE, 'true': TokenType.TRUE,
'false': TokenType.FALSE, 'false': TokenType.FALSE,
'nil': TokenType.NIL, '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