Compare commits

5 Commits

Author SHA1 Message Date
fe747656d8 fix(list): handle cyclic reference when stringifying 2026-02-09 01:55:59 +01:00
fb7723406c feat: add methods and properties on builtin types
added push method on lists and length property
2026-02-09 01:45:09 +01:00
0d5c678932 feat: add list subscript set 2026-02-08 21:47:55 +01:00
983f886397 feat: add list subscript get 2026-02-08 21:36:43 +01:00
802558d681 feat: add basic lists 2026-02-08 19:37:48 +01:00
13 changed files with 319 additions and 10 deletions

View File

@@ -6,6 +6,8 @@
PUNC_RPAREN=")"
PUNC_LBRACE="{"
PUNC_RBRACE="}"
PUNC_LBRACK="["
PUNC_RBRACK="]"
PUNC_COMMA=","
PUNC_DOT="."
PUNC_SEMICOLON=";"
@@ -88,7 +90,7 @@ continueStmt ::= KW_CONTINUE ;
expression ::= assignment ;
assignment ::= ( call PUNC_DOT )? IDENTIFIER OP_EQUAL assignment | logic_or ;
assignment ::= ( call PUNC_DOT )? IDENTIFIER list_index* OP_EQUAL assignment | logic_or ;
logic_or ::= logic_and ( KW_OR logic_and )* ;
logic_and ::= equality ( KW_AND equality )* ;
@@ -98,12 +100,16 @@ term ::= factor ( ( OP_MINUS | OP_PLUS ) factor )* ;
factor ::= unary ( ( OP_SLASH | OP_STAR ) unary )* ;
unary ::= ( OP_BANG | OP_MINUS ) unary | call ;
call ::= primary ( PUNC_LPAREN arguments? PUNC_RPAREN | PUNC_DOT IDENTIFIER )* ;
primary ::= KW_TRUE | KW_FALSE | KW_NULL | KW_THIS | NUMBER | STRING | IDENTIFIER | PUNC_LPAREN expression PUNC_RPAREN | KW_SUPER PUNC_DOT IDENTIFIER ;
call ::= subscript ( PUNC_LPAREN arguments? PUNC_RPAREN | PUNC_DOT IDENTIFIER )* ;
primary ::= KW_TRUE | KW_FALSE | KW_NULL | KW_THIS | NUMBER | STRING | IDENTIFIER | PUNC_LPAREN expression PUNC_RPAREN | KW_SUPER PUNC_DOT IDENTIFIER | list ;
function ::= IDENTIFIER PUNC_LPAREN parameters? PUNC_RPAREN block ;
parameters ::= IDENTIFIER ( PUNC_COMMA IDENTIFIER )* ;
arguments ::= expression ( PUNC_COMMA expression )* ;
list ::= PUNC_LBRACK list_items? PUNC_RBRACK ;
list_items ::= logic_or (PUNC_COMMA logic_or)* PUNC_COMMA? ;
list_index ::= PUNC_LBRACK logic_or PUNC_RBRACK ;
subscript ::= primary list_index* ;
NUMBER ::= DIGIT+ ( PUNC_DOT DIGIT+ ) ?;
IDENTIFIER ::= ALPHA ( ALPHA | DIGIT | SYM_UNDERSCORE )* ;

View File

@@ -0,0 +1,29 @@
let a = [
"a",
"b",
"c"
]
print(a)
print(a[0])
a[1] = "B"
print(a)
let l = [
["a", "b", "c"],
["1", "2", "3"]
]
print(l)
print(l[1][2])
l[1][2] = "three"
print(l)
print(l[1][2])
print(a.length)
a.push("d")
print(a.length)
print(a)

View File

