Commit 5121e024 authored by Roman Alifanov's avatar Roman Alifanov

Add optional static typing with TypeScript-style syntax

- Add TypeAnnotation AST node for type annotations - Add ClassField dataclass for typed class fields - Extend Parameter, FunctionDecl, Assignment with type_annotation field - Add type parsing: parse_type(), parse_type_annotation(), parse_function_type() - Support types: string, int, float, bool, any, void, T[], dict[K,V], classes, (A) => B - Add type checking at compile time with configurable error handling - Add --no-type-check and --warn-types CLI flags - Fix callback variable calls (prioritize callback_vars, use $() capture) - Document closure limitation (lambdas can't capture outer variables) - Add 28 new tests for typing and callbacks (279 total)
parent 15296652
......@@ -26,6 +26,135 @@ data = nil
Экранирование фигурных скобок в строках: `"\{"`
### Статическая типизация
Опциональная система типов в стиле TypeScript. Типы указываются после двоеточия:
```
# Базовые типы
name: string = "Alice"
count: int = 42
score: float = 95.5
active: bool = true
# Массивы
items: int[] = []
names: string[] = ["Alice", "Bob"]
# Словари
config: dict[string, int] = {}
settings: dict[string, string] = {"host": "localhost"}
# any — отключение проверки типов для переменной
data: any = getSomeData ()
```
**Типизированные функции:**
```
# Параметры и возвращаемый тип
func greet (name: string, age: int): string {
return "Hello, {name}! Age: {age}"
}
# Только параметры
func process (data: string, count: int) {
print (data)
}
# Типы функций (для колбеков)
func apply (callback: (int) => int, value: int): int {
return callback (value)
}
```
**Типизированные классы:**
```
class User {
name: string = ""
age: int = 0
scores: float[] = []
construct (name: string, age: int) {
this.name = name
this.age = age
}
func greet (): string {
return "Hello, {this.name}!"
}
}
```
**Типы функций:**
```
# Простой колбек
mapper: (int) => int = x => x * 2
# Несколько аргументов
combiner: (string, string) => string = (a, b) => a .. b
# Колбек в функции
func forEach (items: int[], callback: (int) => void) {
foreach item in items {
callback (item)
}
}
```
**Поддерживаемые типы:**
| Тип | Описание | Пример |
|-----|----------|--------|
| `string` | Строка | `"hello"` |
| `int` | Целое число | `42` |
| `float` | Число с плавающей точкой | `3.14` |
| `bool` | Логическое значение | `true`, `false` |
| `any` | Любой тип (отключает проверку) | — |
| `void` | Отсутствие возвращаемого значения | — |
| `T[]` | Массив типа T | `int[]`, `string[]` |
| `dict[K, V]` | Словарь с ключами K и значениями V | `dict[string, int]` |
| `ClassName` | Экземпляр класса | `User`, `Logger` |
| `(A, B) => C` | Функция с аргументами A, B и возвратом C | `(int) => int` |
**Проверка типов при компиляции:**
```
# Ошибка: несовместимые типы
x: int = "hello" # Error: Type mismatch: expected 'int', got 'string'
# Ошибка: неправильный аргумент
func double (x: int): int { return x * 2 }
double ("hello") # Error: Type mismatch for parameter 'x'
```
**CLI-флаги:**
```bash
# Отключить проверку типов полностью
python3 content build main.ct --no-type-check
# Ошибки типов как предупреждения (не останавливают компиляцию)
python3 content build main.ct --warn-types
```
**Обратная совместимость:**
Типы полностью опциональны. Код без аннотаций работает через автоматическую инференцию типов:
```
# Без типов — инференция из значения
name = "Alice" # inferred: string
count = 42 # inferred: int
items = [1, 2, 3] # inferred: array
# Смешанный код
typed_name: string = "Bob"
untyped_count = 100 # инференция
```
### Строковая интерполяция
```
......@@ -450,6 +579,41 @@ square = x => x * x
result = p.process (square, 5) # 25
```
### Ограничение: замыкания не поддерживаются
Лямбды не могут захватывать переменные из внешней функции — это ограничение bash, где вложенные функции не имеют доступа к локальным переменным родительской функции после её завершения.
**Не работает:**
```
func makeAdder (n) {
return x => x + n # n недоступна после выхода из makeAdder
}
add5 = makeAdder (5)
result = add5 (10) # Ошибка: n не определена
```
**Работает:**
```
# Лямбда использует только свои аргументы
doubler = x => x * 2
result = doubler (5) # 10
# Передача лямбды в функцию
func apply (f, x) {
return f (x)
}
result = apply (x => x * 3, 4) # 12
# map/filter с лямбдами
nums = [1, 2, 3]
squared = nums.map (x => x * x) # [1, 4, 9]
```
**Правило:** лямбда может использовать только свои параметры, не переменные из внешней функции.
### Циклы
```
......@@ -970,6 +1134,13 @@ content test lib.ct main.ct # запустить тесты из неско
content build main.ct --lint # запускает ShellCheck
```
### type checking
```bash
content build main.ct --no-type-check # отключить проверку типов
content build main.ct --warn-types # ошибки типов как предупреждения
```
---
## Пользовательские библиотеки
......@@ -1020,6 +1191,18 @@ Error: Missing closing brace
--> main.ct:15:1
```
### Ошибки типов
```
Error: Type mismatch: expected 'int', got 'string'
--> main.ct:3:5
Error: Type mismatch for parameter 'name': expected 'string', got 'int'
--> main.ct:10:1
```
Ошибки типов можно отключить флагом `--no-type-check` или превратить в предупреждения флагом `--warn-types`.
### Проверка методов
Компилятор проверяет существование методов при компиляции:
......
......@@ -7,6 +7,7 @@
## Features
- **Clean syntax** — Python-like readability with Go/Vala influences
- **Optional static typing** — TypeScript-style type annotations: `name: string`, `func greet(x: int): string`
- **Classes & inheritance** — OOP with constructors and method calls
- **Lambdas**`x => x * 2`, `(a, b) => a + b`, multiline blocks
- **Decorators**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`, user decorators `@obj.method()`
......@@ -53,6 +54,36 @@ count = 42
message = "Hello, {name}!" # interpolation
```
### Static Typing (Optional)
TypeScript-style type annotations for variables, parameters, and return types:
```
# Typed variables
name: string = "Alice"
count: int = 0
items: int[] = []
config: dict[string, int] = {}
# Typed functions
func greet (name: string, age: int): string {
return "Hello, {name}!"
}
# Typed class fields
class User {
name: string = ""
age: int = 0
}
# Function types (for callbacks)
mapper: (int) => int = x => x * 2
```
**Supported types:** `string`, `int`, `float`, `bool`, `any`, `void`, `T[]`, `dict[K, V]`, classes, `(A, B) => C`
Types are optional — untyped code works through inference.
### Functions
```
......@@ -160,6 +191,8 @@ result = apply (double, 5) # 10
result = apply (x => x + 10, 3) # 13
```
**Note:** Closures are not supported — lambdas cannot capture variables from outer functions (bash limitation).
### Functional Arrays
```
......@@ -265,6 +298,10 @@ content --build-lib MyLib.ct # -> MyLib.sh
# Lint
content build main.ct --lint # run ShellCheck
# Type checking
content build main.ct --no-type-check # disable type checking
content build main.ct --warn-types # type errors as warnings
```
## @awk — High-Performance Functions
......
......@@ -7,6 +7,7 @@
## Возможности
- **Чистый синтаксис** — читаемость Python с влиянием Go/Vala
- **Опциональная статическая типизация** — аннотации типов в стиле TypeScript: `name: string`, `func greet(x: int): string`
- **Классы и наследование** — ООП с конструкторами и вызовами методов
- **Лямбды**`x => x * 2`, `(a, b) => a + b`, многострочные блоки
- **Декораторы**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`, пользовательские `@obj.method()`
......@@ -53,6 +54,36 @@ count = 42
message = "Привет, {name}!" # интерполяция
```
### Статическая типизация (опционально)
Аннотации типов в стиле TypeScript для переменных, параметров и возвращаемых значений:
```
# Типизированные переменные
name: string = "Alice"
count: int = 0
items: int[] = []
config: dict[string, int] = {}
# Типизированные функции
func greet (name: string, age: int): string {
return "Привет, {name}!"
}
# Типизированные поля классов
class User {
name: string = ""
age: int = 0
}
# Типы функций (для колбеков)
mapper: (int) => int = x => x * 2
```
**Поддерживаемые типы:** `string`, `int`, `float`, `bool`, `any`, `void`, `T[]`, `dict[K, V]`, классы, `(A, B) => C`
Типы опциональны — код без типов работает через инференцию.
### Функции
```
......@@ -160,6 +191,8 @@ result = apply (double, 5) # 10
result = apply (x => x + 10, 3) # 13
```
**Примечание:** Замыкания не поддерживаются — лямбды не могут захватывать переменные из внешних функций (ограничение bash).
### Функциональные массивы
```
......@@ -265,6 +298,10 @@ content --build-lib MyLib.ct # -> MyLib.sh
# Линтинг
content build main.ct --lint # запуск ShellCheck
# Проверка типов
content build main.ct --no-type-check # отключить проверку типов
content build main.ct --warn-types # ошибки типов как предупреждения
```
## @awk — Высокопроизводительные функции
......
......@@ -14,6 +14,18 @@ class ASTNode:
pass
@dataclass
class TypeAnnotation (ASTNode):
name: str = ""
is_array: bool = False
element_type: Optional['TypeAnnotation'] = None
key_type: Optional['TypeAnnotation'] = None
value_type: Optional['TypeAnnotation'] = None
param_types: List['TypeAnnotation'] = field (default_factory=list)
return_type: Optional['TypeAnnotation'] = None
location: Optional[SourceLocation] = None
@dataclass
class Expression (ASTNode):
......@@ -150,6 +162,7 @@ class ExpressionStmt (Statement):
@dataclass
class Assignment (Statement):
target: Optional[Expression] = None
type_annotation: Optional[TypeAnnotation] = None
operator: str = "="
value: Optional[Expression] = None
location: Optional[SourceLocation] = None
......@@ -271,6 +284,7 @@ class Declaration (ASTNode):
@dataclass
class Parameter:
name: str = ""
type_annotation: Optional[TypeAnnotation] = None
default: Optional[Expression] = None
is_variadic: bool = False
......@@ -287,16 +301,25 @@ class Decorator:
class FunctionDecl (Declaration):
name: str = ""
params: List[Parameter] = field (default_factory=list)
return_type: Optional[TypeAnnotation] = None
body: Optional[Block] = None
decorators: List[Decorator] = field (default_factory=list)
location: Optional[SourceLocation] = None
@dataclass
class ClassField:
name: str = ""
type_annotation: Optional[TypeAnnotation] = None
default: Optional[Expression] = None
location: Optional[SourceLocation] = None
@dataclass
class ClassDecl (Declaration):
name: str = ""
parent: Optional[str] = None
fields: List[tuple] = field (default_factory=list)
fields: List[ClassField] = field (default_factory=list)
constructor: Optional['ConstructorDecl'] = None
methods: List[FunctionDecl] = field (default_factory=list)
location: Optional[SourceLocation] = None
......
from .ast_nodes import (
ClassDecl, FunctionDecl, ArrayLiteral, DictLiteral, NilLiteral, NewExpr,
ClassDecl, ClassField, FunctionDecl, ArrayLiteral, DictLiteral, NilLiteral, NewExpr,
CallExpr, Identifier, Assignment, MemberAccess, ThisExpr, ReturnStmt,
ConstructorDecl, Parameter, Block, ForeachStmt, IfStmt, WhileStmt, ForStmt,
ExpressionStmt, BinaryOp, IndexAccess
ExpressionStmt, BinaryOp, IndexAccess, TypeAnnotation
)
from .methods import ARRAY_METHODS, DICT_METHODS
......@@ -19,23 +19,37 @@ ALL_KNOWN_METHODS = ARRAY_METHODS_ALL | DICT_METHODS_ALL | STRING_METHODS_ALL
class ClassMixin:
"""Mixin for class and method generation."""
def _get_field_info(self, field):
if isinstance(field, ClassField):
return field.name, field.type_annotation, field.default
else:
return field[0], None, field[1] if len(field) > 1 else None
def generate_class(self, cls: ClassDecl):
self.current_class = cls.name
self.current_class_fields = set(field_name for field_name, _ in cls.fields)
self.current_class_fields = set(self._get_field_info(f)[0] for f in cls.fields)
for field in cls.fields:
field_name, type_annotation, default_value = self._get_field_info(field)
for field_name, default_value in cls.fields:
if isinstance(default_value, ArrayLiteral):
if type_annotation:
if type_annotation.name == "array" or type_annotation.is_array:
self.class_field_types[(cls.name, field_name)] = "array"
elif type_annotation.name == "dict":
self.class_field_types[(cls.name, field_name)] = "dict"
elif type_annotation.name in self.classes:
self.class_field_types[(cls.name, field_name)] = "object"
else:
self.class_field_types[(cls.name, field_name)] = "scalar"
elif isinstance(default_value, ArrayLiteral):
self.class_field_types[(cls.name, field_name)] = "array"
elif isinstance(default_value, DictLiteral):
self.class_field_types[(cls.name, field_name)] = "dict"
elif isinstance(default_value, NilLiteral):
# nil typically means object reference will be assigned later
self.class_field_types[(cls.name, field_name)] = "object"
elif isinstance(default_value, NewExpr):
# new SomeClass() is an object
self.class_field_types[(cls.name, field_name)] = "object"
elif isinstance(default_value, CallExpr) and isinstance(default_value.callee, Identifier):
# SomeClass() constructor call is an object
callee_name = default_value.callee.name
if callee_name in self.classes:
self.class_field_types[(cls.name, field_name)] = "object"
......@@ -94,7 +108,8 @@ class ClassMixin:
self.emit('local __ct_this_instance="__ct_inst_$RANDOM$RANDOM"')
self.emit('__ct_obj_class["$__ct_this_instance"]="{}"'.format(cls.name))
for field_name, default_value in cls.fields:
for field in cls.fields:
field_name, type_annotation, default_value = self._get_field_info(field)
if isinstance(default_value, ArrayLiteral):
elements = [self.generate_expr(e) for e in default_value.elements]
if elements:
......@@ -316,7 +331,7 @@ class ClassMixin:
if isinstance(value, MemberAccess) and isinstance(value.object, ThisExpr):
field = value.member
field_names = [f for f, _ in cls.fields]
field_names = [self._get_field_info(f)[0] for f in cls.fields]
if field in field_names:
self.inlineable_methods[(cls.name, method.name)] = f'${{__CT_OBJ["$this.{field}"]}}'
return
......@@ -333,9 +348,22 @@ class ClassMixin:
def _analyze_param_types(self, func: FunctionDecl) -> dict:
"""Analyze function body to determine parameter types (array/dict/scalar)."""
param_names = {p.name for p in func.params}
param_types = {p.name: "scalar" for p in func.params}
param_types = {}
param_methods = {p.name: set() for p in func.params}
for p in func.params:
if p.type_annotation:
if p.type_annotation.name == "array" or p.type_annotation.is_array:
param_types[p.name] = "array"
elif p.type_annotation.name == "dict":
param_types[p.name] = "dict"
elif p.type_annotation.name in self.classes:
param_types[p.name] = "object"
else:
param_types[p.name] = "scalar"
else:
param_types[p.name] = "scalar"
def analyze_expr(expr):
if isinstance(expr, CallExpr) and isinstance(expr.callee, MemberAccess):
if isinstance(expr.callee.object, Identifier):
......@@ -397,7 +425,11 @@ class ClassMixin:
for stmt in func.body.statements:
analyze_stmt(stmt)
explicit_typed = {p.name for p in func.params if p.type_annotation}
for param_name, methods in param_methods.items():
if param_name in explicit_typed:
continue
unknown_methods = methods - ALL_KNOWN_METHODS
if unknown_methods:
param_types[param_name] = "object"
......
......@@ -28,11 +28,14 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
- CseMixin: common subexpression elimination
"""
def __init__(self):
def __init__(self, type_check: bool = True, warn_types: bool = False):
self.output: List[str] = []
self.indent_level = 0
self.errors = ErrorCollector()
self.type_check = type_check
self.warn_types = warn_types
self.current_class: Optional[str] = None
self.current_class_fields: Set[str] = set()
self.in_class_method = False
......
......@@ -68,8 +68,14 @@ class UsageAnalyzer:
return self.used
def _collect_class_fields(self, cls: ClassDecl):
from .ast_nodes import ClassField
self.class_fields[cls.name] = {}
for field_name, default_value in cls.fields:
for field in cls.fields:
if isinstance(field, ClassField):
field_name = field.name
default_value = field.default
else:
field_name, default_value = field
field_class = None
if default_value:
if isinstance(default_value, NewExpr):
......
from .ast_nodes import (
CallExpr, MemberAccess, Identifier, ThisExpr, Assignment, ArrayLiteral,
DictLiteral, NewExpr, Lambda, ExpressionStmt, BaseCall, ReturnStmt,
StringLiteral, BinaryOp, IndexAccess
StringLiteral, BinaryOp, IndexAccess, TypeAnnotation, IntegerLiteral,
FloatLiteral, BoolLiteral
)
from .methods import (
STRING_METHODS, ARRAY_METHODS, DICT_METHODS, FILE_HANDLE_METHODS,
......@@ -100,6 +101,65 @@ class DispatchMixin:
elif operator == "/=":
self.emit(f'{target}=$((${{target}} / {value}))'.replace('target', target))
def _infer_expr_type(self, expr) -> str:
if isinstance(expr, IntegerLiteral):
return "int"
elif isinstance(expr, FloatLiteral):
return "float"
elif isinstance(expr, StringLiteral):
return "string"
elif isinstance(expr, BoolLiteral):
return "bool"
elif isinstance(expr, ArrayLiteral):
return "array"
elif isinstance(expr, DictLiteral):
return "dict"
elif isinstance(expr, NewExpr):
return expr.class_name
elif isinstance(expr, Lambda):
return "func"
elif isinstance(expr, Identifier):
name = expr.name
if name in self.array_vars:
return "array"
elif name in self.dict_vars:
return "dict"
elif name in self.instance_vars:
return self.instance_vars[name]
elif name in self.callback_vars:
return "func"
return "any"
def _check_type_compatibility(self, expected: TypeAnnotation, actual_type: str, location) -> bool:
expected_name = expected.name
if expected.is_array:
expected_name = "array"
if expected_name == "any" or actual_type == "any":
return True
if expected_name == actual_type:
return True
if expected_name in ("int", "float") and actual_type in ("int", "float"):
return True
if expected_name in self.classes and actual_type in self.classes:
return True
return False
def _apply_type_annotation(self, target: str, type_annotation: TypeAnnotation):
if type_annotation.name == "array" or type_annotation.is_array:
self.array_vars.add(target)
elif type_annotation.name == "dict":
self.dict_vars.add(target)
elif type_annotation.name in self.classes:
self.object_vars.add(target)
self.instance_vars[target] = type_annotation.name
elif type_annotation.name == "func":
self.callback_vars.add(target)
def generate_assignment(self, stmt: Assignment):
if isinstance(stmt.target, MemberAccess):
if isinstance(stmt.target.object, ThisExpr):
......@@ -108,6 +168,28 @@ class DispatchMixin:
target = self.generate_lvalue(stmt.target)
if stmt.type_annotation:
self._apply_type_annotation(target, stmt.type_annotation)
if self.type_check and stmt.value:
actual_type = self._infer_expr_type(stmt.value)
if not self._check_type_compatibility(stmt.type_annotation, actual_type, stmt.location):
expected = stmt.type_annotation.name
if stmt.type_annotation.is_array:
expected = f"{expected}[]"
msg = f"Type mismatch: expected '{expected}', got '{actual_type}'"
if self.warn_types:
import sys
loc = stmt.location
print(f"Warning: {msg} at {loc.filename}:{loc.line}:{loc.column}", file=sys.stderr)
else:
self.errors.add_error(
message=msg,
filename=stmt.location.filename if stmt.location else "<unknown>",
line=stmt.location.line if stmt.location else 0,
column=stmt.location.column if stmt.location else 0
)
if isinstance(stmt.value, BinaryOp) and stmt.value.operator == "|":
self._generate_pipe_assignment(stmt, target)
return
......@@ -159,8 +241,11 @@ class DispatchMixin:
func_name = stmt.value.callee.name
if func_name not in BUILTIN_FUNCS and func_name not in self.classes:
args_str = self._generate_call_args_str(stmt.value.arguments)
self.emit(f'{func_name} {args_str} >/dev/null')
self.emit_var_assign(target, f'${RET_VAR}')
if func_name in self.callback_vars:
self.emit_var_assign(target, f'$("${{{func_name}}}" {args_str})')
else:
self.emit(f'{func_name} {args_str} >/dev/null')
self.emit_var_assign(target, f'${RET_VAR}')
return
if isinstance(stmt.value, ArrayLiteral):
......@@ -857,10 +942,10 @@ class DispatchMixin:
def _is_callback_var(self, name: str) -> bool:
"""Check if name is a variable holding a callback (function name)."""
if name in self.functions or name in self.classes:
return False
if name in getattr(self, 'callback_vars', set()):
return True
if name in self.functions or name in self.classes:
return False
if name in self.local_vars:
return True
if name in getattr(self, 'current_param_positions', {}):
......
from dataclasses import dataclass
from typing import List, Optional
import sys
@dataclass
......@@ -48,8 +49,8 @@ class ErrorCollector:
def print_errors (self):
for error in self.errors:
print (str (error))
print ()
print (str (error), file=sys.stderr)
print (file=sys.stderr)
def clear (self):
self.errors = []
......@@ -47,7 +47,7 @@ def compile_file (source_path: str, output_path: str = None) -> tuple[bool, str]
return compile_files ([source_path])
def compile_files (source_paths: list) -> tuple[bool, str]:
def compile_files (source_paths: list, type_check: bool = True, warn_types: bool = False) -> tuple[bool, str]:
"""Compile multiple .ct files to bash (Vala-style multi-file)."""
asts = []
......@@ -57,7 +57,7 @@ def compile_files (source_paths: list) -> tuple[bool, str]:
return False, ""
asts.append (ast)
codegen = CodeGenerator ()
codegen = CodeGenerator (type_check=type_check, warn_types=warn_types)
output = codegen.generate_multi (asts)
if codegen.errors.has_errors ():
......@@ -109,7 +109,9 @@ def cmd_build (args):
else:
output_path = source_paths[0].replace (".ct", ".sh")
success, output = compile_files (source_paths)
type_check = not getattr (args, 'no_type_check', False)
warn_types = getattr (args, 'warn_types', False)
success, output = compile_files (source_paths, type_check=type_check, warn_types=warn_types)
if not success:
return 1
......@@ -312,6 +314,8 @@ def main ():
build_parser.add_argument ("sources", nargs="+", help="Source files (.ct)")
build_parser.add_argument ("-o", "--output", help="Output file (.sh)")
build_parser.add_argument ("--lint", action="store_true", help="Run ShellCheck")
build_parser.add_argument ("--no-type-check", action="store_true", help="Disable type checking")
build_parser.add_argument ("--warn-types", action="store_true", help="Show type errors as warnings")
run_parser = subparsers.add_parser ("run", help="Compile and run (use -- to separate script args)")
run_parser.add_argument ("sources_and_args", nargs="+", help="Source files (.ct) [-- script args]")
......
......@@ -2,13 +2,13 @@ from typing import List, Optional, Callable, Union
from .tokens import Token, TokenType
from .ast_nodes import (
SourceLocation, Program, Declaration, Statement, Decorator, FunctionDecl,
Parameter, ClassDecl, ConstructorDecl, ImportStmt, Block, ReturnStmt,
Parameter, ClassDecl, ClassField, ConstructorDecl, ImportStmt, Block, ReturnStmt,
BreakStmt, ContinueStmt, IfStmt, WhileStmt, ForStmt, ForeachStmt, WithStmt,
TryStmt, ThrowStmt, DeferStmt, WhenStmt, WhenBranch, RangePattern,
ExpressionStmt, Assignment, IntegerLiteral, FloatLiteral, StringLiteral,
BoolLiteral, NilLiteral, ThisExpr, ArrayLiteral, DictLiteral, Identifier,
BinaryOp, UnaryOp, CallExpr, MemberAccess, IndexAccess, NewExpr, Lambda,
BaseCall, Expression
BaseCall, Expression, TypeAnnotation
)
from .errors import CompileError, ErrorCollector
......@@ -70,6 +70,45 @@ class Parser:
while self.match (TokenType.NEWLINE):
pass
def parse_type_annotation (self) -> Optional[TypeAnnotation]:
if not self.match (TokenType.COLON):
return None
return self.parse_type ()
def parse_type (self) -> TypeAnnotation:
loc = self.location ()
if self.check (TokenType.LPAREN):
return self.parse_function_type (loc)
name = self.expect (TokenType.IDENTIFIER, "Expected type name").value
if self.match (TokenType.LBRACKET):
if name == "dict":
key_type = self.parse_type ()
self.expect (TokenType.COMMA, "Expected ',' in dict type")
value_type = self.parse_type ()
self.expect (TokenType.RBRACKET, "Expected ']'")
return TypeAnnotation (name="dict", key_type=key_type, value_type=value_type, location=loc)
else:
self.expect (TokenType.RBRACKET, "Expected ']'")
return TypeAnnotation (name=name, is_array=True, location=loc)
return TypeAnnotation (name=name, location=loc)
def parse_function_type (self, loc: SourceLocation) -> TypeAnnotation:
self.expect (TokenType.LPAREN, "Expected '('")
param_types = []
if not self.check (TokenType.RPAREN):
while True:
param_types.append (self.parse_type ())
if not self.match (TokenType.COMMA):
break
self.expect (TokenType.RPAREN, "Expected ')'")
self.expect (TokenType.ARROW, "Expected '=>'")
return_type = self.parse_type ()
return TypeAnnotation (name="func", param_types=param_types, return_type=return_type, location=loc)
def parse (self) -> Program:
statements = []
self.skip_newlines ()
......@@ -155,12 +194,17 @@ class Parser:
params = self.parse_parameters ()
self.expect (TokenType.RPAREN, "Expected ')' after parameters")
return_type = None
if self.check (TokenType.COLON):
return_type = self.parse_type_annotation ()
self.skip_newlines ()
body = self.parse_block ()
return FunctionDecl (
name=name,
params=params,
return_type=return_type,
body=body,
decorators=decorators or [],
location=loc
......@@ -173,15 +217,19 @@ class Parser:
while True:
name = self.expect (TokenType.IDENTIFIER, "Expected parameter name").value
type_annotation = None
is_variadic = False
default = None
if self.check (TokenType.COLON):
type_annotation = self.parse_type_annotation ()
if self.match (TokenType.DOTDOTDOT):
is_variadic = True
elif self.match (TokenType.ASSIGN):
default = self.parse_expression ()
params.append (Parameter (name=name, default=default, is_variadic=is_variadic))
params.append (Parameter (name=name, type_annotation=type_annotation, default=default, is_variadic=is_variadic))
if not self.match (TokenType.COMMA):
break
......@@ -216,11 +264,15 @@ class Parser:
elif self.check (TokenType.FUNC):
methods.append (self.parse_function (decorators))
elif self.check (TokenType.IDENTIFIER):
field_loc = self.location ()
field_name = self.advance ().value
type_annotation = None
default_value = None
if self.check (TokenType.COLON):
type_annotation = self.parse_type_annotation ()
if self.match (TokenType.ASSIGN):
default_value = self.parse_expression ()
fields.append ((field_name, default_value))
fields.append (ClassField (name=field_name, type_annotation=type_annotation, default=default_value, location=field_loc))
else:
self.error (f"Unexpected token in class body: {self.current ().type.name}")
self.advance ()
......@@ -531,11 +583,15 @@ class Parser:
loc = self.location ()
expr = self.parse_expression ()
type_annotation = None
if isinstance (expr, Identifier) and self.check (TokenType.COLON):
type_annotation = self.parse_type_annotation ()
if self.check (TokenType.ASSIGN, TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN,
TokenType.STAR_ASSIGN, TokenType.SLASH_ASSIGN):
operator = self.advance ().value
value = self.parse_expression ()
return Assignment (target=expr, operator=operator, value=value, location=loc)
return Assignment (target=expr, type_annotation=type_annotation, operator=operator, value=value, location=loc)
return ExpressionStmt (expression=expr, location=loc)
......
......@@ -47,6 +47,31 @@ def compile_ct(source: str) -> tuple[int, str, str]:
os.unlink(ct_file)
def compile_ct_with_flags(source: str, flags: list = None) -> tuple[int, str, str]:
with tempfile.NamedTemporaryFile(mode='w', suffix='.ct', delete=False) as f:
f.write(source)
f.flush()
ct_file = f.name
sh_file = ct_file.replace('.ct', '.sh')
cmd = ['python3', 'content', 'build', ct_file, '-o', sh_file]
if flags:
cmd.extend(flags)
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=10
)
if os.path.exists(sh_file):
os.unlink(sh_file)
return result.returncode, result.stdout, result.stderr
finally:
os.unlink(ct_file)
def compile_ct_check(source: str) -> tuple[int, str, str]:
with tempfile.NamedTemporaryFile(mode='w', suffix='.ct', delete=False) as f:
f.write(source)
......@@ -339,6 +364,61 @@ print (add (3, 4))
assert "7" in stdout
class TestCallbackVariables:
def test_lambda_assigned_and_called(self):
code, stdout, _ = run_ct('''
doubler = x => x * 2
result = doubler (5)
print (result)
''')
assert code == 0
assert "10" in stdout
def test_typed_callback_variable(self):
code, stdout, _ = run_ct('''
tripler: (int) => int = x => x * 3
result = tripler (4)
print (result)
''')
assert code == 0
assert "12" in stdout
def test_callback_passed_to_function(self):
code, stdout, _ = run_ct('''
func apply (callback, value) {
return callback (value)
}
double = x => x * 2
result = apply (double, 7)
print (result)
''')
assert code == 0
assert "14" in stdout
def test_callback_with_multiple_uses(self):
code, stdout, _ = run_ct('''
square = x => x * x
a = square (3)
b = square (4)
c = square (5)
print ("{a} {b} {c}")
''')
assert code == 0
assert "9 16 25" in stdout
def test_typed_callback_passed_to_function(self):
code, stdout, _ = run_ct('''
func applyTwice (f: (int) => int, x: int): int {
return f (f (x))
}
double: (int) => int = x => x * 2
result = applyTwice (double, 3)
print (result)
''')
assert code == 0
assert "12" in stdout
class TestStringMethods:
def test_upper(self):
code, stdout, _ = run_ct('''
......@@ -1010,8 +1090,8 @@ arr = [1, 2, 3]
x = arr.nonexistent()
''')
assert code != 0
assert "Unknown method 'nonexistent'" in stdout
assert "array" in stdout
assert "Unknown method 'nonexistent'" in stderr
assert "array" in stderr
def test_unknown_dict_method(self):
code, stdout, stderr = compile_ct_check('''
......@@ -1019,16 +1099,16 @@ d = {"key": "value"}
x = d.nonexistent()
''')
assert code != 0
assert "Unknown method 'nonexistent'" in stdout
assert "dict" in stdout
assert "Unknown method 'nonexistent'" in stderr
assert "dict" in stderr
def test_unknown_namespace_method(self):
code, stdout, stderr = compile_ct_check('''
x = fs.nonexistent()
''')
assert code != 0
assert "Unknown method 'nonexistent'" in stdout
assert "fs" in stdout
assert "Unknown method 'nonexistent'" in stderr
assert "fs" in stderr
def test_valid_string_methods(self):
code, stdout, stderr = compile_ct_check('''
......@@ -1074,9 +1154,9 @@ arr = [1, 2, 3]
x = arr.foo()
''')
assert code != 0
assert "Available:" in stdout
assert "push" in stdout
assert "pop" in stdout
assert "Available:" in stderr
assert "push" in stderr
assert "pop" in stderr
class TestFunctionParameterPassing:
......@@ -1621,3 +1701,91 @@ print("a={a.get()}, b={b.get()}")
''')
assert code == 0
assert "a=15, b=15" in stdout
class TestTypeAnnotations:
def test_typed_variables_run(self):
code, stdout, _ = run_ct('''
name: string = "Alice"
age: int = 30
print("{name} is {age}")
''')
assert code == 0
assert "Alice is 30" in stdout
def test_typed_function_params(self):
code, stdout, _ = run_ct('''
func greet(name: string, age: int): string {
return "Hello, {name}! Age: {age}"
}
print(greet("Bob", 25))
''')
assert code == 0
assert "Hello, Bob! Age: 25" in stdout
def test_typed_class_fields(self):
code, stdout, _ = run_ct('''
class User {
name: string = ""
age: int = 0
construct(name: string, age: int) {
this.name = name
this.age = age
}
}
u = new User("Charlie", 35)
print("{u.name} ({u.age})")
''')
assert code == 0
assert "Charlie (35)" in stdout
def test_type_error_string_to_int(self):
code, stdout, stderr = compile_ct_with_flags('x: int = "hello"')
assert code == 1
assert "Type mismatch" in stderr
assert "expected 'int'" in stderr
assert "got 'string'" in stderr
def test_type_error_int_to_array(self):
code, stdout, stderr = compile_ct_with_flags('arr: array = 42')
assert code == 1
assert "Type mismatch" in stderr
def test_warn_types_flag(self):
code, stdout, stderr = compile_ct_with_flags('x: int = "hello"', ['--warn-types'])
assert code == 0
assert "Warning: Type mismatch" in stderr
def test_no_type_check_flag(self):
code, stdout, stderr = compile_ct_with_flags('x: int = "hello"', ['--no-type-check'])
assert code == 0
assert "Type mismatch" not in stderr
def test_typed_array_param_works(self):
code, stdout, _ = run_ct('''
func sum_array(arr: array): int {
total = 0
foreach item in arr {
total += item
}
return total
}
nums = [1, 2, 3, 4, 5]
print(sum_array(nums))
''')
assert code == 0
assert "15" in stdout
def test_correct_types_no_error(self):
code, stdout, stderr = compile_ct_with_flags('''
name: string = "test"
count: int = 42
flag: bool = true
items: array = [1, 2, 3]
config: dict = {"key": "value"}
''')
assert code == 0
assert "Type mismatch" not in stderr
import pytest
from bootstrap.ast_nodes import (
Program, FunctionDecl, ClassDecl, ConstructorDecl,
Program, FunctionDecl, ClassDecl, ClassField, ConstructorDecl,
IntegerLiteral, FloatLiteral, StringLiteral, BoolLiteral, NilLiteral,
Identifier, ArrayLiteral, DictLiteral, BinaryOp, UnaryOp,
CallExpr, MemberAccess, IndexAccess, Lambda, NewExpr,
Block, Assignment, ReturnStmt, BreakStmt, ContinueStmt,
IfStmt, WhileStmt, ForStmt, ForeachStmt, TryStmt, ThrowStmt, DeferStmt,
WhenStmt, WhenBranch, ExpressionStmt
WhenStmt, WhenBranch, ExpressionStmt, TypeAnnotation
)
......@@ -385,3 +385,104 @@ class TestParserDecorators:
assert decl.decorators[0].object == "bot"
assert decl.decorators[1].object is None
assert decl.decorators[1].name == "log"
class TestParserTypeAnnotations:
def test_typed_variable_simple(self, parse):
ast = parse('name: string = "hello"')
stmt = ast.statements[0]
assert isinstance(stmt, Assignment)
assert stmt.type_annotation is not None
assert stmt.type_annotation.name == "string"
def test_typed_variable_int(self, parse):
ast = parse('count: int = 42')
stmt = ast.statements[0]
assert isinstance(stmt, Assignment)
assert stmt.type_annotation.name == "int"
def test_typed_variable_array(self, parse):
ast = parse('items: array = []')
stmt = ast.statements[0]
assert stmt.type_annotation.name == "array"
def test_typed_variable_array_suffix(self, parse):
ast = parse('numbers: int[] = []')
stmt = ast.statements[0]
assert stmt.type_annotation.name == "int"
assert stmt.type_annotation.is_array is True
def test_typed_variable_dict(self, parse):
ast = parse('config: dict = {}')
stmt = ast.statements[0]
assert stmt.type_annotation.name == "dict"
def test_typed_variable_dict_generic(self, parse):
ast = parse('data: dict[string, int] = {}')
stmt = ast.statements[0]
assert stmt.type_annotation.name == "dict"
assert stmt.type_annotation.key_type.name == "string"
assert stmt.type_annotation.value_type.name == "int"
def test_typed_parameter(self, parse):
ast = parse('func greet(name: string) { }')
decl = ast.statements[0]
assert isinstance(decl, FunctionDecl)
assert len(decl.params) == 1
assert decl.params[0].name == "name"
assert decl.params[0].type_annotation is not None
assert decl.params[0].type_annotation.name == "string"
def test_typed_parameters_multiple(self, parse):
ast = parse('func add(a: int, b: int) { }')
decl = ast.statements[0]
assert len(decl.params) == 2
assert decl.params[0].type_annotation.name == "int"
assert decl.params[1].type_annotation.name == "int"
def test_function_return_type(self, parse):
ast = parse('func greet(name: string): string { return name }')
decl = ast.statements[0]
assert decl.return_type is not None
assert decl.return_type.name == "string"
def test_class_typed_field(self, parse):
ast = parse('class User { name: string = "" }')
decl = ast.statements[0]
assert isinstance(decl, ClassDecl)
assert len(decl.fields) == 1
field = decl.fields[0]
assert isinstance(field, ClassField)
assert field.name == "name"
assert field.type_annotation is not None
assert field.type_annotation.name == "string"
def test_class_multiple_typed_fields(self, parse):
ast = parse('class User {\n name: string = ""\n age: int = 0\n}')
decl = ast.statements[0]
assert len(decl.fields) == 2
assert decl.fields[0].type_annotation.name == "string"
assert decl.fields[1].type_annotation.name == "int"
def test_function_type_annotation(self, parse):
ast = parse('callback: (int) => int = x => x * 2')
stmt = ast.statements[0]
assert stmt.type_annotation is not None
assert stmt.type_annotation.name == "func"
assert len(stmt.type_annotation.param_types) == 1
assert stmt.type_annotation.param_types[0].name == "int"
assert stmt.type_annotation.return_type.name == "int"
def test_untyped_still_works(self, parse):
ast = parse('x = 42')
stmt = ast.statements[0]
assert isinstance(stmt, Assignment)
assert stmt.type_annotation is None
def test_typed_constructor_params(self, parse):
ast = parse('class User { construct(name: string, age: int) { } }')
decl = ast.statements[0]
assert decl.constructor is not None
assert len(decl.constructor.params) == 2
assert decl.constructor.params[0].type_annotation.name == "string"
assert decl.constructor.params[1].type_annotation.name == "int"
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