summaryrefslogtreecommitdiff
path: root/monkey/rules.py
diff options
context:
space:
mode:
Diffstat (limited to 'monkey/rules.py')
-rw-r--r--monkey/rules.py131
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