From 017aefa7501046a21236df0afc7b2414cdf97f48 Mon Sep 17 00:00:00 2001 From: Antoine Viallon Date: Wed, 10 May 2023 01:21:59 +0200 Subject: [PATCH] logger+parser: add a Tracer class featuring function-wrappers and reflection Generates clean function call stack traces as they are called --- compiler/logger.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++ compiler/parser.py | 16 ++++++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/compiler/logger.py b/compiler/logger.py index ab1980b..2783bde 100644 --- a/compiler/logger.py +++ b/compiler/logger.py @@ -1,8 +1,11 @@ from __future__ import annotations import enum +import inspect import logging import os +import typing +from typing import Callable _TRACE = (logging.DEBUG + logging.NOTSET) // 2 logging.addLevelName(_TRACE, "TRACE") @@ -78,4 +81,53 @@ class Logger: self.log(LogLevel.Trace, *args) +T = typing.TypeVar("T") + + +class Tracer: + def __init__(self, logger: Logger, level: LogLevel = LogLevel.Trace, max_depth: int = 15): + self.depth: int = 0 + self.level = level + self.logger = logger + self.max_depth: int = max_depth + + def trace_method(self, func_or_nothing: Callable[..., T] | None = None, **kwargs): + if func_or_nothing is not None: + return self._trace_method(func=func_or_nothing, **kwargs) + + return lambda func: self._trace_method(func, **kwargs) + + def _trace_method(self, func: Callable[..., T], level: LogLevel = None) -> Callable[..., T]: + if level is None: + level = self.level + + if self.logger.level > level: + return func + + func_info = inspect.signature(func) + + def _wrapped(*args, **kwargs) -> T: + bound_args = func_info.bind(*args, **kwargs) + signature = ", ".join( + key if key == "self" else f"{key}={repr(value)}" + for key, value in bound_args.arguments.items() + ) + prefix = f"[{self.depth: >4}]" + " " * self.depth + + if self.depth == self.max_depth: + self.logger.log(level, f"{prefix} > ... (max depth of {self.max_depth} reached)") + if self.depth < self.max_depth: + self.logger.log(level, f"{prefix} > Entering {func.__name__}({signature})") + + self.depth += 1 + r: T = func(*args, **kwargs) + self.depth -= 1 + + if self.depth < self.max_depth: + self.logger.log(level, f"{prefix} < Leaving {func.__name__}({signature})") + return r + + return _wrapped + + logger = Logger("compiler") diff --git a/compiler/parser.py b/compiler/parser.py index 0716d84..1be6096 100644 --- a/compiler/parser.py +++ b/compiler/parser.py @@ -3,13 +3,13 @@ from __future__ import annotations from beartype.typing import List, Dict, Callable from .errors import CompilationError, UnexpectedTokenError -from .logger import Logger +from .logger import Logger, Tracer, LogLevel from .nodes import Float, Sum, Value, Product, Node, Division, Sub, Integer, Expression, Identifier, Assignment, \ Variable from .tokenizer import Tokens, Token logger = Logger(__name__) - +tracer = Tracer(logger, level=LogLevel.Debug) class Parser: def __init__(self, tokens: List[Token]): @@ -31,6 +31,7 @@ class Parser: self.pos += 1 logger.debug(f"Advancing to token {self.pos} {self.token}") + @tracer.trace_method(level=LogLevel.Trace) def accept(self, *token_types: Tokens) -> False | Token: tok = self.token if self.token.kind in token_types: @@ -45,6 +46,7 @@ class Parser: return tok return False + @tracer.trace_method(level=LogLevel.Trace) def expect(self, token_type: Tokens) -> Token: r = self.accept(token_type) logger.debug(f"Expecting {token_type}, got {r}") @@ -52,6 +54,7 @@ class Parser: raise UnexpectedTokenError(self.token, token_type) return r + @tracer.trace_method def number(self, mandatory: bool = False): if tok := self.accept(Tokens.Float): logger.debug(f"Found float {tok}") @@ -62,18 +65,21 @@ class Parser: elif mandatory: raise UnexpectedTokenError(self.token, "integer or float") + @tracer.trace_method def identifier(self, mandatory: bool = False) -> Identifier: if ident := self.accept(Tokens.Identifier): return Identifier(location=ident.loc, name=str(ident.value)) elif mandatory: raise UnexpectedTokenError(self.token, "identifier") + @tracer.trace_method def variable(self, mandatory: bool = False) -> Variable: if ident := self.identifier(mandatory=False): return Variable(location=ident.location(), identifier=ident) elif mandatory: raise UnexpectedTokenError(self.token, "variable identifier") + @tracer.trace_method def binary_op(self, operand_func: Callable[[], Value], operators: Dict[Tokens, Value]): operand = operand_func() @@ -85,6 +91,7 @@ class Parser: return operand + @tracer.trace_method def factor(self) -> Value: if self.accept(Tokens.Parens_Left): v = self.expression() @@ -97,18 +104,21 @@ class Parser: else: raise UnexpectedTokenError(self.token, "parenthesized expression, number or variable") + @tracer.trace_method def term(self) -> Value: return self.binary_op(self.factor, operators={ Tokens.Op_Multiply: Product, Tokens.Op_Divide: Division, }) + @tracer.trace_method def summation(self) -> Sum: return self.binary_op(self.term, operators={ Tokens.Op_Plus: Sum, Tokens.Op_Minus: Sub, }) + @tracer.trace_method def assignment(self, mandatory: bool = False) -> Assignment: if ident := self.identifier(mandatory): self.expect(Tokens.Equal) @@ -117,12 +127,14 @@ class Parser: elif mandatory: raise UnexpectedTokenError(self.token, "assignment") + @tracer.trace_method def expression(self) -> Value: if self.peek(Tokens.Identifier): return Expression(self.assignment()) else: return Expression(self.summation()) + @tracer.trace_method def root(self) -> Node: return self.expression()