@@ -38,6 +38,10 @@ class Expr(ABC):
def visit_get_expr(self, expr: GetExpr) -> T:
...
@abstractmethod
def visit_subscript_get_expr(self, expr: SubscriptGetExpr) -> T:
...
@abstractmethod
def visit_grouping_expr(self, expr: GroupingExpr) -> T:
...
@@ -54,6 +58,10 @@ class Expr(ABC):
def visit_fstring_embed_expr(self, expr: FStringEmbedExpr) -> T:
...
@abstractmethod
def visit_list_expr(self, expr: ListExpr) -> T:
...
@abstractmethod
def visit_variable_expr(self, expr: VariableExpr) -> T:
...
@@ -66,6 +74,10 @@ class Expr(ABC):
def visit_set_expr(self, expr: SetExpr) -> T:
...
@abstractmethod
def visit_subscript_set_expr(self, expr: SubscriptSetExpr) -> T:
...
@abstractmethod
def visit_this_expr(self, expr: ThisExpr) -> T:
...
@@ -122,6 +134,16 @@ class GetExpr(Expr):
return visitor.visit_get_expr(self)
@dataclass(frozen=True)
class SubscriptGetExpr(Expr):
object: Expr
index: Expr
bracket: Token
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_subscript_get_expr(self)
@dataclass(frozen=True)
class GroupingExpr(Expr):
expression: Expr
@@ -159,6 +181,15 @@ class FStringEmbedExpr(Expr):
return visitor.visit_fstring_embed_expr(self)
@dataclass(frozen=True)
class ListExpr(Expr):
bracket: Token
items: list[Expr]
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_list_expr(self)
@dataclass(frozen=True)
class VariableExpr(Expr):
name: Token
@@ -187,6 +218,17 @@ class SetExpr(Expr):
return visitor.visit_set_expr(self)
@dataclass(frozen=True)
class SubscriptSetExpr(Expr):
object: Expr
index: Expr
bracket: Token
value: Expr
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_subscript_set_expr(self)
@dataclass(frozen=True)
class ThisExpr(Expr):
keyword: Token

76
src/core/builtin_type.py Normal file
View File

@@ -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

30
src/core/cast.py Normal file
View File

@@ -0,0 +1,30 @@
from typing import Any
from src.interpreter.error import PebbleRuntimeError
from src.token.token import Token
class Cast:
@staticmethod
def as_number(token: Token, value: Any) -> int | float:
if not isinstance(value, (int, float, bool)):
raise PebbleRuntimeError(token, "Expected number value.")
if isinstance(value, bool):
value = int(value)
return value
@staticmethod
def as_int(token: Token, value: Any):
try:
number: int | float = Cast.as_number(token, value)
except PebbleRuntimeError:
raise PebbleRuntimeError(token, "Expected integer value.")
return int(number)
@staticmethod
def as_float(token: Token, value: Any):
try:
number: int | float = Cast.as_number(token, value)
except PebbleRuntimeError:
raise PebbleRuntimeError(token, "Expected float value.")
return float(number)

View File

@@ -8,7 +8,7 @@ from src.interpreter.error import PebbleRuntimeError
class StringFormatter:
@staticmethod
def stringify(obj: Any):
def stringify(obj: Any, quote_str: bool = False):
if obj is None:
return "null"
if obj is True:
@@ -19,7 +19,9 @@ class StringFormatter:
if obj.is_integer():
obj = int(obj)
return str(obj)
return obj
if isinstance(obj, str) and quote_str:
return '"' + obj + '"'
return str(obj)
@staticmethod
def assert_type(token: Token, obj: Any, expected_type: type | tuple[type, ...]):

43
src/core/list.py Normal file
View File

@@ -0,0 +1,43 @@
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(metaclass=BuiltinType):
def __init__(self, items: Optional[list[Any]] = None):
self.items: list[Any] = items or []
self._stringify_visited: bool = False
def get(self, index: Any, bracket: Token):
idx: int = Cast.as_int(bracket, index)
return self.items[idx]
def set(self, index: Any, value: Any, bracket: Token):
idx: int = Cast.as_int(bracket, index)
self.items[idx] = value
def __str__(self):
if self._stringify_visited:
return "[...]"
self._stringify_visited = True
res: str = "[" + ", ".join(map(lambda item: StringFormatter.stringify(item, True), self.items)) + "]"
self._stringify_visited = False
return res
# 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)

View File

@@ -2,7 +2,8 @@ from enum import Enum, auto
from typing import Any
from src.ast.expr import Expr, VariableExpr, LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, AssignExpr, LogicalExpr, \
CallExpr, SetExpr, GetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr
CallExpr, SetExpr, GetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr, ListExpr, SubscriptGetExpr, \
SubscriptSetExpr
from src.ast.stmt import Stmt, LetStmt, IfStmt, ExpressionStmt, BlockStmt, WhileStmt, ForStmt, FunctionStmt, \
ReturnStmt, BreakStmt, ContinueStmt, ClassStmt
from src.core.format_spec.spec import FormatSpec
@@ -44,6 +45,9 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]):
def visit_set_expr(self, expr: SetExpr) -> str:
return f"{self.format(expr.object)}.{expr.name.lexeme} = {self.format(expr.value)}"
def visit_subscript_set_expr(self, expr: SubscriptSetExpr) -> str:
return f"{self.format(expr.object)}[{self.format(expr.index)}] = {self.format(expr.value)}"
def visit_this_expr(self, expr: ThisExpr) -> str:
return expr.keyword.lexeme
@@ -59,6 +63,9 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]):
def visit_get_expr(self, expr: GetExpr) -> str:
return f"{self.format(expr.object)}.{expr.name.lexeme}"
def visit_subscript_get_expr(self, expr: SubscriptGetExpr) -> str:
return f"{self.format(expr.object)}[{self.format(expr.index)}]"
def visit_grouping_expr(self, expr: GroupingExpr) -> str:
return f"({self.format(expr.expression)})"
@@ -95,6 +102,9 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]):
res += "}"
return res
def visit_list_expr(self, expr: ListExpr) -> str:
return "[" + ", ".join(map(self.format, expr.items)) + "]"
def visit_variable_expr(self, expr: VariableExpr) -> str:
return expr.name.lexeme

View File

@@ -1,15 +1,18 @@
from typing import Any, Optional
from src.ast.expr import LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, Expr, VariableExpr, AssignExpr, LogicalExpr, \
CallExpr, GetExpr, SetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr
CallExpr, GetExpr, SetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr, ListExpr, SubscriptGetExpr, \
SubscriptSetExpr
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
from src.core.instance import PebbleInstance
from src.core.klass import PebbleClass
from src.core.list import PebbleList
from src.interpreter.environment import Environment
from src.interpreter.error import PebbleRuntimeError
from src.interpreter.exceptions import ReturnException, BreakException, ContinueException
@@ -89,6 +92,14 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
obj.set(expr.name, value)
return value
def visit_subscript_set_expr(self, expr: SubscriptSetExpr) -> Any:
obj: Any = self.evaluate(expr.object)
if not isinstance(obj, PebbleList):
raise PebbleRuntimeError(expr.bracket, "Only lists can be indexed.")
value: Any = self.evaluate(expr.value)
idx: Any = self.evaluate(expr.index)
obj.set(idx, value, expr.bracket)
def visit_this_expr(self, expr: ThisExpr) -> Any:
return self.look_up_variable(expr.keyword, expr)
@@ -172,8 +183,17 @@ 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:
obj: Any = self.evaluate(expr.object)
if not isinstance(obj, PebbleList):
raise PebbleRuntimeError(expr.bracket, "Only lists can be indexed.")
idx: Any = self.evaluate(expr.index)
return obj.get(idx, expr.bracket)
def visit_grouping_expr(self, expr: GroupingExpr) -> Any:
return self.evaluate(expr.expression)
@@ -192,6 +212,12 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
return self.stringify(value)
return StringFormatter().format(value, expr.spec)
def visit_list_expr(self, expr: ListExpr) -> Any:
items: list[Any] = []
for item in expr.items:
items.append(self.evaluate(item))
return PebbleList(items)
def visit_variable_expr(self, expr: VariableExpr) -> Any:
return self.look_up_variable(expr.name, expr)

View File

@@ -4,7 +4,8 @@ from enum import Enum, auto
from typing import TYPE_CHECKING
from src.ast.expr import Expr, LogicalExpr, VariableExpr, LiteralExpr, GroupingExpr, CallExpr, UnaryExpr, BinaryExpr, \
AssignExpr, GetExpr, SetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr
AssignExpr, GetExpr, SetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr, ListExpr, SubscriptGetExpr, \
SubscriptSetExpr
from src.ast.stmt import Stmt, ForStmt, WhileStmt, LetStmt, ReturnStmt, IfStmt, FunctionStmt, \
ExpressionStmt, BlockStmt, BreakStmt, ContinueStmt, ClassStmt
from src.consts import CONSTRUCTOR_NAME
@@ -100,6 +101,10 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]):
def visit_get_expr(self, expr: GetExpr) -> None:
self.resolve(expr.object)
def visit_subscript_get_expr(self, expr: SubscriptGetExpr) -> None:
self.resolve(expr.object)
self.resolve(expr.index)
def visit_grouping_expr(self, expr: GroupingExpr) -> None:
self.resolve(expr.expression)
@@ -113,6 +118,10 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]):
def visit_fstring_embed_expr(self, expr: FStringEmbedExpr) -> None:
self.resolve(expr.expression)
def visit_list_expr(self, expr: ListExpr) -> None:
for item in expr.items:
self.resolve(item)
def visit_variable_expr(self, expr: VariableExpr) -> None:
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name.lexeme) is False:
Pebble.token_error(expr.name, "Variable is not initialized.")
@@ -126,6 +135,11 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]):
self.resolve(expr.value)
self.resolve(expr.object)
def visit_subscript_set_expr(self, expr: SubscriptSetExpr) -> None:
self.resolve(expr.value)
self.resolve(expr.object)
self.resolve(expr.index)
def visit_this_expr(self, expr: ThisExpr) -> None:
if self.current_class == ClassType.NONE:
Pebble.token_error(expr.keyword, "Cannot use 'this' outside of a class.")

