diff options
Diffstat (limited to 'monkey/rules.py')
-rw-r--r-- | monkey/rules.py | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/monkey/rules.py b/monkey/rules.py new file mode 100644 index 0000000..fcd9582 --- /dev/null +++ b/monkey/rules.py @@ -0,0 +1,131 @@ +# CodeQ: an online programming tutor. +# Copyright (C) 2017 UL FRI +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import collections + +from nltk import Tree + +from .patterns import get_patterns + +# return a hint for the best applicable buggy rule +def suggest_buggy(bugs, program, patterns): + for rule in [r for r in bugs['rules'] if not r['class']]: + matches = [] + for rule_pattern in rule['condition']: + for pattern, nodes in patterns: + if rule_pattern == pattern: + matches.append((pattern, nodes)) + break + + # found a rule, now do the cha-cha-hint + if len(matches) == len(rule['condition']): + hints = [] + for pattern, nodes in matches: + # find subtrees to point out to student + top_nodes = [] + for node in nodes: + # highlight the node + hints.append({'id': 'monkey_highlight', 'start': node.start, 'end': node.end}) + # find top-level clause (or similar) node containing the highlighted node + while node.parent().label() in {'compound', 'binop', 'unop', 'args', 'list', 'h', 't'}: + node = node.parent() + if not any(n is node for n in top_nodes): + top_nodes.append(node) + + hint = {'args': {'fragments': [program[n.start:n.end] for n in top_nodes]}} + if all(isinstance(n, Tree) and n.label() == 'variable' for n in nodes): + # two instances of a variable + hint['id'] = 'monkey_singleton' if len(nodes) == 1 else 'monkey_buggy_variable' + hint['args']['variable'] = str(nodes[0][0]) + else: + # a literal and another literal/variable in the same clause + hint['id'] = 'monkey_buggy_literal' + hint['args']['literals'] = [program[n.start:n.end] for n in nodes] + hints.append(hint) + return hints + return None + +# return a hint from rules with the most matching patterns +# (most common nonmatching pattern is used as hint) +def suggest_true(bugs, program, patterns): + # find a list of found / missing patterns for each intent rule + matches = collections.defaultdict(list) + for rule in [r for r in bugs['rules'] if r['class']]: + match = {True: [], False: []} # lists of found / missing patterns + for rule_pattern in rule['condition']: + found = any(pattern == rule_pattern for pattern, nodes in patterns) + match[found].append(rule_pattern) + n_found = len(match[True]) + matches[n_found].append(match) + + # consider only rules that have maximum matching patterns + candidate_rules = matches[max(matches)] + + # if there is candidate with no missing patterns, the program should already + # be correct, so do not try hinting + if any(not match[False] for match in candidate_rules): + return None + + # find the number of candidate rules missing each pattern + missing_patterns = collections.Counter() + for match in candidate_rules: + for pattern in match[False]: + missing_patterns[pattern] += 1 + + # consider only the most commonly missing patterns + max_count = max(missing_patterns.values()) + candidate_patterns = [pattern for pattern, count in missing_patterns.items() if count == max_count] + + # find the most common (in the learning set) pattern among the candidates + for pattern in bugs['patterns']: + if pattern in candidate_patterns: + return [{'id': 'monkey_missing'}] + return None + +# check if any pattern in the program has not been observed before, and return as buggy +def suggest_unknown_pattern(bugs, program, patterns): + hints = [] + top_nodes = [] + for pattern, nodes in patterns: + if len(nodes) == 2 and pattern not in bugs['patterns']: + for node in nodes: + # highlight the node + hints.append({'id': 'monkey_highlight', 'start': node.start, 'end': node.end}) + # find top-level clause (or similar) node containing the highlighted node + while node.parent().label() in {'compound', 'binop', 'unop', 'args', 'list', 'h', 't'}: + node = node.parent() + if not any(n is node for n in top_nodes): + top_nodes.append(node) + if hints: + hints.append({ + 'id': 'monkey_unknown', + 'args': {'fragments': [program[n.start:n.end] for n in top_nodes]} + }) + return hints + return None + +def suggest(bugs, program): + try: + patterns = list(get_patterns(program)) + hints = suggest_buggy(bugs, program, patterns) + if not hints: + hints = suggest_true(bugs, program, patterns) + if not hints: + hints = suggest_unknown_pattern(bugs, program, patterns) + return hints + except: + # should never happen + return None |