Commit ec4a018f authored by Roman Alifanov's avatar Roman Alifanov

Add compile-time method validation

parent 2aab914d
......@@ -907,7 +907,35 @@ Error: Missing closing brace
--> main.ct:15:1
```
Показывает все ошибки сразу.
### Проверка методов
Компилятор проверяет существование методов при компиляции:
```
Error: Unknown method 'nonexistent' for type 'array'. Available: filter, get, join, len, map, pop, push, set, shift, slice
--> main.ct:5:1
Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list, mkdir, open, read, remove, write
--> main.ct:10:8
```
Проверяются методы для:
- **Массивов**`filter`, `get`, `join`, `len`, `map`, `pop`, `push`, `set`, `shift`, `slice`
- **Словарей**`del`, `get`, `has`, `keys`, `set`
- **Строк**`charAt`, `contains`, `ends`, `index`, `len`, `lower`, `replace`, `split`, `starts`, `substr`, `trim`, `upper`
- **Файловых дескрипторов**`close`, `read`, `readline`, `write`, `writeln`
- **Stdlib namespaces**:
- `fs``append`, `exists`, `list`, `mkdir`, `open`, `read`, `remove`, `write`
- `http``delete`, `get`, `post`, `put`
- `json``parse`, `stringify`
- `logger``debug`, `error`, `info`, `warn`
- `regex``extract`, `match`
- `args``count`, `get`
- `shell``capture`, `exec`, `source`
- `time``ms`, `now`
- `math``abs`, `add`, `div`, `max`, `min`, `mod`, `mul`, `sub`
Показывает все ошибки сразу (без дубликатов).
---
......
......@@ -17,6 +17,7 @@
- **Error handling**`try/except/finally/throw/defer`
- **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
- **Optimized output** - no unnecessary subshells, inlined methods
## Installation
......
......@@ -17,6 +17,7 @@
- **Обработка ошибок**`try/except/finally/throw/defer`
- **Строковая интерполяция**`"Привет, {name}!"`
- **@awk функции** — компиляция в AWK для ускорения ~300x на строковых/числовых операциях
- **Проверка методов при компиляции** — выявление несуществующих методов до запуска
- **Оптимизированный вывод** — без лишних subshell, инлайнинг методов
## Установка
......
......@@ -30,10 +30,68 @@ FILE_HANDLE_METHODS = {
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"}
FS_METHODS = {"read", "write", "append", "exists", "remove", "mkdir", "list", "open"}
HTTP_METHODS = {"get", "post", "put", "delete"}
JSON_METHODS = {"parse", "stringify"}
LOGGER_METHODS = {"info", "warn", "error", "debug"}
REGEX_METHODS = {"match", "extract"}
ARGS_METHODS = {"count", "get"}
SHELL_METHODS = {"exec", "capture", "source"}
TIME_METHODS = {"now", "ms"}
MATH_METHODS = {"add", "sub", "mul", "div", "mod", "min", "max", "abs"}
NAMESPACE_METHODS = {
"fs": FS_METHODS,
"http": HTTP_METHODS,
"json": JSON_METHODS,
"logger": LOGGER_METHODS,
"regex": REGEX_METHODS,
"args": ARGS_METHODS,
"shell": SHELL_METHODS,
"time": TIME_METHODS,
"math": MATH_METHODS,
}
class DispatchMixin:
"""Mixin for method dispatch and assignment."""
def _report_unknown_method(self, obj_type: str, method: str, available: set, location=None):
"""Report unknown method error."""
from .errors import CompileError
available_str = ", ".join(sorted(available))
msg = f"Unknown method '{method}' for type '{obj_type}'. Available: {available_str}"
filename = location.filename if location else "<unknown>"
line = location.line if location else 0
col = location.column if location else 0
self.errors.add(CompileError(msg, filename, line, col))
def _validate_namespace_method(self, namespace: str, method: str, location=None) -> bool:
"""Validate that method exists for namespace. Returns True if valid."""
if namespace not in NAMESPACE_METHODS:
return True
methods = NAMESPACE_METHODS[namespace]
if method not in methods:
self._report_unknown_method(namespace, method, methods, location)
return False
return True
def _validate_type_method(self, var_type: str, method: str, location=None) -> bool:
"""Validate that method exists for variable type. Returns True if valid."""
type_methods = {
"array": set(ARR_METHODS.keys()),
"dict": set(DICT_METHODS.keys()),
"string": set(STR_METHODS.keys()),
"file_handle": set(FILE_HANDLE_METHODS.keys()),
}
if var_type not in type_methods:
return True
available = type_methods[var_type]
if method not in available:
self._report_unknown_method(var_type, method, available, location)
return False
return True
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:
......@@ -176,6 +234,7 @@ class DispatchMixin:
"""Generate method call assignment. Returns True if handled."""
callee = stmt.value.callee
method = callee.member if isinstance(callee, MemberAccess) else None
location = getattr(stmt.value, 'location', None) or getattr(callee, 'location', None)
if method in ("map", "filter"):
args = [self.generate_expr(arg) for arg in stmt.value.arguments if not isinstance(arg, Lambda)]
......@@ -189,32 +248,34 @@ class DispatchMixin:
self.emit_var_assign(target, '$__CT_RET')
return True
# Handle this.field.method() - field holds an object reference
if isinstance(callee.object, MemberAccess) and isinstance(callee.object.object, ThisExpr):
field_name = callee.object.member
method = callee.member
args_str = " ".join([f'"{a}"' for a in args])
# Check field type
field_type = self.class_field_types.get((self.current_class, field_name)) if self.current_class else None
if field_type == "array" and method in ARR_METHODS:
arr_name = f'"${{this}}_{field_name}"'
self.emit(f'{ARR_METHODS[method]} {arr_name} {args_str} >/dev/null'.strip())
self.emit_var_assign(target, '$__CT_RET')
return True
if field_type == "dict" and method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
self.emit(f'{DICT_METHODS[method]} {dict_ref} {args_str} >/dev/null'.strip())
self.emit_var_assign(target, '$__CT_RET')
return True
if field_type == "array":
if method in ARR_METHODS:
arr_name = f'"${{this}}_{field_name}"'
self.emit(f'{ARR_METHODS[method]} {arr_name} {args_str} >/dev/null'.strip())
self.emit_var_assign(target, '$__CT_RET')
return True
else:
self._validate_type_method("array", method, location)
if field_type == "dict":
if method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
self.emit(f'{DICT_METHODS[method]} {dict_ref} {args_str} >/dev/null'.strip())
self.emit_var_assign(target, '$__CT_RET')
return True
else:
self._validate_type_method("dict", method, location)
if field_type == "object":
# Object field - use dynamic dispatch
obj_ref = f'${{__CT_OBJ["$this.{field_name}"]}}'
self.emit(f'__ct_call_method "{obj_ref}" "{method}" {args_str} >/dev/null')
self.emit_var_assign(target, '$__CT_RET')
return True
# Scalar field - fall through to default handling
return False
if isinstance(callee.object, Identifier):
......@@ -222,40 +283,50 @@ class DispatchMixin:
method = callee.member
if obj_name in BUILTIN_NAMESPACES:
self._validate_namespace_method(obj_name, method, location)
return False
args_str = " ".join([f'"{a}"' for a in args])
if obj_name in self.array_vars and method in ARR_METHODS:
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}"')
if obj_name in self.array_vars:
if method in ARR_METHODS:
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}'.replace(' ', ' '))
self.array_vars.add(target)
self._emit_array_assign(target)
self.emit(f'{func_name} "{obj_name}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit_var_assign(target, '$__CT_RET')
return True
else:
self._validate_type_method("array", method, location)
if obj_name in self.dict_vars:
if method in DICT_METHODS:
func_name = DICT_METHODS[method]
self.emit(f'{func_name} "{obj_name}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit_var_assign(target, '$__CT_RET')
return True
if obj_name in self.dict_vars and method in DICT_METHODS:
func_name = DICT_METHODS[method]
self.emit(f'{func_name} "{obj_name}" {args_str} >/dev/null'.replace(' ', ' '))
self.emit_var_assign(target, '$__CT_RET')
return True
return True
else:
self._validate_type_method("dict", method, location)
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 obj_name in self.file_handle_vars:
if 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
else:
self._validate_type_method("file_handle", method, location)
if method in STR_METHODS:
obj = self.generate_expr(callee.object)
......@@ -507,6 +578,7 @@ class DispatchMixin:
var_name = expr.callee.object.name
method = expr.callee.member
args = [self.generate_expr(arg) for arg in expr.arguments]
location = getattr(expr, 'location', None) or getattr(expr.callee, 'location', None)
if var_name in self.array_vars:
if method == "push":
......@@ -519,10 +591,13 @@ class DispatchMixin:
elif method == "set" and len(args) >= 2:
self.emit(f'{var_name}[{args[0]}]="{args[1]}"')
return True
elif method in ("len", "shift", "join", "get", "slice"):
elif method in ("len", "shift", "join", "get", "slice", "map", "filter"):
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'__ct_arr_{method} "{var_name}" {args_str} >/dev/null'.strip())
return True
else:
self._validate_type_method("array", method, location)
return False
if var_name in self.dict_vars:
if method == "set" and len(args) >= 2:
......@@ -535,11 +610,18 @@ class DispatchMixin:
args_str = " ".join([f'"{a}"' for a in args])
self.emit(f'__ct_dict_{method} "{var_name}" {args_str} >/dev/null'.strip())
return True
else:
self._validate_type_method("dict", method, location)
return False
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
if var_name in self.file_handle_vars:
if 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
else:
self._validate_type_method("file_handle", method, location)
return False
return False
......@@ -547,12 +629,13 @@ class DispatchMixin:
callee = expr.callee
args = [self.generate_expr(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
location = getattr(expr, 'location', None) or getattr(callee, 'location', None)
if isinstance(callee, Identifier):
return self._generate_builtin_call(callee.name, args, args_str)
if isinstance(callee, MemberAccess):
return self._generate_member_call(callee, args, args_str)
return self._generate_member_call(callee, args, args_str, location)
return ""
......@@ -585,7 +668,7 @@ class DispatchMixin:
else:
return f'{name} {args_str}'
def _generate_member_call(self, callee: MemberAccess, args: list, args_str: str) -> str:
def _generate_member_call(self, callee: MemberAccess, args: list, args_str: str, location=None) -> str:
"""Generate member access call."""
obj = self.generate_expr(callee.object)
method = callee.member
......@@ -597,37 +680,52 @@ class DispatchMixin:
return f'__ct_class_{self.current_class}_{method} "$this" {args_str}'
if isinstance(callee.object, Identifier):
result = self._generate_stdlib_call(callee.object.name, method, args, args_str)
result = self._generate_stdlib_call(callee.object.name, method, args, args_str, location)
if result:
return result
if isinstance(callee.object, MemberAccess) and isinstance(callee.object.object, ThisExpr):
result = self._generate_this_field_call(callee, args_str)
result = self._generate_this_field_call(callee, args_str, location)
if result:
return result
if isinstance(callee.object, MemberAccess) and isinstance(callee.object.object, Identifier):
result = self._generate_var_field_call(callee, args_str)
result = self._generate_var_field_call(callee, args_str, location)
if result:
return result
var_name = callee.object.name if isinstance(callee.object, Identifier) else None
if var_name and var_name in self.array_vars and method in ARR_METHODS:
if method == "push" and len(args) == 1:
return f'{var_name}+=("{args[0]}")'
return f'{ARR_METHODS[method]} "{var_name}" {args_str}'.strip()
if var_name and var_name in self.array_vars:
if method in ARR_METHODS:
if method == "push" and len(args) == 1:
return f'{var_name}+=("{args[0]}")'
return f'{ARR_METHODS[method]} "{var_name}" {args_str}'.strip()
else:
self._validate_type_method("array", method, location)
if var_name and var_name in self.dict_vars:
if method in DICT_METHODS:
return f'{DICT_METHODS[method]} "{var_name}" {args_str}'.strip()
else:
self._validate_type_method("dict", method, location)
if var_name and var_name in self.dict_vars and method in DICT_METHODS:
return f'{DICT_METHODS[method]} "{var_name}" {args_str}'.strip()
if var_name and var_name in self.file_handle_vars:
if method in FILE_HANDLE_METHODS:
return f'{FILE_HANDLE_METHODS[method]} "${var_name}" {args_str}'.strip()
else:
self._validate_type_method("file_handle", method, location)
if method in STR_METHODS:
return f'{STR_METHODS[method]} "{obj}" {args_str}'.strip()
return f'__ct_call_method "{obj}" "{method}" {args_str}'
def _generate_stdlib_call(self, obj_name: str, method: str, args: list, args_str: str) -> str:
def _generate_stdlib_call(self, obj_name: str, method: str, args: list, args_str: str, location=None) -> str:
"""Generate stdlib call. Returns None if not stdlib."""
if obj_name in BUILTIN_NAMESPACES:
self._validate_namespace_method(obj_name, method, location)
if obj_name == "http":
return f'__ct_http_{method} {args_str}'
elif obj_name == "fs":
......@@ -659,31 +757,34 @@ class DispatchMixin:
return f'__ct_math_{method} {args_str}'
return None
def _generate_this_field_call(self, callee: MemberAccess, args_str: str) -> str:
def _generate_this_field_call(self, callee: MemberAccess, args_str: str, location=None) -> str:
"""Generate this.field.method() call."""
field_name = callee.object.member
method = callee.member
# Check field type if known
field_type = self.class_field_types.get((self.current_class, field_name)) if self.current_class else None
if field_type == "array" and method in ARR_METHODS:
arr_name = f'"${{this}}_{field_name}"'
return f'{ARR_METHODS[method]} {arr_name} {args_str}'.strip()
if field_type == "array":
if method in ARR_METHODS:
arr_name = f'"${{this}}_{field_name}"'
return f'{ARR_METHODS[method]} {arr_name} {args_str}'.strip()
else:
self._validate_type_method("array", method, location)
if field_type == "dict" and method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
return f'{DICT_METHODS[method]} {dict_ref} {args_str}'.strip()
if field_type == "dict":
if method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
return f'{DICT_METHODS[method]} {dict_ref} {args_str}'.strip()
else:
self._validate_type_method("dict", method, location)
if field_type == "object":
# Object field - use dynamic dispatch
obj_ref = f'${{__CT_OBJ["$this.{field_name}"]}}'
return f'__ct_call_method "{obj_ref}" "{method}" {args_str}'
# Scalar/unknown field - return None to fall through to default handling
return None
def _generate_var_field_call(self, callee: MemberAccess, args_str: str) -> str:
def _generate_var_field_call(self, callee: MemberAccess, args_str: str, location=None) -> str:
"""Generate var.field.method() call."""
var_name = callee.object.object.name
field_name = callee.object.member
......@@ -702,16 +803,20 @@ class DispatchMixin:
for cls in self.classes
)
if is_array_field and method in ARR_METHODS:
return f'{ARR_METHODS[method]} "${{{var_name}}}_{field_name}" {args_str}'.strip()
if is_array_field:
if method in ARR_METHODS:
return f'{ARR_METHODS[method]} "${{{var_name}}}_{field_name}" {args_str}'.strip()
else:
self._validate_type_method("array", method, location)
if is_dict_field and method in DICT_METHODS:
return f'{DICT_METHODS[method]} "${{{var_name}}}_{field_name}" {args_str}'.strip()
if is_dict_field:
if method in DICT_METHODS:
return f'{DICT_METHODS[method]} "${{{var_name}}}_{field_name}" {args_str}'.strip()
else:
self._validate_type_method("dict", method, location)
if is_object_field:
# Object field - use dynamic dispatch
obj_ref = f'${{__CT_OBJ["${{{var_name}}}.{field_name}"]}}'
return f'__ct_call_method "{obj_ref}" "{method}" {args_str}'
# Scalar/unknown field - return None to fall through to default handling
return None
......@@ -20,18 +20,28 @@ class CompileError:
class ErrorCollector:
def __init__ (self):
self.errors: List[CompileError] = []
self._seen: set = set()
def _error_key(self, msg: str, filename: str, line: int, column: int) -> tuple:
return (msg, filename, line, column)
def add (self, error: CompileError):
self.errors.append (error)
key = self._error_key(error.message, error.filename, error.line, error.column)
if key not in self._seen:
self._seen.add(key)
self.errors.append(error)
def add_error (self, message: str, filename: str, line: int, column: int, hint: str = None):
self.errors.append (CompileError (
message=message,
filename=filename,
line=line,
column=column,
hint=hint
))
key = self._error_key(message, filename, line, column)
if key not in self._seen:
self._seen.add(key)
self.errors.append (CompileError (
message=message,
filename=filename,
line=line,
column=column,
hint=hint
))
def has_errors (self) -> bool:
return len (self.errors) > 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