diff options
author | Timotej Lazar <timotej.lazar@araneo.org> | 2014-12-08 20:43:22 +0100 |
---|---|---|
committer | Aleš Smodiš <aless@guru.si> | 2015-08-11 14:26:00 +0200 |
commit | f4bee33bfae518ac04e67232d301900bcf3ebd4e (patch) | |
tree | abb46e2c365d2a81ad1e081f104be86399664dae | |
parent | 8d468ca2abf07eac9092b08390856f761dd701cd (diff) |
Start refactoring testing code
Testing logic now lives in PrologEngine. The engine now has some notion
of problems and users, which is necessary to avoid repeatedly loading
code into Prolog.
TODO:
- support library loading
- fix PrologEngine.test for unusual cases (more than one solution, …)
- memoization of correct answers
-rw-r--r--[-rwxr-xr-x] | prolog/engine.py | 142 | ||||
-rw-r--r--[-rwxr-xr-x] | prolog/lexer.py | 0 | ||||
-rw-r--r-- | prolog/util.py | 9 |
3 files changed, 78 insertions, 73 deletions
diff --git a/prolog/engine.py b/prolog/engine.py index 299d2eb..ec27e27 100755..100644 --- a/prolog/engine.py +++ b/prolog/engine.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 from prolog.core import * -from prolog.util import * +import prolog.util class Atom(object): __slots__ = 'ref' @@ -80,29 +80,25 @@ class PrologEngine(object): # construct some predicates self.p = { - 'assertz': PL_predicate(b'assertz', 2, None), - 'erase': PL_predicate(b'erase', 1, None), - 'call_with_depth_limit': PL_predicate(b'call_with_depth_limit', 3, None), - 'call_with_time_limit': PL_predicate(b'call_with_time_limit', 2, None), - 'consult': PL_predicate(b'consult', 1, None), - 'findnsols': PL_predicate(b'findnsols', 4, None), - 'message_to_string': PL_predicate(b'message_to_string', 2, None), - 'read_term_from_atom': PL_predicate(b'read_term_from_atom', 3, None), - 'safe_goal': PL_predicate(b'safe_goal', 1, None), - 'set_prolog_stack': PL_predicate(b'set_prolog_stack', 2, None) + 'add_import_module/3': PL_predicate(b'add_import_module', 3, None), + 'assertz/1': PL_predicate(b'assertz', 1, None), + 'assertz/2': PL_predicate(b'assertz', 2, None), + 'erase/1': PL_predicate(b'erase', 1, None), + 'call_with_time_limit/2': PL_predicate(b'call_with_time_limit', 2, None), + 'consult/1': PL_predicate(b'consult', 1, None), + 'message_to_string/2': PL_predicate(b'message_to_string', 2, None), + 'read_term_from_atom/3': PL_predicate(b'read_term_from_atom', 3, None), + 'safe_goal/1': PL_predicate(b'safe_goal', 1, None), + 'set_prolog_stack/2': PL_predicate(b'set_prolog_stack', 2, None) } self.err_flags = PL_Q_NODEBUG|PL_Q_CATCH_EXCEPTION - # database of current asserts; each load gets a new index - self.refs = {} - self.max_index = 0 + # Increase memory limits. + self.call('set_prolog_stack/2', [Term('global'), Term('limit(2*10**9)')]) + self.call('set_prolog_stack/2', [Term('local'), Term('limit(2*10**9)')]) - # increase memory limits - self.call('set_prolog_stack', [Term('global'), Term('limit(2*10**9)')]) - self.call('set_prolog_stack', [Term('local'), Term('limit(2*10**9)')]) - - # discard messages from swipl library - self.load('message_hook(_, _, _). ') + # Discard messages from swipl library. + self.call('assertz/1', [Term('message_hook(_, _, _)')]) def exception(self, qid): ref = PL_exception(qid) @@ -113,7 +109,7 @@ class PrologEngine(object): if ex is not None: msg = Term() if PL_call_predicate(None, self.err_flags, - self.p['message_to_string'], Termv([ex, msg]).ref): + self.p['message_to_string/2'], Termv([ex, msg]).ref): raise Exception(str(msg)) raise Exception('Unknown error') finally: @@ -131,43 +127,7 @@ class PrologEngine(object): return True def consult(self, filename): - return self.call('consult', [Term(Atom(filename))]) - - def load(self, program, module=None): - tokens = tokenize(program) - refs = [] - try: - start = 0 - for idx in range(len(tokens)): - if tokens[idx].type != 'PERIOD' or idx - start <= 1: - continue - rule = stringify(tokens[start:idx]) - orig_rule = rule - start = idx + 1 - - if module is not None: - rule = module + ':(' + rule + ')' - ref = Term() - if self.call('assertz', [Term(rule), ref]): - refs.append(ref) - except Exception as ex: - # unload already loaded rules (hacky, mustfix, todo) - for ref in refs: - self.call('erase', [ref]) - raise Exception('Error loading program: {}\nBad rule: {}'.format(ex, orig_rule)) - - self.max_index += 1 - self.refs[self.max_index] = refs - return self.max_index - - def unload(self, index): - try: - for ref in self.refs[index]: - self.call('erase', [ref]) - del self.refs[index] - except Exception as ex: - # This should never happen. - sys.stderr.write(str(ex) + '\n') + return self.call('consult/1', [Term(Atom(filename))]) # Get up to [n] solutions to query [q]. def query(self, q, module=None, n=1): @@ -181,17 +141,17 @@ class PrologEngine(object): goal = Term() try: # Parse term and store variable names. - if not self.call('read_term_from_atom', [Term(Atom(q)), goal, options]): + if not self.call('read_term_from_atom/3', [Term(Atom(q)), goal, options]): raise Exception('Warning: Could not read term from {}\n'.format(q)) # Check if goal is safe with currently loaded rules. - if not self.call('safe_goal', [goal]): + if not self.call('safe_goal/1', [goal]): raise Exception('Warning: Unsafe goal: {}\n'.format(goal)) except Exception as ex: raise solutions = Term() goal_aux = Term('findnsols', [Term(n), goal, goal, solutions]) - qid = PL_open_query(None, self.err_flags, self.p['call_with_time_limit'], + qid = PL_open_query(None, self.err_flags, self.p['call_with_time_limit/2'], Termv([Term(0.01), goal_aux]).ref) result = [] @@ -215,15 +175,51 @@ class PrologEngine(object): return result -# Basic sanity check. -if __name__ == '__main__': - prolog = PrologEngine() - prolog.load('a(1). a(2). a(3).', 'test') - n = 1 - while True: - solution = prolog.query('a(X), a(Y), X\=Y', 'test', n=n) - if solution is None or len(solution) != n: - print('{} solutions found!'.format(n-1)) - break - print(solution[-1]) - n += 1 + # Loads the correct solution [code] to problem [pid]. + # TODO handle library loading. + def load_solution(self, pid, code): + module = 'solution{}'.format(pid) + for rule in prolog.util.split(code): + self.call('assertz/1', [Term('{}:({})'.format(module, rule))]) + + # Import the correct solution for problem [pid] into module for user [uid]. + def mark_solved(self, uid, pid): + m_user = 'user{}'.format(uid) + m_solution = 'solution{}'.format(pid) + return self.call('add_import_module/3', [Term(m_user), Term(m_solution), Term('end')]) + + # Test whether [code] gives the same answer to [query] as the correct + # solution. + def test(self, uid, pid, code, queries): + m_user = 'user{}'.format(uid) + m_solution = 'solution{}'.format(pid) + + # Find the correct answers. Code for [pid] should be loaded beforehand. + # TODO Memoize correct answers. + answers = [] + for query in queries: + result = self.query(query, m_solution) + if result is None or len(result) < 1 or 'X' not in result[0]: + raise Exception('Error finding correct answer to query "{}"'.format(query)) + answers.append(result[0]['X']) # TODO maybe we could check all vars? + + correct = True + refs = [] + try: + # Load the user program [code]. + for rule in prolog.util.split(code): + ref = Term() + if self.call('assertz/2', [Term('{}:({})'.format(m_user, rule)), ref]): + refs.append(ref) + + for i, query in enumerate(queries): + result = self.query(query, m_user) + correct &= (len(result) == 1 and result[0]['X'] == answers[i]) + except Exception as ex: + correct = False + finally: + # Unload all loaded rules. + for ref in refs: + self.call('erase/1', [ref]) + + return correct diff --git a/prolog/lexer.py b/prolog/lexer.py index 971e8a6..971e8a6 100755..100644 --- a/prolog/lexer.py +++ b/prolog/lexer.py diff --git a/prolog/util.py b/prolog/util.py index b7e536b..0ab3c8b 100644 --- a/prolog/util.py +++ b/prolog/util.py @@ -27,6 +27,15 @@ def stringify(tokens): return str(t) return ''.join(map(token_str, tokens)) +# Yield the sequence of rules in [code]. +def split(code): + tokens = tokenize(code) + start = 0 + for idx, token in enumerate(tokens): + if token.type == 'PERIOD' and idx - start > 1: + yield stringify(tokens[start:idx]) + start = idx + 1 + # return a list of lines in 'code', and a list of rule indexes def decompose(code): lines = [] |