22for the Kirin-based compilers.
33"""
44
5- import abc
5+ from __future__ import annotations
6+
7+ import os
68import sys
9+ import math
710import types
8-
9- stacktrace = False
10-
11-
12- class NoPythonStackTrace (Exception ):
13- pass
14-
15-
16- class CustomStackTrace (Exception ):
17-
18- @abc .abstractmethod
19- def print_stacktrace (self ) -> None : ...
11+ import shutil
12+ import textwrap
13+ from typing import TYPE_CHECKING
14+
15+ from rich .console import Console
16+
17+ if TYPE_CHECKING :
18+ from kirin import interp
19+ from kirin .source import SourceInfo
20+
21+ KIRIN_INTERP_STATE = "__kirin_interp_state"
22+ KIRIN_PYTHON_STACKTRACE = os .environ .get ("KIRIN_PYTHON_STACKTRACE" , "0" ) == "1"
23+ KIRIN_STATIC_CHECK_LINENO = os .environ .get ("KIRIN_STATIC_CHECK_LINENO" , "1" ) == "1"
24+ KIRIN_STATIC_CHECK_INDENT = int (os .environ .get ("KIRIN_STATIC_CHECK_INDENT" , "2" ))
25+ KIRIN_STATIC_CHECK_MAX_LINES = int (os .environ .get ("KIRIN_STATIC_CHECK_MAX_LINES" , "3" ))
26+
27+
28+ class StaticCheckError (Exception ):
29+ def __init__ (self , * messages : str , help : str | None = None ) -> None :
30+ super ().__init__ (* messages )
31+ self .help : str | None = help
32+ self .source : SourceInfo | None = None
33+ self .lines : list [str ] | None = None
34+ self .indent : int = KIRIN_STATIC_CHECK_INDENT
35+ self .max_lines : int = KIRIN_STATIC_CHECK_MAX_LINES
36+ self .show_lineno : bool = KIRIN_STATIC_CHECK_LINENO
37+
38+ def hint (self ):
39+ help = self .help or ""
40+ source = self .source or SourceInfo (0 , 0 , 0 , 0 )
41+ lines = self .lines or []
42+ begin = max (0 , source .lineno - self .max_lines )
43+ end = max (
44+ max (source .lineno + self .max_lines , source .end_lineno or 1 ),
45+ 1 ,
46+ )
47+ end = min (len (lines ), end ) # make sure end is within bounds
48+ lines = lines [begin :end ]
49+ error_lineno = source .lineno + source .lineno_begin
50+ error_lineno_len = len (str (error_lineno ))
51+ code_indent = min (map (self .__get_indent , lines ), default = 0 )
52+
53+ console = Console (force_terminal = True )
54+ with console .capture () as capture :
55+ console .print (
56+ f" { source or 'stdin' } " ,
57+ markup = True ,
58+ highlight = False ,
59+ )
60+ for lineno , line in enumerate (lines , begin ):
61+ line = " " * self .indent + line [code_indent :]
62+ if self .show_lineno :
63+ if lineno + 1 == source .lineno :
64+ line = f"{ error_lineno } [dim]│[/dim]" + line
65+ else :
66+ line = "[dim]" + " " * (error_lineno_len ) + "│[/dim]" + line
67+ console .print (" " + line , markup = True , highlight = False )
68+ if lineno + 1 == source .lineno :
69+ console .print (
70+ " "
71+ + self .__arrow (
72+ source ,
73+ code_indent ,
74+ error_lineno_len ,
75+ help ,
76+ self .indent ,
77+ self .show_lineno ,
78+ ),
79+ markup = True ,
80+ highlight = False ,
81+ )
82+ return capture .get ()
83+
84+ def __arrow (
85+ self ,
86+ source : SourceInfo ,
87+ code_indent : int ,
88+ error_lineno_len : int ,
89+ help ,
90+ indent : int ,
91+ show_lineno : bool ,
92+ ) -> str :
93+ ret = " " * (source .col_offset - code_indent )
94+ if source .end_col_offset :
95+ ret += "^" * (source .end_col_offset - source .col_offset )
96+ else :
97+ ret += "^"
98+
99+ ret = " " * indent + "[red]" + ret
100+ if help :
101+ hint_indent = len (ret ) - len ("[ret]" ) + len (" help: " )
102+ terminal_width = math .floor (shutil .get_terminal_size ().columns * 0.7 )
103+ terminal_width = max (terminal_width - hint_indent , 10 )
104+ wrapped = textwrap .fill (str (help ), width = terminal_width )
105+ lines = wrapped .splitlines ()
106+ ret += " help: " + lines [0 ] + "[/red]"
107+ for line in lines [1 :]:
108+ ret += (
109+ "\n "
110+ + " " * (error_lineno_len + indent )
111+ + "[dim]│[/dim]"
112+ + " " * hint_indent
113+ + "[red]"
114+ + line
115+ + "[/red]"
116+ )
117+ if show_lineno :
118+ ret = " " * error_lineno_len + "[dim]│[/dim]" + ret
119+ return ret
120+
121+ @staticmethod
122+ def __get_indent (line : str ) -> int :
123+ if len (line ) == 0 :
124+ return int (1e9 ) # very large number
125+ return len (line ) - len (line .lstrip ())
20126
21127
22128def enable_stracetrace ():
23129 """Enable the stacktrace for all exceptions."""
24- global stacktrace
25- stacktrace = True
130+ global KIRIN_PYTHON_STACKTRACE
131+ KIRIN_PYTHON_STACKTRACE = True
26132
27133
28134def disable_stracetrace ():
29135 """Disable the stacktrace for all exceptions."""
30- global stacktrace
31- stacktrace = False
136+ global KIRIN_PYTHON_STACKTRACE
137+ KIRIN_PYTHON_STACKTRACE = False
138+
139+
140+ def print_stacktrace (exception : Exception , state : interp .InterpreterState ):
141+ frame : interp .FrameABC | None = state .current_frame
142+ print (
143+ "==== Python stacktrace has been disabled for simplicity, set KIRIN_PYTHON_STACKTRACE=1 to enable it ===="
144+ )
145+ print (f"{ type (exception ).__name__ } : { exception } " , file = sys .stderr )
146+ print ("Traceback (most recent call last):" , file = sys .stderr )
147+ frames : list [interp .FrameABC ] = []
148+ while frame is not None :
149+ frames .append (frame )
150+ frame = frame .parent
151+ frames .reverse ()
152+ for frame in frames :
153+ if stmt := frame .current_stmt :
154+ print (" " + repr (stmt .source ), file = sys .stderr )
155+ print (" " + stmt .print_str (end = "" ), file = sys .stderr )
32156
33157
34158def exception_handler (exc_type , exc_value , exc_tb : types .TracebackType ):
35159 """Custom exception handler to format and print exceptions."""
36- if not stacktrace and issubclass (exc_type , NoPythonStackTrace ):
37- print ("" .join (msg for msg in exc_value .args ), file = sys .stderr )
160+ if not KIRIN_PYTHON_STACKTRACE and issubclass (exc_type , StaticCheckError ):
161+ console = Console (force_terminal = True )
162+ with console .capture () as capture :
163+ console .print (f"[bold red]{ exc_type .__name__ } :[/bold red]" , end = "" )
164+ print (capture .get (), * exc_value .args , file = sys .stderr )
165+ print ("Source Traceback:" , file = sys .stderr )
166+ print (exc_value .hint (), file = sys .stderr , end = "" )
38167 return
39168
40- if not stacktrace and issubclass (exc_type , CustomStackTrace ):
169+ if (
170+ not KIRIN_PYTHON_STACKTRACE
171+ and (state := getattr (exc_value , KIRIN_INTERP_STATE , None )) is not None
172+ ):
41173 # Handle custom stack trace exceptions
42- exc_value . print_stacktrace ()
174+ print_stacktrace (exc_value , state )
43175 return
44176
45177 # Call the default exception handler
@@ -51,7 +183,7 @@ def exception_handler(exc_type, exc_value, exc_tb: types.TracebackType):
51183
52184
53185def custom_exc (shell , etype , evalue , tb , tb_offset = None ):
54- if issubclass (etype , NoPythonStackTrace ):
186+ if issubclass (etype , StaticCheckError ):
55187 # Handle BuildError exceptions
56188 print (evalue , file = sys .stderr )
57189 return
0 commit comments