@tracecode/harness 0.5.0 → 0.6.0
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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +67 -80
- package/README.md +31 -4
- package/dist/browser.cjs +974 -19
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +3 -2
- package/dist/browser.d.ts +3 -2
- package/dist/browser.js +974 -19
- package/dist/browser.js.map +1 -1
- package/dist/cli.cjs +24 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -1
- package/dist/core.cjs +611 -4
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +20 -6
- package/dist/core.d.ts +20 -6
- package/dist/core.js +605 -4
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +1055 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1049 -52
- package/dist/index.js.map +1 -1
- package/dist/internal/browser.cjs +211 -0
- package/dist/internal/browser.cjs.map +1 -1
- package/dist/internal/browser.d.cts +62 -6
- package/dist/internal/browser.d.ts +62 -6
- package/dist/internal/browser.js +210 -0
- package/dist/internal/browser.js.map +1 -1
- package/dist/javascript.cjs +37 -19
- package/dist/javascript.cjs.map +1 -1
- package/dist/javascript.d.cts +2 -2
- package/dist/javascript.d.ts +2 -2
- package/dist/javascript.js +37 -19
- package/dist/javascript.js.map +1 -1
- package/dist/python.cjs +14 -14
- package/dist/python.cjs.map +1 -1
- package/dist/python.d.cts +8 -8
- package/dist/python.d.ts +8 -8
- package/dist/python.js +14 -14
- package/dist/python.js.map +1 -1
- package/dist/{runtime-types-DtaaAhHL.d.ts → runtime-types-89nchXlY.d.cts} +8 -4
- package/dist/{runtime-types--lBQ6rYu.d.cts → runtime-types-CCQ-ZLc9.d.ts} +8 -4
- package/dist/{types-DwIYM3Ku.d.cts → types-zyvpJKCi.d.cts} +1 -0
- package/dist/{types-DwIYM3Ku.d.ts → types-zyvpJKCi.d.ts} +1 -0
- package/package.json +12 -6
- package/workers/java/.build/classes/harness/browser/JavaRewriteLibrary.class +0 -0
- package/workers/java/java-worker.js +712 -0
- package/workers/java/src/harness/browser/JavaRewriteLibrary.java +54 -0
- package/workers/javascript/javascript-worker.js +343 -63
- package/workers/python/generated-python-harness-snippets.js +3 -3
- package/workers/python/pyodide-worker.js +419 -68
- package/workers/python/runtime-core.js +52 -3
- package/workers/vendor/java-browser-spike-helper.jar +0 -0
- package/workers/vendor/java-practice-rewriter.jar +0 -0
- package/workers/vendor/java-rewrite-bridge.jar +0 -0
- package/workers/vendor/javaparser-core-3.25.10.jar +0 -0
- 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
|
|
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
|
-
|
|
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: '+=',
|
|
769
|
-
ast.
|
|
770
|
-
ast.
|
|
771
|
-
ast.
|
|
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: '<',
|
|
776
|
-
ast.
|
|
777
|
-
ast.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|