aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuan J. Martinez <jjm@usebox.net>2024-05-26 21:02:45 +0100
committerJuan J. Martinez <jjm@usebox.net>2024-05-26 21:02:45 +0100
commita751b3de1773735ed75384b293269353d94d38bd (patch)
treef1f98a2778c0ffad231e109883c0b94d7d595dde
parentbd466aa98a25f1a4d5689ae868ab9e8c9ea4ea3b (diff)
downloadfunco-a751b3de1773735ed75384b293269353d94d38bd.tar.gz
funco-a751b3de1773735ed75384b293269353d94d38bd.zip
Add better error reporting0.2
-rw-r--r--README.md2
-rw-r--r--TODO.md5
-rwxr-xr-xfunco235
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)