-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathexemplar.py
3761 lines (3288 loc) · 196 KB
/
exemplar.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# License GPL. Email copyright 2019 holder gherson-@-snet dot-net for other terms.
import sys
import sqlite3 # See reset_db()
import re
from inspect import currentframe, getframeinfo # For line #
import importlib # To import and run the test file we create.
import unittest
from typing import List, Tuple, Dict, Any
import factoradic # Credit https://pypi.org/project/factoradic/ Author: Robert Smallshire
import math
import sympy # See SymPy LICENSE.txt
# from sympy.parsing.sympy_parser import *
from threading import Lock
import keyword
from collections import OrderedDict
DEBUG = True # True turns on testing and more feedback.
DEBUG_DB = False # True sets database testing to always on.
pid = '' # Tracks parent vs child forks.
SUCCESS = True
# (For speed, replace the db filename with ':memory:') (isolation_level=None for auto-commit.)
db = sqlite3.connect('exemplar.db', isolation_level=None, check_same_thread=False) # 3rd arg is for repl.it
# For Row objects instead of tuples. Use row[0] or row['column_name'] (The objects are opaque to the debugger.)
# db.row_factory = sqlite3.Row
cursor = db.cursor()
"""
reverse_trace(file) reverse engineers a function from the examples in a given .exem file.
Glossary:
code == Python code generated.
control_id == A control structure's id, could be for1:3 from 'for' + example_id + ':' + control_count['for']
example == An imagined trace of a complete input/output interaction with a function to be generated, with added
assertions/hints, provided by the user.
exem == The user's traced example interactions collected in a file of extension .exem.
line == An example_lines.line.
loop == All the iterations in one end-to-end execution of a particular loop block.
loop top == An example line that represents the re/starting of a loop. The first such top is the loop 'start'.
pretest == A 'reason' that serves as an IF or ELIF condition above other ELIF/s (in a single if/elif/else).
trace == Record of a function execution, imagined in our case.
Prefixes:
are_ == Boolean function
cbt_ == control_block_traces. E.g., cbt_id could be for0:0_40 from control_id + '_' + first_el_id.
ctlei_ == cbt_temp_last_el_ids table
el_ == example_line
fill_ == function that fills a table.
insert_ == function that inserts one table record per call.
is_ == Boolean function
store_ == function that lifts lower level data into a higher level table.
When retiring a function, in a comment just above it, note that date so matching calling code can be found for use in
stepping through the retired function should that become of interest again.
"""
def assertion_triple(truth_line: str) -> Tuple[any]:
"""
Return the input as a standardized triplet of operand, relop, operand, if possible. Else, return Nones.
Ie, (left_operand, relational_operator, right_operand) or (None, None, None).
A valid identifier *not* of form i[0-9]+ is placed on the right in equality expressions, for consistency.
Double quotes are swapped for single.
:database: not involved.
:param truth_line:
:return: The left operand, rel op, and right operand in truth_line (or 3 Nones)
"""
# Qualify truth_line to be a simple relational comparison, or return Nones.
t = truth_line
if "==" not in t and "!=" not in t and '<' not in t and '>' not in t:
return None, None, None # None relational
if " and " in t or " or " in t:
return None, None, None # Compound
# if '%' in t or '*' in t or '+' in t or '/' in t or '-' in t:
# return None, None, None # Also compound
assertion = truth_line.translate(str.maketrans({'"': "'"})) # Single quotes for consistency. todo ignore escaped "s
# Create the relation triple: left_operand, relational_operator, right_operand.
left_operand, relational_operator, right_operand = '', '', ''
for char in assertion: # Separate assertion into 3 parts, the left, relop, and right.
if char in " \t\r\n": # Ignore whitespace.
continue
elif char in "=<>!":
relational_operator += char
else:
if relational_operator: # Then we're up to the right-hand side of relation.
right_operand += char
else:
left_operand += char
if relational_operator == '==' and left_operand.isidentifier() and not re.match('i[0-9]+', left_operand):
# Except for i[0-9]+ (input variable) names, put identifier in an equivalence on *right* for consistency.
temporary = left_operand
left_operand = right_operand
right_operand = temporary
return left_operand, relational_operator, right_operand
if DEBUG and __name__ == "__main__":
assert ('input-1', '==', 'i') == assertion_triple("input-1==i")
assert ('i1%400', '==', '0') == assertion_triple("i1%400==0")
assert ('10', '==', 'guess') == assertion_triple("guess==10"), "We instead got " + str(
assertion_triple("guess==10"))
assert ('10', '>', '4') == assertion_triple("10>4"), "We instead got " + str(assertion_triple("10>4"))
assert ('1', '==', 'guess_count') == assertion_triple("guess_count==1"), \
"We instead got " + str(assertion_triple("guess_count==1"))
assert ('3', '==', 'count+1') == assertion_triple('3==count + 1'), "\nExpected: '3', '==', 'count+1'\nActual: " + \
str(assertion_triple("3==count + 1"))
def positions_of_given_chars_outside_of_quotations(string: str, characters: str) -> List[int]:
"""
Find and return the positions of the given characters, unescaped and outside of embedded strings, in the given string.
:database: not involved.
:param string:
:param characters: chars (not quotes) to search for
:return: list of `characters` positions, [] if no `characters` are found
"""
positions = []
open_string = False
previous_char = ''
i = 0
for c in string:
if c == '"' and previous_char != '\\':
if not open_string:
open_string = '"' # Note we're in a "-delimited string.
else: # Already in a string.
if open_string == '"': # String closed.
open_string = False
# Repeat above, for single quote.
if c == "'" and previous_char != '\\':
if not open_string:
open_string = "'"
else:
if open_string == "'":
open_string = False
if c in characters and not open_string and previous_char != '\\':
positions.append(i)
previous_char = c # Set up for next iteration.
i += 1
return positions
if DEBUG and __name__ == '__main__':
assert [47, 51, 53, 54], \
positions_of_given_chars_outside_of_quotations("'# These ttt will be ignored' including \this but not tthese 4", 't')
def denude(line: str) -> str:
"""
Remove any surrounding whitespace and line comment from `line` and return it.
:database: not involved.
:rtype: str
:param line: String to denude.
:return:
"""
hash_positions = positions_of_given_chars_outside_of_quotations(line, '#')
if not hash_positions: # Hash not found.
return line.strip()
else:
return line[0:hash_positions[0]].strip()
if DEBUG and __name__ == '__main__':
assert "code" == denude(' code ')
assert "code" == denude(" code # This should be removed. # 2nd comment ")
assert "code \# This should NOT be removed." == denude(" code \# This should NOT be removed. # 1st comment ")
assert "code '# This should NOT be removed.'" == denude(" code '# This should NOT be removed.' # 1st comment ")
def clean(examples: List[str]) -> List[str]:
"""
Remove header and line comments, trim each line, and escape single quotes.
:database: not involved.
:param examples: An exem file's contents.
:return:
"""
previous_line = ''
triple_quote = "'''"
result = []
for line in examples:
if len(line.lstrip()) and line.lstrip()[0] == '#':
continue # Skip full line comments.
line = denude(line) # (def denude() is above.)
line = line.translate(str.maketrans({"'": r"\'"})) # Escape single quotes.
if previous_line == '': # Then on first line.
if line == '': # Skip initial blank lines.
continue
# assert line, "The examples must begin with a triple quoted comment or an actual example, not a blank."
if line.startswith('"""') or line.startswith("'''"):
previous_line = 'M' # Will now skip lines until end of Multi-line string.
if line.startswith('"""'):
triple_quote = '"""' # Instead of single quotes.
if len(line) > 3 and line.endswith(triple_quote):
previous_line = 'B' # Pretending line is blank to expect an example to start on the next line.
assert previous_line == 'M' or previous_line == 'B'
else: # We're at start with no header comment.
previous_line = 'E' # In an example.
result.append(line)
elif previous_line == 'M': # Then still in the header comment.
if line.endswith(triple_quote):
previous_line = 'B' # Pretending line is blank so as to expect an example on next line.
elif previous_line == 'B':
if not line:
continue # Allow a blank line after a blank line and after the multiline comment.
# assert line, "An example, not another blank line, must follow blank lines and the header comment."
previous_line = 'E'
result.append(line)
elif previous_line == 'E':
if not line:
previous_line = 'B' # On the next line must start an example.
result.append(line) # Retain both examples and their delimiters, blank lines.
if not line:
result.pop(len(result) - 1) # Remove last, blank line.
assert result[-1], "result's last line is unexpectedly blank"
return result
if DEBUG and __name__ == '__main__':
assert ["code"] == clean([' code '])
assert ["code"] == clean([" code # This should be removed. # 2nd comment "])
assert ["code \# This should NOT be removed."] == clean([" code \# This should NOT be removed. # 1st comment "])
# Why 3 backslashes? "'".translate(str.maketrans({"'": r"\'"})) returns only 2: "\\'"
assert ['code \\\'# This should NOT be removed.\\\''] == clean([" code '# This should NOT be removed.' # 1st comment "])
assert ["code"] == clean([" ",' code ',''])
assert ["code"] == clean([" code # This should be removed. # 2nd comment "," "])
assert ["code \# This should NOT be removed."] == clean([' '," code \# This should NOT be removed. # comment ",""])
assert ["code \\\'# This should NOT be removed.\\\'"] == clean([''," code '# This should NOT be removed.' # comment ",""])
assert ["co\\\'de"] == clean([" co'de "])
# c labels aren't being used. 2/20/19
def remove_c_labels(trace_line: str) -> str:
"""
Created 2018-10-13.
For each "c" found:
Remove it if it is not quoted AND character to left is a digit AND character to right (if present) is not
alphanumeric.
Example: 'i1 % (i1-1) != 0c, (i1-1)>2c' --> 'i1 % (i1-1) != 0, (i1-1)>2'
:database: not involved.
:param trace_line:
:return return_value The trace_line without its c labels:
"""
if not hasattr(trace_line, '__iter__'): # trace_line isn't iterable.
return trace_line
open_string = False
check_for_alphanumeric = False
return_value = ""
previous_character = ""
for character in trace_line:
if character == '"' and previous_character != '\\':
if not open_string:
open_string = '"' # Note we're in a "-delimited string.
else: # Already in a string.
if open_string == '"': # String closed.
open_string = False
# Repeat above, for single quote.
if character == "'" and previous_character != '\\':
if not open_string:
open_string = "'"
else:
if open_string == "'":
open_string = False
if check_for_alphanumeric:
if not character.isalnum():
return_value = return_value[0:-1] # Elide prior "c".
if character == "c" and previous_character.isdigit():
check_for_alphanumeric = True # Check during next character's processing (or after loop if at end of
# trace_line).
else:
check_for_alphanumeric = False
previous_character = character # Set up for next iteration.
return_value += character # return_value built one character at a time.
# After loop.
if check_for_alphanumeric: # Last two characters were a digit and "c".
return_value = return_value[0:-1]
return return_value
if DEBUG and __name__ == '__main__':
assert 'i1 % (i1-1) != 0, (i1-1)>2' == remove_c_labels('i1 % (i1-1) != 0c, (i1-1)>2c')
def unused_get_last_condition(reason: str) -> str:
"""
Find the last condition in reason. (This is useful because often it's a loop-termination condition.)
E.g., 'i1 % (i1-1) != 0c, (i1-1)>2c' --> '(i1-1)>2c'
:database: not involved.
:param reason:
:return the last condition in reason:
"""
commas = positions_of_given_chars_outside_of_quotations(reason, ',')
if not commas: # `reason` does not have multiple conditions.
return reason
return reason[commas[-1] + 1:].strip() # return last condition.
# Replaced with scheme() 2/12/19
def unused_replace_increment(condition: str) -> str:
"""
Find the increment in the given condition, eg, 23, and replace with underscore.
Used to match iterations of the same loop step. Example:
Matching starts--v here. v--ends here.
'i1 % (len(i1)-23) <= 0c' ->
'i1 % (len(i1)-_) <= 0c'
todo Remove spaces not in quotes
:database: not involved.
:param condition:
:return: 'condition' with the dec/increment replaced with underscore.
"""
# # i1)-23) => i1)-_)
regex = re.compile(r'(' # Start of capture group 1. (This is a metacharacter.)
r'i' # i i
r'\d+' # # 1
r'\)*' # optional right parens )
r'\s?' # optional space
r'(\+|-)' # +|- -
r'\s?' # optional space
r'(\+|-)?' # optional +|-
r')' # End of capture group 1. (Another metacharacter.)
r'\d+' # #, ie, the inc 23
r'(' # Start of capture group 4.
r'\)*' # optional right parens )
r'\s?' # optional space
r'((==)|(!=)|(>=)|>|(<=)|<)?' # optional relop, e.g., <=
r')') # End of capture group 4. (Groups 2 and 3 are within group 1.)
# Replace regex's matches in condition with capture groups 1 and 4 separated by underscore. Eg, i1)-23) => i1)-_)
return regex.sub(r'\1_\4', condition)
# Squeeze out all insignificant whitespace.
def deflate(condition):
# Settled on 2-pronged approach of using word splitting if condition is not a simple relation.
return_value = ''
triple = assertion_triple(condition) # Standardize simple assertions.
if triple[0] is not None:
return_value = triple[0] + triple[1] + triple[2]
else:
for word in condition.split():
if word in ('and', 'or'):
return_value += ' ' + word + ' '
elif word in ('not', '(not'):
return_value += word + ' '
else:
return_value += word
return return_value
if __name__ == '__main__':
value = '(a_smile and b_smile) or (not a_smile and not b_smile)'
assert value == deflate(value), value + " expected, actual: " + deflate(value)
assert 'i1%4==0 and i1%100!=0' == deflate('i1 % 4 == 0 and i1 % 100 != 0')
def get_scheme(condition: str) -> str:
"""
Replace the (non-c-suffixed) integers with underscore and remove unquoted whitespace, so that, eg,
scheme('guess_count==0') and scheme('guess_count == 1') reduce to the same '_==guess_count'.
Ie, replace any number not appended to a valid name and not followed immediately by a period or 'c'.
Used to find instances of looping, i.e., to match iterations of the same loop step in the target function.
:database: not involved.
:param condition:
:return:
"""
# Only reduce to underscore those integers not immediately following a valid Python identifier (via negative
# lookbehind) AND not immediately before a 'c' or period (via negative lookahead).
regex = re.compile(r'(?<![A-z_])\d+(?![\.c])') # From https://regexr.com/48b5v 2/12/19.
underscored = regex.sub(r'_', condition)
return underscored
# todo Why are the below tests, and none of get_scheme()'s actual usages, using deflate()?
if DEBUG and __name__ == '__main__':
assert "guess+_==_" == get_scheme(deflate('guess + 1 == 3'))
assert get_scheme(deflate('guess_count==0')) == get_scheme(deflate('guess_count == 1'))
assert 'guess_count1' == get_scheme(deflate('guess_count 1'))
assert 'guess_count1' == get_scheme('guess_count1')
assert '_==guess_count' == get_scheme(deflate('guess_count == 1'))
assert '_==guess_count' == get_scheme(deflate('guess_count==1'))
assert '1c==guess_count' == get_scheme(deflate('guess_count == 1c')) # Leave constants alone.
assert '1.==guess_count' == get_scheme(deflate('guess_count==1.')) # Leave floats alone.
assert '_==guess_count' == get_scheme(deflate('1 == guess_count'))
assert '_==guess_count' == get_scheme('1==guess_count')
assert '1.==guess_count' == get_scheme(deflate('1. == guess_count'))
assert '1c==guess_count' == get_scheme('1c==guess_count')
assert 'i1>_' == get_scheme('i1>4')
assert '_<i1' == get_scheme('4<i1')
assert "guess+_==_" == get_scheme(get_scheme(deflate('guess + 1 == 3')))
assert get_scheme('_==guess_count') == get_scheme(get_scheme(deflate('guess_count == 1')))
assert 'guess_count1' == get_scheme(get_scheme(deflate('guess_count 1')))
assert 'guess_count1' == get_scheme(get_scheme('guess_count1'))
assert '_==guess_count' == get_scheme(get_scheme(deflate('guess_count == 1')))
assert 'guess_count==_' == get_scheme(get_scheme('guess_count==1'))
assert '1c==guess_count' == get_scheme(get_scheme(deflate('guess_count == 1c'))) # Leave constants alone.
assert '1.==guess_count' == get_scheme(get_scheme(deflate('guess_count==1.'))) # Leave floats alone.
assert '_==guess_count' == get_scheme(get_scheme(deflate('1 == guess_count')))
assert '_==guess_count' == get_scheme(get_scheme('1==guess_count'))
assert '1.==guess_count' == get_scheme(get_scheme(deflate('1. == guess_count')))
assert '1c==guess_count' == get_scheme(get_scheme('1c==guess_count'))
assert 'i1>_' == get_scheme(get_scheme('i1>4'))
assert '_<i1' == get_scheme(get_scheme('4<i1'))
assert "i1%(len(i1)-_)<=0c" == get_scheme(deflate('i1 % (len(i1)-13) <= 0c'))
def list_conditions(reason: str) -> List[str]:
"""
Determine 'reason's list of conditions (in two steps to strip() away whitespace).
:database: not involved.
:param reason: str
:return list of conditions, e.g., ["i1 % (i1-1) != 0c","(i1-1)==2c"]:
"""
commas = positions_of_given_chars_outside_of_quotations(reason, ',')
result = []
start = 0
for comma in commas:
condition = reason[start: comma]
result.append(condition.strip())
start = comma + 1
result.append(reason[start:].strip()) # Append last condition in 'reason'
return result
if DEBUG and __name__ == '__main__':
assert ['i1 % (i1-1) != 0c'] == list_conditions(' i1 % (i1-1) != 0c ')
assert ['i1 % (i1-1) != 0c', '(i1-1)=="hi, joe"'] == list_conditions(' i1 % (i1-1) != 0c , (i1-1)=="hi, joe" ')
assert [''] == list_conditions('')
def insert_example_line(el_id: int, example_id: int, line: str) -> int:
"""
Insert the given line into the database and return next line_id to use.
:database: INSERTs example_lines.
:param el_id:
:param example_id:
:param line: a line or comma separated value from exem, with only leading < or > removed.
:return: line_id incremented by five for each INSERT (== # of conditions if `line` is of line_type 'truth').
"""
if line.startswith('<'):
line_type = 'in'
elif line.startswith('>'):
line_type = 'out'
else:
line_type = 'truth' # True condition
if line_type == 'in' or line_type == 'out':
line = line[1:] # Skip less/greater than symbol.
cursor.execute('''INSERT INTO example_lines (el_id, example_id, line, line_type) VALUES (?,?,?,?)''',
(el_id, example_id, remove_c_labels(line), line_type))
el_id += 5
else:
for assertion in list_conditions(line):
cursor.execute('''INSERT INTO example_lines (el_id, example_id, line, line_type) VALUES (?,?,?,?)''',
(el_id, example_id, deflate(remove_c_labels(assertion)), line_type))
el_id += 5
# cursor.execute('''SELECT * FROM example_lines''')
# print("fetchall:", cursor.fetchall())
return el_id
def fill_example_lines(example_lines: List) -> None:
"""
Fill example_lines table with the raw trace (taken from .exem file). Also fill examples table with counts of lines
and assertion ("truth") lines.
:database: indirectly INSERTs example_lines.
:param example_lines: all the lines from exem.
"""
global first_io_only_example
first_io_only_example = None
example_id, el_id_count, truth_count = -1, 0, 0
line_id = 5 # += 5 each insert call
previous_line_blank = False
example_lines = [''] + clean(example_lines)
for line in example_lines:
if previous_line_blank: # Then starting on a new example.
assert line, "This line must be part of an example, not be blank."
line_id = insert_example_line(line_id, example_id, line) # ***** INSERT_EXAMPLE_LINE *****
previous_line_blank = False
# Below, every `line`, even blanks, trigger an immediate insertion into example_lines.
else: # Previous line not blank, so current line can either continue the example or be blank.
if not line: # Current line is blank, signifying a new example.
if el_id_count > 0:
cursor.execute("INSERT INTO examples VALUES (?,?,?)", (example_id, el_id_count, truth_count))
el_id_count, truth_count = 0, 0
example_id += 1
loop_heading = "__example__==" + str(example_id) # Isn't counted in truth_count.
line_id = insert_example_line(line_id, example_id, loop_heading) # ***** INSERT_EXAMPLE_LINE *****
previous_line_blank = True
else: # Example continues.
line_id = insert_example_line(line_id, example_id, line) # ***** INSERT_EXAMPLE_LINE *****
el_id_count += 1
if len(line) > 0 and line[0] not in "<>":
truth_count += 1
if el_id_count > 0: # Store final example.
cursor.execute("INSERT INTO examples VALUES (?,?,?)", (example_id, el_id_count, truth_count))
# # The above records were inserted into temp.example_lines. Here we insert from that table...
# execute = cursor.execute # To shorten next line.
# execute("SELECT example_id, count(*) cnt FROM temp.example_lines GROUP BY example_id ORDER BY cnt DESC, example_id")
# original_example_ids = cursor.fetchall() # Eg, [(0, 3), (1, 3), (4, 5), (5, 5), (2, 7), (3, 10)]
# # ...into main.example_lines, with the only difference being their order; we want example_id (and thus el_id)
# # sorted such that the smallest examples are last. That is to put the assertion-less examples at the end. (This
# # assumes that the assertion-less user examples are all 2 lines (an input and an output) long.)
# el_id, example_id = 5, 0
# for row in original_example_ids:
# old_example_id, cnt = row
# if cnt == 2 and first_io_only_example is None: # first_io_only_example is global.
# first_io_only_example = example_id # This is where generate_code() should stop (exclusive).
# cursor.execute("INSERT INTO main.example_lines (el_id, example_id, line, line_type) VALUES (" + str(el_id) +
# ", " + str(example_id) + ", '__example__==" + str(example_id) + "', 'truth')")
# el_id += 5 # Using multiples of 5 in case inserting a line without re-numbering becomes desirable.
# cursor.execute("SELECT line, line_type FROM temp.example_lines WHERE example_id=?", (old_example_id,))
# lines_of_example = cursor.fetchall()
# for row in lines_of_example:
# cursor.execute("INSERT INTO main.example_lines (el_id, example_id, line, line_type) VALUES (" +
# str(el_id) + ', ' + str(example_id) + ", ?, ?)", (row[0], row[1]))
# el_id += 5
# example_id += 1
# cursor.execute("DROP TABLE temp.example_lines")
# # cursor.execute("""CREATE TABLE example_lines AS SELECT * FROM temp.example_lines WHERE example_id IN
# # (SELECT example_id FROM example_lines GROUP BY example_id HAVING count(*) <= 3)""")
# if first_io_only_example is None: # Then there are no assertion-less examples.
# first_io_only_example = example_id + 1 # So that generate_code() pulls all examples.
# also need to mark each condition as indicating the *start* of a loop or iteration, because an iterative condition
# can be repeated due to a loop in an enclosing scope.
# This whole thing may be redundant with the fill_*_table() functions... 3/9/19
def unused_mark_loop_likely() -> None:
"""
Assign the example_lines.loop_likely and conditions.loop_likely columns per
-1 default, 0==IF, 1==FOR, 2==WHILE (These designations may be refined by if_or_while().)
Old: To identify WHILE reasons, UPDATE examples.loop_likely and termination.loop_likely to 1 (true)
to indicate those examples with 'reason's judged likely to indicate a loop in the target function.
The criterion is simply, does the schematized(line) reappear in any 1 example? if yes, mark all
matches on schematized(line) in all examples as loop_likely. This strategy can create false positives
but is hopefully cost effective.
loop_likely=-1-setting mark_sequences() is called at the end, leaving loop_likely's value 0, for selection,
the default.
todo CONFIRM The corresponding terminal conditions are then likewise set (see last cursor.execute() below).
:database: SELECTs example_lines, conditions. UPDATEs example_lines, conditions.
:return void:
"""
# Identical 'out' messages within an example: loop likely==1 (not too meaningful: these are just print()s)
cursor.execute('''SELECT example_id, line FROM example_lines WHERE line_type = 'out'
GROUP BY example_id, line HAVING COUNT(*) > 1''')
repeats = cursor.fetchall()
for row in repeats:
example_id, line = row # vVv
cursor.execute("UPDATE example_lines SET loop_likely = 1 WHERE line_type='out' AND example_id = ? AND line = ?",
(example_id, line))
# Scheme repeats within an example with relop ==, an identifier, and a integer: also loop_likely==1 (for)
cursor.execute('''SELECT example_id, scheme FROM conditions GROUP BY example_id, scheme HAVING COUNT(*) > 1''')
rows = cursor.fetchall()
for row in rows: # These rows are scheme dupes, now see if they have relop ==, an identifier, and an integer.
example_id, scheme = row
scheme_qualified = False
cursor.execute('''SELECT left_side, relop, right_side FROM conditions
WHERE example_id=? AND scheme=?''', (example_id, scheme))
rows2 = cursor.fetchall()
for row2 in rows2:
left_side, relop, right_side = row2
if relop == '==' and left_side.isdigit() and right_side.isidentifier():
if not scheme_qualified:
scheme_qualified = True
else: # This is the second scheme in this example.
# vVv
cursor.execute("UPDATE example_lines SET loop_likely = 1 WHERE el_id IN ("
"SELECT el_id FROM conditions WHERE example_id = ? AND scheme = ?)",
(example_id, scheme))
"""
Below produces:
el_id_operators: {
'0|2|guess_count': [(90, '==')],
'0|i1|secret': [(20, '==')],
'0|guess|secret': [(50, '>'), (80, '<'), (110, '<'), (140, '>'), (170,
'>'), (200, '>')],
'0|i1|name': [(10, '==')],
'0|1|guess_count': [(60, '==')],
'0|4|guess_count': [(150, '==')],
'0|guess|i1': [(45, '=='), (75, '=='), (105, '=='), (135, '=='), (165,
'=='), (195, '==')],
'0|3|guess_count': [(120, '==')],
'0|0|guess_count': [(30, '==')],
'0|5|guess_count': [(180, '==')]}
So the guess vs. secret relationship involves both IF and WHILE: the IFs are tested within the loop while their
equality should be tested in the WHILE loop top. how can that be seen from examples?
# WHILE (loop_likely==2) is likely if same scheme repeats with possible exception that in their terminal
appearance the relop can change.
# Create a dict with key example_id | operand1 | operand2 and value [(el_id, relop), ...] where operand1
# and operand2 are in alphabetical order.
cursor.execute('''SELECT DISTINCT example_id, el_id, left_side, relop, right_side FROM conditions
ORDER BY example_id, el_id''')
rows = cursor.fetchall()
el_id_operators = {}
for row in rows:
example_id, el_id, left_operand, operator, right_operand = row
if left_operand < right_operand:
key = str(example_id) + '|' + left_operand + '|' + right_operand
else:
key = str(example_id) + '|' + right_operand + '|' + left_operand
if key not in el_id_operators:
el_id_operators[key] = [(el_id, operator)]
else:
el_id_operators[key].append((el_id, operator))
print("el_id_operators:", str(el_id_operators))
# for el_id_operator in el_id_operators:
# if len(el_id_operator) > 1:
"""
# The remainder, i.e., unique assertion schemes /not/ equating an identifier to an input variable
# (eg, /not/ guess==i1): loop_likely==0 (if/elif/else)
cursor.execute('''SELECT el_id, line FROM example_lines WHERE loop_likely == -1 AND line_type = 'truth' ''')
selections = []
rows = cursor.fetchall()
for row in rows:
el_id, relation = row
if not get_variable_name(relation):
selections.append(el_id)
if len(selections) > 0: # vVv
cursor.execute("UPDATE example_lines SET loop_likely = 0 WHERE el_id IN (" + ','.join('?' * len(selections)) +
')', selections)
# Put what we learned today into conditions table, to obviate JOINs.
cursor.execute("""UPDATE conditions SET loop_likely =
(SELECT el.loop_likely FROM example_lines el WHERE conditions.el_id = el.el_id)""")
return
# c labels aren't being used. 2/20/19
def remove_all_c_labels() -> None:
# :database: SELECTs example_lines. UPDATEs example_lines.
# The "c" labels are not needed. (Constants can have a "c" suffix in `reason` and
# `output`.) Since they interfere with eval(), update each such record to remove_c_labels().
cursor.execute('''SELECT el_id, line FROM example_lines WHERE line_type != 'in' ''') # WHERE loop_likely = 0''')
#non_looping = cursor.fetchall()
for row in cursor.fetchall(): #non_looping:
el_id = row[0]
line = remove_c_labels(row[1])
query = "UPDATE example_lines SET line = ? WHERE el_id = ?"
cursor.execute(query, (line, el_id))
def unused_build_reason_evals() -> None:
"""
FOR IF CONDITION ORDERING
Using the data of the `example_lines` table, build a `reason_evals` table that has columns for
inp (text), `reason` (text), and reason_value (Boolean).
reason_evals.reason_value shows T/F result of each inp being substituted for i1 in each `reason`.
Loop step == Each condition of a loop_likely 'reason' maps to one of a limited # steps that is the target loop.
Pretest == A 'reason' that serves as an IF or ELIF condition above other ELIF/s (in a single if/elif/else).
Reason == The conditions the user expects will be true for the associated input in the target function ==
SELECT line FROM example_lines WHERE line_type = 'truth'
:database: SELECTs example_lines, reason_evals. INSERTs reason_evals, example_lines.
"""
# All step 5 'reason's not involved in looping.
cursor.execute("SELECT DISTINCT line FROM example_lines WHERE line_type = 'truth' AND loop_likely = 0") # AND step_id = 1")
all_reasons = cursor.fetchall()
# All step 0 inputs not involved in looping.
cursor.execute("SELECT DISTINCT line FROM example_lines WHERE line_type = 'in' AND loop_likely = 0") # AND step_id = 5")
all_inputs = cursor.fetchall()
locals_dict = locals() # Used in our exec() calls.
if DEBUG:
print("Function", getframeinfo(currentframe()).function, "line #", getframeinfo(currentframe()).lineno)
for a_reason in all_reasons:
a_reason = a_reason[0] # There is only element 0.
for an_inp in all_inputs: # All inputs.
an_inp = an_inp[0]
if an_inp.isdigit(): # Let numbers be numbers (rather than text).
an_inp = int(an_inp)
# Substitute an_inp for i1 in a_reason and exec() to see if true or false. *** MAGIC ***
# globals overrides locals (it doesn't usually)
exec("reason = " + a_reason, {"i1": an_inp}, locals_dict) # Eg i1 < 5 ?
reason_value = locals_dict['reason']
# Determine and store reason_explains_io, which indicates whether inp is associated with 'reason'
# in the examples.
cursor.execute('''SELECT * FROM example_lines reason, example_lines inp WHERE
reason.example_id = inp.example_id and reason.line = ? AND inp.line = ? AND
reason.loop_likely = 0 and inp.loop_likely = 0''', (a_reason, an_inp,))
# These also should have been illustrated with an example: ^^^^^^^^^^^^^^^^
if cursor.fetchone():
# Yes, the current an_inp value shares an example with a_reason.
reason_explains_io = 1
else:
reason_explains_io = 0
if DEBUG:
print("input:", an_inp, " reason:", a_reason, " reason_explains_io:", reason_explains_io,
" reason_value:", reason_value) # (Sqlite inserts Python None values as null.)
# Store determinations.
cursor.execute('''INSERT INTO reason_evals VALUES (?,?,?,?)''',
(an_inp, a_reason, reason_explains_io, reason_value))
# EXPLAIN THE INPUTS WITHOUT EXPLANATION
# Provide a *** i1==input *** 'reason' for all reason-less examples whose input does not make any of the given
# 'reason's true, i.e., those examples whose input does not make /any/ 'reason's true (even if there are no 'reason's)
# fixme This should prolly go in another function. And the above prolly needs to be re-run once new 'reason's are created.
# 10/29/19 my archeology is inadequate to explain this.
if 'a_reason' in locals(): # Select inputs that do not make any extant 'reason's true.
# (Interestingly, locals_dict works here only if the line is breakpointed. 1/22/19)
cursor.execute("SELECT inp FROM reason_evals GROUP BY inp HAVING max(reason_value)=0")
# This whole method needs rethinking... todo
else: # There are /no/ 'reason's, so select /all/ inputs (from loop unlikely rows).
cursor.execute("SELECT el_id, line FROM example_lines WHERE loop_likely = 0 AND line_type = 'in'") #line as inp fixme
special_cases = 0
for row in cursor.fetchall():
el_id = row[0]
inp = quote_if_str(row[1])
#special_cases.append(case[0])
# Using inp's value, with the below modifications.
cursor.execute("SELECT example_id, 'step_id' FROM example_lines WHERE el_id = ? ORDER BY example_id, "
"el_id, 'step_id'", (el_id,)) # First, fetch inp's el_id
row_one = cursor.fetchone()
el_id += 1
example_id = row_one[0]
reason = "i1 == " + inp
step_id = row_one[1] + 1
cursor.execute("""INSERT INTO example_lines (el_id, example_id, 'step_id', line, 'line_scheme', line_type) VALUES
(?,?,?,?,?,?)""", (el_id, example_id, step_id, reason, get_scheme(reason), 'truth'))
# "SET line = ('i1 == ' || line) WHERE line IN (" + ','.join('?' * len(special_cases)) + ')'
special_cases += 1
#print("I just gave ", cursor.execute(query, special_cases).rowcount, "example_lines reason ('i1 == ' || line)")
print("I just gave", str(special_cases), "example_lines reason i1 == inp")
if DEBUG:
print()
def unused_find_safe_pretests() -> None:
"""
For if/elif/else generation.
Build table pretests that shows for every reason A, each reason ("safe pretest") B that is *false* with *all* of A's
example inputs (thereby allowing those inputs to reach A if B was listed above it in an if/elif).
:database: SELECTs example_lines, reason_evals. INSERTs pretests.
"""
# All if/elif/else reasons.
cursor.execute('''SELECT DISTINCT example_id FROM example_lines WHERE line_type = 'truth' AND loop_likely = 0''')
all_reason_eids = cursor.fetchall()
# Here we find the safe pretests to each reason (again, those `reason`s false with all of its inputs).
for reason_eid in all_reason_eids:
reason_eid = reason_eid[0] # The zeroth is the only element.
sql = """
SELECT DISTINCT reason AS potential_pretest FROM reason_evals WHERE 0 = -- potential_pretest never
(SELECT count(*) FROM reason_evals re2 WHERE re2.reason =
potential_pretest AND re2.reason_value = 1 -- true
AND re2.inp IN
(SELECT e.line FROM example_lines e WHERE e.line_type = 'in' AND -- with input
e.example_id = ? AND e.loop_likely = 0)) -- of reason_eid
"""
# Eg, 'i1 % 5 == 0' is a safe pretest to 'i1 % 3 == 0' (and v.v.) in Fizz_Buzz because example inputs for the
# latter, such as 3 and 9, are *not* also evenly divisible by 5. And an input that is (ie, multiples of 15) is
# never an example input for either.
cursor.execute(sql, (reason_eid,))
pretests = cursor.fetchall() # Eg, ('i1 % 3 == 0 and i1 % 5 == 0',)
for pre in pretests: # Store each.
pre = pre[0]
cursor.execute("""INSERT INTO pretests VALUES (?,?)""", (pre, reason_eid)) # All columns TEXT
def debug_db() -> None:
"""
Run the integration/database tests if DEBUG_DB or (DEBUG and it's been >1 hour).
:return: None
"""
Lock().acquire(True) # Insufficient to avoid "sqlite3.ProgrammingError: Recursive use of cursors not allowed" on repl.it.
cursor.execute("""CREATE TABLE IF NOT EXISTS history (prior_db_test_run INTEGER NOT NULL)""")
# Causes "RuntimeError: release unlocked lock" locally and on repl.it: Lock().release()
global DEBUG_DB
if False: # DEBUG:
hour_in_seconds = 60 * 60 # minutes in an hour * seconds in a minute
cursor.execute("""SELECT STRFTIME('%s','now'), prior_db_test_run FROM history""")
row = cursor.fetchone()
if row:
now, prior_db_test_run = row
if (int(now) - prior_db_test_run) > hour_in_seconds:
DEBUG_DB = True
cursor.execute("""UPDATE history SET prior_db_test_run = STRFTIME('%s','now')""")
else:
DEBUG_DB = True # (May have been True already.)
cursor.execute("""INSERT INTO history (prior_db_test_run) VALUES (STRFTIME('%s','now'))""")
if DEBUG_DB:
# Run TestExemplarIntegration tests.
TestClass = importlib.import_module('TestExemplarIntegration')
suite = unittest.TestLoader().loadTestsFromModule(TestClass)
test_results = unittest.TextTestRunner().run(suite)
print("In TestExemplarIntegration there were", len(test_results.errors), "errors and",
len(test_results.failures), "failures.")
# Uncomment when Exemplar is no longer in development:
# if (len(test_results.errors) + len(test_results.failures)) > 0:
# cursor.execute("""DELETE FROM history""")
def reset_db() -> None:
"""
With the exception of table history, clear out the database. (A database is used for the advantages of SQL, not for multi-session persistence.)
:database: CREATEs all tables.
:return: None
"""
# cursor.execute("""DROP TABLE IF EXISTS io_log""")
# cursor.execute("""CREATE TABLE io_log (
# iid ROWID,
# entry TEXT NOT NULL)""")
#sequential_function (line_number, line)
# cursor.execute("""DROP TABLE IF EXISTS """)
# cursor.execute("""CREATE TABLE ()""")
# cursor.execute("""CREATE UNIQUE INDEX ON ()""")
# Below top tables, down to loops, will be deleted. 3/8/19
# iterations (1:1 with suspected loops) with columns python (eg, 'while i<4:', 'for i in range(5):', target_line to
# place that code within the sequential program, and last_line, to note the # of the control block's last line.
# cursor.execute("""DROP TABLE IF EXISTS iterations""")
# cursor.execute("""CREATE TABLE iterations (
# python TEXT NOT NULL,
# target_line INTEGER NOT NULL,
# last_line INTEGER NOT NULL)""")
# cursor.execute("""CREATE UNIQUE INDEX ipt ON iterations(python, target_line)""")
# selections (1:1 with suspected IFs) with columns python (eg, 'if i<5'), target_line to place that code within
# the sequential program, an elif column to point to the selection_id of the next IF in the structure (if any).
# For records housing the last IF in a selection structure, two more columns may be non-null: an else_line column
# (holding, eg, 55, for line 55) to place an else in the sequential program, and last_line (required), to note where
# the selection's block ends.
# cursor.execute("""DROP TABLE IF EXISTS selections""")
# cursor.execute("""CREATE TABLE selections (
# python TEXT NOT NULL,
# target_line INTEGER NOT NULL,
# elif INTEGER,
# else_line INTEGER,
# last_line INTEGER)""")
# cursor.execute("""CREATE UNIQUE INDEX spt ON selections(python, target_line)""")
# This table is to note contiguous FOR blocks (iteration traces) so as to constrain the possible last_el_id's.
# The rows are 1:1 with whole loop executions (including those broken off with BREAK).
# Controls can nest but not straddle (since an outer scope must enclose /all/ of any local block scopes that open
# within it, an entire FOR loop must end before those controls w/an earlier first_el_id).
cursor.execute('''DROP TABLE IF EXISTS for_loops''')
cursor.execute('''CREATE TABLE for_loops (
control_id TEXT NOT NULL, -- Eg, 'for0:1'. Not unique in this table: an outer loop can restart inner. Retained across examples.
example_id INTEGER NOT NULL,
first_el_id INTEGER NOT NULL,
last_el_id INTEGER,
period INTEGER, -- # of iterations per use of this loop, breaks excepted.
increment INTEGER
)''')
cursor.execute('''CREATE UNIQUE INDEX flcf ON for_loops(control_id, first_el_id)''')
# The last_el_id data herein provides all cbt end points for a given synthesis. (And no extra.) Note that
# the cbt includes all trace blocks (iteration traces) of a loop, not just the first and last.
cursor.execute("""DROP TABLE IF EXISTS cbt_temp_last_el_ids""")
cursor.execute("""CREATE TABLE cbt_temp_last_el_ids (
cbt_id TEXT PRIMARY KEY,
example_id INTEGER NOT NULL,
first_el_id INTEGER NOT NULL,
control_id TEXT NOT NULL,
last_el_id INTEGER NOT NULL)""")
# 1-to-1 with *code* controls: if's and for's. (while's are todo.)
# Does not assign a last_el_id because it is usually unknown.
cursor.execute("""DROP TABLE IF EXISTS controls""")
cursor.execute("""CREATE TABLE controls (
control_id TEXT PRIMARY KEY, -- 'for' + starting example_id + ':' + increment
example_id INTEGER NOT NULL, -- example of first occurrence
python TEXT NOT NULL,
first_el_id INTEGER NOT NULL,
indents INTEGER NOT NULL DEFAULT 1)""") # Unused
# Control block extent information to track all possible control endpoints.
# 1 row for each block (eg, iteration) in the trace corresponding to a control (FOR or IF).
# 1-to-1 with conditions table. (N.B. There are also non-control conditions -- the 'assign's.)
# Many-to-1 with controls.control_id, as that represents target code, not a trace.
cursor.execute('''DROP TABLE IF EXISTS control_block_traces''')
cursor.execute('''CREATE TABLE control_block_traces (
cbt_id TEXT NOT NULL, -- Eg, 'for0:0_40'. Not unique due to last_el_id_maybe rows.
example_id INTEGER NOT NULL,
first_el_id INTEGER NOT NULL, -- "first" for the block, not the control.
last_el_id_maybe INTEGER, -- These are manufactured to demarcate all possible last_el_ids.
last_el_id_min INTEGER, -- Earliest possible last line of the control trace.
last_el_id INTEGER, -- Actual last line of the control trace.
last_el_id_max INTEGER, -- Last possible last line of the control trace. (Duplicated across IF clauses.)
iteration INTEGER, -- 0 based global count (FOR only)
local_iteration INTEGER, -- 0 based count of total iterations for this usage of loop (FOR only)
control_id TEXT NOT NULL)''') # if#/for# (while# is todo)
cursor.execute('''CREATE UNIQUE INDEX cbtfl ON control_block_traces(first_el_id, last_el_id_maybe)''')
# 1 row for each comma-delimited assertion in lines of (ie, example_line of line_type) 'truth'.
# Each row, most importantly types the condition and assigns it a control_id.
cursor.execute("""DROP TABLE IF EXISTS conditions""")
cursor.execute("""CREATE TABLE conditions (
el_id INTEGER PRIMARY KEY,
example_id INTEGER NOT NULL,
condition TEXT NOT NULL,
scheme TEXT NOT NULL,
left_side TEXT,
relop TEXT,
right_side TEXT,
control_id TEXT, -- eg, 'for0:0'. (Unassigned 10/4/19)
condition_type TEXT, -- assign/if/for/while
FOREIGN KEY(el_id) REFERENCES example_lines(el_id))""")
#-- python TEXT)
#-- schematized(condition)
# --type TEXT NOT NULL, -- 'simple assignment', 'iterative', or 'selective'
# --intraexample_repetition INTEGER NOT NULL DEFAULT 0
# lp todo For these repetitions, how many can be considered to be followed with the same # of code
# lines of exactly 1 further indent? 2/10/19
# satisfies INTEGER NOT NULL DEFAULT 0, -- Python code satisfies all examples?
# approved INTEGER NOT NULL DEFAULT 0)""") # Python code is user confirmed?
# Many to 1 with trace lines, as comma-delimited assertions are broken out.
cursor.execute("""DROP TABLE IF EXISTS example_lines""")
cursor.execute('''CREATE TABLE example_lines (
el_id INTEGER PRIMARY KEY, -- will follow line order of given exem
example_id INTEGER NOT NULL,
line TEXT NOT NULL, -- normalized where line_type='truth'
line_type TEXT NOT NULL, -- in/out/truth
control_id TEXT, -- from conditions.control_id
controller TEXT -- the control_id most directly controlling this line, ie, the nearest, earlier
-- control_id whose block hasn't ended. (Unused 10/4/19)
)''')
cursor.execute("""DROP TABLE IF EXISTS examples""")
cursor.execute('''CREATE TABLE examples (
example_id INTEGER PRIMARY KEY, -- will follow order of appearance in given exem.
el_id_count INTEGER NOT NULL, -- The # of el_id lines in the example.
truth_count INTEGER NOT NULL)''') # The # of assertions in the example.
# Unused but may be better than the simpler approach in assertion_triple(). 2/20/19
def unused_find_rel_op(condition: str) -> Tuple:
"""
Return the position of condition's relational operator.
:database: not involved.
:param condition:
:return two string positions, or () if a rel op is not found
"""
open_string = False
previous_character = ""
i = 0
start = 0
stop = 0
for character in condition:
if character == '"' and previous_character != '\\':
if not open_string:
open_string = '"' # Note we're in a "-delimited string.
else: # Already in a string.
if open_string == '"': # String closed.
open_string = False
# Repeat above, for single quote.
if character == "'" and previous_character != '\\':
if not open_string:
open_string = "'"
else:
if open_string == "'":
open_string = False
if not start: # Looking for >, <, ==, <=, >=, and !=.
if not open_string and (character == '>' or character == '<' or character == '=' or character == '!'):
start = i
else: # Now looking for end of relational operator.
if character == '=': # This is only legit 2nd character for a relational operator.
stop = i + 1
else: # Must be a > or <.
if condition[i-1] != '>' and condition[i-1] != '<':
sys.exit("Misplaced", condition[i-1], "character found in condition", condition)
stop = i
break
i += 1
if not stop: