feat(frontend): big O complexity estimator

This commit is contained in:
2025-07-31 15:05:41 +01:00
parent cbd16b3f60
commit 321bc07eb1
2 changed files with 598 additions and 0 deletions

View 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;
}>;
};
}

View 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));
}