From a751b3de1773735ed75384b293269353d94d38bd Mon Sep 17 00:00:00 2001 From: "Juan J. Martinez" Date: Sun, 26 May 2024 21:02:45 +0100 Subject: Add better error reporting --- README.md | 2 +- TODO.md | 5 +- funco | 235 +++++++++++++++++++++++++++++++++++++++++++------------------- 3 files changed, 166 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 0a4f3f2..fa84f62 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Literals include: * numbers (int or float): `1`, `1.5` * strings: `"this is a string"` -* booleans (true or false), as result of some logical functions; although 0 is false and non-zero is true +* booleans (`true` or `false`), as result of some logical functions; although 0 is false and non-zero is true * lists * functions * none for "no value" diff --git a/TODO.md b/TODO.md index ba84968..39dc044 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,4 @@ -* improve error reporting - * probably wrapping the tokens in an object -- what will be the impact in performance? +* improve error reporting further * write tests? * more examples -* an Funco interpreter in Funco? +* a Funco interpreter in Funco? diff --git a/funco b/funco index 542980a..d433fde 100755 --- a/funco +++ b/funco @@ -2,17 +2,79 @@ import operator import itertools +import sys from argparse import ArgumentParser +from dataclasses import dataclass -__version__ = "0.1" +__version__ = "0.2" -Ident = str -List = list -Number = (int, float) +def error(msg): + sys.exit(f"Error: {msg}") -class String(str): - pass + +@dataclass() +class Location(object): + line: int + col: int + + def __str__(self): + return f"(line: {self.line}, col: {self.col})" + + +@dataclass() +class Token(object): + pos: Location + + +@dataclass() +class Lpar(Token): + value = "(" + + +@dataclass() +class Rpar(Token): + value = ")" + + +@dataclass() +class Colon(Token): + value = ":" + + +@dataclass() +class Tag(Token): + value = "@" + + +@dataclass() +class Ident(Token): + value: str + + +@dataclass() +class List(Token): + value: list + + +@dataclass() +class Int(Token): + value: int + + +@dataclass() +class Float(Token): + value: float + + +@dataclass() +class String(Token): + value: str + + +@dataclass() +class Boolean(Token): + value: bool class TailRec(Exception): @@ -23,17 +85,17 @@ class TailRec(Exception): class Env(dict): def __init__(self, parms=(), args=(), parent=None): - self.update(zip(parms, args)) + self.update(zip([p.value for p in parms], args)) self.parent = parent - def find(self, var: str): - if var in self: + def find(self, var: Ident): + if var.value in self: return self else: if self.parent: return self.parent.find(var) else: - raise ValueError(f"not found: {var}") + error(f"{var.pos}: Ident not found {var.value}") class Function(object): @@ -56,8 +118,8 @@ class Function(object): call_env = Env(self.parms, args, self.env) return eval_block(self.body, call_env) except TailRec as tr: - if self.env.find(tr.ident)[tr.ident] != self: - raise RuntimeError("tagged tail call can't be optimized") + if self.env.find(tr.ident)[tr.ident.value] != self: + error(f"{tr.pos}: tagged tail call can't be optimized") args = [eval(expr, call_env) for expr in tr.args] @@ -85,32 +147,54 @@ class If(object): return out -def tokenize(program: str) -> list[str]: +def tokenize(program: str) -> list[Token]: single: str = "():@" - + line: int = 1 + col: int = 1 i = 0 while i < len(program): if program[i].isspace(): while i < len(program) and program[i].isspace(): + if program[i] == "\n": + line += 1 + col = 1 + else: + col += 1 i += 1 continue c = program[i] - if program[i] in single: - yield c + if program[i] == "(": + yield Lpar(Location(line, col)) i += 1 + col += 1 + elif program[i] == ")": + yield Rpar(Location(line, col)) + i += 1 + col += 1 + elif program[i] == ":": + yield Colon(Location(line, col)) + i += 1 + col += 1 + elif program[i] == "@": + yield Tag(Location(line, col)) + i += 1 + col += 1 elif c == '"': j = i + 1 while j < len(program) and program[j] != '"': j += 1 if j == len(program): - raise SyntaxError('EOF in open string') - yield program[i:j + 1] + error(f"{Location(line, col)}: EOF in open string") + yield String(Location(line, col), program[i + 1:j]) + col += (j - i) + 1 i = j + 1 - elif c == '#': + elif c == "#": j = i + 1 - while j < len(program) and program[j] != '\n': + while j < len(program) and program[j] != "\n": j += 1 + line += 1 + col = 1 i = j + 1 else: j = i @@ -118,8 +202,17 @@ def tokenize(program: str) -> list[str]: not program[j].isspace() and program[j] not in single ): j += 1 - yield program[i:j] + lit = program[i:j] + if lit[0].isdigit() and "." in lit: + yield Float(Location(line, col), float(lit)) + elif lit[0].isdigit(): + yield Int(Location(line, col), int(lit)) + elif lit in ("true", "false"): + yield Boolean(Location(line, col), lit == "true") + else: + yield Ident(Location(line, col), lit) i = j + col += len(lit) class Parser(object): @@ -134,33 +227,34 @@ class Parser(object): self.tokens = itertools.chain([n], self.tokens) return n - def expect(self, expected: str): - if self.next() != expected: - raise SyntaxError(f"{expected} expected") + def expect(self, expected): + found = self.next() + if not isinstance(found, expected): + error(f"{found.pos}: {expected.value} expected, {found.value} found instead") def parse_parms(self) -> list: parms = [] while True: param = self.next() - if param == ")": + if isinstance(param, Rpar): break - if param.isdigit() or not param.isalnum(): - raise SyntaxError("ident expected") + if not isinstance(param, Ident): + error(f"{param.pos}: Ident expected, {type(param).__name__} found") parms.append(param) return parms def parse_function(self): ident = self.next() - self.expect("(") + self.expect(Lpar) parms = self.parse_parms() return Function(ident, parms, self.parse(in_block=True)) def parse_call(self, ident): - self.expect("(") + self.expect(Lpar) args = [] while True: arg = self.next() - if arg == ")": + if isinstance(arg, Rpar): break args.append(self.parse_expr(arg)) return Call(ident, args) @@ -175,7 +269,7 @@ class Parser(object): exprs = [] while True: expr = self.next() - if expr == "end": + if isinstance(expr, Ident) and expr.value == "end": break exprs.append(self.parse_expr(expr)) return exprs @@ -188,55 +282,49 @@ class Parser(object): otherwise = None while True: token = self.next() - if token == "end": - break - if token == "elif": - conds.append(new_cond()) - continue - elif token == "else": - otherwise = self.parse_else() - break - else: - conds[-1][1].append(self.parse_expr(token)) + if isinstance(token, Ident): + if token.value == "end": + break + elif token.value == "elif": + conds.append(new_cond()) + continue + elif token.value == "else": + otherwise = self.parse_else() + break + conds[-1][1].append(self.parse_expr(token)) return If(conds, otherwise) def parse_expr(self, expr): n = self.peek() - if expr == "@": + if isinstance(expr, Tag): return self.parse_tailrec() - elif expr[0] == '"': - return String(expr[1:len(expr) - 1]) - elif expr == "if": + elif isinstance(expr, Ident) and expr.value == "if": return self.parse_if() - elif n == "(": + elif isinstance(n, Lpar): return self.parse_call(expr) - try: - return int(expr) - except ValueError: - try: - return float(expr) - except ValueError: - return Ident(expr) + else: + return expr def parse(self, in_block: bool = False) -> list: try: self.peek() except StopIteration: - raise SyntaxError("unexpected EOF") + error("unexpected EOF") out: list = [] try: while True: - match self.next(): - case "def": + token = self.next() + if isinstance(token, Ident): + if token.value == "def": out.append(self.parse_function()) - case "end": + continue + elif token.value == "end": if in_block: return out else: - raise SyntaxError("end not in open block") - case expr: - out.append(self.parse_expr(expr)) + error(f"{token.pos}: end not in open block") + out.append(self.parse_expr(token)) except StopIteration: return out @@ -248,12 +336,10 @@ def eval_block(exprs, env): def eval(x, env): - if isinstance(x, String): - return x - elif isinstance(x, Ident): - return env.find(x)[x] + if isinstance(x, Ident): + return env.find(x)[x.value] elif isinstance(x, Function): - env[x.ident] = x.with_env(env) + env[x.ident.value] = x.with_env(env) elif isinstance(x, If): for cond, exprs in x.conds: if eval(cond, env) != 0: @@ -266,7 +352,12 @@ def eval(x, env): if x.tail_rec: raise TailRec(x.ident, x.args) else: - return fn(*args) + try: + return fn(*args) + except Exception as ex: + error(f"{x.ident.pos}: {ex}") + elif x is not None: + return x.value else: return x @@ -295,7 +386,6 @@ def default_env(): "and": operator.and_, "or": operator.or_, "not": operator.not_, - "int": int, "float": float, "string": str, @@ -307,7 +397,6 @@ def default_env(): "none?": lambda x: x is None, "boolean?": lambda x: isinstance(x, bool), "function?": lambda x: isinstance(x, Function), - "head": lambda xs: xs[0], "tail": lambda xs: xs[1:], "empty?": lambda xs: xs == [], @@ -315,7 +404,6 @@ def default_env(): "min": lambda xs: min(xs) if isinstance(xs, list) else None, "max": lambda xs: max(xs) if isinstance(xs, list) else None, "contains": lambda x, xs: x in xs, - "map": lambda f, xs: list(map(f, xs)), "filter": lambda f, xs: list(filter(f, xs)), "fold": fold, @@ -339,8 +427,11 @@ def main(): args = parser.parse_args() - with open(args.file, "rt") as fd: - src = fd.read() + try: + with open(args.file, "rt") as fd: + src = fd.read() + except Exception as ex: + error(ex) tokens = tokenize(src) parser = Parser(tokens) @@ -349,7 +440,7 @@ def main(): for expr in ast: eval(expr, env) if "main" not in env: - raise ValueError("main not found") + error("main not found") eval(env["main"](), env) -- cgit v1.2.3