diff --git a/README.markdown b/README.markdown index 481183e..dcda79b 100644 --- a/README.markdown +++ b/README.markdown @@ -221,6 +221,22 @@ + ignored = (whatever stuff) +And note that variables are subject to backtracking, too; if a variable is +set while parsing something that failed, it is no longer set in the `|` +alternative. + + | main = set E = original & + | (set E = changed && "0" && "1" | "0" && "2") & + | return E. + + 0 1 + = changed + + | main = set E = original & + | (set E = changed && "0" && "1" | "0" && "2") & + | return E. + + 0 2 + = original + The rule `fail` always fails. This lets you establish global flags, of a sort. diff --git a/eg/backtrack.tamsin b/eg/backtrack.tamsin new file mode 100644 index 0000000..2db2fd5 --- /dev/null +++ b/eg/backtrack.tamsin @@ -0,0 +1,3 @@ +main = set E = original & + (set E = changed && "0" && "1" | "0" && "2") & + return E. diff --git a/eg/bitpair.tamsin b/eg/bitpair.tamsin new file mode 100644 index 0000000..b611870 --- /dev/null +++ b/eg/bitpair.tamsin @@ -0,0 +1,3 @@ +main = bit → A & bit → B & return (A B). +bit = "0" | "1". + diff --git a/src/tamsin.py b/src/tamsin.py index 2f03cdb..b4fa048 100755 --- a/src/tamsin.py +++ b/src/tamsin.py @@ -30,8 +30,11 @@ return result def error(self, expected): + report = self.buffer[:20] + if len(self.buffer) > 20: + report += '...' raise ValueError("Expected %s, found '%s' at '%s...'" % - (expected, self.token, self.buffer[:20])) + (expected, self.token, report)) def scan(self): self.scan_impl() @@ -186,7 +189,44 @@ self.input = input_.split(' ') self.position = 0 self.scan() + self.contexts = [] #print repr(self.input) + + ### context stuff ---------------------------------------- ### + + def push_context(self, purpose): + debug("pushing new context for %r" % purpose) + self.contexts.append({}) + + def pop_context(self, purpose): + debug("popping context for %r" % purpose) + self.contexts.pop() + + def current_context(self): + """Don't assume anything about what this returns except that + you can pass it to install_context(). + + """ + debug("retrieving current context %r" % self.contexts[-1]) + return self.contexts[-1].copy() + + def install_context(self, context): + debug("installing context %r" % context) + self.contexts[-1] = context + + def fetch(self, name): + debug("fetching %s (it's %r)" % + (name, self.contexts[-1].get(name, 'undefined')) + ) + return self.contexts[-1][name] + + def store(self, name, value): + debug("updating %s (was %s) to %r" % + (name, self.contexts[-1].get(name, 'undefined'), value) + ) + self.contexts[-1][name] = value + + ### scanner stuff ---------------------------------------- ### def scan(self): if self.position >= len(self.input): @@ -199,6 +239,8 @@ self.position = position self.token = self.input[position - 1] + ### grammar stuff ---------------------------------------- ### + def find_production(self, name): found = None for x in self.program[1]: @@ -209,15 +251,18 @@ raise ValueError("No '%s' production defined" % name) return found - def replace_vars(self, ast, context): - """Expands a term.""" + ### term stuff ---------------------------------------- ### + + def replace_vars(self, ast): + """Expands a term, replacing all (VAR x) with the value of x + in the current context.""" if ast[0] == 'ATOM': return ast elif ast[0] == 'LIST': - return ('LIST', [self.replace_vars(x, context) for x in ast[1]]) + return ('LIST', [self.replace_vars(x) for x in ast[1]]) elif ast[0] == 'VAR': - return context[ast[1]] # context.get(ast[1], None) + return self.fetch(ast[1]) else: raise NotImplementedError("internal error: bad term") @@ -231,58 +276,56 @@ else: raise NotImplementedError("internal error: bad term") - def interpret(self, ast, context): + ### interpreter proper ---------------------------------- ### + + def interpret(self, ast): if ast[0] == 'PROGRAM': - return self.interpret(self.find_production('main'), context) + return self.interpret(self.find_production('main')) elif ast[0] == 'PROD': #print "interpreting %s" % repr(ast) - return self.interpret(ast[2], {}) + self.push_context(ast[1]) + x = self.interpret(ast[2]) + self.pop_context(ast[1]) + return x elif ast[0] == 'CALL': - new_context = {} - result = self.interpret(self.find_production(ast[1]), new_context) + result = self.interpret(self.find_production(ast[1])) if ast[2] is not None: assert ast[2][0] == 'VAR', ast varname = ast[2][1] - debug("updating %s (was %s) to %r" % - (varname, context.get(varname, 'undefined'), result) - ) - context[varname] = result + self.store(varname, result) return result elif ast[0] == 'SET': assert ast[1][0] == 'VAR', ast varname = ast[1][1] - result = self.replace_vars(ast[2], context) - debug("setting %s (was %s) to %r" % - (varname, context.get(varname, 'undefined'), result) - ) - context[varname] = result + result = self.replace_vars(ast[2]) + self.store(varname, result) return result elif ast[0] == 'AND': lhs = ast[1] rhs = ast[2] - value_lhs = self.interpret(lhs, context) - value_rhs = self.interpret(rhs, context) + value_lhs = self.interpret(lhs) + value_rhs = self.interpret(rhs) return value_rhs elif ast[0] == 'OR': lhs = ast[1] rhs = ast[2] - saved_context = context.copy() + saved_context = self.current_context() saved_position = self.position try: - return self.interpret(lhs, context) + return self.interpret(lhs) except TamsinParseError as e: - context = saved_context + self.install_context(saved_context) self.rewind(saved_position) - return self.interpret(rhs, context) + return self.interpret(rhs) elif ast[0] == 'RETURN': - return self.replace_vars(ast[1], context) + return self.replace_vars(ast[1]) elif ast[0] == 'FAIL': raise TamsinParseError("fail") elif ast[0] == 'WHILE': result = ('ATOM', 'nil') while True: try: - result = self.interpret(ast[1], context) + result = self.interpret(ast[1]) except TamsinParseError as e: return result elif ast[0] == 'LITERAL': @@ -297,21 +340,29 @@ raise NotImplementedError(repr(ast)) -if __name__ == '__main__': - if sys.argv[1] == 'parse': - with codecs.open(sys.argv[2], 'r', 'UTF-8') as f: +def main(args): + global DEBUG + if args[0] == '--debug': + DEBUG = True + args = args[1:] + if args[0] == 'parse': + with codecs.open(args[1], 'r', 'UTF-8') as f: contents = f.read() parser = Parser(contents) ast = parser.grammar() print repr(ast) - elif sys.argv[1] == 'run': - with codecs.open(sys.argv[2], 'r', 'UTF-8') as f: + elif args[0] == 'run': + with codecs.open(args[1], 'r', 'UTF-8') as f: contents = f.read() parser = Parser(contents) ast = parser.grammar() #print repr(ast) interpreter = Interpreter(ast, sys.stdin.read()) - result = interpreter.interpret(ast, {}) + result = interpreter.interpret(ast) print interpreter.stringify_term(result) else: raise ValueError("first argument must be 'parse' or 'run'") + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/test.sh b/test.sh index 61eaf40..c3fd733 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,3 @@ #!/bin/sh -falderal --substring-error README.md +falderal --substring-error README.markdown