diff --git a/examples/basic/24_list.peb b/examples/basic/24_list.peb index df17a1e..ebf6619 100644 --- a/examples/basic/24_list.peb +++ b/examples/basic/24_list.peb @@ -21,4 +21,9 @@ print(l[1][2]) l[1][2] = "three" print(l) -print(l[1][2]) \ No newline at end of file +print(l[1][2]) + +print(a.length) +a.push("d") +print(a.length) +print(a) diff --git a/src/core/builtin_type.py b/src/core/builtin_type.py new file mode 100644 index 0000000..a72cd91 --- /dev/null +++ b/src/core/builtin_type.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass +from typing import Callable, Any, Optional + +from src.core.callable import PebbleCallable, make_builtin +from src.interpreter.error import PebbleRuntimeError +from src.token.token import Token + + +@dataclass(frozen=True) +class ExposedMeta: + name: str + nargs: int = 0 + + +Method = Callable[..., Any] +Exposable = Method | property + + +def exposed(func: Exposable = None, /, *, name: Optional[str] = None, nargs: int = 0): + """ + Decorator to mark methods/properties as exposed to the interpreter + :param func: the method/property to expose + :param name: the exposed name in the language. If None, the Python name is used + :param nargs: the number of arguments (for methods only, will raise an error if a value != 0 is passed for a property) + :return: the decorated method/property + """ + def wrap(f: Exposable): + if isinstance(f, property) and nargs != 0: + raise ValueError("Properties cannot accept arguments") + + target: Exposable = f.fget if isinstance(f, property) else f + target._exposed = ExposedMeta( + name=name or target.__name__, + nargs=nargs + ) + return f + return wrap if func is None else wrap(func) + + +def _get_exposed_meta(attr: Any) -> Optional[ExposedMeta]: + target: Exposable = attr.fget if isinstance(attr, property) else attr + return getattr(target, "_exposed", None) + + +class BuiltinType(type): + def __new__(mcs, cls_name, bases, namespace): + exposed_props: dict[str, property] = {} + exposed_methods: dict[str, tuple[Method, int]] = {} + for attr_name, attr_value in namespace.items(): + meta: Optional[ExposedMeta] = _get_exposed_meta(attr_value) + if meta is None: + continue + if isinstance(attr_value, property): + exposed_props[meta.name] = attr_value + elif callable(attr_value): + exposed_methods[meta.name] = (attr_value, meta.nargs) + + namespace["exposed_props"] = exposed_props + namespace["exposed_methods"] = exposed_methods + namespace["get_exposed"] = mcs._make_get_exposed() + return super().__new__(mcs, cls_name, bases, namespace) + + @staticmethod + def _make_get_exposed() -> Callable: + def get_exposed(self, name: Token) -> Any: + name_str: str = name.lexeme + cls_prop: Optional[property] = self.__class__.exposed_props.get(name_str) + if cls_prop is not None: + return getattr(self, cls_prop.__name__) + + cls_method: Optional[tuple[PebbleCallable, int]] = self.__class__.exposed_methods.get(name_str) + if cls_method is not None: + return make_builtin(getattr(self, cls_method[0].__name__), cls_method[1]) + + raise PebbleRuntimeError(name, f"Undefined property '{name_str}'.") + return get_exposed diff --git a/src/core/list.py b/src/core/list.py index ae5e0c7..c740861 100644 --- a/src/core/list.py +++ b/src/core/list.py @@ -1,11 +1,17 @@ -from typing import Optional, Any +from __future__ import annotations +from typing import Optional, Any, TYPE_CHECKING + +from src.core.builtin_type import BuiltinType, exposed from src.core.cast import Cast from src.core.format_spec.string_formatter import StringFormatter from src.token.token import Token +if TYPE_CHECKING: + from src.interpreter.interpreter import Interpreter -class PebbleList: + +class PebbleList(metaclass=BuiltinType): def __init__(self, items: Optional[list[Any]] = None): self.items: list[Any] = items or [] @@ -19,3 +25,13 @@ class PebbleList: def __str__(self): return "[" + ", ".join(map(lambda item: StringFormatter.stringify(item, True), self.items)) + "]" + + # Exposed methods + @exposed(nargs=1) + def push(self, interpreter: Interpreter, args: list[Any]) -> None: + self.items.append(args[0]) + + @exposed + @property + def length(self) -> int: + return len(self.items) diff --git a/src/interpreter/interpreter.py b/src/interpreter/interpreter.py index a8842b4..5189da9 100644 --- a/src/interpreter/interpreter.py +++ b/src/interpreter/interpreter.py @@ -6,6 +6,7 @@ from src.ast.expr import LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, Expr, from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt from src.consts import CONSTRUCTOR_NAME +from src.core.builtin_type import BuiltinType from src.core.callable import PebbleCallable from src.core.format_spec.string_formatter import StringFormatter from src.core.function import PebbleFunction @@ -182,6 +183,8 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]): obj: Any = self.evaluate(expr.object) if isinstance(obj, PebbleInstance): return obj.get(expr.name) + if isinstance(obj.__class__, BuiltinType): + return obj.get_exposed(expr.name) raise PebbleRuntimeError(expr.name, "Only class instances have properties.") def visit_subscript_get_expr(self, expr: SubscriptGetExpr) -> Any: