Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/hscript/parser.py

382 lines
14 KiB
Python

"""Parser بازگشتی برای زیرمجموعهٔ شبه‌پایتون HScript."""
from __future__ import annotations
from typing import Any, Optional
from app.services.hscript import ast_nodes as ast
from app.services.hscript.errors import SyntaxErrorHS
from app.services.hscript.tokens import Token, TokenType
class Parser:
def __init__(self, tokens: list[Token]):
self.tokens = tokens
self.current = 0
def parse(self) -> ast.Module:
body: list[Any] = []
self._skip_newlines()
while not self._check(TokenType.EOF):
body.append(self._statement())
self._skip_newlines()
return ast.Module(body=body, line=1, column=1)
def _statement(self) -> Any:
if self._check(TokenType.IF):
return self._if_stmt()
if self._check(TokenType.FOR):
return self._for_stmt()
if self._check(TokenType.WHILE):
return self._while_stmt()
if self._check(TokenType.DEF):
return self._def_stmt()
if self._check(TokenType.RETURN):
return self._return_stmt()
if self._check(TokenType.PASS):
tok = self._advance()
self._consume_newline_or_dedent_end()
return ast.Pass(line=tok.line, column=tok.column)
if self._check(TokenType.BREAK):
tok = self._advance()
self._consume_newline_or_dedent_end()
return ast.Break(line=tok.line, column=tok.column)
if self._check(TokenType.CONTINUE):
tok = self._advance()
self._consume_newline_or_dedent_end()
return ast.Continue(line=tok.line, column=tok.column)
# assign or expr
if self._check(TokenType.IDENT) and self._check_next(TokenType.EQ):
return self._assign_stmt()
expr = self._expression()
self._consume_newline_or_dedent_end()
return ast.ExprStmt(value=expr, line=expr.line, column=expr.column)
def _assign_stmt(self) -> ast.Assign:
name_tok = self._consume(TokenType.IDENT, "انتظار نام متغیر")
eq = self._consume(TokenType.EQ, "انتظار '='")
value = self._expression()
self._consume_newline_or_dedent_end()
return ast.Assign(target=str(name_tok.value), value=value, line=name_tok.line, column=name_tok.column)
def _if_stmt(self) -> ast.If:
tok = self._consume(TokenType.IF, "انتظار if")
test = self._expression()
self._consume(TokenType.COLON, "انتظار ':' بعد از if")
self._consume(TokenType.NEWLINE, "انتظار خط جدید بعد از ':'")
body = self._block()
orelse: list[Any] = []
if self._match(TokenType.ELIF):
# تبدیل elif به if تو در تو در orelse
# بعد از match، current روی توکن بعد از ELIF است — باید expression بخوانیم
# اما _match قبلاً ELIF را خورده؛ پس مثل if ادامه می‌دهیم با ساخت If تو در تو
self.current -= 1 # برگرداندن ELIF برای reuse با _if_like
orelse = [self._elif_chain()]
elif self._match(TokenType.ELSE):
self._consume(TokenType.COLON, "انتظار ':' بعد از else")
self._consume(TokenType.NEWLINE, "انتظار خط جدید")
orelse = self._block()
return ast.If(test=test, body=body, orelse=orelse, line=tok.line, column=tok.column)
def _elif_chain(self) -> ast.If:
tok = self._consume(TokenType.ELIF, "انتظار elif")
test = self._expression()
self._consume(TokenType.COLON, "انتظار ':'")
self._consume(TokenType.NEWLINE, "انتظار خط جدید")
body = self._block()
orelse: list[Any] = []
if self._match(TokenType.ELIF):
self.current -= 1
orelse = [self._elif_chain()]
elif self._match(TokenType.ELSE):
self._consume(TokenType.COLON, "انتظار ':'")
self._consume(TokenType.NEWLINE, "انتظار خط جدید")
orelse = self._block()
return ast.If(test=test, body=body, orelse=orelse, line=tok.line, column=tok.column)
def _for_stmt(self) -> ast.For:
tok = self._consume(TokenType.FOR, "انتظار for")
target = self._consume(TokenType.IDENT, "انتظار نام متغیر حلقه")
self._consume(TokenType.IN, "انتظار in")
iterable = self._expression()
self._consume(TokenType.COLON, "انتظار ':'")
self._consume(TokenType.NEWLINE, "انتظار خط جدید")
body = self._block()
return ast.For(target=str(target.value), iter=iterable, body=body, line=tok.line, column=tok.column)
def _while_stmt(self) -> ast.While:
tok = self._consume(TokenType.WHILE, "انتظار while")
test = self._expression()
self._consume(TokenType.COLON, "انتظار ':'")
self._consume(TokenType.NEWLINE, "انتظار خط جدید")
body = self._block()
return ast.While(test=test, body=body, line=tok.line, column=tok.column)
def _def_stmt(self) -> ast.FunctionDef:
tok = self._consume(TokenType.DEF, "انتظار def")
name = self._consume(TokenType.IDENT, "انتظار نام تابع")
self._consume(TokenType.LPAREN, "انتظار '('")
params: list[str] = []
if not self._check(TokenType.RPAREN):
params.append(str(self._consume(TokenType.IDENT, "انتظار پارامتر").value))
while self._match(TokenType.COMMA):
params.append(str(self._consume(TokenType.IDENT, "انتظار پارامتر").value))
self._consume(TokenType.RPAREN, "انتظار ')'")
self._consume(TokenType.COLON, "انتظار ':'")
self._consume(TokenType.NEWLINE, "انتظار خط جدید")
body = self._block()
return ast.FunctionDef(name=str(name.value), params=params, body=body, line=tok.line, column=tok.column)
def _return_stmt(self) -> ast.Return:
tok = self._consume(TokenType.RETURN, "انتظار return")
value = None
if not self._check(TokenType.NEWLINE) and not self._check(TokenType.DEDENT) and not self._check(TokenType.EOF):
value = self._expression()
self._consume_newline_or_dedent_end()
return ast.Return(value=value, line=tok.line, column=tok.column)
def _block(self) -> list[Any]:
self._consume(TokenType.INDENT, "انتظار تورفتگی")
body: list[Any] = []
while not self._check(TokenType.DEDENT) and not self._check(TokenType.EOF):
self._skip_newlines()
if self._check(TokenType.DEDENT) or self._check(TokenType.EOF):
break
body.append(self._statement())
self._skip_newlines()
self._consume(TokenType.DEDENT, "انتظار پایان بلوک")
if not body:
raise SyntaxErrorHS("بلوک خالی مجاز نیست؛ از pass استفاده کنید", line=self._peek().line, column=self._peek().column)
return body
# --- expressions ---
def _expression(self) -> Any:
return self._or()
def _or(self) -> Any:
expr = self._and()
while self._match(TokenType.OR):
right = self._and()
if isinstance(expr, ast.BoolOp) and expr.op == "or":
expr.values.append(right)
else:
expr = ast.BoolOp(op="or", values=[expr, right], line=expr.line, column=expr.column)
return expr
def _and(self) -> Any:
expr = self._not()
while self._match(TokenType.AND):
right = self._not()
if isinstance(expr, ast.BoolOp) and expr.op == "and":
expr.values.append(right)
else:
expr = ast.BoolOp(op="and", values=[expr, right], line=expr.line, column=expr.column)
return expr
def _not(self) -> Any:
if self._match(TokenType.NOT):
tok = self._previous()
operand = self._not()
return ast.UnaryOp(op="not", operand=operand, line=tok.line, column=tok.column)
return self._comparison()
def _comparison(self) -> Any:
expr = self._term()
ops: list[str] = []
comps: list[Any] = []
while True:
if self._match(TokenType.EQEQ):
ops.append("==")
comps.append(self._term())
elif self._match(TokenType.NE):
ops.append("!=")
comps.append(self._term())
elif self._match(TokenType.LT):
ops.append("<")
comps.append(self._term())
elif self._match(TokenType.LE):
ops.append("<=")
comps.append(self._term())
elif self._match(TokenType.GT):
ops.append(">")
comps.append(self._term())
elif self._match(TokenType.GE):
ops.append(">=")
comps.append(self._term())
else:
break
if ops:
return ast.Compare(left=expr, ops=ops, comparators=comps, line=expr.line, column=expr.column)
return expr
def _term(self) -> Any:
expr = self._factor()
while True:
if self._match(TokenType.PLUS):
op = "+"
elif self._match(TokenType.MINUS):
op = "-"
else:
break
right = self._factor()
expr = ast.BinOp(left=expr, op=op, right=right, line=expr.line, column=expr.column)
return expr
def _factor(self) -> Any:
expr = self._unary()
while True:
if self._match(TokenType.STAR):
op = "*"
elif self._match(TokenType.SLASH):
op = "/"
elif self._match(TokenType.PERCENT):
op = "%"
else:
break
right = self._unary()
expr = ast.BinOp(left=expr, op=op, right=right, line=expr.line, column=expr.column)
return expr
def _unary(self) -> Any:
if self._match(TokenType.MINUS):
tok = self._previous()
return ast.UnaryOp(op="-", operand=self._unary(), line=tok.line, column=tok.column)
if self._match(TokenType.PLUS):
tok = self._previous()
return ast.UnaryOp(op="+", operand=self._unary(), line=tok.line, column=tok.column)
return self._call_attr()
def _call_attr(self) -> Any:
expr = self._primary()
while True:
if self._match(TokenType.LPAREN):
args, kwargs = self._arguments()
self._consume(TokenType.RPAREN, "انتظار ')'")
expr = ast.Call(func=expr, args=args, keywords=kwargs, line=expr.line, column=expr.column)
elif self._match(TokenType.DOT):
attr = self._consume(TokenType.IDENT, "انتظار نام ویژگی")
expr = ast.Attribute(value=expr, attr=str(attr.value), line=expr.line, column=expr.column)
elif self._match(TokenType.LBRACK):
index = self._expression()
self._consume(TokenType.RBRACK, "انتظار ']'")
expr = ast.Subscript(value=expr, index=index, line=expr.line, column=expr.column)
else:
break
return expr
def _arguments(self) -> tuple[list[Any], dict[str, Any]]:
args: list[Any] = []
kwargs: dict[str, Any] = {}
if self._check(TokenType.RPAREN):
return args, kwargs
while True:
if self._check(TokenType.IDENT) and self._check_next(TokenType.EQ):
key = str(self._advance().value)
self._consume(TokenType.EQ, "انتظار '='")
kwargs[key] = self._expression()
else:
if kwargs:
raise SyntaxErrorHS("آرگومان موقعیتی بعد از نام‌دار مجاز نیست", line=self._peek().line, column=self._peek().column)
args.append(self._expression())
if not self._match(TokenType.COMMA):
break
return args, kwargs
def _primary(self) -> Any:
if self._match(TokenType.NUMBER):
tok = self._previous()
return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
if self._match(TokenType.STRING):
tok = self._previous()
return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
if self._match(TokenType.TRUE) or self._match(TokenType.FALSE) or self._match(TokenType.NULL):
tok = self._previous()
return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
if self._match(TokenType.IDENT):
tok = self._previous()
return ast.Name(id=str(tok.value), line=tok.line, column=tok.column)
if self._match(TokenType.LPAREN):
expr = self._expression()
self._consume(TokenType.RPAREN, "انتظار ')'")
return expr
if self._match(TokenType.LBRACK):
tok = self._previous()
elts: list[Any] = []
if not self._check(TokenType.RBRACK):
elts.append(self._expression())
while self._match(TokenType.COMMA):
if self._check(TokenType.RBRACK):
break
elts.append(self._expression())
self._consume(TokenType.RBRACK, "انتظار ']'")
return ast.ListExpr(elts=elts, line=tok.line, column=tok.column)
if self._match(TokenType.LBRACE):
tok = self._previous()
keys: list[Any] = []
vals: list[Any] = []
if not self._check(TokenType.RBRACE):
keys.append(self._expression())
self._consume(TokenType.COLON, "انتظار ':' در dict")
vals.append(self._expression())
while self._match(TokenType.COMMA):
if self._check(TokenType.RBRACE):
break
keys.append(self._expression())
self._consume(TokenType.COLON, "انتظار ':' در dict")
vals.append(self._expression())
self._consume(TokenType.RBRACE, "انتظار '}'")
return ast.DictExpr(keys=keys, values=vals, line=tok.line, column=tok.column)
tok = self._peek()
raise SyntaxErrorHS(f"عبارت غیرمنتظره: {tok.type.name}", line=tok.line, column=tok.column)
# --- helpers ---
def _consume_newline_or_dedent_end(self) -> None:
if self._check(TokenType.NEWLINE):
self._advance()
return
if self._check(TokenType.DEDENT) or self._check(TokenType.EOF):
return
# اجازه پایان فایل بدون newline
if self._check(TokenType.ELIF) or self._check(TokenType.ELSE):
return
raise SyntaxErrorHS("انتظار پایان دستور (خط جدید)", line=self._peek().line, column=self._peek().column)
def _skip_newlines(self) -> None:
while self._match(TokenType.NEWLINE):
pass
def _match(self, t: TokenType) -> bool:
if self._check(t):
self._advance()
return True
return False
def _consume(self, t: TokenType, message: str) -> Token:
if self._check(t):
return self._advance()
tok = self._peek()
raise SyntaxErrorHS(message, line=tok.line, column=tok.column)
def _check(self, t: TokenType) -> bool:
return self._peek().type == t
def _check_next(self, t: TokenType) -> bool:
if self.current + 1 >= len(self.tokens):
return False
return self.tokens[self.current + 1].type == t
def _advance(self) -> Token:
if not self._is_at_end():
self.current += 1
return self._previous()
def _previous(self) -> Token:
return self.tokens[self.current - 1]
def _peek(self) -> Token:
return self.tokens[self.current]
def _is_at_end(self) -> bool:
return self._peek().type == TokenType.EOF