View File

@@ -1,7 +1,8 @@
from typing import Optional
from src.ast.expr import Expr, BinaryExpr, UnaryExpr, LiteralExpr, GroupingExpr, VariableExpr, AssignExpr, LogicalExpr, \
CallExpr, GetExpr, SetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr
CallExpr, GetExpr, SetExpr, ThisExpr, SuperExpr, FStringExpr, FStringEmbedExpr, ListExpr, SubscriptGetExpr, \
SubscriptSetExpr
from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \
ReturnStmt, BreakStmt, ContinueStmt, ClassStmt
from src.consts import MAX_FUNCTION_ARGS
@@ -275,6 +276,9 @@ class Parser:
elif isinstance(expr, GetExpr):
return SetExpr(expr.object, expr.name, value)
elif isinstance(expr, SubscriptGetExpr):
return SubscriptSetExpr(expr.object, expr.index, expr.bracket, value)
self.error(operator, "Invalid assignment target.")
return expr
@@ -334,7 +338,7 @@ class Parser:
return self.call()
def call(self) -> Expr:
expr: Expr = self.primary()
expr: Expr = self.subscript()
while True:
if self.match(TokenType.LEFT_PAREN):
expr = self.finish_call(expr)
@@ -358,6 +362,14 @@ class Parser:
paren: Token = self.consume(TokenType.RIGHT_PAREN, "Expected ')' after arguments.")
return CallExpr(callee, paren, arguments)
def subscript(self) -> Expr:
expr: Expr = self.primary()
while self.match(TokenType.LEFT_BRACKET):
idx: Expr = self.expression()
bracket: Token = self.consume(TokenType.RIGHT_BRACKET, "Unclosed list index")
expr = SubscriptGetExpr(expr, idx, bracket)
return expr
def primary(self) -> Expr:
if self.match(TokenType.FALSE):
return LiteralExpr(False)
@@ -369,6 +381,9 @@ class Parser:
if self.match(TokenType.FSTRING_START):
return self.fstring()
if self.match(TokenType.LEFT_BRACKET):
return self.list()
if self.match(TokenType.NUMBER, TokenType.STRING):
return LiteralExpr(self.previous().value)
@@ -410,3 +425,13 @@ class Parser:
self.consume(TokenType.FSTRING_END, "Unclosed f-string")
return FStringExpr(start, parts, self.previous())
def list(self) -> Expr:
items: list[Expr] = []
while not self.check(TokenType.RIGHT_BRACKET) and not self.is_at_end():
items.append(self.expression())
if not self.check(TokenType.RIGHT_BRACKET):
self.consume(TokenType.COMMA, "Expected ',' between list items")
bracket: Token = self.consume(TokenType.RIGHT_BRACKET, "Unclosed list")
return ListExpr(bracket, items)

View File

@@ -93,6 +93,10 @@ class Lexer:
self.add_token(TokenType.LEFT_BRACE)
case "}":
self.add_token(TokenType.RIGHT_BRACE)
case "[":
self.add_token(TokenType.LEFT_BRACKET)
case "]":
self.add_token(TokenType.RIGHT_BRACKET)
case ",":
self.add_token(TokenType.COMMA)
case ".":

View File

@@ -11,6 +11,8 @@ class TokenType(Enum):
RIGHT_PAREN = auto()
LEFT_BRACE = auto()
RIGHT_BRACE = auto()
LEFT_BRACKET = auto()
RIGHT_BRACKET = auto()
COMMA = auto()
DOT = auto()
SEMICOLON = auto()