@tracecode/harness 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +30 -3
  3. package/dist/browser.cjs +974 -19
  4. package/dist/browser.cjs.map +1 -1
  5. package/dist/browser.d.cts +3 -2
  6. package/dist/browser.d.ts +3 -2
  7. package/dist/browser.js +974 -19
  8. package/dist/browser.js.map +1 -1
  9. package/dist/cli.cjs +24 -0
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +24 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/core.cjs +611 -4
  14. package/dist/core.cjs.map +1 -1
  15. package/dist/core.d.cts +20 -6
  16. package/dist/core.d.ts +20 -6
  17. package/dist/core.js +605 -4
  18. package/dist/core.js.map +1 -1
  19. package/dist/index.cjs +1004 -19
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +3 -3
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.js +998 -19
  24. package/dist/index.js.map +1 -1
  25. package/dist/internal/browser.cjs +211 -0
  26. package/dist/internal/browser.cjs.map +1 -1
  27. package/dist/internal/browser.d.cts +62 -6
  28. package/dist/internal/browser.d.ts +62 -6
  29. package/dist/internal/browser.js +210 -0
  30. package/dist/internal/browser.js.map +1 -1
  31. package/dist/javascript.d.cts +2 -2
  32. package/dist/javascript.d.ts +2 -2
  33. package/dist/{runtime-types-DtaaAhHL.d.ts → runtime-types-89nchXlY.d.cts} +8 -4
  34. package/dist/{runtime-types--lBQ6rYu.d.cts → runtime-types-CCQ-ZLc9.d.ts} +8 -4
  35. package/dist/{types-DwIYM3Ku.d.cts → types-zyvpJKCi.d.cts} +1 -0
  36. package/dist/{types-DwIYM3Ku.d.ts → types-zyvpJKCi.d.ts} +1 -0
  37. package/package.json +17 -5
  38. package/workers/java/.build/classes/harness/browser/JavaRewriteLibrary.class +0 -0
  39. package/workers/java/java-worker.js +712 -0
  40. package/workers/java/src/harness/browser/JavaRewriteLibrary.java +54 -0
  41. package/workers/javascript/javascript-worker.js +343 -63
  42. package/workers/python/generated-python-harness-snippets.js +3 -3
  43. package/workers/python/pyodide-worker.js +419 -68
  44. package/workers/python/runtime-core.js +52 -3
  45. package/workers/vendor/java-browser-spike-helper.jar +0 -0
  46. package/workers/vendor/java-practice-rewriter.jar +0 -0
  47. package/workers/vendor/java-rewrite-bridge.jar +0 -0
  48. package/workers/vendor/javaparser-core-3.25.10.jar +0 -0
  49. package/workers/vendor/jdk.compiler-17.jar +0 -0
@@ -671,7 +671,7 @@ async function initAnalyzer() {
671
671
 
672
672
  await loadPyodideInstance();
673
673
 
674
- // The AST analyzer Python code - must match lib/analysis/ast-analyzer.ts
674
+ // The AST analyzer Python code - must match the semantic facts contract.
675
675
  const analyzerCode = `
676
676
  import ast
677
677
  import json
@@ -707,7 +707,8 @@ HEAP_FUNCS = frozenset([
707
707
  'nlargest', 'nsmallest',
708
708
  ])
709
709
 
710
- def analyze_code(code):
710
+
711
+ def analyze_code(code: str) -> dict:
711
712
  facts = {
712
713
  'valid': True,
713
714
  'syntaxError': None,
@@ -737,17 +738,30 @@ def analyze_code(code):
737
738
  'slidingWindowPattern': None,
738
739
  'indexExpressions': [],
739
740
  'windowPatterns': [],
741
+ 'sliceExpressions': [],
742
+ 'subtractionAssignments': [],
743
+ 'hashLookupChecks': [],
744
+ 'hashAssignments': [],
745
+ 'returnCollectionShapes': [],
746
+ 'returnExpressions': [],
747
+ 'comparisonExpressions': [],
748
+ 'variableAssignments': [],
749
+ 'augmentedAssignments': [],
750
+ 'propertyAssignments': [],
751
+ 'methodCalls': [],
752
+ 'functionCalls': [],
753
+ 'loopIterations': [],
740
754
  'variablesAssigned': [],
741
755
  'functionParams': [],
742
756
  }
743
-
757
+
744
758
  try:
745
759
  tree = ast.parse(code)
746
760
  except SyntaxError as e:
747
761
  facts['valid'] = False
748
762
  facts['syntaxError'] = f"Line {e.lineno}: {e.msg}" if e.lineno else str(e.msg)
749
763
  return facts
750
-
764
+
751
765
  function_names = set()
752
766
  builtins_used = set()
753
767
  aug_assign_ops = set()
@@ -757,24 +771,52 @@ def analyze_code(code):
757
771
  string_ops = set()
758
772
  variables_assigned = set()
759
773
  function_params = set()
760
-
774
+
761
775
  loop_depth = 0
762
776
  in_conditional = False
763
777
  deque_imported = False
764
778
  current_loop_var = None
765
779
  canonical_index_expressions = []
766
-
780
+ subtraction_assignments = []
781
+ hash_lookup_checks = []
782
+ hash_assignments = []
783
+ return_collection_shapes = []
784
+ return_expressions = []
785
+ comparison_expressions = []
786
+ variable_assignments = []
787
+ augmented_assignments = []
788
+ property_assignments = []
789
+ method_calls = []
790
+ function_calls = []
791
+ loop_iterations = []
792
+ slice_expressions = []
793
+
767
794
  AUG_OP_MAP = {
768
- ast.Add: '+=', ast.Sub: '-=', ast.Mult: '*=', ast.Div: '/=',
769
- ast.FloorDiv: '//=', ast.Mod: '%=', ast.Pow: '**=',
770
- ast.BitOr: '|=', ast.BitAnd: '&=', ast.BitXor: '^=',
771
- ast.LShift: '<<=', ast.RShift: '>>=',
795
+ ast.Add: '+=',
796
+ ast.Sub: '-=',
797
+ ast.Mult: '*=',
798
+ ast.Div: '/=',
799
+ ast.FloorDiv: '//=',
800
+ ast.Mod: '%=',
801
+ ast.Pow: '**=',
802
+ ast.BitOr: '|=',
803
+ ast.BitAnd: '&=',
804
+ ast.BitXor: '^=',
805
+ ast.LShift: '<<=',
806
+ ast.RShift: '>>=',
772
807
  }
773
-
808
+
774
809
  CMP_OP_MAP = {
775
- ast.Lt: '<', ast.LtE: '<=', ast.Gt: '>', ast.GtE: '>=',
776
- ast.Eq: '==', ast.NotEq: '!=', ast.In: 'in', ast.NotIn: 'not in',
777
- ast.Is: 'is', ast.IsNot: 'is not',
810
+ ast.Lt: '<',
811
+ ast.LtE: '<=',
812
+ ast.Gt: '>',
813
+ ast.GtE: '>=',
814
+ ast.Eq: '==',
815
+ ast.NotEq: '!=',
816
+ ast.In: 'in',
817
+ ast.NotIn: 'not in',
818
+ ast.Is: 'is',
819
+ ast.IsNot: 'is not',
778
820
  }
779
821
 
780
822
  def _merge_coeffs(left_coeffs, right_coeffs):
@@ -788,14 +830,12 @@ def analyze_code(code):
788
830
  def _linearize_index_expr(node):
789
831
  if isinstance(node, ast.Name):
790
832
  return (0, {node.id: 1}, [node.id])
791
-
792
833
  if isinstance(node, ast.Constant):
793
834
  if isinstance(node.value, bool):
794
835
  return None
795
836
  if isinstance(node.value, int):
796
837
  return (int(node.value), {}, [])
797
838
  return None
798
-
799
839
  if isinstance(node, ast.UnaryOp):
800
840
  child = _linearize_index_expr(node.operand)
801
841
  if child is None:
@@ -807,7 +847,6 @@ def analyze_code(code):
807
847
  neg_coeffs = {key: -value for key, value in child_coeffs.items()}
808
848
  return (-child_const, neg_coeffs, child_order)
809
849
  return None
810
-
811
850
  if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub)):
812
851
  left = _linearize_index_expr(node.left)
813
852
  right = _linearize_index_expr(node.right)
@@ -825,18 +864,15 @@ def analyze_code(code):
825
864
  if key not in order:
826
865
  order.append(key)
827
866
  return (const_delta, coeffs, order)
828
-
829
867
  return None
830
868
 
831
869
  def _to_canonical_index_expr(array_name, expr_node, preferred_base_var=None):
832
870
  linear = _linearize_index_expr(expr_node)
833
871
  if linear is None:
834
872
  return None
835
-
836
873
  const_delta, coeffs, order = linear
837
874
  if not coeffs:
838
875
  return None
839
-
840
876
  mutable_coeffs = dict(coeffs)
841
877
  base_var = None
842
878
  if preferred_base_var and mutable_coeffs.get(preferred_base_var) == 1:
@@ -846,14 +882,11 @@ def analyze_code(code):
846
882
  if mutable_coeffs.get(var_name) == 1:
847
883
  base_var = var_name
848
884
  break
849
-
850
885
  if base_var is None:
851
886
  return None
852
-
853
887
  mutable_coeffs[base_var] = mutable_coeffs.get(base_var, 0) - 1
854
888
  if mutable_coeffs[base_var] == 0:
855
889
  del mutable_coeffs[base_var]
856
-
857
890
  variable_delta_name = None
858
891
  variable_delta_sign = 0
859
892
  if len(mutable_coeffs) > 1:
@@ -863,7 +896,6 @@ def analyze_code(code):
863
896
  if coeff not in (-1, 1):
864
897
  return None
865
898
  variable_delta_sign = coeff
866
-
867
899
  return {
868
900
  'arrayVar': array_name,
869
901
  'baseVar': base_var,
@@ -893,7 +925,6 @@ def analyze_code(code):
893
925
  for expr in index_exprs:
894
926
  group_key = (expr['arrayVar'], expr['baseVar'])
895
927
  grouped.setdefault(group_key, []).append(expr)
896
-
897
928
  patterns = []
898
929
  for (array_var, base_var), expressions in grouped.items():
899
930
  unique = []
@@ -904,10 +935,8 @@ def analyze_code(code):
904
935
  continue
905
936
  seen.add(key)
906
937
  unique.append(expr)
907
-
908
938
  if len(unique) < 2:
909
939
  continue
910
-
911
940
  plain = next((expr for expr in unique if _is_plain_base_expr(expr)), None)
912
941
  if plain is not None:
913
942
  shifted = next(
@@ -929,14 +958,12 @@ def analyze_code(code):
929
958
  'rightExpr': plain,
930
959
  })
931
960
  continue
932
-
933
961
  patterns.append({
934
962
  'arrayVar': array_var,
935
963
  'baseVar': base_var,
936
964
  'leftExpr': unique[0],
937
965
  'rightExpr': unique[1],
938
966
  })
939
-
940
967
  return patterns
941
968
 
942
969
  def _project_legacy_sliding_window(window_patterns):
@@ -953,11 +980,9 @@ def analyze_code(code):
953
980
  shifted = left
954
981
  else:
955
982
  continue
956
-
957
983
  offset_name = shifted.get('variableDeltaName')
958
984
  offset_sign = int(shifted.get('variableDeltaSign') or 0)
959
985
  offset_constant = int(shifted.get('constantDelta', 0))
960
-
961
986
  if offset_name and offset_sign in (-1, 1) and offset_constant == 0:
962
987
  return {
963
988
  'loopVar': plain['baseVar'],
@@ -965,7 +990,6 @@ def analyze_code(code):
965
990
  'arrayVar': pattern['arrayVar'],
966
991
  'offsetDirection': 'subtract' if offset_sign < 0 else 'add',
967
992
  }
968
-
969
993
  if not offset_name and offset_constant != 0:
970
994
  return {
971
995
  'loopVar': plain['baseVar'],
@@ -973,9 +997,204 @@ def analyze_code(code):
973
997
  'arrayVar': pattern['arrayVar'],
974
998
  'offsetDirection': 'subtract' if offset_constant < 0 else 'add',
975
999
  }
1000
+ return None
1001
+
1002
+ def _get_name_id(node):
1003
+ if isinstance(node, ast.Name):
1004
+ return node.id
1005
+ if isinstance(node, ast.Attribute):
1006
+ return _get_name_id(node.value) or node.attr
1007
+ if isinstance(node, ast.Subscript):
1008
+ return _get_name_id(node.value)
1009
+ if isinstance(node, ast.Constant) and isinstance(node.value, (str, int)):
1010
+ return str(node.value)
1011
+ return None
1012
+
1013
+ def _get_subtraction_operands(node):
1014
+ if (
1015
+ isinstance(node, ast.BinOp)
1016
+ and isinstance(node.op, ast.Sub)
1017
+ and isinstance(node.left, ast.Name)
1018
+ and isinstance(node.right, ast.Name)
1019
+ ):
1020
+ return {
1021
+ 'leftVar': node.left.id,
1022
+ 'rightVar': node.right.id,
1023
+ }
1024
+ return None
976
1025
 
1026
+ def _get_loop_target_name(node):
1027
+ if isinstance(node, ast.Name):
1028
+ return node.id
1029
+ if isinstance(node, (ast.Tuple, ast.List)):
1030
+ for element in node.elts:
1031
+ candidate = _get_loop_target_name(element)
1032
+ if candidate:
1033
+ return candidate
977
1034
  return None
978
-
1035
+
1036
+ def _extract_range_sequence_source(node):
1037
+ if not isinstance(node, ast.Call):
1038
+ return None
1039
+ if not isinstance(node.func, ast.Name) or node.func.id != 'range':
1040
+ return None
1041
+ if not node.args:
1042
+ return None
1043
+ upper_candidate = node.args[-1]
1044
+ if (
1045
+ isinstance(upper_candidate, ast.Call)
1046
+ and isinstance(upper_candidate.func, ast.Name)
1047
+ and upper_candidate.func.id == 'len'
1048
+ and len(upper_candidate.args) == 1
1049
+ and isinstance(upper_candidate.args[0], ast.Name)
1050
+ ):
1051
+ return upper_candidate.args[0].id
1052
+ return None
1053
+
1054
+ def _describe_loop_iteration(node):
1055
+ loop_var = _get_loop_target_name(node.target)
1056
+ if isinstance(node.iter, ast.Name):
1057
+ return {
1058
+ 'kind': 'direct-sequence',
1059
+ 'sourceVar': node.iter.id,
1060
+ 'loopVar': loop_var,
1061
+ }
1062
+ if isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name):
1063
+ if (
1064
+ node.iter.func.id == 'enumerate'
1065
+ and len(node.iter.args) >= 1
1066
+ and isinstance(node.iter.args[0], ast.Name)
1067
+ ):
1068
+ return {
1069
+ 'kind': 'direct-sequence',
1070
+ 'sourceVar': node.iter.args[0].id,
1071
+ 'loopVar': loop_var,
1072
+ }
1073
+ range_source = _extract_range_sequence_source(node.iter)
1074
+ if range_source:
1075
+ return {
1076
+ 'kind': 'indexed-sequence',
1077
+ 'sourceVar': range_source,
1078
+ 'loopVar': loop_var,
1079
+ }
1080
+ if node.iter.func.id == 'range':
1081
+ return {
1082
+ 'kind': 'other',
1083
+ 'sourceVar': _get_name_id(node.iter.args[-1]) if node.iter.args else None,
1084
+ 'loopVar': loop_var,
1085
+ }
1086
+ return {
1087
+ 'kind': 'other',
1088
+ 'sourceVar': _get_name_id(node.iter),
1089
+ 'loopVar': loop_var,
1090
+ }
1091
+
1092
+ def _is_hash_lookup_expr(node):
1093
+ if isinstance(node, ast.Subscript):
1094
+ return True
1095
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
1096
+ return node.func.attr in ('get',)
1097
+ return False
1098
+
1099
+ def _is_hash_value_read_expr(node):
1100
+ if isinstance(node, ast.Subscript):
1101
+ return True
1102
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
1103
+ return node.func.attr in ('get',)
1104
+ return False
1105
+
1106
+ def _get_call_name(node):
1107
+ if isinstance(node, ast.Name):
1108
+ return node.id
1109
+ if isinstance(node, ast.Attribute):
1110
+ return node.attr
1111
+ return None
1112
+
1113
+ def _get_call_arg_names(args):
1114
+ names = []
1115
+ for arg in args:
1116
+ if isinstance(arg, ast.Name):
1117
+ names.append(arg.id)
1118
+ return names
1119
+
1120
+ def _collect_identifier_names(node, names=None):
1121
+ if names is None:
1122
+ names = set()
1123
+ if node is None:
1124
+ return sorted(names)
1125
+ if isinstance(node, ast.Name):
1126
+ names.add(node.id)
1127
+ return sorted(names)
1128
+ for child in ast.iter_child_nodes(node):
1129
+ _collect_identifier_names(child, names)
1130
+ return sorted(names)
1131
+
1132
+ def _get_property_path(node):
1133
+ if isinstance(node, ast.Attribute):
1134
+ return _get_property_path(node.value) + [node.attr]
1135
+ if isinstance(node, ast.Subscript):
1136
+ return _get_property_path(node.value) + [_get_name_id(node.slice) or '[computed]']
1137
+ return []
1138
+
1139
+ def _describe_slice_bound(node):
1140
+ if node is None:
1141
+ return {'kind': 'omitted'}
1142
+ if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool):
1143
+ return {'kind': 'number', 'number': int(node.value)}
1144
+ if isinstance(node, ast.Name):
1145
+ return {'kind': 'identifier', 'var': node.id}
1146
+ return {'kind': 'omitted'}
1147
+
1148
+ def _describe_value(node):
1149
+ if node is None:
1150
+ return {'valueKind': 'object'}
1151
+ if isinstance(node, ast.Constant):
1152
+ if node.value is None:
1153
+ return {'valueKind': 'null'}
1154
+ if isinstance(node.value, bool):
1155
+ return {'valueKind': 'boolean', 'booleanValue': bool(node.value)}
1156
+ if isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
1157
+ return {'valueKind': 'number', 'numberValue': node.value}
1158
+ if isinstance(node.value, str):
1159
+ return {'valueKind': 'string', 'stringValue': node.value}
1160
+ if isinstance(node, ast.Name):
1161
+ return {'valueKind': 'identifier', 'valueVar': node.id}
1162
+ if isinstance(node, ast.Attribute):
1163
+ return {
1164
+ 'valueKind': 'property',
1165
+ 'objectVar': _get_name_id(node.value),
1166
+ 'propertyName': node.attr,
1167
+ 'propertyPath': _get_property_path(node),
1168
+ 'argumentVars': _collect_identifier_names(node.value),
1169
+ }
1170
+ if isinstance(node, ast.Subscript):
1171
+ return {
1172
+ 'valueKind': 'property',
1173
+ 'objectVar': _get_name_id(node.value),
1174
+ 'propertyName': _get_name_id(node.slice),
1175
+ 'propertyPath': _get_property_path(node),
1176
+ 'argumentVars': _collect_identifier_names(node.slice),
1177
+ }
1178
+ if isinstance(node, ast.Call):
1179
+ return {
1180
+ 'valueKind': 'call',
1181
+ 'callName': _get_call_name(node.func),
1182
+ 'argumentVars': _collect_identifier_names(node),
1183
+ }
1184
+ if isinstance(node, ast.BinOp):
1185
+ return {
1186
+ 'valueKind': 'binary',
1187
+ 'operator': type(node.op).__name__,
1188
+ 'leftVar': _get_name_id(node.left),
1189
+ 'rightVar': _get_name_id(node.right),
1190
+ 'argumentVars': _collect_identifier_names(node),
1191
+ }
1192
+ if isinstance(node, ast.List):
1193
+ return {'valueKind': 'array'}
1194
+ if isinstance(node, ast.Dict):
1195
+ return {'valueKind': 'object'}
1196
+ return {'valueKind': 'object'}
1197
+
979
1198
  class FactExtractor(ast.NodeVisitor):
980
1199
  def visit_FunctionDef(self, node):
981
1200
  nonlocal function_names, function_params
@@ -984,12 +1203,12 @@ def analyze_code(code):
984
1203
  for arg in node.args.args:
985
1204
  function_params.add(arg.arg)
986
1205
  self.generic_visit(node)
987
-
1206
+
988
1207
  def visit_AsyncFunctionDef(self, node):
989
1208
  self.visit_FunctionDef(node)
990
-
1209
+
991
1210
  def visit_For(self, node):
992
- nonlocal loop_depth, current_loop_var
1211
+ nonlocal loop_depth, current_loop_var, loop_iterations
993
1212
  facts['hasForLoop'] = True
994
1213
  loop_depth += 1
995
1214
  if loop_depth > 1:
@@ -997,10 +1216,11 @@ def analyze_code(code):
997
1216
  old_loop_var = current_loop_var
998
1217
  if isinstance(node.target, ast.Name):
999
1218
  current_loop_var = node.target.id
1219
+ loop_iterations.append(_describe_loop_iteration(node))
1000
1220
  self.generic_visit(node)
1001
1221
  current_loop_var = old_loop_var
1002
1222
  loop_depth -= 1
1003
-
1223
+
1004
1224
  def visit_While(self, node):
1005
1225
  nonlocal loop_depth
1006
1226
  facts['hasWhileLoop'] = True
@@ -1009,7 +1229,7 @@ def analyze_code(code):
1009
1229
  facts['hasNestedLoop'] = True
1010
1230
  self.generic_visit(node)
1011
1231
  loop_depth -= 1
1012
-
1232
+
1013
1233
  def visit_If(self, node):
1014
1234
  nonlocal in_conditional
1015
1235
  facts['hasConditional'] = True
@@ -1017,11 +1237,15 @@ def analyze_code(code):
1017
1237
  in_conditional = True
1018
1238
  self.generic_visit(node)
1019
1239
  in_conditional = was_in_conditional
1020
-
1240
+
1021
1241
  def visit_Call(self, node):
1022
- nonlocal builtins_used, dict_ops, list_ops, string_ops
1242
+ nonlocal builtins_used, dict_ops, list_ops, string_ops, hash_lookup_checks, method_calls, function_calls
1023
1243
  if isinstance(node.func, ast.Name):
1024
1244
  name = node.func.id
1245
+ function_calls.append({
1246
+ 'functionName': name,
1247
+ 'argumentVars': _get_call_arg_names(node.args),
1248
+ })
1025
1249
  if name in TRACKED_BUILTINS:
1026
1250
  builtins_used.add(name)
1027
1251
  if name == 'dict':
@@ -1036,8 +1260,20 @@ def analyze_code(code):
1036
1260
  facts['hasRecursion'] = True
1037
1261
  elif isinstance(node.func, ast.Attribute):
1038
1262
  method = node.func.attr
1263
+ method_calls.append({
1264
+ 'receiverVar': _get_name_id(node.func.value),
1265
+ 'methodName': method,
1266
+ 'argumentVars': _get_call_arg_names(node.args),
1267
+ })
1039
1268
  if method in DICT_METHODS:
1040
1269
  dict_ops.add(method)
1270
+ if method in ('get',):
1271
+ hash_lookup_checks.append({
1272
+ 'operation': method,
1273
+ 'containerVar': _get_name_id(node.func.value),
1274
+ 'argumentVar': _get_name_id(node.args[0]) if len(node.args) > 0 else None,
1275
+ 'directSubtraction': _get_subtraction_operands(node.args[0]) if len(node.args) > 0 else None,
1276
+ })
1041
1277
  if method in LIST_METHODS:
1042
1278
  list_ops.add(method)
1043
1279
  if method in STRING_METHODS:
@@ -1049,70 +1285,161 @@ def analyze_code(code):
1049
1285
  if method in HEAP_FUNCS:
1050
1286
  facts['usesHeap'] = True
1051
1287
  self.generic_visit(node)
1052
-
1288
+
1053
1289
  def visit_Dict(self, node):
1054
1290
  facts['usesDict'] = True
1055
1291
  self.generic_visit(node)
1056
-
1292
+
1057
1293
  def visit_List(self, node):
1058
1294
  facts['usesList'] = True
1059
1295
  self.generic_visit(node)
1060
-
1296
+
1061
1297
  def visit_Set(self, node):
1062
1298
  facts['usesSet'] = True
1063
1299
  self.generic_visit(node)
1064
-
1300
+
1065
1301
  def visit_ListComp(self, node):
1066
1302
  facts['usesList'] = True
1067
1303
  self.generic_visit(node)
1068
-
1304
+
1069
1305
  def visit_DictComp(self, node):
1070
1306
  facts['usesDict'] = True
1071
1307
  self.generic_visit(node)
1072
-
1308
+
1073
1309
  def visit_SetComp(self, node):
1074
1310
  facts['usesSet'] = True
1075
1311
  self.generic_visit(node)
1076
-
1312
+
1077
1313
  def visit_AugAssign(self, node):
1078
- nonlocal aug_assign_ops
1314
+ nonlocal aug_assign_ops, augmented_assignments
1079
1315
  op_type = type(node.op)
1080
1316
  if op_type in AUG_OP_MAP:
1081
- aug_assign_ops.add(AUG_OP_MAP[op_type])
1317
+ operator = AUG_OP_MAP[op_type]
1318
+ aug_assign_ops.add(operator)
1319
+ fact = {
1320
+ 'operator': operator,
1321
+ 'value': _describe_value(node.value),
1322
+ }
1323
+ if isinstance(node.target, ast.Name):
1324
+ fact['targetVar'] = node.target.id
1325
+ elif isinstance(node.target, ast.Attribute):
1326
+ fact['targetObjectVar'] = _get_name_id(node.target.value)
1327
+ fact['targetPropertyName'] = node.target.attr
1328
+ fact['targetPropertyPath'] = _get_property_path(node.target)
1329
+ elif isinstance(node.target, ast.Subscript):
1330
+ fact['targetObjectVar'] = _get_name_id(node.target.value)
1331
+ fact['targetPropertyName'] = _get_name_id(node.target.slice)
1332
+ fact['targetPropertyPath'] = _get_property_path(node.target)
1333
+ augmented_assignments.append(fact)
1082
1334
  self.generic_visit(node)
1083
-
1335
+
1084
1336
  def visit_Compare(self, node):
1085
- nonlocal comparison_ops, dict_ops
1337
+ nonlocal comparison_ops, dict_ops, hash_lookup_checks, comparison_expressions
1086
1338
  for op in node.ops:
1087
1339
  op_type = type(op)
1088
1340
  if op_type in CMP_OP_MAP:
1089
1341
  op_str = CMP_OP_MAP[op_type]
1090
1342
  comparison_ops.add(op_str)
1343
+ comparator = node.comparators[0] if node.comparators else None
1344
+ comparison_expressions.append({
1345
+ 'operator': op_str,
1346
+ 'left': _describe_value(node.left),
1347
+ 'right': _describe_value(comparator),
1348
+ })
1091
1349
  if op_str == 'in' or op_str == 'not in':
1092
1350
  dict_ops.add(op_str)
1351
+ hash_lookup_checks.append({
1352
+ 'operation': op_str,
1353
+ 'containerVar': _get_name_id(comparator),
1354
+ 'argumentVar': _get_name_id(node.left),
1355
+ 'directSubtraction': _get_subtraction_operands(node.left),
1356
+ })
1093
1357
  self.generic_visit(node)
1094
-
1358
+
1095
1359
  def visit_Assign(self, node):
1096
- nonlocal variables_assigned
1360
+ nonlocal variables_assigned, subtraction_assignments, hash_assignments, variable_assignments, property_assignments
1097
1361
  for target in node.targets:
1098
1362
  if isinstance(target, ast.Name):
1099
1363
  variables_assigned.add(target.id)
1100
- elif isinstance(target, (ast.Tuple, ast.List)):
1364
+ assignment_fact = {'targetVar': target.id}
1365
+ assignment_fact.update(_describe_value(node.value))
1366
+ variable_assignments.append(assignment_fact)
1367
+ subtraction = _get_subtraction_operands(node.value)
1368
+ if subtraction:
1369
+ subtraction_assignments.append({
1370
+ 'targetVar': target.id,
1371
+ 'leftVar': subtraction['leftVar'],
1372
+ 'rightVar': subtraction['rightVar'],
1373
+ })
1374
+ elif isinstance(target, ast.Tuple) or isinstance(target, ast.List):
1101
1375
  for elt in target.elts:
1102
1376
  if isinstance(elt, ast.Name):
1103
1377
  variables_assigned.add(elt.id)
1378
+ assignment_fact = {'targetVar': elt.id}
1379
+ assignment_fact.update(_describe_value(node.value))
1380
+ variable_assignments.append(assignment_fact)
1381
+ elif isinstance(target, ast.Subscript):
1382
+ hash_assignments.append({
1383
+ 'operation': 'setitem',
1384
+ 'containerVar': _get_name_id(target.value),
1385
+ 'keyVar': _get_name_id(target.slice),
1386
+ 'valueVar': _get_name_id(node.value),
1387
+ })
1388
+ property_assignment_fact = {
1389
+ 'objectVar': _get_name_id(target.value),
1390
+ 'propertyName': _get_name_id(target.slice),
1391
+ }
1392
+ property_assignment_fact.update(_describe_value(node.value))
1393
+ property_assignments.append(property_assignment_fact)
1394
+ elif isinstance(target, ast.Attribute):
1395
+ property_assignment_fact = {
1396
+ 'objectVar': _get_name_id(target.value),
1397
+ 'propertyName': target.attr,
1398
+ }
1399
+ property_assignment_fact.update(_describe_value(node.value))
1400
+ property_assignments.append(property_assignment_fact)
1104
1401
  self.generic_visit(node)
1105
-
1402
+
1106
1403
  def visit_AnnAssign(self, node):
1107
- nonlocal variables_assigned
1404
+ nonlocal variables_assigned, subtraction_assignments, variable_assignments, property_assignments
1108
1405
  if isinstance(node.target, ast.Name):
1109
1406
  variables_assigned.add(node.target.id)
1407
+ assignment_fact = {'targetVar': node.target.id}
1408
+ assignment_fact.update(_describe_value(node.value))
1409
+ variable_assignments.append(assignment_fact)
1410
+ subtraction = _get_subtraction_operands(node.value)
1411
+ if subtraction:
1412
+ subtraction_assignments.append({
1413
+ 'targetVar': node.target.id,
1414
+ 'leftVar': subtraction['leftVar'],
1415
+ 'rightVar': subtraction['rightVar'],
1416
+ })
1417
+ elif isinstance(node.target, ast.Attribute):
1418
+ property_assignment_fact = {
1419
+ 'objectVar': _get_name_id(node.target.value),
1420
+ 'propertyName': node.target.attr,
1421
+ }
1422
+ property_assignment_fact.update(_describe_value(node.value))
1423
+ property_assignments.append(property_assignment_fact)
1110
1424
  self.generic_visit(node)
1111
-
1425
+
1112
1426
  def visit_Subscript(self, node):
1113
- nonlocal canonical_index_expressions, current_loop_var
1427
+ nonlocal canonical_index_expressions, current_loop_var, slice_expressions
1114
1428
  if isinstance(node.slice, ast.Slice):
1115
1429
  facts['sliceAccesses'] = True
1430
+ object_var = _get_name_id(node.value)
1431
+ if object_var:
1432
+ lower = _describe_slice_bound(node.slice.lower)
1433
+ upper = _describe_slice_bound(node.slice.upper)
1434
+ slice_expressions.append({
1435
+ 'objectVar': object_var,
1436
+ 'lowerKind': lower['kind'],
1437
+ 'lowerVar': lower.get('var'),
1438
+ 'lowerNumber': lower.get('number'),
1439
+ 'upperKind': upper['kind'],
1440
+ 'upperVar': upper.get('var'),
1441
+ 'upperNumber': upper.get('number'),
1442
+ })
1116
1443
  else:
1117
1444
  facts['indexAccesses'] = True
1118
1445
  if isinstance(node.value, ast.Name):
@@ -1125,21 +1452,32 @@ def analyze_code(code):
1125
1452
  if canonical_expr:
1126
1453
  canonical_index_expressions.append(canonical_expr)
1127
1454
  self.generic_visit(node)
1128
-
1455
+
1129
1456
  def visit_Return(self, node):
1130
- nonlocal in_conditional
1457
+ nonlocal in_conditional, return_collection_shapes, return_expressions
1131
1458
  facts['hasReturn'] = True
1132
1459
  facts['returnCount'] += 1
1133
1460
  if in_conditional:
1134
1461
  facts['hasEarlyReturn'] = True
1462
+ if node.value is not None:
1463
+ return_expressions.append(_describe_value(node.value))
1464
+ if isinstance(node.value, (ast.List, ast.Tuple)):
1465
+ items = list(node.value.elts)
1466
+ return_collection_shapes.append({
1467
+ 'kind': 'array' if isinstance(node.value, ast.List) else 'tuple',
1468
+ 'itemCount': len(items),
1469
+ 'containsHashLookup': any(_is_hash_lookup_expr(item) for item in items),
1470
+ 'containsHashValueRead': any(_is_hash_value_read_expr(item) for item in items),
1471
+ 'containsIdentifier': any(isinstance(item, ast.Name) for item in items),
1472
+ })
1135
1473
  self.generic_visit(node)
1136
-
1474
+
1137
1475
  def visit_Import(self, node):
1138
1476
  for alias in node.names:
1139
1477
  if alias.name == 'heapq':
1140
1478
  facts['usesHeap'] = True
1141
1479
  self.generic_visit(node)
1142
-
1480
+
1143
1481
  def visit_ImportFrom(self, node):
1144
1482
  nonlocal deque_imported
1145
1483
  if node.module == 'heapq':
@@ -1150,10 +1488,10 @@ def analyze_code(code):
1150
1488
  facts['usesDeque'] = True
1151
1489
  deque_imported = True
1152
1490
  self.generic_visit(node)
1153
-
1491
+
1154
1492
  extractor = FactExtractor()
1155
1493
  extractor.visit(tree)
1156
-
1494
+
1157
1495
  facts['functionNames'] = sorted(function_names)
1158
1496
  facts['builtinsUsed'] = sorted(builtins_used)
1159
1497
  facts['augmentedAssignOps'] = sorted(aug_assign_ops)
@@ -1163,7 +1501,20 @@ def analyze_code(code):
1163
1501
  facts['stringOps'] = sorted(string_ops)
1164
1502
  facts['variablesAssigned'] = sorted(variables_assigned)
1165
1503
  facts['functionParams'] = sorted(function_params)
1166
-
1504
+ facts['subtractionAssignments'] = subtraction_assignments
1505
+ facts['hashLookupChecks'] = hash_lookup_checks
1506
+ facts['hashAssignments'] = hash_assignments
1507
+ facts['returnCollectionShapes'] = return_collection_shapes
1508
+ facts['returnExpressions'] = return_expressions
1509
+ facts['comparisonExpressions'] = comparison_expressions
1510
+ facts['variableAssignments'] = variable_assignments
1511
+ facts['augmentedAssignments'] = augmented_assignments
1512
+ facts['propertyAssignments'] = property_assignments
1513
+ facts['methodCalls'] = method_calls
1514
+ facts['functionCalls'] = function_calls
1515
+ facts['loopIterations'] = loop_iterations
1516
+ facts['sliceExpressions'] = slice_expressions
1517
+
1167
1518
  deduped_index_exprs = []
1168
1519
  seen_expr_keys = set()
1169
1520
  for expr in canonical_index_expressions:
@@ -1176,12 +1527,12 @@ def analyze_code(code):
1176
1527
  facts['indexExpressions'] = deduped_index_exprs
1177
1528
  facts['windowPatterns'] = _build_window_patterns(deduped_index_exprs)
1178
1529
  facts['slidingWindowPattern'] = _project_legacy_sliding_window(facts['windowPatterns'])
1179
-
1530
+
1180
1531
  return facts
1181
1532
 
1182
- def analyze(code):
1533
+ def analyze(code: str) -> str:
1183
1534
  return json.dumps(analyze_code(code))
1184
- `;
1535
+ `;
1185
1536
 
1186
1537
  // The analyzer code defines `analyze()` in Pyodide's default globals.
1187
1538
  // We mark initialization done so we don't redefine each time.