feat: add format spec parser
This commit is contained in:
@@ -6,4 +6,4 @@ print(f"{b:_}")
|
|||||||
|
|
||||||
let pts = 19
|
let pts = 19
|
||||||
let total = 22
|
let total = 22
|
||||||
print(f"Correct answers: {points/total:.2%}")
|
print(f"Correct answers: {pts/total:.2%}")
|
||||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, TypeVar, Generic
|
from typing import Any, TypeVar, Generic, Optional
|
||||||
|
|
||||||
|
from src.core.format_spec.spec import FormatSpec
|
||||||
from src.token.token import Token
|
from src.token.token import Token
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +152,7 @@ class FStringExpr(Expr):
|
|||||||
class FStringEmbedExpr(Expr):
|
class FStringEmbedExpr(Expr):
|
||||||
start: Token
|
start: Token
|
||||||
expression: Expr
|
expression: Expr
|
||||||
|
spec: Optional[FormatSpec]
|
||||||
end: Token
|
end: Token
|
||||||
|
|
||||||
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
def accept(self, visitor: Expr.Visitor[T]) -> T:
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ class FormatSpecLexer:
|
|||||||
self.add_token(TokenType.T_FIX)
|
self.add_token(TokenType.T_FIX)
|
||||||
case "%":
|
case "%":
|
||||||
self.add_token(TokenType.T_PCT)
|
self.add_token(TokenType.T_PCT)
|
||||||
case "." if self.peek().isdigit():
|
case ".":
|
||||||
self.scan_number(True)
|
self.add_token(TokenType.DOT)
|
||||||
case _:
|
case _:
|
||||||
if char.isdigit():
|
if char.isdigit():
|
||||||
self.scan_number()
|
self.scan_number()
|
||||||
@@ -112,18 +112,9 @@ class FormatSpecLexer:
|
|||||||
self.error("Unexpected character")
|
self.error("Unexpected character")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def scan_number(self, decimal_only: bool = False):
|
def scan_number(self):
|
||||||
while self.peek().isdigit():
|
while self.peek().isdigit():
|
||||||
self.advance()
|
self.advance()
|
||||||
|
|
||||||
if not decimal_only:
|
value: float = float(self.source[self.start:self.idx])
|
||||||
if self.peek() == "." and self.peek_next().isdigit():
|
|
||||||
self.advance()
|
|
||||||
while self.peek().isdigit():
|
|
||||||
self.advance()
|
|
||||||
|
|
||||||
value_str: str = self.source[self.start:self.idx]
|
|
||||||
if decimal_only:
|
|
||||||
value_str = f"0{value_str}"
|
|
||||||
value: float = float(value_str)
|
|
||||||
self.add_token(TokenType.NUMBER, value)
|
self.add_token(TokenType.NUMBER, value)
|
||||||
|
|||||||
119
src/core/format_spec/parser.py
Normal file
119
src/core/format_spec/parser.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.core.format_spec.spec import FormatSpec, FormatSpecOptions, FormatSpecNumber, FormatSpecIntegral, \
|
||||||
|
FormatSpecDecimal
|
||||||
|
from src.core.format_spec.token import Token, TokenType
|
||||||
|
from src.parser.error import ParsingError
|
||||||
|
from src.pebble import Pebble
|
||||||
|
|
||||||
|
|
||||||
|
class FormatSpecParser:
|
||||||
|
TYPES: set[TokenType] = {
|
||||||
|
TokenType.T_STR,
|
||||||
|
TokenType.T_BIN,
|
||||||
|
TokenType.T_DEC,
|
||||||
|
TokenType.T_OCT,
|
||||||
|
TokenType.T_HEX,
|
||||||
|
TokenType.T_SCI,
|
||||||
|
TokenType.T_FIX,
|
||||||
|
TokenType.T_PCT,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, tokens: list[Token]):
|
||||||
|
self.tokens: list[Token] = tokens
|
||||||
|
self.current: int = 0
|
||||||
|
self.length: int = len(self.tokens)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error(token: Token, msg: str):
|
||||||
|
Pebble.token_error(token, msg)
|
||||||
|
return ParsingError()
|
||||||
|
|
||||||
|
def parse(self) -> FormatSpec:
|
||||||
|
return self.spec()
|
||||||
|
|
||||||
|
def is_at_end(self) -> bool:
|
||||||
|
return self.peek().type == TokenType.EOF
|
||||||
|
|
||||||
|
def peek(self) -> Token:
|
||||||
|
return self.tokens[self.current]
|
||||||
|
|
||||||
|
def previous(self) -> Token:
|
||||||
|
return self.tokens[self.current - 1]
|
||||||
|
|
||||||
|
def check(self, token_type: TokenType) -> bool:
|
||||||
|
if self.is_at_end():
|
||||||
|
return False
|
||||||
|
return self.peek().type == token_type
|
||||||
|
|
||||||
|
def advance(self):
|
||||||
|
token: Token = self.peek()
|
||||||
|
self.current += 1
|
||||||
|
return token
|
||||||
|
|
||||||
|
def match(self, *types: TokenType) -> bool:
|
||||||
|
for token_type in types:
|
||||||
|
if self.check(token_type):
|
||||||
|
self.advance()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def consume(self, token_type: TokenType, error_msg: str) -> Token:
|
||||||
|
if self.check(token_type):
|
||||||
|
return self.advance()
|
||||||
|
raise self.error(self.peek(), error_msg)
|
||||||
|
|
||||||
|
# Parsing
|
||||||
|
def spec(self) -> FormatSpec:
|
||||||
|
options: FormatSpecOptions = self.options()
|
||||||
|
number: FormatSpecNumber = self.number()
|
||||||
|
type: Optional[Token] = self.type()
|
||||||
|
return FormatSpec(
|
||||||
|
options=options,
|
||||||
|
number=number,
|
||||||
|
type=type
|
||||||
|
)
|
||||||
|
|
||||||
|
def options(self) -> FormatSpecOptions:
|
||||||
|
sign: Optional[Token] = None
|
||||||
|
if self.match(TokenType.PLUS, TokenType.MINUS, TokenType.SPACE):
|
||||||
|
sign = self.previous()
|
||||||
|
|
||||||
|
return FormatSpecOptions(
|
||||||
|
sign=sign
|
||||||
|
)
|
||||||
|
|
||||||
|
def number(self) -> FormatSpecNumber:
|
||||||
|
integral: FormatSpecIntegral = self.integral()
|
||||||
|
decimal: FormatSpecDecimal = self.decimal()
|
||||||
|
return FormatSpecNumber(integral=integral, decimal=decimal)
|
||||||
|
|
||||||
|
def integral(self) -> FormatSpecIntegral:
|
||||||
|
width: Optional[int] = None
|
||||||
|
grouping: Optional[Token] = None
|
||||||
|
if self.match(TokenType.NUMBER):
|
||||||
|
width = self.previous().value
|
||||||
|
if self.match(TokenType.COMMA, TokenType.UNDERSCORE):
|
||||||
|
grouping = self.previous().value
|
||||||
|
return FormatSpecIntegral(
|
||||||
|
width=width,
|
||||||
|
grouping=grouping
|
||||||
|
)
|
||||||
|
|
||||||
|
def decimal(self) -> FormatSpecDecimal:
|
||||||
|
precision: Optional[int] = None
|
||||||
|
grouping: Optional[Token] = None
|
||||||
|
if self.match(TokenType.DOT):
|
||||||
|
if self.match(TokenType.NUMBER):
|
||||||
|
precision = self.previous().value
|
||||||
|
if self.match(TokenType.COMMA, TokenType.UNDERSCORE):
|
||||||
|
grouping = self.previous().value
|
||||||
|
return FormatSpecDecimal(
|
||||||
|
precision=precision,
|
||||||
|
grouping=grouping
|
||||||
|
)
|
||||||
|
|
||||||
|
def type(self) -> Optional[Token]:
|
||||||
|
if self.match(*self.TYPES):
|
||||||
|
return self.previous()
|
||||||
|
return None
|
||||||
34
src/core/format_spec/spec.py
Normal file
34
src/core/format_spec/spec.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.core.format_spec.token import Token
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormatSpecOptions:
|
||||||
|
sign: Optional[Token]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormatSpecIntegral:
|
||||||
|
width: Optional[int]
|
||||||
|
grouping: Optional[Token]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormatSpecDecimal:
|
||||||
|
precision: Optional[int]
|
||||||
|
grouping: Optional[Token]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormatSpecNumber:
|
||||||
|
integral: FormatSpecIntegral
|
||||||
|
decimal: FormatSpecDecimal
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormatSpec:
|
||||||
|
options: FormatSpecOptions
|
||||||
|
number: FormatSpecNumber
|
||||||
|
type: Optional[Token]
|
||||||
30
src/core/format_spec/string_formatter.py
Normal file
30
src/core/format_spec/string_formatter.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from src.core.format_spec.spec import FormatSpec
|
||||||
|
from src.core.format_spec.token import TokenType, Token
|
||||||
|
from src.interpreter.error import PebbleRuntimeError
|
||||||
|
|
||||||
|
|
||||||
|
class StringFormatter:
|
||||||
|
@staticmethod
|
||||||
|
def stringify(obj: Any):
|
||||||
|
if obj is None:
|
||||||
|
return "null"
|
||||||
|
if obj is True:
|
||||||
|
return "true"
|
||||||
|
if obj is False:
|
||||||
|
return "false"
|
||||||
|
if isinstance(obj, (int, float)):
|
||||||
|
if obj.is_integer():
|
||||||
|
obj = int(obj)
|
||||||
|
return str(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_type(token: Token, obj: Any, expected_type: type | tuple[type, ...]):
|
||||||
|
if not isinstance(obj, expected_type):
|
||||||
|
raise PebbleRuntimeError(token, f"Invalid value type. Expected {expected_type}, got {type(obj)}")
|
||||||
|
|
||||||
|
def format(self, obj: Any, spec: FormatSpec) -> str:
|
||||||
|
# TODO
|
||||||
|
return str(obj)
|
||||||
@@ -32,6 +32,7 @@ class TokenType(Enum):
|
|||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
NUMBER = auto()
|
NUMBER = auto()
|
||||||
|
DOT = auto()
|
||||||
EOF = auto()
|
EOF = auto()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, While
|
|||||||
ReturnStmt, BreakStmt, ContinueStmt, ClassStmt
|
ReturnStmt, BreakStmt, ContinueStmt, ClassStmt
|
||||||
from src.consts import CONSTRUCTOR_NAME
|
from src.consts import CONSTRUCTOR_NAME
|
||||||
from src.core.callable import PebbleCallable
|
from src.core.callable import PebbleCallable
|
||||||
|
from src.core.format_spec.string_formatter import StringFormatter
|
||||||
from src.core.function import PebbleFunction
|
from src.core.function import PebbleFunction
|
||||||
from src.core.instance import PebbleInstance
|
from src.core.instance import PebbleInstance
|
||||||
from src.core.klass import PebbleClass
|
from src.core.klass import PebbleClass
|
||||||
@@ -186,7 +187,10 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def visit_fstring_embed_expr(self, expr: FStringEmbedExpr) -> Any:
|
def visit_fstring_embed_expr(self, expr: FStringEmbedExpr) -> Any:
|
||||||
return self.stringify(self.evaluate(expr.expression))
|
value: Any = self.evaluate(expr.expression)
|
||||||
|
if expr.spec is None:
|
||||||
|
return self.stringify(value)
|
||||||
|
return StringFormatter().format(value, expr.spec)
|
||||||
|
|
||||||
def visit_variable_expr(self, expr: VariableExpr) -> Any:
|
def visit_variable_expr(self, expr: VariableExpr) -> Any:
|
||||||
return self.look_up_variable(expr.name, expr)
|
return self.look_up_variable(expr.name, expr)
|
||||||
@@ -343,14 +347,4 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def stringify(obj: Any) -> str:
|
def stringify(obj: Any) -> str:
|
||||||
if obj is None:
|
return StringFormatter.stringify(obj)
|
||||||
return "null"
|
|
||||||
if obj is True:
|
|
||||||
return "true"
|
|
||||||
if obj is False:
|
|
||||||
return "false"
|
|
||||||
if isinstance(obj, (int, float)):
|
|
||||||
if obj.is_integer():
|
|
||||||
obj = int(obj)
|
|
||||||
return str(obj)
|
|
||||||
return obj
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from src.ast.expr import Expr, BinaryExpr, UnaryExpr, LiteralExpr, GroupingExpr,
|
|||||||
from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \
|
from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \
|
||||||
ReturnStmt, BreakStmt, ContinueStmt, ClassStmt
|
ReturnStmt, BreakStmt, ContinueStmt, ClassStmt
|
||||||
from src.consts import MAX_FUNCTION_ARGS
|
from src.consts import MAX_FUNCTION_ARGS
|
||||||
|
from src.core.format_spec.parser import FormatSpecParser
|
||||||
|
from src.core.format_spec.spec import FormatSpec
|
||||||
from src.parser.error import ParsingError
|
from src.parser.error import ParsingError
|
||||||
from src.pebble import Pebble
|
from src.pebble import Pebble
|
||||||
from src.token.token import Token, TokenType
|
from src.token.token import Token, TokenType
|
||||||
@@ -391,14 +393,17 @@ class Parser:
|
|||||||
|
|
||||||
def fstring(self) -> Expr:
|
def fstring(self) -> Expr:
|
||||||
start: Token = self.previous()
|
start: Token = self.previous()
|
||||||
parts: list[Expr] = []
|
parts: list[LiteralExpr | FStringEmbedExpr] = []
|
||||||
|
|
||||||
while not self.check(TokenType.FSTRING_END) and not self.is_at_end():
|
while not self.check(TokenType.FSTRING_END) and not self.is_at_end():
|
||||||
if self.match(TokenType.LEFT_BRACE):
|
if self.match(TokenType.LEFT_BRACE):
|
||||||
brace: Token = self.previous()
|
brace: Token = self.previous()
|
||||||
expr: Expr = self.expression()
|
expr: Expr = self.expression()
|
||||||
|
spec: Optional[FormatSpec] = None
|
||||||
|
if self.match(TokenType.FORMAT_SPEC):
|
||||||
|
spec = FormatSpecParser(self.previous().value).parse()
|
||||||
self.consume(TokenType.RIGHT_BRACE, "Expected '}' after f-string embed")
|
self.consume(TokenType.RIGHT_BRACE, "Expected '}' after f-string embed")
|
||||||
parts.append(FStringEmbedExpr(brace, expr, self.previous()))
|
parts.append(FStringEmbedExpr(brace, expr, spec, self.previous()))
|
||||||
else:
|
else:
|
||||||
self.consume(TokenType.FSTRING_TEXT, "Unexpected token")
|
self.consume(TokenType.FSTRING_TEXT, "Unexpected token")
|
||||||
parts.append(LiteralExpr(self.previous().value))
|
parts.append(LiteralExpr(self.previous().value))
|
||||||
|
|||||||
Reference in New Issue
Block a user