Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
ContenT
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Ximper Linux
ContenT
Commits
fc7a945f
Commit
fc7a945f
authored
Feb 19, 2026
by
Roman Alifanov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add async/await background processes, on signal handlers, pid()
parent
12c3a0f1
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
640 additions
and
25 deletions
+640
-25
LANGUAGE_SPEC.md
LANGUAGE_SPEC.md
+52
-0
README.md
README.md
+35
-3
README_ru.md
README_ru.md
+35
-3
ast_nodes.py
bootstrap/ast_nodes.py
+18
-0
codegen.py
bootstrap/codegen.py
+3
-0
constants.py
bootstrap/constants.py
+2
-0
dce.py
bootstrap/dce.py
+15
-2
dispatch_codegen.py
bootstrap/dispatch_codegen.py
+77
-2
expr_codegen.py
bootstrap/expr_codegen.py
+52
-8
__init__.py
bootstrap/methods/__init__.py
+4
-0
core.py
bootstrap/methods/core.py
+5
-0
process_handle.py
bootstrap/methods/process_handle.py
+30
-0
parser.py
bootstrap/parser.py
+41
-5
stdlib.py
bootstrap/stdlib.py
+2
-0
stmt_codegen.py
bootstrap/stmt_codegen.py
+57
-2
tokens.py
bootstrap/tokens.py
+6
-0
test_async.py
tests/test_async.py
+206
-0
No files found.
LANGUAGE_SPEC.md
View file @
fc7a945f
...
...
@@ -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`
...
...
README.md
View file @
fc7a945f
...
...
@@ -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
```
...
...
README_ru.md
View file @
fc7a945f
...
...
@@ -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 программ
```
...
...
bootstrap/ast_nodes.py
View file @
fc7a945f
...
...
@@ -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
...
...
bootstrap/codegen.py
View file @
fc7a945f
...
...
@@ -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
...
...
bootstrap/constants.py
View file @
fc7a945f
...
...
@@ -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"
bootstrap/dce.py
View file @
fc7a945f
...
...
@@ -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
)
...
...
bootstrap/dispatch_codegen.py
View file @
fc7a945f
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
()
...
...
bootstrap/expr_codegen.py
View file @
fc7a945f
...
...
@@ -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
,
B
aseCall
,
B
lock
,
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
(
'
\x00
DOLLAR
\x00
{'
,
'
\x01
ESCAPED_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
()):
...
...
bootstrap/methods/__init__.py
View file @
fc7a945f
...
...
@@ -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
())
...
...
bootstrap/methods/core.py
View file @
fc7a945f
...
...
@@ -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
:
...
...
bootstrap/methods/process_handle.py
0 → 100644
View file @
fc7a945f
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
,
)
bootstrap/parser.py
View file @
fc7a945f
...
...
@@ -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
):
...
...
bootstrap/stdlib.py
View file @
fc7a945f
...
...
@@ -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."""
...
...
bootstrap/stmt_codegen.py
View file @
fc7a945f
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"
)
...
...
bootstrap/tokens.py
View file @
fc7a945f
...
...
@@ -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
,
...
...
tests/test_async.py
0 → 100644
View file @
fc7a945f
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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment