feat(frontend): big O complexity estimator
This commit is contained in:
398
frontend/src/lib/complexity-analyzer.ts
Normal file
398
frontend/src/lib/complexity-analyzer.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* Python code for analyzing Big O complexity of user solutions.
|
||||
* Runs in Pyodide and returns an estimate with explanation.
|
||||
*/
|
||||
|
||||
export const COMPLEXITY_ANALYZER = `
|
||||
import ast
|
||||
import json
|
||||
|
||||
class ComplexityAnalyzer(ast.NodeVisitor):
|
||||
"""Analyzes Python code to estimate Big O complexity."""
|
||||
|
||||
def __init__(self, source_code):
|
||||
self.source_code = source_code
|
||||
self.source_lines = source_code.split('\\n')
|
||||
self.findings = []
|
||||
self.loop_depth = 0
|
||||
self.max_loop_depth = 0
|
||||
self.has_recursion = False
|
||||
self.recursive_calls = []
|
||||
self.has_sorting = False
|
||||
self.has_linear_search = False
|
||||
self.has_binary_search_pattern = False
|
||||
self.function_name = None
|
||||
self.input_params = []
|
||||
# Track variables that are dicts or sets (O(1) lookup)
|
||||
self.hash_containers = set()
|
||||
# Track variables that are lists (O(n) lookup)
|
||||
self.list_containers = set()
|
||||
|
||||
def get_line_snippet(self, lineno):
|
||||
"""Get a short snippet of the line."""
|
||||
if 1 <= lineno <= len(self.source_lines):
|
||||
line = self.source_lines[lineno - 1].strip()
|
||||
if len(line) > 50:
|
||||
return line[:47] + "..."
|
||||
return line
|
||||
return ""
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
if self.function_name is None:
|
||||
self.function_name = node.name
|
||||
self.input_params = [arg.arg for arg in node.args.args]
|
||||
# Input params that look like lists
|
||||
for arg in node.args.args:
|
||||
if arg.annotation:
|
||||
ann_str = self._node_to_str(arg.annotation)
|
||||
if 'list' in ann_str.lower() or 'List' in ann_str:
|
||||
self.list_containers.add(arg.arg)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Assign(self, node):
|
||||
"""Track variable assignments to identify container types."""
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name):
|
||||
var_name = target.id
|
||||
# Check what's being assigned
|
||||
if isinstance(node.value, ast.Dict):
|
||||
self.hash_containers.add(var_name)
|
||||
elif isinstance(node.value, ast.Set):
|
||||
self.hash_containers.add(var_name)
|
||||
elif isinstance(node.value, ast.Call):
|
||||
if isinstance(node.value.func, ast.Name):
|
||||
func_name = node.value.func.id
|
||||
if func_name in ('dict', 'set', 'defaultdict', 'Counter', 'OrderedDict'):
|
||||
self.hash_containers.add(var_name)
|
||||
elif func_name == 'list':
|
||||
self.list_containers.add(var_name)
|
||||
elif isinstance(node.value, ast.List):
|
||||
self.list_containers.add(var_name)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_For(self, node):
|
||||
self.loop_depth += 1
|
||||
self.max_loop_depth = max(self.max_loop_depth, self.loop_depth)
|
||||
|
||||
# Determine what we're iterating over
|
||||
iter_desc = self._describe_iterator(node.iter)
|
||||
|
||||
self.findings.append({
|
||||
"type": "loop",
|
||||
"kind": "for",
|
||||
"line": node.lineno,
|
||||
"snippet": self.get_line_snippet(node.lineno),
|
||||
"depth": self.loop_depth,
|
||||
"iterates_over": iter_desc
|
||||
})
|
||||
|
||||
self.generic_visit(node)
|
||||
self.loop_depth -= 1
|
||||
|
||||
def visit_While(self, node):
|
||||
self.loop_depth += 1
|
||||
self.max_loop_depth = max(self.max_loop_depth, self.loop_depth)
|
||||
|
||||
# Check for binary search pattern (low < high, left <= right, etc.)
|
||||
condition = self._get_condition_pattern(node.test)
|
||||
|
||||
self.findings.append({
|
||||
"type": "loop",
|
||||
"kind": "while",
|
||||
"line": node.lineno,
|
||||
"snippet": self.get_line_snippet(node.lineno),
|
||||
"depth": self.loop_depth,
|
||||
"condition": condition
|
||||
})
|
||||
|
||||
# Check body for binary search indicators (mid = ..., //= 2, etc.)
|
||||
for child in ast.walk(node):
|
||||
if isinstance(child, ast.BinOp) and isinstance(child.op, ast.FloorDiv):
|
||||
if isinstance(child.right, ast.Constant) and child.right.value == 2:
|
||||
self.has_binary_search_pattern = True
|
||||
|
||||
self.generic_visit(node)
|
||||
self.loop_depth -= 1
|
||||
|
||||
def visit_Call(self, node):
|
||||
# Check for recursive calls
|
||||
if isinstance(node.func, ast.Name) and node.func.id == self.function_name:
|
||||
self.has_recursion = True
|
||||
self.recursive_calls.append({
|
||||
"line": node.lineno,
|
||||
"snippet": self.get_line_snippet(node.lineno)
|
||||
})
|
||||
|
||||
# Check for sorting
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
if node.func.attr in ('sort', 'sorted'):
|
||||
self.has_sorting = True
|
||||
self.findings.append({
|
||||
"type": "operation",
|
||||
"kind": "sort",
|
||||
"line": node.lineno,
|
||||
"snippet": self.get_line_snippet(node.lineno),
|
||||
"complexity": "O(n log n)"
|
||||
})
|
||||
elif isinstance(node.func, ast.Name) and node.func.id == 'sorted':
|
||||
self.has_sorting = True
|
||||
self.findings.append({
|
||||
"type": "operation",
|
||||
"kind": "sort",
|
||||
"line": node.lineno,
|
||||
"snippet": self.get_line_snippet(node.lineno),
|
||||
"complexity": "O(n log n)"
|
||||
})
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Compare(self, node):
|
||||
# Check for "x in container" - O(1) for dict/set, O(n) for list
|
||||
for op in node.ops:
|
||||
if isinstance(op, (ast.In, ast.NotIn)):
|
||||
for comparator in node.comparators:
|
||||
if isinstance(comparator, ast.Name):
|
||||
var_name = comparator.id
|
||||
# Check if it's a known hash container (O(1))
|
||||
if var_name in self.hash_containers:
|
||||
# O(1) lookup, no need to flag
|
||||
pass
|
||||
# Check if it's a known list or input param that's a list (O(n))
|
||||
elif var_name in self.list_containers:
|
||||
if self.loop_depth > 0:
|
||||
self.has_linear_search = True
|
||||
self.findings.append({
|
||||
"type": "operation",
|
||||
"kind": "linear_search",
|
||||
"line": node.lineno,
|
||||
"snippet": self.get_line_snippet(node.lineno),
|
||||
"note": f"'in' on list '{var_name}' is O(n)"
|
||||
})
|
||||
# Unknown variable - could be either, don't flag
|
||||
self.generic_visit(node)
|
||||
|
||||
def _describe_iterator(self, node):
|
||||
"""Describe what a for loop is iterating over."""
|
||||
if isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Name):
|
||||
if node.func.id == 'range':
|
||||
if node.args:
|
||||
return self._describe_range_args(node.args)
|
||||
elif node.func.id == 'enumerate':
|
||||
if node.args:
|
||||
return f"enumerate({self._node_to_str(node.args[0])})"
|
||||
elif node.func.id == 'zip':
|
||||
return "zip(...)"
|
||||
elif isinstance(node, ast.Name):
|
||||
return node.id
|
||||
elif isinstance(node, ast.Attribute):
|
||||
return f"{self._node_to_str(node.value)}.{node.attr}"
|
||||
elif isinstance(node, ast.Subscript):
|
||||
return self._node_to_str(node)
|
||||
return "iterable"
|
||||
|
||||
def _describe_range_args(self, args):
|
||||
"""Describe range() arguments."""
|
||||
if len(args) == 1:
|
||||
return f"range({self._node_to_str(args[0])})"
|
||||
elif len(args) >= 2:
|
||||
return f"range({self._node_to_str(args[0])}, {self._node_to_str(args[1])})"
|
||||
return "range(...)"
|
||||
|
||||
def _node_to_str(self, node):
|
||||
"""Convert an AST node to a simple string representation."""
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
elif isinstance(node, ast.Constant):
|
||||
return str(node.value)
|
||||
elif isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Name):
|
||||
return f"{node.func.id}(...)"
|
||||
elif isinstance(node, ast.Attribute):
|
||||
return f"{self._node_to_str(node.value)}.{node.attr}"
|
||||
elif isinstance(node, ast.BinOp):
|
||||
left = self._node_to_str(node.left)
|
||||
right = self._node_to_str(node.right)
|
||||
op = self._op_to_str(node.op)
|
||||
return f"{left} {op} {right}"
|
||||
elif isinstance(node, ast.Subscript):
|
||||
return f"{self._node_to_str(node.value)}[...]"
|
||||
return "..."
|
||||
|
||||
def _op_to_str(self, op):
|
||||
"""Convert operator to string."""
|
||||
ops = {
|
||||
ast.Add: "+", ast.Sub: "-", ast.Mult: "*", ast.Div: "/",
|
||||
ast.FloorDiv: "//", ast.Mod: "%", ast.Pow: "**"
|
||||
}
|
||||
return ops.get(type(op), "?")
|
||||
|
||||
def _get_condition_pattern(self, node):
|
||||
"""Describe a while loop condition."""
|
||||
return self._node_to_str(node)
|
||||
|
||||
def analyze(self):
|
||||
"""Run the analysis and return results."""
|
||||
try:
|
||||
tree = ast.parse(self.source_code)
|
||||
self.visit(tree)
|
||||
except SyntaxError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Syntax error: {e}"
|
||||
}
|
||||
|
||||
# Determine overall complexity
|
||||
complexity, explanation = self._determine_complexity()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"complexity": complexity,
|
||||
"explanation": explanation,
|
||||
"details": {
|
||||
"max_loop_depth": self.max_loop_depth,
|
||||
"has_recursion": self.has_recursion,
|
||||
"has_sorting": self.has_sorting,
|
||||
"has_binary_search_pattern": self.has_binary_search_pattern,
|
||||
"findings": self.findings,
|
||||
"recursive_calls": self.recursive_calls
|
||||
}
|
||||
}
|
||||
|
||||
def _determine_complexity(self):
|
||||
"""Determine the overall complexity based on findings."""
|
||||
explanations = []
|
||||
complexity_factors = []
|
||||
|
||||
# Handle recursion
|
||||
if self.has_recursion:
|
||||
if self.has_binary_search_pattern:
|
||||
complexity_factors.append(("log n", "binary search recursion"))
|
||||
explanations.append("Recursion with halving pattern suggests O(log n)")
|
||||
else:
|
||||
complexity_factors.append(("?", "recursion detected"))
|
||||
explanations.append(f"Recursion found - complexity depends on recurrence relation")
|
||||
for rc in self.recursive_calls:
|
||||
explanations.append(f" Line {rc['line']}: {rc['snippet']}")
|
||||
|
||||
# Handle loops
|
||||
if self.max_loop_depth > 0:
|
||||
loop_findings = [f for f in self.findings if f["type"] == "loop"]
|
||||
|
||||
if self.has_binary_search_pattern and self.max_loop_depth == 1:
|
||||
complexity_factors.append(("log n", "binary search loop"))
|
||||
explanations.append("While loop with halving pattern: O(log n)")
|
||||
else:
|
||||
# Describe each loop level
|
||||
for finding in loop_findings:
|
||||
snippet = finding.get("snippet", "")
|
||||
depth_info = f" (nested, depth {finding['depth']})" if finding['depth'] > 1 else ""
|
||||
if finding["kind"] == "for":
|
||||
iter_over = finding.get("iterates_over", "iterable")
|
||||
explanations.append(
|
||||
f"Loop over {iter_over}{depth_info} → O(n)"
|
||||
)
|
||||
if snippet:
|
||||
explanations.append(f" └─ {snippet}")
|
||||
else:
|
||||
condition = finding.get("condition", "...")
|
||||
explanations.append(
|
||||
f"While loop{depth_info}"
|
||||
)
|
||||
if snippet:
|
||||
explanations.append(f" └─ {snippet}")
|
||||
|
||||
# Estimate based on nesting
|
||||
if self.max_loop_depth == 1:
|
||||
complexity_factors.append(("n", "single loop"))
|
||||
elif self.max_loop_depth == 2:
|
||||
complexity_factors.append(("n²", "nested loops"))
|
||||
elif self.max_loop_depth == 3:
|
||||
complexity_factors.append(("n³", "triple nested loops"))
|
||||
else:
|
||||
complexity_factors.append((f"n^{self.max_loop_depth}", f"{self.max_loop_depth} nested loops"))
|
||||
|
||||
# Handle sorting
|
||||
if self.has_sorting:
|
||||
complexity_factors.append(("n log n", "sorting"))
|
||||
sort_findings = [f for f in self.findings if f.get("kind") == "sort"]
|
||||
for sf in sort_findings:
|
||||
explanations.append(f"Line {sf['line']}: sorting operation O(n log n)")
|
||||
|
||||
# Handle linear search in loop
|
||||
if self.has_linear_search and self.max_loop_depth >= 1:
|
||||
explanations.append("Note: 'in' operator on list inside loop adds O(n) per iteration")
|
||||
# This could bump n to n², but let's just note it
|
||||
|
||||
# Combine factors to get final complexity
|
||||
if not complexity_factors:
|
||||
return "O(1)", ["No loops or recursion detected - constant time"]
|
||||
|
||||
# Take the dominant term
|
||||
complexity = self._get_dominant_complexity(complexity_factors)
|
||||
|
||||
return f"O({complexity})", explanations
|
||||
|
||||
def _get_dominant_complexity(self, factors):
|
||||
"""Get the dominant complexity from a list of factors."""
|
||||
# Priority order (higher = more complex)
|
||||
priority = {
|
||||
"1": 0, "log n": 1, "n": 2, "n log n": 3,
|
||||
"n²": 4, "n³": 5, "n^4": 6, "2^n": 7, "n!": 8, "?": 10
|
||||
}
|
||||
|
||||
dominant = "1"
|
||||
for factor, _ in factors:
|
||||
if factor.startswith("n^"):
|
||||
try:
|
||||
exp = int(factor[2:])
|
||||
if exp > priority.get(dominant, 0):
|
||||
dominant = factor
|
||||
except:
|
||||
pass
|
||||
elif priority.get(factor, 0) > priority.get(dominant, 0):
|
||||
dominant = factor
|
||||
|
||||
# Handle sorting + loops
|
||||
has_sort = any(f[0] == "n log n" for f in factors)
|
||||
has_n2 = any(f[0] == "n²" for f in factors)
|
||||
|
||||
if has_sort and not has_n2 and dominant == "n":
|
||||
return "n log n"
|
||||
|
||||
return dominant
|
||||
|
||||
|
||||
def analyze_complexity(code):
|
||||
"""Main entry point for complexity analysis."""
|
||||
analyzer = ComplexityAnalyzer(code)
|
||||
return analyzer.analyze()
|
||||
`.trim();
|
||||
|
||||
export interface ComplexityResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
complexity?: string;
|
||||
explanation?: string[];
|
||||
details?: {
|
||||
max_loop_depth: number;
|
||||
has_recursion: boolean;
|
||||
has_sorting: boolean;
|
||||
has_binary_search_pattern: boolean;
|
||||
findings: Array<{
|
||||
type: string;
|
||||
kind: string;
|
||||
line: number;
|
||||
snippet: string;
|
||||
depth?: number;
|
||||
iterates_over?: string;
|
||||
condition?: string;
|
||||
complexity?: string;
|
||||
note?: string;
|
||||
}>;
|
||||
recursive_calls: Array<{
|
||||
line: number;
|
||||
snippet: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
200
frontend/src/lib/python-helpers.ts
Normal file
200
frontend/src/lib/python-helpers.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Python helper code and detection utilities for the test runner.
|
||||
*
|
||||
* This module provides Python code as string constants for data structure
|
||||
* classes, conversion functions, and utilities to detect problem types.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Data Structure Classes
|
||||
// =============================================================================
|
||||
|
||||
export const TREE_NODE_CLASS = `
|
||||
class TreeNode:
|
||||
def __init__(self, val=0, left=None, right=None):
|
||||
self.val = val
|
||||
self.left = left
|
||||
self.right = right
|
||||
`.trim();
|
||||
|
||||
export const LIST_NODE_CLASS = `
|
||||
class ListNode:
|
||||
def __init__(self, val=0, next=None):
|
||||
self.val = val
|
||||
self.next = next
|
||||
`.trim();
|
||||
|
||||
// =============================================================================
|
||||
// Conversion Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Builds a binary tree from a level-order array representation.
|
||||
* Example: [1, 2, 3, null, 4] -> TreeNode(1, TreeNode(2, None, TreeNode(4)), TreeNode(3))
|
||||
*/
|
||||
export const BUILD_TREE = `
|
||||
def __build_tree(arr):
|
||||
if not arr or arr[0] is None:
|
||||
return None
|
||||
root = TreeNode(arr[0])
|
||||
queue = [root]
|
||||
i = 1
|
||||
while queue and i < len(arr):
|
||||
node = queue.pop(0)
|
||||
if i < len(arr) and arr[i] is not None:
|
||||
node.left = TreeNode(arr[i])
|
||||
queue.append(node.left)
|
||||
i += 1
|
||||
if i < len(arr) and arr[i] is not None:
|
||||
node.right = TreeNode(arr[i])
|
||||
queue.append(node.right)
|
||||
i += 1
|
||||
return root
|
||||
`.trim();
|
||||
|
||||
/**
|
||||
* Converts a binary tree to a level-order array representation.
|
||||
* Example: TreeNode(1, TreeNode(2), TreeNode(3)) -> [1, 2, 3]
|
||||
*/
|
||||
export const TREE_TO_ARRAY = `
|
||||
def __tree_to_array(root):
|
||||
if root is None:
|
||||
return []
|
||||
result = []
|
||||
queue = [root]
|
||||
while queue:
|
||||
node = queue.pop(0)
|
||||
if node is None:
|
||||
result.append(None)
|
||||
else:
|
||||
result.append(node.val)
|
||||
queue.append(node.left)
|
||||
queue.append(node.right)
|
||||
# Trim trailing nulls
|
||||
while result and result[-1] is None:
|
||||
result.pop()
|
||||
return result
|
||||
`.trim();
|
||||
|
||||
/**
|
||||
* Builds a linked list from an array.
|
||||
* Example: [1, 2, 3] -> ListNode(1, ListNode(2, ListNode(3)))
|
||||
*/
|
||||
export const BUILD_LIST = `
|
||||
def __build_list(arr):
|
||||
if not arr:
|
||||
return None
|
||||
head = ListNode(arr[0])
|
||||
current = head
|
||||
for val in arr[1:]:
|
||||
current.next = ListNode(val)
|
||||
current = current.next
|
||||
return head
|
||||
`.trim();
|
||||
|
||||
/**
|
||||
* Converts a linked list to an array.
|
||||
* Example: ListNode(1, ListNode(2, ListNode(3))) -> [1, 2, 3]
|
||||
*/
|
||||
export const LIST_TO_ARRAY = `
|
||||
def __list_to_array(head):
|
||||
result = []
|
||||
current = head
|
||||
while current:
|
||||
result.append(current.val)
|
||||
current = current.next
|
||||
return result
|
||||
`.trim();
|
||||
|
||||
// =============================================================================
|
||||
// Problem Type Detection
|
||||
// =============================================================================
|
||||
|
||||
/** Parameter names that indicate a tree-based problem */
|
||||
export const TREE_PARAMS = ["root", "tree", "p", "q"];
|
||||
|
||||
/** Parameter names that indicate a linked-list problem */
|
||||
export const LIST_PARAMS = ["head", "l1", "l2", "list1", "list2", "node"];
|
||||
|
||||
export type ProblemType = "simple" | "tree" | "linkedlist" | "class-based";
|
||||
|
||||
/**
|
||||
* Detects the problem type based on the function signature and input.
|
||||
*
|
||||
* @param signature - The function signature (e.g., "def two_sum(nums, target):")
|
||||
* @param input - The test case input object
|
||||
* @returns The detected problem type
|
||||
*/
|
||||
export function detectProblemType(
|
||||
signature: string,
|
||||
input: Record<string, unknown>
|
||||
): ProblemType {
|
||||
// Class-based: check if input has "operations" key
|
||||
if ("operations" in input) {
|
||||
return "class-based";
|
||||
}
|
||||
|
||||
// Check signature for type hints
|
||||
const hasTreeNode = signature.includes("TreeNode");
|
||||
const hasListNode = signature.includes("ListNode");
|
||||
|
||||
if (hasTreeNode) {
|
||||
return "tree";
|
||||
}
|
||||
|
||||
if (hasListNode) {
|
||||
return "linkedlist";
|
||||
}
|
||||
|
||||
// Check parameter names in signature and input keys
|
||||
const inputKeys = Object.keys(input);
|
||||
|
||||
const hasTreeParam =
|
||||
TREE_PARAMS.some((param) => signature.includes(param)) ||
|
||||
inputKeys.some((key) => TREE_PARAMS.includes(key));
|
||||
|
||||
const hasListParam =
|
||||
LIST_PARAMS.some((param) => signature.includes(param)) ||
|
||||
inputKeys.some((key) => LIST_PARAMS.includes(key));
|
||||
|
||||
if (hasTreeParam) {
|
||||
return "tree";
|
||||
}
|
||||
|
||||
if (hasListParam) {
|
||||
return "linkedlist";
|
||||
}
|
||||
|
||||
return "simple";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Python helper code needed for a given problem type.
|
||||
*/
|
||||
export function getPythonHelpers(problemType: ProblemType): string {
|
||||
switch (problemType) {
|
||||
case "tree":
|
||||
return [TREE_NODE_CLASS, BUILD_TREE, TREE_TO_ARRAY].join("\n\n");
|
||||
case "linkedlist":
|
||||
return [LIST_NODE_CLASS, BUILD_LIST, LIST_TO_ARRAY].join("\n\n");
|
||||
case "class-based":
|
||||
return ""; // Class-based problems define their own class
|
||||
case "simple":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of parameter names that need tree conversion.
|
||||
*/
|
||||
export function getTreeParams(input: Record<string, unknown>): string[] {
|
||||
return Object.keys(input).filter((key) => TREE_PARAMS.includes(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of parameter names that need linked list conversion.
|
||||
*/
|
||||
export function getListParams(input: Record<string, unknown>): string[] {
|
||||
return Object.keys(input).filter((key) => LIST_PARAMS.includes(key));
|
||||
}
|
||||
Reference in New Issue
Block a user