Commit 5785e16d authored by Roman Alifanov's avatar Roman Alifanov

Add map/filter, @validate, file handles, and with statement

New features: - .map() / .filter() for arrays with lambda support - @validate decorator for argument validation - fs.open() / f.read() / f.write() / f.close() file handles - with X in Y { } context manager for auto-closing resources All features work in both Bash and @awk codegen. Added 18 new pytest tests for the new functionality. Updated documentation (LANGUAGE_SPEC.md, README.md, README_ru.md).
parent aa131aa2
......@@ -151,6 +151,7 @@ result = value | func1 | func2
- `@retry(attempts, delay)` — повторные попытки при ошибке
- `@log` — логирование вызовов (выводит в stderr)
- `@cache(ttl)` — кеширование результатов на ttl секунд
- `@validate(param: "rule")` — валидация аргументов (см. ниже)
- `@awk` — компиляция в AWK (см. ниже)
- `@test("description")` — пометить функцию как тест (см. ниже)
......@@ -172,6 +173,37 @@ func getConfig () {
}
```
### @validate — валидация аргументов
Декоратор `@validate` проверяет аргументы функции перед выполнением:
```
@validate (x: "int > 0", name: "string nonempty")
func process (x, name) {
print ("Processing {x} for {name}")
}
process (5, "Alice") # OK
process (-1, "Bob") # Error: x must be > 0
process (5, "") # Error: name cannot be empty
```
**Правила валидации:**
- `"int"` — должно быть целое число
- `"int > 0"` — целое число больше 0
- `"int >= 1 <= 100"` — целое число в диапазоне
- `"string nonempty"` — непустая строка
**Комбинация с @awk:**
```
@validate (x: "int > 0")
@awk
func double (x) {
return x * 2
}
```
```
# На методах классов
class ApiClient {
......@@ -235,10 +267,11 @@ func sumNumbers (text) {
**Доступные методы в @awk:**
- Строки: `text.len ()`, `text.upper ()`, `text.lower ()`, `text.trim ()`, `text.substr ()`, `text.index ()`, `text.contains ()`, `text.split ()`, `text.replace ()`, `text.starts ()`, `text.ends ()`, `text.charAt ()`
- Массивы: `arr.len ()`, `arr.get ()`, `arr.set ()`, `arr.push ()`, `arr.pop ()`, `arr.shift ()`, `arr.has ()`, `arr.del ()`
- Массивы: `arr.len ()`, `arr.get ()`, `arr.set ()`, `arr.push ()`, `arr.pop ()`, `arr.shift ()`, `arr.has ()`, `arr.del ()`, `arr.map ()`, `arr.filter ()`
- Словари: `dict.get ()`, `dict.set ()`, `dict.has ()`, `dict.del ()`, `dict.keys ()`
- Математика: `math.sin ()`, `math.cos ()`, `math.sqrt ()`, `math.log ()`, `math.exp ()`, `math.int ()`, `math.rand ()`, `math.atan2 ()`
- Вывод: `print ()`, `printf ()`, `sprintf ()`
- Декораторы: `@validate` работает с `@awk` функциями
**Ограничения AWK:**
- Массивы в AWK ассоциативные — `pop ()`, `shift ()`, `join ()`, `slice ()` имеют ограниченную поддержку
......@@ -488,6 +521,55 @@ fs.mkdir (path)
files = fs.list (path)
```
### fs.open — файловые дескрипторы
Для работы с файлами через дескрипторы:
```
# Открыть файл
f = fs.open (path, "r") # режим: "r" (чтение), "w" (запись), "a" (добавление)
# Методы файлового дескриптора
data = f.read () # прочитать всё содержимое
line = f.readline () # прочитать строку
f.write ("text") # записать текст
f.writeln ("text") # записать строку с переводом строки
f.close () # закрыть файл
```
**Пример:**
```
f = fs.open ("/tmp/test.txt", "w")
f.write ("hello world")
f.close ()
f = fs.open ("/tmp/test.txt", "r")
content = f.read ()
f.close ()
print (content) # hello world
```
### with — автоматическое закрытие ресурсов
Оператор `with` автоматически закрывает ресурсы при выходе из блока:
```
# Один ресурс
with f in fs.open ("/tmp/test.txt") {
data = f.read ()
print (data)
} # f.close () вызывается автоматически
# Несколько ресурсов
with input, output in fs.open (src), fs.open (dst, "w") {
data = input.read ()
output.write (data)
} # оба файла закрываются автоматически
```
Синтаксис аналогичен `foreach`: `with VAR in RESOURCE { ... }`
### json
```
......@@ -558,7 +640,33 @@ items.set (1, "x") # items[1] = "x"
# Преобразование
joined = items.join (", ") # "a, b, c"
slice = items.slice (0, 2) # ["a", "b"]
# Функциональные методы
squared = items.map (x => x * 2) # применить функцию к каждому
filtered = items.filter (x => x > 10) # отфильтровать по условию
```
### map / filter
Высокоуровневые методы для работы с массивами через лямбды:
```
numbers = [1, 2, 3, 4, 5]
# map - применить функцию к каждому элементу
squared = numbers.map (x => x * x) # [1, 4, 9, 16, 25]
doubled = numbers.map (x => x * 2) # [2, 4, 6, 8, 10]
# filter - оставить элементы, удовлетворяющие условию
evens = numbers.filter (x => x % 2 == 0) # [2, 4]
big = numbers.filter (x => x > 3) # [4, 5]
# Комбинирование (через промежуточную переменную)
temp = numbers.map (x => x * 2)
result = temp.filter (x => x > 5) # [6, 8, 10]
```
Работает в Bash и в `@awk` функциях.
### regex
......
......@@ -9,7 +9,10 @@
- **Clean syntax** — Python-like readability with Go/Vala influences
- **Classes & inheritance** — OOP with constructors and method calls
- **Lambdas**`x => x * 2`, `(a, b) => a + b`, multiline blocks
- **Decorators**`@retry`, `@log`, `@cache`, `@awk`, `@test`
- **Decorators**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`
- **Functional arrays**`.map()`, `.filter()` with lambdas
- **File handles**`fs.open()`, `f.read()`, `f.write()`, `f.close()`
- **Context managers**`with f in fs.open(path) { ... }`
- **Pipe operator** — shell-like `|` for command chaining and functional composition
- **Error handling**`try/except/finally/throw/defer`
- **String interpolation**`"Hello, {name}!"`
......@@ -101,6 +104,11 @@ func fetch_data (url) {
return http.get (url)
}
@validate (x: "int > 0", name: "string nonempty")
func process (x, name) {
print ("Processing {x} for {name}")
}
@awk
func fast_sum (text) {
total = 0
......@@ -118,6 +126,29 @@ func test_add () {
}
```
### Functional Arrays
```
numbers = [1, 2, 3, 4, 5]
squared = numbers.map (x => x * x) # [1, 4, 9, 16, 25]
evens = numbers.filter (x => x % 2 == 0) # [2, 4]
```
### File Handles & Context Managers
```
# Manual file handling
f = fs.open ("/tmp/test.txt", "w")
f.write ("hello world")
f.close ()
# Automatic cleanup with 'with'
with f in fs.open ("/tmp/test.txt") {
data = f.read ()
print (data)
} # f.close() called automatically
```
### Control Flow
```
......@@ -165,10 +196,11 @@ try {
|--------|-----------|
| **I/O** | `print()`, `exit()` |
| **HTTP** | `http.get/post/put/delete` |
| **Filesystem** | `fs.read/write/append/exists/remove/mkdir/list` |
| **Filesystem** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **File handles** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
| **JSON** | `json.parse/stringify` |
| **Strings** | `.len()`, `.upper()`, `.lower()`, `.trim()`, `.contains()`, `.replace()`, `.split()`, `.substr()` |
| **Arrays** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()` |
| **Arrays** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` |
| **Dicts** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` |
| **Regex** | `regex.match/extract` |
| **Shell** | `shell.exec/capture/source` |
......@@ -252,8 +284,9 @@ class Calculator {
- Conditions: `if/else if/else`, `when`
- Operators: `+`, `-`, `*`, `/`, `%`, `^`, `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`
- String methods: `.len()`, `.upper()`, `.lower()`, `.trim()`, `.split()`, `.substr()`, `.contains()`, `.replace()`
- Array methods: `.len()`, `.get()`, `.set()`, `.push()`, `.pop()`
- Array methods: `.len()`, `.get()`, `.set()`, `.push()`, `.pop()`, `.map()`, `.filter()`
- Math: `math.sin()`, `math.cos()`, `math.sqrt()`, `math.log()`, `math.exp()`
- Decorators: `@validate` works with `@awk` functions
- `break`, `continue`, `return`
### Limitations
......
......@@ -9,7 +9,10 @@
- **Чистый синтаксис** — читаемость Python с влиянием Go/Vala
- **Классы и наследование** — ООП с конструкторами и вызовами методов
- **Лямбды**`x => x * 2`, `(a, b) => a + b`, многострочные блоки
- **Декораторы**`@retry`, `@log`, `@cache`, `@awk`, `@test`
- **Декораторы**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`
- **Функциональные массивы**`.map()`, `.filter()` с лямбдами
- **Файловые дескрипторы**`fs.open()`, `f.read()`, `f.write()`, `f.close()`
- **Контекстные менеджеры**`with f in fs.open(path) { ... }`
- **Pipe-оператор** — shell-like `|` для цепочек команд и функциональной композиции
- **Обработка ошибок**`try/except/finally/throw/defer`
- **Строковая интерполяция**`"Привет, {name}!"`
......@@ -101,6 +104,11 @@ func fetch_data (url) {
return http.get (url)
}
@validate (x: "int > 0", name: "string nonempty")
func process (x, name) {
print ("Обработка {x} для {name}")
}
@awk
func fast_sum (text) {
total = 0
......@@ -118,6 +126,29 @@ func test_add () {
}
```
### Функциональные массивы
```
numbers = [1, 2, 3, 4, 5]
squared = numbers.map (x => x * x) # [1, 4, 9, 16, 25]
evens = numbers.filter (x => x % 2 == 0) # [2, 4]
```
### Файловые дескрипторы и контекстные менеджеры
```
# Ручная работа с файлами
f = fs.open ("/tmp/test.txt", "w")
f.write ("hello world")
f.close ()
# Автоматическое закрытие через 'with'
with f in fs.open ("/tmp/test.txt") {
data = f.read ()
print (data)
} # f.close() вызывается автоматически
```
### Управление потоком
```
......@@ -165,10 +196,11 @@ try {
|--------|---------|
| **Ввод/вывод** | `print()`, `exit()` |
| **HTTP** | `http.get/post/put/delete` |
| **Файловая система** | `fs.read/write/append/exists/remove/mkdir/list` |
| **Файловая система** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **Файловые дескрипторы** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
| **JSON** | `json.parse/stringify` |
| **Строки** | `.len()`, `.upper()`, `.lower()`, `.trim()`, `.contains()`, `.replace()`, `.split()`, `.substr()` |
| **Массивы** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()` |
| **Массивы** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` |
| **Словари** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` |
| **Regex** | `regex.match/extract` |
| **Shell** | `shell.exec/capture/source` |
......@@ -252,8 +284,9 @@ class Calculator {
- Условия: `if/else if/else`, `when`
- Операторы: `+`, `-`, `*`, `/`, `%`, `^`, `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`
- Методы строк: `.len()`, `.upper()`, `.lower()`, `.trim()`, `.split()`, `.substr()`, `.contains()`, `.replace()`
- Методы массивов: `.len()`, `.get()`, `.set()`, `.push()`, `.pop()`
- Методы массивов: `.len()`, `.get()`, `.set()`, `.push()`, `.pop()`, `.map()`, `.filter()`
- Математика: `math.sin()`, `math.cos()`, `math.sqrt()`, `math.log()`, `math.exp()`
- Декораторы: `@validate` работает с `@awk` функциями
- `break`, `continue`, `return`
### Ограничения
......
......@@ -204,6 +204,14 @@ class ForeachStmt (Statement):
@dataclass
class WithStmt (Statement):
variables: List[str] = field (default_factory=list)
resources: List[Expression] = field (default_factory=list)
body: Optional[Block] = None
location: Optional[SourceLocation] = None
@dataclass
class TryStmt (Statement):
try_block: Optional[Block] = None
except_clauses: List[tuple] = field (default_factory=list)
......
import re
from typing import List
from .ast_nodes import (
FunctionDecl, Assignment, ReturnStmt, IfStmt, WhileStmt,
ForStmt, ForeachStmt, ExpressionStmt, BreakStmt, ContinueStmt,
WhenStmt, RangePattern,
WhenStmt, RangePattern, Lambda, Decorator,
Block, Identifier, IntegerLiteral, FloatLiteral, StringLiteral,
BoolLiteral, NilLiteral, ArrayLiteral, DictLiteral, BinaryOp,
UnaryOp, CallExpr, IndexAccess, MemberAccess
......@@ -94,6 +95,12 @@ class AwkCodegenMixin:
self.emit (f"{name} () {{")
self.indent_level += 1
validate_decorator = None
for dec in func.decorators:
if dec.name == "validate":
validate_decorator = dec
break
nested_funcs = []
main_stmts = []
for stmt in func.body.statements:
......@@ -103,6 +110,7 @@ class AwkCodegenMixin:
main_stmts.append (stmt)
self._awk_var_types = self._awk_scan_types (func.body.statements)
self._awk_validate = validate_decorator
input_foreach = None
input_idx = -1
......@@ -126,6 +134,10 @@ class AwkCodegenMixin:
after_stmts = main_stmts[input_idx + 1:]
begin_lines, begin_emit, begin_inc, begin_dec = self._awk_make_emitter ()
if self._awk_validate:
self._awk_emit_validation (func.params, begin_emit)
for stmt in before_stmts:
self._awk_stmt (stmt, begin_emit, begin_inc, begin_dec)
......@@ -167,6 +179,10 @@ class AwkCodegenMixin:
self.emit ("' \"$1\")")
else:
awk_lines, awk_emit, awk_inc, awk_dec = self._awk_make_emitter ()
if self._awk_validate:
self._awk_emit_validation (func.params, awk_emit)
for stmt in main_stmts:
self._awk_stmt (stmt, awk_emit, awk_inc, awk_dec)
......@@ -217,6 +233,10 @@ class AwkCodegenMixin:
self._awk_var_types = var_types
emit (f"delete {target}")
return
if self._awk_handle_map_filter (stmt, target, emit, inc, dec):
return
value = self._awk_expr (stmt.value)
op = stmt.operator
if op == "=":
......@@ -510,6 +530,9 @@ class AwkCodegenMixin:
if method == "del" and len (args) >= 1:
key = self._awk_expr (args[0])
return f"delete {ns}[{key}]"
if method == "join" and len(args) >= 1:
sep = self._awk_expr(args[0])
return f"__ct_awk_join({ns}, {sep})"
elif var_type == "dict":
if method == "get" and len (args) >= 1:
......@@ -592,3 +615,91 @@ class AwkCodegenMixin:
if result.startswith ('(') and result.endswith (')'):
return result[1:-1]
return result
def _awk_handle_map_filter (self, stmt: Assignment, target: str, emit, inc, dec) -> bool:
"""Handle array.map() and array.filter() calls for assignments."""
if not isinstance (stmt.value, CallExpr):
return False
if not isinstance (stmt.value.callee, MemberAccess):
return False
if not isinstance (stmt.value.callee.object, Identifier):
return False
method = stmt.value.callee.member
arr_name = stmt.value.callee.object.name
args = stmt.value.arguments
if method not in ("map", "filter") or len (args) < 1:
return False
lambda_arg = args[0]
if not isinstance (lambda_arg, Lambda):
return False
var_types = getattr (self, '_awk_var_types', {})
var_types[target] = "array"
self._awk_var_types = var_types
param = lambda_arg.params[0] if lambda_arg.params else "__x"
body_expr = self._awk_lambda_body (lambda_arg.body, param)
emit (f"delete {target}")
if method == "map":
emit (f"__ct_len = length({arr_name})")
emit (f"for (__i = 1; __i <= __ct_len; __i++) {{")
inc ()
emit (f"{param} = {arr_name}[__i]")
emit (f"{target}[__i] = {body_expr}")
dec ()
emit ("}")
elif method == "filter":
emit (f"__ct_len = length({arr_name})")
emit (f"__ct_fidx = 0")
emit (f"for (__i = 1; __i <= __ct_len; __i++) {{")
inc ()
emit (f"{param} = {arr_name}[__i]")
emit (f"if ({body_expr}) {{")
inc ()
emit (f"__ct_fidx++")
emit (f"{target}[__ct_fidx] = {arr_name}[__i]")
dec ()
emit ("}")
dec ()
emit ("}")
return True
def _awk_lambda_body (self, body, param: str) -> str:
"""Generate AWK expression for lambda body."""
if isinstance (body, Block):
for stmt in body.statements:
if isinstance (stmt, ReturnStmt) and stmt.value:
return self._awk_expr (stmt.value)
return '""'
return self._awk_expr (body)
def _awk_emit_validation (self, params, emit):
"""Generate AWK validation code for @validate decorator."""
validate = self._awk_validate
if not validate:
return
validations = {}
for arg_name, arg_val in validate.arguments:
if arg_name and hasattr (arg_val, 'value'):
validations[arg_name] = arg_val.value
for param in params:
rule = validations.get (param.name)
if not rule:
continue
pname = param.name
if "int" in rule:
emit (f'if ({pname} !~ /^-?[0-9]+$/) {{ print "{pname} must be integer" > "/dev/stderr"; exit 1 }}')
for op, val in re.findall (r'(>|<|>=|<=|==|!=)\s*(-?\d+)', rule):
awk_cond = f"{pname} {op} {val}"
emit (f'if (!({awk_cond})) {{ print "{pname} must be {op} {val}" > "/dev/stderr"; exit 1 }}')
......@@ -48,6 +48,7 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.array_vars: Set[str] = set()
self.dict_vars: Set[str] = set()
self.object_vars: Set[str] = set()
self.file_handle_vars: Set[str] = set()
self.class_field_types: Dict[tuple, str] = {}
self.local_vars: Set[str] = set()
......
......@@ -299,6 +299,10 @@ class UsageAnalyzer:
self.used_methods[field_class] = set()
self.used_methods[field_class].add(method)
elif isinstance(callee.object, CallExpr):
self._analyze_call(callee.object)
self._check_method(callee.member)
elif isinstance(callee.object, Identifier):
ns = callee.object.name
method = callee.member
......
import re
from typing import List
from .ast_nodes import *
......@@ -14,6 +15,8 @@ class DecoratorMixin:
self._generate_log_wrapper(wrapped_name, wrapper_name, params, is_method=False)
elif decorator.name == "cache":
self._generate_cache_wrapper(decorator, wrapped_name, wrapper_name, params, is_method=False)
elif decorator.name == "validate":
self._generate_validate_wrapper(decorator, wrapped_name, wrapper_name, params, is_method=False)
else:
self._generate_passthrough_wrapper(wrapped_name, wrapper_name, params, is_method=False)
......@@ -26,6 +29,8 @@ class DecoratorMixin:
self._generate_log_wrapper(wrapped_name, wrapper_name, params, is_method=True)
elif decorator.name == "cache":
self._generate_cache_wrapper(decorator, wrapped_name, wrapper_name, params, is_method=True)
elif decorator.name == "validate":
self._generate_validate_wrapper(decorator, wrapped_name, wrapper_name, params, is_method=True)
else:
self._generate_passthrough_wrapper(wrapped_name, wrapper_name, params, is_method=True)
......@@ -159,3 +164,62 @@ class DecoratorMixin:
self.indent_level -= 1
self.emit("}")
self.emit()
def _generate_validate_wrapper(self, decorator: Decorator, wrapped_name: str,
wrapper_name: str, params: List[Parameter], is_method: bool):
validations = {}
for arg_name, arg_val in decorator.arguments:
if arg_name and hasattr(arg_val, 'value'):
validations[arg_name] = arg_val.value
self.emit(f"{wrapper_name} () {{")
self.indent_level += 1
if is_method:
self.emit('local this="$1"')
self.emit('shift')
for i, param in enumerate(params):
rule = validations.get(param.name)
if rule:
self._emit_validation_check(param.name, rule, i + 1)
params_str = " ".join([f'"${{{i + 1}}}"' for i in range(len(params))])
if is_method:
self.emit(f'{wrapped_name} "$this" {params_str}')
else:
self.emit(f'{wrapped_name} {params_str}')
self.indent_level -= 1
self.emit("}")
self.emit()
def _emit_validation_check(self, param_name: str, rule: str, arg_pos: int):
self.emit(f'local {param_name}="${{{arg_pos}}}"')
if "int" in rule:
self.emit(f'if ! [[ "${param_name}" =~ ^-?[0-9]+$ ]]; then')
self.indent_level += 1
self.emit(f'echo "Validation error: {param_name} must be integer" >&2')
self.emit('return 1')
self.indent_level -= 1
self.emit('fi')
for match in re.finditer(r'(>=|<=|>|<|==|!=)\s*(-?\d+)', rule):
op, val = match.groups()
bash_op = {'>': '-gt', '<': '-lt', '>=': '-ge', '<=': '-le', '==': '-eq', '!=': '-ne'}[op]
self.emit(f'if ! [[ ${param_name} {bash_op} {val} ]]; then')
self.indent_level += 1
self.emit(f'echo "Validation error: {param_name} must be {op} {val}" >&2')
self.emit('return 1')
self.indent_level -= 1
self.emit('fi')
elif "string" in rule:
if "nonempty" in rule or "required" in rule:
self.emit(f'if [[ -z "${param_name}" ]]; then')
self.indent_level += 1
self.emit(f'echo "Validation error: {param_name} cannot be empty" >&2')
self.emit('return 1')
self.indent_level -= 1
self.emit('fi')
......@@ -5,6 +5,7 @@ ARR_METHODS = {
"len": "__ct_arr_len", "push": "__ct_arr_push", "pop": "__ct_arr_pop",
"shift": "__ct_arr_shift", "join": "__ct_arr_join", "get": "__ct_arr_get",
"set": "__ct_arr_set", "slice": "__ct_arr_slice",
"map": "__ct_arr_map", "filter": "__ct_arr_filter",
}
DICT_METHODS = {
......@@ -19,6 +20,13 @@ STR_METHODS = {
"substr": "__ct_str_substr", "split": "__ct_str_split", "charAt": "__ct_str_char_at",
}
FILE_HANDLE_METHODS = {
"read": "__ct_fh_read", "readline": "__ct_fh_readline",
"write": "__ct_fh_write", "writeln": "__ct_fh_writeln",
"close": "__ct_fh_close",
"__enter__": "__ct_fh___enter__", "__exit__": "__ct_fh___exit__",
}
BUILTIN_NAMESPACES = {"fs", "http", "json", "logger", "regex", "args", "shell", "time", "math"}
BUILTIN_FUNCS = {"print", "exit", "len", "range", "ngrep", "is_number", "is_empty", "chr", "ord", "assert", "assert_eq"}
......@@ -26,6 +34,14 @@ BUILTIN_FUNCS = {"print", "exit", "len", "range", "ngrep", "is_number", "is_empt
class DispatchMixin:
"""Mixin for method dispatch and assignment."""
def _emit_array_assign(self, target: str):
"""Emit array assignment from __CT_RET."""
if self.in_function and target not in self.local_vars and target not in self.global_vars:
self.local_vars.add(target)
self.emit(f'local -a {target}=("${{__CT_RET[@]}}")')
else:
self.emit(f'{target}=("${{__CT_RET[@]}}")')
def generate_assignment(self, stmt: Assignment):
if isinstance(stmt.target, MemberAccess):
if isinstance(stmt.target.object, ThisExpr):
......@@ -61,6 +77,14 @@ class DispatchMixin:
return
if isinstance(stmt.value, CallExpr) and isinstance(stmt.value.callee, MemberAccess):
callee = stmt.value.callee
if isinstance(callee.object, Identifier) and callee.object.name == "fs" and callee.member == "open":
args = [self.generate_expr(arg) for arg in stmt.value.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'__ct_fs_open {args_str} >/dev/null')
self.emit_var_assign(target, '$__CT_RET')
self.file_handle_vars.add(target)
return
if self._generate_method_call_assignment(stmt, target):
return
......@@ -151,7 +175,12 @@ class DispatchMixin:
def _generate_method_call_assignment(self, stmt: Assignment, target: str) -> bool:
"""Generate method call assignment. Returns True if handled."""
callee = stmt.value.callee
args = [self.generate_expr(arg) for arg in stmt.value.arguments]
method = callee.member if isinstance(callee, MemberAccess) else None
if method in ("map", "filter"):
args = [self.generate_expr(arg) for arg in stmt.value.arguments if not isinstance(arg, Lambda)]
else:
args = [self.generate_expr(arg) for arg in stmt.value.arguments]
if isinstance(callee.object, ThisExpr) and self.current_class:
method = callee.member
......@@ -201,9 +230,19 @@ class DispatchMixin:
func_name = ARR_METHODS[method]
if method == "push" and len(args) == 1:
self.emit(f'{obj_name}+=("{args[0]}")')
self.emit_var_assign(target, '$__CT_RET')
elif method in ("map", "filter") and len(stmt.value.arguments) >= 1:
first_arg = stmt.value.arguments[0]
if isinstance(first_arg, Lambda):
lambda_name = self.generate_lambda(first_arg)
self.emit(f'{func_name} "{obj_name}" "{lambda_name}"')
else:
self.emit(f'{func_name} "{obj_name}" {args_str}'.replace(' ', ' '))
self.array_vars.add(target)
self._emit_array_assign(target)
else:
self.emit(f'{func_name} "{obj_name}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit_var_assign(target, '$__CT_RET')
self.emit_var_assign(target, '$__CT_RET')
return True
if obj_name in self.dict_vars and method in DICT_METHODS:
......@@ -212,6 +251,12 @@ class DispatchMixin:
self.emit_var_assign(target, '$__CT_RET')
return True
if obj_name in self.file_handle_vars and method in FILE_HANDLE_METHODS:
func_name = FILE_HANDLE_METHODS[method]
self.emit(f'{func_name} "${obj_name}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit_var_assign(target, '$__CT_RET')
return True
if method in STR_METHODS:
obj = self.generate_expr(callee.object)
func_name = STR_METHODS[method]
......@@ -491,6 +536,11 @@ class DispatchMixin:
self.emit(f'__ct_dict_{method} "{var_name}" {args_str} >/dev/null'.strip())
return True
if var_name in self.file_handle_vars and method in FILE_HANDLE_METHODS:
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'{FILE_HANDLE_METHODS[method]} "${var_name}" {args_str}'.strip())
return True
return False
def generate_call_statement(self, expr: CallExpr) -> str:
......
......@@ -482,13 +482,27 @@ class ExprMixin:
for stmt in expr.body.statements:
self.generate_statement(stmt)
else:
result = self.generate_expr(expr.body)
self.emit(f'echo "{result}"')
if self._is_boolean_expr(expr.body):
cond = self.generate_condition(expr.body)
self.emit(f'{cond} && echo "true" || echo "false"')
else:
result = self.generate_expr(expr.body)
self.emit(f'echo "{result}"')
self.indent_level -= 1
self.emit("}")
self.emit()
def _is_boolean_expr(self, expr: Expression) -> bool:
"""Check if expression returns a boolean value."""
if isinstance(expr, BinaryOp):
return expr.operator in ("==", "!=", "<", ">", "<=", ">=", "&&", "||")
if isinstance(expr, UnaryOp):
return expr.operator == "!"
if isinstance(expr, BoolLiteral):
return True
return False
def generate_lvalue(self, expr: Expression) -> str:
"""Generate left-hand side of assignment (without $ prefix)."""
if isinstance(expr, Identifier):
......
......@@ -109,11 +109,21 @@ class Parser:
def parse_decorator_args (self) -> List[tuple]:
args = []
while True:
if self.check (TokenType.IDENTIFIER) and self.peek ().type == TokenType.ASSIGN:
name = self.advance ().value
self.advance ()
value = self.parse_expression ()
args.append ((name, value))
if self.check (TokenType.IDENTIFIER):
next_tok = self.peek ()
if next_tok.type == TokenType.ASSIGN:
name = self.advance ().value
self.advance ()
value = self.parse_expression ()
args.append ((name, value))
elif next_tok.type == TokenType.COLON:
name = self.advance ().value
self.advance ()
value = self.parse_expression ()
args.append ((name, value))
else:
value = self.parse_expression ()
args.append ((None, value))
else:
value = self.parse_expression ()
args.append ((None, value))
......@@ -249,6 +259,8 @@ class Parser:
return self.parse_for ()
if self.check (TokenType.FOREACH):
return self.parse_foreach ()
if self.check (TokenType.WITH):
return self.parse_with ()
if self.check (TokenType.TRY):
return self.parse_try ()
if self.check (TokenType.THROW):
......@@ -372,6 +384,33 @@ class Parser:
return ForeachStmt (variables=variables, iterable=iterable, body=body, location=loc)
def parse_with (self) -> WithStmt:
loc = self.location ()
self.expect (TokenType.WITH)
variables = []
resources = []
variables.append (self.expect (TokenType.IDENTIFIER, "Expected variable name").value)
while self.match (TokenType.COMMA):
variables.append (self.expect (TokenType.IDENTIFIER, "Expected variable name").value)
self.expect (TokenType.IN, "Expected 'in'")
resources.append (self.parse_expression ())
while self.match (TokenType.COMMA):
resources.append (self.parse_expression ())
if len (variables) != len (resources):
self.error ("Number of variables must match number of resources in 'with' statement")
self.skip_newlines ()
body = self.parse_block ()
return WithStmt (variables=variables, resources=resources, body=body, location=loc)
def parse_try (self) -> TryStmt:
loc = self.location ()
self.expect (TokenType.TRY)
......
......@@ -197,6 +197,121 @@ class StdlibMixin:
self.emit ("}")
self.emit ()
self._emit_file_handles()
def _emit_file_handles(self):
"""File handle functions for fs.open() / f.close()."""
self.emit ("# File handle system")
self.emit ("declare -gA __ct_file_handles=()")
self.emit ("declare -g __ct_file_fd=10")
self.emit ()
self.emit ("__ct_fs_open () {")
self.indent_level += 1
self.emit ('local path="$1"')
self.emit ('local mode="${2:-r}"')
self.emit ('local fd=$__ct_file_fd')
self.emit ('__ct_file_fd=$((fd + 1))')
self.emit ('local h="__fh_${fd}"')
self.emit ('local __key_path="${h}_path"')
self.emit ('local __key_fd="${h}_fd"')
self.emit ('local __key_mode="${h}_mode"')
self.emit ('__ct_file_handles[$__key_path]="$path"')
self.emit ('__ct_file_handles[$__key_fd]="$fd"')
self.emit ('__ct_file_handles[$__key_mode]="$mode"')
self.emit ('__CT_RET="$h"')
self.emit ('echo "$h"')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("__ct_fh_read () {")
self.indent_level += 1
self.emit ('local h="$1"')
self.emit ('local __key="${h}_path"')
self.emit ('local path="${__ct_file_handles[$__key]}"')
self.emit ('__CT_RET=$(cat "$path")')
self.emit ('echo "$__CT_RET"')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("__ct_fh_readline () {")
self.indent_level += 1
self.emit ('local h="$1"')
self.emit ('local __key_path="${h}_path"')
self.emit ('local __key_pos="${h}_pos"')
self.emit ('local path="${__ct_file_handles[$__key_path]}"')
self.emit ('local pos="${__ct_file_handles[$__key_pos]:-1}"')
self.emit ('__CT_RET=$(sed -n "${pos}p" "$path")')
self.emit ('__ct_file_handles[$__key_pos]=$((pos + 1))')
self.emit ('echo "$__CT_RET"')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("__ct_fh_write () {")
self.indent_level += 1
self.emit ('local h="$1"')
self.emit ('local data="$2"')
self.emit ('local __key_path="${h}_path"')
self.emit ('local __key_mode="${h}_mode"')
self.emit ('local path="${__ct_file_handles[$__key_path]}"')
self.emit ('local mode="${__ct_file_handles[$__key_mode]}"')
self.emit ('if [[ "$mode" == "a" ]]; then')
self.indent_level += 1
self.emit ('echo -n "$data" >> "$path"')
self.indent_level -= 1
self.emit ('else')
self.indent_level += 1
self.emit ('echo -n "$data" > "$path"')
self.indent_level -= 1
self.emit ('fi')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("__ct_fh_writeln () {")
self.indent_level += 1
self.emit ('local h="$1"')
self.emit ('local data="$2"')
self.emit ('local __key_path="${h}_path"')
self.emit ('local __key_mode="${h}_mode"')
self.emit ('local path="${__ct_file_handles[$__key_path]}"')
self.emit ('local mode="${__ct_file_handles[$__key_mode]}"')
self.emit ('if [[ "$mode" == "a" ]]; then')
self.indent_level += 1
self.emit ('echo "$data" >> "$path"')
self.indent_level -= 1
self.emit ('else')
self.indent_level += 1
self.emit ('echo "$data" > "$path"')
self.indent_level -= 1
self.emit ('fi')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("__ct_fh_close () {")
self.indent_level += 1
self.emit ('local h="$1"')
self.emit ('local __key_path="${h}_path"')
self.emit ('local __key_fd="${h}_fd"')
self.emit ('local __key_mode="${h}_mode"')
self.emit ('local __key_pos="${h}_pos"')
self.emit ('unset "__ct_file_handles[$__key_path]"')
self.emit ('unset "__ct_file_handles[$__key_fd]"')
self.emit ('unset "__ct_file_handles[$__key_mode]"')
self.emit ('unset "__ct_file_handles[$__key_pos]"')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("# Context manager support for file handles")
self.emit ('__ct_fh___enter__ () { echo "$1"; }')
self.emit ('__ct_fh___exit__ () { __ct_fh_close "$1"; }')
self.emit ()
def _emit_json (self):
"""JSON functions: parse, stringify."""
self.emit ("__ct_json_parse () {")
......@@ -328,6 +443,41 @@ class StdlibMixin:
self.emit ("__ct_arr_slice () { local -n __a=$1; local -a __r=(\"${__a[@]:$2:$3}\"); printf '%s\\n' \"${__r[@]}\"; }")
self.emit ()
self.emit ("# Array map/filter with lambda functions")
self.emit ("__ct_arr_map () {")
self.indent_level += 1
self.emit ('local -n __a=$1')
self.emit ('local __fn=$2')
self.emit ('local -a __result=()')
self.emit ('for __item in "${__a[@]}"; do')
self.indent_level += 1
self.emit ('__result+=("$($__fn "$__item")")')
self.indent_level -= 1
self.emit ('done')
self.emit ('__CT_RET=("${__result[@]}")')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("__ct_arr_filter () {")
self.indent_level += 1
self.emit ('local -n __a=$1')
self.emit ('local __fn=$2')
self.emit ('local -a __result=()')
self.emit ('for __item in "${__a[@]}"; do')
self.indent_level += 1
self.emit ('if [[ "$($__fn "$__item")" == "true" ]]; then')
self.indent_level += 1
self.emit ('__result+=("$__item")')
self.indent_level -= 1
self.emit ('fi')
self.indent_level -= 1
self.emit ('done')
self.emit ('__CT_RET=("${__result[@]}")')
self.indent_level -= 1
self.emit ("}")
self.emit ()
self.emit ("# Fast array functions (no echo - use __CT_RET directly)")
self.emit ("__ct_arr_len_fast () { local -n __a=$1; __CT_RET=${#__a[@]}; }")
self.emit ("__ct_arr_get_fast () { local -n __a=$1; __CT_RET=\"${__a[$2]}\"; }")
......
......@@ -26,6 +26,8 @@ class StmtMixin:
self.generate_for(stmt)
elif isinstance(stmt, ForeachStmt):
self.generate_foreach(stmt)
elif isinstance(stmt, WithStmt):
self.generate_with(stmt)
elif isinstance(stmt, TryStmt):
self.generate_try(stmt)
elif isinstance(stmt, ThrowStmt):
......@@ -300,6 +302,33 @@ class StmtMixin:
self.indent_level -= 1
self.emit("done")
def generate_with(self, stmt: WithStmt):
self.emit("# with statement")
local_kw = "local " if self.in_function else ""
for i, (var, resource) in enumerate(zip(stmt.variables, stmt.resources)):
if isinstance(resource, CallExpr) and isinstance(resource.callee, MemberAccess):
if resource.callee.member == "open":
self.file_handle_vars.add(var)
args = [self.generate_expr(a) for a in resource.arguments]
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'__ct_fs_open {args_str} >/dev/null')
self.emit(f'{local_kw}__ct_with_{i}="$__CT_RET"')
self.emit(f'{local_kw}{var}="$__ct_with_{i}"')
continue
res_expr = self.generate_call_statement(resource) if isinstance(resource, CallExpr) else self.generate_expr(resource)
self.emit(f'{local_kw}__ct_with_{i}=$({res_expr})')
self.emit(f'{local_kw}{var}="$__ct_with_{i}"')
for s in stmt.body.statements:
self.generate_statement(s)
self.emit("# with cleanup")
for i in range(len(stmt.variables) - 1, -1, -1):
self.emit(f'__ct_fh___exit__ "$__ct_with_{i}"')
def generate_try(self, stmt: TryStmt):
self.emit("# try/except block")
self.emit("set +e")
......
......@@ -35,6 +35,7 @@ class TokenType (Enum):
DEFER = auto ()
RANGE = auto ()
WHEN = auto ()
WITH = auto ()
NEW = auto ()
PLUS = auto ()
......@@ -100,6 +101,7 @@ KEYWORDS = {
'defer': TokenType.DEFER,
'range': TokenType.RANGE,
'when': TokenType.WHEN,
'with': TokenType.WITH,
'new': TokenType.NEW,
'true': TokenType.TRUE,
'false': TokenType.FALSE,
......
......@@ -303,6 +303,67 @@ if s.contains ("world") {
assert "yes" in stdout
class TestMethodChaining:
def test_function_result_chaining(self):
code, stdout, _ = run_ct('''
func getValue () {
return "hello"
}
result = getValue ().upper ()
print (result)
''')
assert code == 0
assert "HELLO" in stdout
def test_class_method_chaining(self):
code, stdout, _ = run_ct('''
class Data {
construct () {}
func getText () {
return "hello world"
}
}
d = new Data ()
result = d.getText ().upper ()
print (result)
''')
assert code == 0
assert "HELLO WORLD" in stdout
def test_triple_chaining(self):
code, stdout, _ = run_ct('''
class Data {
construct () {}
func getText () {
return " hello "
}
}
d = new Data ()
result = d.getText ().trim ().upper ()
print (result)
''')
assert code == 0
assert "HELLO" in stdout
def test_dict_method_chaining(self):
code, stdout, _ = run_ct('''
d = {"name": "john"}
result = d.get ("name").upper ()
print (result)
''')
assert code == 0
assert "JOHN" in stdout
def test_array_method_chaining(self):
code, stdout, _ = run_ct('''
arr = ["hello", "world"]
result = arr.get (0).upper ()
print (result)
''')
assert code == 0
assert "HELLO" in stdout
class TestExceptionHandling:
def test_try_except(self):
code, stdout, _ = run_ct('''
......@@ -517,3 +578,231 @@ print ("hello")
assert code == 0
assert "DCE: skipped @test" in stdout
assert "test_skipped" not in stdout or "skipped" in stdout
class TestArrayMapFilter:
def test_array_map_simple(self):
code, stdout, _ = run_ct('''
arr = [1, 2, 3]
result = arr.map(x => x * 2)
print(result.join(" "))
''')
assert code == 0
assert "2 4 6" in stdout
def test_array_map_square(self):
code, stdout, _ = run_ct('''
arr = [1, 2, 3, 4]
squared = arr.map(x => x * x)
print(squared.join(" "))
''')
assert code == 0
assert "1 4 9 16" in stdout
def test_array_filter_even(self):
code, stdout, _ = run_ct('''
arr = [1, 2, 3, 4, 5, 6]
evens = arr.filter(x => x % 2 == 0)
print(evens.join(" "))
''')
assert code == 0
assert "2 4 6" in stdout
def test_array_filter_greater_than(self):
code, stdout, _ = run_ct('''
arr = [5, 10, 15, 20]
big = arr.filter(x => x > 10)
print(big.join(" "))
''')
assert code == 0
assert "15 20" in stdout
class TestValidateDecorator:
def test_validate_int_positive(self):
code, stdout, stderr = run_ct('''
@validate(x: "int > 0")
func process(x) {
print("ok: {x}")
}
process(5)
''')
assert code == 0
assert "ok: 5" in stdout
def test_validate_int_positive_fails(self):
code, stdout, stderr = run_ct('''
@validate(x: "int > 0")
func process(x) {
print("ok: {x}")
}
process(-1)
''')
assert code != 0 or "Validation error" in stderr
def test_validate_int_not_integer(self):
code, stdout, stderr = run_ct('''
@validate(x: "int")
func process(x) {
print("ok: {x}")
}
process("hello")
''')
assert code != 0 or "must be integer" in stderr
def test_validate_multiple_params(self):
code, stdout, _ = run_ct('''
@validate(x: "int > 0", y: "int >= 0")
func add(x, y) {
print(x + y)
}
add(5, 10)
''')
assert code == 0
assert "15" in stdout
class TestFileHandles:
def test_fs_open_write_close(self):
code, stdout, _ = run_ct('''
path = "/tmp/ct_test_fh.txt"
f = fs.open(path, "w")
f.write("hello world")
f.close()
content = fs.read(path)
print(content)
fs.remove(path)
''')
assert code == 0
assert "hello world" in stdout
def test_fs_open_read_close(self):
code, stdout, _ = run_ct('''
path = "/tmp/ct_test_fh2.txt"
fs.write(path, "test content")
f = fs.open(path, "r")
data = f.read()
f.close()
print(data)
fs.remove(path)
''')
assert code == 0
assert "test content" in stdout
class TestWithStatement:
def test_with_single_resource(self):
code, stdout, _ = run_ct('''
path = "/tmp/ct_with_test.txt"
fs.write(path, "test content")
with f in fs.open(path) {
data = f.read()
print(data)
}
fs.remove(path)
''')
assert code == 0
assert "test content" in stdout
def test_with_auto_close(self):
code, bash_output, _ = compile_ct('''
with f in fs.open("/tmp/test.txt") {
data = f.read()
}
''')
assert code == 0
assert "__ct_fh___exit__" in bash_output
def test_with_multiple_resources(self):
code, stdout, _ = run_ct('''
src = "/tmp/ct_src.txt"
dst = "/tmp/ct_dst.txt"
fs.write(src, "copy me")
with input, output in fs.open(src), fs.open(dst, "w") {
data = input.read()
output.write(data)
}
result = fs.read(dst)
print(result)
fs.remove(src)
fs.remove(dst)
''')
assert code == 0
assert "copy me" in stdout
class TestAwkMapFilter:
def test_awk_map(self):
code, stdout, _ = run_ct('''
@awk
func testMap(text) {
arr = []
n = text.split(" ")
for i in range(1, n + 1) {
arr.push(__split_arr[i])
}
result = arr.map(x => x * 2)
out = ""
__ct_len = length(result)
for i in range(1, __ct_len + 1) {
if (out != "") {
out = out .. " "
}
out = out .. result[i]
}
return out
}
print(testMap("1 2 3"))
''')
assert code == 0
assert "2 4 6" in stdout
def test_awk_filter(self):
code, stdout, _ = run_ct('''
@awk
func testFilter(text) {
arr = []
n = text.split(" ")
for i in range(1, n + 1) {
arr.push(__split_arr[i])
}
result = arr.filter(x => x > 2)
out = ""
__ct_len = length(result)
for i in range(1, __ct_len + 1) {
if (out != "") {
out = out .. " "
}
out = out .. result[i]
}
return out
}
print(testFilter("1 2 3 4 5"))
''')
assert code == 0
assert "3 4 5" in stdout
def test_awk_validate(self):
code, stdout, stderr = run_ct('''
@validate(x: "int > 0")
@awk
func double(x) {
return x * 2
}
print(double(5))
''')
assert code == 0
assert "10" in stdout
def test_awk_validate_fails(self):
code, stdout, stderr = run_ct('''
@validate(x: "int > 0")
@awk
func double(x) {
return x * 2
}
print(double(-1))
''')
assert code != 0 or "must be" in stderr
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