diff --git a/auto_editor/lang/palet.py b/auto_editor/lang/palet.py index 81223bf09..f3e7536ad 100644 --- a/auto_editor/lang/palet.py +++ b/auto_editor/lang/palet.py @@ -60,6 +60,8 @@ class ClosingError(MyError): class Token: type: str value: Any + lineno: int + column: int class Lexer: @@ -156,21 +158,31 @@ def number(self) -> Token: elif unit == "dB": token = DB elif unit != "i" and unit != "%": - return Token(VAL, Sym(result + unit)) + return Token( + VAL, + Sym(result + unit, self.lineno, self.column), + self.lineno, + self.column, + ) try: if unit == "i": - return Token(VAL, complex(result + "j")) + return Token(VAL, complex(result + "j"), self.lineno, self.column) elif unit == "%": - return Token(VAL, float(result) / 100) + return Token(VAL, float(result) / 100, self.lineno, self.column) elif "/" in result: - return Token(token, Fraction(result)) + return Token(token, Fraction(result), self.lineno, self.column) elif "." in result: - return Token(token, float(result)) + return Token(token, float(result), self.lineno, self.column) else: - return Token(token, int(result)) + return Token(token, int(result), self.lineno, self.column) except ValueError: - return Token(VAL, Sym(result + unit)) + return Token( + VAL, + Sym(result + unit, self.lineno, self.column), + self.lineno, + self.column, + ) def hash_literal(self) -> Token: if self.char == "\\": @@ -180,7 +192,7 @@ def hash_literal(self) -> Token: char = self.char self.advance() - return Token(VAL, Char(char)) + return Token(VAL, Char(char), self.lineno, self.column) if self.char == ":": self.advance() @@ -190,14 +202,14 @@ def hash_literal(self) -> Token: buf.write(self.char) self.advance() - return Token(VAL, Keyword(buf.getvalue())) + return Token(VAL, Keyword(buf.getvalue()), self.lineno, self.column) if self.char is not None and self.char in "([{": brac_type = self.char self.advance() if self.char is None: self.close_err(f"Expected a character after #{brac_type}") - return Token(VLIT, brac_pairs[brac_type]) + return Token(VLIT, brac_pairs[brac_type], self.lineno, self.column) buf = StringIO() while self.char_is_norm(): @@ -207,10 +219,10 @@ def hash_literal(self) -> Token: result = buf.getvalue() if result in ("t", "T", "true"): - return Token(VAL, True) + return Token(VAL, True, self.lineno, self.column) if result in ("f", "F", "false"): - return Token(VAL, False) + return Token(VAL, False, self.lineno, self.column) self.error(f"Unknown hash literal `#{result}`") @@ -231,17 +243,19 @@ def get_next_token(self) -> Token: my_str = self.string() if self.char == ".": # handle `object.method` syntax self.advance() - return Token(DOT, (my_str, self.get_next_token())) - return Token(VAL, my_str) + return Token( + DOT, (my_str, self.get_next_token()), self.lineno, self.column + ) + return Token(VAL, my_str, self.lineno, self.column) if self.char == "'": self.advance() - return Token(QUOTE, "'") + return Token(QUOTE, "'", self.lineno, self.column) if self.char in "(){}[]": _par = self.char self.advance() - return Token(_par, _par) + return Token(_par, _par, self.lineno, self.column) if self.char in "+-": _peek = self.peek() @@ -339,18 +353,27 @@ def handle_strings() -> bool: if is_method: from auto_editor.utils.cmdkw import parse_method - return Token(M, parse_method(name, result, env)) + return Token( + M, parse_method(name, result, env), self.lineno, self.column + ) if self.char == ".": # handle `object.method` syntax self.advance() - return Token(DOT, (Sym(result), self.get_next_token())) + return Token( + DOT, + (Sym(result, self.lineno, self.column), self.get_next_token()), + self.lineno, + self.column, + ) if has_illegal: self.error(f"Symbol has illegal character(s): {result}") - return Token(VAL, Sym(result)) + return Token( + VAL, Sym(result, self.lineno, self.column), self.lineno, self.column + ) - return Token(EOF, "EOF") + return Token(EOF, "EOF", self.lineno, self.column) ############################################################################### @@ -370,6 +393,7 @@ def eat(self) -> None: def expr(self) -> Any: token = self.current_token + lineno, column = token.lineno, token.column if token.type == VAL: self.eat() @@ -397,7 +421,7 @@ def expr(self) -> Any: if token.type == M: self.eat() name, args, kwargs = token.value - _result = [Sym(name)] + args + _result = [Sym(name, lineno, column)] + args for key, val in kwargs.items(): _result.append(Keyword(key)) _result.append(val) @@ -413,7 +437,7 @@ def expr(self) -> Any: if token.type == QUOTE: self.eat() - return (Sym("quote"), self.expr()) + return (Sym("quote", lineno, column), self.expr()) if token.type in brac_pairs: self.eat() @@ -610,16 +634,37 @@ def edit_subtitle(pattern, stream=0, **kwargs): return raise_(e) if levels.strict else levels.all() +class StackTraceManager: + def __init__(self) -> None: + self.stack: list[Sym] = [] + + def push(self, sym: Sym) -> None: + self.stack.append(sym) + + def pop(self) -> None: + self.stack.pop() + + def get_stacktrace(self) -> str: + return "\n".join( + f" at {sym.val} ({sym.lineno}:{sym.column})" + for sym in reversed(self.stack) + ) + + +stack_trace_manager = StackTraceManager() + + def my_eval(env: Env, node: object) -> Any: if type(node) is Sym: val = env.get(node.val) if type(val) is NotFound: + stacktrace = stack_trace_manager.get_stacktrace() if mat := get_close_matches(node.val, env.data): raise MyError( - f"variable `{node.val}` not found. Did you mean: {mat[0]}" + f"variable `{node.val}` not found. Did you mean: {mat[0]}\n{stacktrace}" ) raise MyError( - f"variable `{node.val}` not found. Did you mean a string literal." + f"variable `{node.val}` not found. Did you mean a string literal.\n{stacktrace}" ) return val @@ -631,6 +676,9 @@ def my_eval(env: Env, node: object) -> Any: raise MyError("Illegal () expression") oper = my_eval(env, node[0]) + if isinstance(node[0], Sym): + stack_trace_manager.push(node[0]) + if not callable(oper): """ ...No one wants to write (aref a x y) when they could write a[x,y]. diff --git a/auto_editor/lang/stdenv.py b/auto_editor/lang/stdenv.py index fd622039b..f5003d943 100644 --- a/auto_editor/lang/stdenv.py +++ b/auto_editor/lang/stdenv.py @@ -222,14 +222,20 @@ def syn_lambda(env: Env, node: Node) -> UserProc: parms: list[str] = [] for item in node[1]: if type(item) is not Sym: - raise MyError(f"{node[0]}: must be an identifier") + raise MyError(f"{node[0]}: must be an identifier, got: {item} {type(item)}") parms.append(f"{item}") return UserProc(env, "", parms, (), node[2:]) def syn_define(env: Env, node: Node) -> None: + if len(node) < 2: + raise MyError(f"{node[0]}: too few terms") if len(node) < 3: + if type(node[1]) is Sym: + raise MyError(f"{node[0]}: what should `{node[1]}` be defined as?") + elif type(node[1]) is tuple and len(node[1]) > 0: + raise MyError(f"{node[0]}: function `{node[1][0]}` needs a body.") raise MyError(f"{node[0]}: too few terms") if type(node[1]) is tuple: diff --git a/auto_editor/lib/data_structs.py b/auto_editor/lib/data_structs.py index 8356ca088..f77d09c7f 100644 --- a/auto_editor/lib/data_structs.py +++ b/auto_editor/lib/data_structs.py @@ -54,12 +54,14 @@ def get(self, key: str) -> Any: class Sym: - __slots__ = ("val", "hash") + __slots__ = ("val", "hash", "lineno", "column") - def __init__(self, val: str): + def __init__(self, val: str, lineno: int = -1, column: int = -1): assert isinstance(val, str) self.val = val self.hash = hash(val) + self.lineno = lineno + self.column = column def __str__(self) -> str: return self.val diff --git a/test.pal b/test.pal new file mode 100644 index 000000000..d6c24a838 --- /dev/null +++ b/test.pal @@ -0,0 +1,10 @@ + +(define (func2 b) + c + b +) +(define (func1 a) + (func2 a) +) + +(func1 "hot")