shell-lite 0.4.3__py3-none-any.whl → 0.4.5__py3-none-any.whl
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.
- shell_lite/interpreter.py +66 -47
- shell_lite/lexer.py +16 -8
- shell_lite/main.py +8 -9
- shell_lite/parser.py +264 -114
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.5.dist-info}/METADATA +1 -1
- shell_lite-0.4.5.dist-info/RECORD +15 -0
- shell_lite/fix_nulls.py +0 -29
- shell_lite/formatter.py +0 -75
- shell_lite/interpreter_backup.py +0 -1781
- shell_lite/interpreter_final.py +0 -1773
- shell_lite/interpreter_new.py +0 -1773
- shell_lite/js_compiler.py +0 -220
- shell_lite/lexer_new.py +0 -252
- shell_lite/minimal_interpreter.py +0 -25
- shell_lite/parser_new.py +0 -2229
- shell_lite/patch_parser.py +0 -41
- shell_lite-0.4.3.dist-info/RECORD +0 -25
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.5.dist-info}/LICENSE +0 -0
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.5.dist-info}/WHEEL +0 -0
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.5.dist-info}/entry_points.txt +0 -0
- {shell_lite-0.4.3.dist-info → shell_lite-0.4.5.dist-info}/top_level.txt +0 -0
shell_lite/parser.py
CHANGED
|
@@ -80,10 +80,20 @@ class Parser:
|
|
|
80
80
|
return self.parse_make()
|
|
81
81
|
elif self.check('INPUT'):
|
|
82
82
|
next_t = self.peek(1)
|
|
83
|
-
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
83
|
+
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD', 'PLACEHOLDER'):
|
|
84
84
|
input_token = self.consume()
|
|
85
85
|
return self.parse_id_start_statement(passed_name_token=input_token)
|
|
86
86
|
return self.parse_expression_stmt()
|
|
87
|
+
elif self.check('BUTTON'):
|
|
88
|
+
return self.parse_id_start_statement(passed_name_token=self.consume('BUTTON'))
|
|
89
|
+
elif self.check('COLUMN'):
|
|
90
|
+
return self.parse_id_start_statement(passed_name_token=self.consume('COLUMN'))
|
|
91
|
+
elif self.check('ROW'):
|
|
92
|
+
return self.parse_id_start_statement(passed_name_token=self.consume('ROW'))
|
|
93
|
+
elif self.check('IMAGE'):
|
|
94
|
+
return self.parse_id_start_statement(passed_name_token=self.consume('IMAGE'))
|
|
95
|
+
elif self.check('SIZE'):
|
|
96
|
+
return self.parse_id_start_statement(passed_name_token=self.consume('SIZE'))
|
|
87
97
|
elif self.check('ID'):
|
|
88
98
|
return self.parse_id_start_statement()
|
|
89
99
|
elif self.check('SPAWN'):
|
|
@@ -124,16 +134,40 @@ class Parser:
|
|
|
124
134
|
elif self.check('BEFORE'):
|
|
125
135
|
return self.parse_middleware()
|
|
126
136
|
elif self.check('DEFINE'):
|
|
137
|
+
if self.peek(1).type == 'FUNCTION':
|
|
138
|
+
return self.parse_function_def()
|
|
127
139
|
return self.parse_define_page()
|
|
128
140
|
elif self.check('ADD'):
|
|
129
|
-
|
|
141
|
+
if self.peek(1).type == 'ID' or self.peek(1).type == 'NUMBER' or self.peek(1).type == 'STRING':
|
|
142
|
+
# Heuristic: "Add X to Y" (List) vs "Add Component" (UI) maybe?
|
|
143
|
+
# Assuming UI "add" uses 'ADD' keyword?
|
|
144
|
+
# Lexer maps 'add' to 'ADD'.
|
|
145
|
+
# parse_add_to -> UI.
|
|
146
|
+
# parse_add_to_list -> List.
|
|
147
|
+
# Let's check parse_add_to signature.
|
|
148
|
+
pass
|
|
149
|
+
# For now prioritize List if 'TO' is present later?
|
|
150
|
+
# Or assume parse_add_to handles UI.
|
|
151
|
+
# Let's peek(1). If it's an expression -> List op. If it's a Component?
|
|
152
|
+
# Creating a unified parse_add dispatcher might be better.
|
|
153
|
+
return self.parse_add_distinguish()
|
|
154
|
+
|
|
130
155
|
elif self.check('START'):
|
|
131
156
|
return self.parse_start_server()
|
|
132
157
|
elif self.check('HEADING'):
|
|
133
158
|
return self.parse_heading()
|
|
134
159
|
elif self.check('PARAGRAPH'):
|
|
135
160
|
return self.parse_paragraph()
|
|
136
|
-
|
|
161
|
+
|
|
162
|
+
# English List/Time Ops
|
|
163
|
+
if self.check('ADD'):
|
|
164
|
+
return self.parse_add_to_list()
|
|
165
|
+
if self.check('REMOVE'):
|
|
166
|
+
return self.parse_remove_from_list()
|
|
167
|
+
if self.check('WAIT'):
|
|
168
|
+
return self.parse_wait()
|
|
169
|
+
|
|
170
|
+
if self.check('INCREMENT'):
|
|
137
171
|
return self.parse_increment()
|
|
138
172
|
elif self.check('DECREMENT'):
|
|
139
173
|
return self.parse_decrement()
|
|
@@ -141,6 +175,17 @@ class Parser:
|
|
|
141
175
|
return self.parse_multiply()
|
|
142
176
|
elif self.check('DIVIDE'):
|
|
143
177
|
return self.parse_divide()
|
|
178
|
+
elif self.check('MAKE'):
|
|
179
|
+
return self.parse_make_assignment()
|
|
180
|
+
elif self.check('AS'):
|
|
181
|
+
return self.parse_as_long_as()
|
|
182
|
+
elif self.check('ASK'):
|
|
183
|
+
# Standalone ask statement? e.g. ask "Questions?"
|
|
184
|
+
# Or ask is expression. If statement, maybe just expression statement.
|
|
185
|
+
return self.parse_expression_statement()
|
|
186
|
+
elif self.check('CHECK'):
|
|
187
|
+
self.consume('CHECK')
|
|
188
|
+
return self.parse_if()
|
|
144
189
|
elif self.check('SET'):
|
|
145
190
|
return self.parse_set()
|
|
146
191
|
else:
|
|
@@ -343,24 +388,6 @@ class Parser:
|
|
|
343
388
|
node = Make(class_name, args)
|
|
344
389
|
node.line = token.line
|
|
345
390
|
return node
|
|
346
|
-
def parse_repeat(self) -> Repeat:
|
|
347
|
-
token = self.consume('REPEAT')
|
|
348
|
-
if self.check('NEWLINE'):
|
|
349
|
-
raise SyntaxError(f"repeat requires a count on line {token.line}")
|
|
350
|
-
count = self.parse_expression()
|
|
351
|
-
if self.check('TIMES'):
|
|
352
|
-
self.consume('TIMES')
|
|
353
|
-
self.consume('NEWLINE')
|
|
354
|
-
self.consume('INDENT')
|
|
355
|
-
body = []
|
|
356
|
-
while not self.check('DEDENT') and not self.check('EOF'):
|
|
357
|
-
while self.check('NEWLINE'): self.consume()
|
|
358
|
-
if self.check('DEDENT'): break
|
|
359
|
-
body.append(self.parse_statement())
|
|
360
|
-
self.consume('DEDENT')
|
|
361
|
-
node = Repeat(count, body)
|
|
362
|
-
node.line = token.line
|
|
363
|
-
return node
|
|
364
391
|
return node
|
|
365
392
|
def parse_db_op(self) -> DatabaseOp:
|
|
366
393
|
token = self.consume('DB')
|
|
@@ -494,7 +521,13 @@ class Parser:
|
|
|
494
521
|
node.line = token.line
|
|
495
522
|
return node
|
|
496
523
|
def parse_function_def(self) -> FunctionDef:
|
|
497
|
-
start_token =
|
|
524
|
+
start_token = None
|
|
525
|
+
if self.check('DEFINE'):
|
|
526
|
+
start_token = self.consume('DEFINE')
|
|
527
|
+
self.consume('FUNCTION')
|
|
528
|
+
else:
|
|
529
|
+
start_token = self.consume('TO') # Fallback to existing 'TO' if not 'DEFINE'
|
|
530
|
+
|
|
498
531
|
name = self.consume('ID').value
|
|
499
532
|
args = []
|
|
500
533
|
while self.check('ID'):
|
|
@@ -517,6 +550,7 @@ class Parser:
|
|
|
517
550
|
self.consume('ASSIGN')
|
|
518
551
|
default_val = self.parse_expression()
|
|
519
552
|
args.append((arg_name, default_val, type_hint))
|
|
553
|
+
if self.check('DOING'): self.consume('DOING')
|
|
520
554
|
if self.check('COLON'):
|
|
521
555
|
self.consume('COLON')
|
|
522
556
|
self.consume('NEWLINE')
|
|
@@ -919,8 +953,8 @@ class Parser:
|
|
|
919
953
|
return node
|
|
920
954
|
def parse_start_server(self) -> Node:
|
|
921
955
|
token = self.consume('START')
|
|
922
|
-
if self.check('SERVER'):
|
|
923
|
-
self.consume(
|
|
956
|
+
if self.check('SERVER') or self.check('WEBSITE') or (self.check('ID') and self.peek().value == 'website'):
|
|
957
|
+
self.consume()
|
|
924
958
|
port = Number(8080)
|
|
925
959
|
if self.check('ON'):
|
|
926
960
|
self.consume('ON')
|
|
@@ -987,6 +1021,7 @@ class Parser:
|
|
|
987
1021
|
def parse_if(self) -> If:
|
|
988
1022
|
self.consume('IF')
|
|
989
1023
|
condition = self.parse_expression()
|
|
1024
|
+
if self.check('COLON'): self.consume('COLON')
|
|
990
1025
|
self.consume('NEWLINE')
|
|
991
1026
|
self.consume('INDENT')
|
|
992
1027
|
body = []
|
|
@@ -998,8 +1033,11 @@ class Parser:
|
|
|
998
1033
|
else_body = None
|
|
999
1034
|
if self.check('ELIF'):
|
|
1000
1035
|
else_body = [self.parse_elif()]
|
|
1001
|
-
elif self.check('ELSE'):
|
|
1002
|
-
self.consume('ELSE')
|
|
1036
|
+
elif self.check('ELSE') or self.check('OTHERWISE'):
|
|
1037
|
+
if self.check('ELSE'): self.consume('ELSE')
|
|
1038
|
+
else: self.consume('OTHERWISE')
|
|
1039
|
+
|
|
1040
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1003
1041
|
self.consume('NEWLINE')
|
|
1004
1042
|
self.consume('INDENT')
|
|
1005
1043
|
else_body = []
|
|
@@ -1009,9 +1047,11 @@ class Parser:
|
|
|
1009
1047
|
else_body.append(self.parse_statement())
|
|
1010
1048
|
self.consume('DEDENT')
|
|
1011
1049
|
return If(condition, body, else_body)
|
|
1050
|
+
|
|
1012
1051
|
def parse_elif(self) -> If:
|
|
1013
1052
|
token = self.consume('ELIF')
|
|
1014
1053
|
condition = self.parse_expression()
|
|
1054
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1015
1055
|
self.consume('NEWLINE')
|
|
1016
1056
|
self.consume('INDENT')
|
|
1017
1057
|
body = []
|
|
@@ -1025,6 +1065,7 @@ class Parser:
|
|
|
1025
1065
|
else_body = [self.parse_elif()]
|
|
1026
1066
|
elif self.check('ELSE'):
|
|
1027
1067
|
self.consume('ELSE')
|
|
1068
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1028
1069
|
self.consume('NEWLINE')
|
|
1029
1070
|
self.consume('INDENT')
|
|
1030
1071
|
else_body = []
|
|
@@ -1033,14 +1074,12 @@ class Parser:
|
|
|
1033
1074
|
if self.check('DEDENT'): break
|
|
1034
1075
|
else_body.append(self.parse_statement())
|
|
1035
1076
|
self.consume('DEDENT')
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
node = If(condition, body, else_body)
|
|
1039
|
-
node.line = token.line
|
|
1040
|
-
return node
|
|
1077
|
+
return If(condition, body, else_body)
|
|
1078
|
+
|
|
1041
1079
|
def parse_while(self) -> While:
|
|
1042
1080
|
start_token = self.consume('WHILE')
|
|
1043
1081
|
condition = self.parse_expression()
|
|
1082
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1044
1083
|
self.consume('NEWLINE')
|
|
1045
1084
|
self.consume('INDENT')
|
|
1046
1085
|
body = []
|
|
@@ -1052,8 +1091,28 @@ class Parser:
|
|
|
1052
1091
|
node = While(condition, body)
|
|
1053
1092
|
node.line = start_token.line
|
|
1054
1093
|
return node
|
|
1094
|
+
|
|
1095
|
+
def parse_repeat(self) -> Repeat:
|
|
1096
|
+
start_token = self.consume('REPEAT')
|
|
1097
|
+
count = self.parse_expression()
|
|
1098
|
+
self.consume('TIMES')
|
|
1099
|
+
if self.check('COLON'): self.consume('COLON')
|
|
1100
|
+
self.consume('NEWLINE')
|
|
1101
|
+
self.consume('INDENT')
|
|
1102
|
+
body = []
|
|
1103
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
1104
|
+
while self.check('NEWLINE'): self.consume()
|
|
1105
|
+
if self.check('DEDENT'): break
|
|
1106
|
+
body.append(self.parse_statement())
|
|
1107
|
+
self.consume('DEDENT')
|
|
1108
|
+
node = Repeat(count, body)
|
|
1109
|
+
node.line = start_token.line
|
|
1110
|
+
return node
|
|
1111
|
+
|
|
1055
1112
|
def parse_try(self) -> Try:
|
|
1056
1113
|
start_token = self.consume('TRY')
|
|
1114
|
+
if self.check('COLON'):
|
|
1115
|
+
self.consume('COLON')
|
|
1057
1116
|
self.consume('NEWLINE')
|
|
1058
1117
|
self.consume('INDENT')
|
|
1059
1118
|
try_body = []
|
|
@@ -1064,6 +1123,8 @@ class Parser:
|
|
|
1064
1123
|
self.consume('DEDENT')
|
|
1065
1124
|
self.consume('CATCH')
|
|
1066
1125
|
catch_var = self.consume('ID').value
|
|
1126
|
+
if self.check('COLON'):
|
|
1127
|
+
self.consume('COLON')
|
|
1067
1128
|
self.consume('NEWLINE')
|
|
1068
1129
|
self.consume('INDENT')
|
|
1069
1130
|
catch_body = []
|
|
@@ -1249,8 +1310,13 @@ class Parser:
|
|
|
1249
1310
|
self.consume('DEDENT')
|
|
1250
1311
|
return Layout(layout_type, children)
|
|
1251
1312
|
|
|
1252
|
-
elif token.type in ('BUTTON', 'INPUT', 'HEADING'
|
|
1253
|
-
|
|
1313
|
+
elif (token.type in ('BUTTON', 'INPUT', 'HEADING') or
|
|
1314
|
+
(token.type == 'ID' and token.value == 'text')):
|
|
1315
|
+
if token.type == 'ID' and token.value == 'text':
|
|
1316
|
+
widget_type = 'TEXT'
|
|
1317
|
+
self.consume() # consume 'text' ID
|
|
1318
|
+
else:
|
|
1319
|
+
widget_type = self.consume().value
|
|
1254
1320
|
label = self.consume('STRING').value
|
|
1255
1321
|
|
|
1256
1322
|
var_name = None
|
|
@@ -1276,7 +1342,13 @@ class Parser:
|
|
|
1276
1342
|
|
|
1277
1343
|
def parse_factor_simple(self) -> Node:
|
|
1278
1344
|
token = self.peek()
|
|
1279
|
-
if token.type == '
|
|
1345
|
+
if token.type == 'ASK':
|
|
1346
|
+
self.consume('ASK')
|
|
1347
|
+
prompt = self.parse_expression()
|
|
1348
|
+
node = Call('input', [prompt]) # Alias to input
|
|
1349
|
+
node.line = token.line
|
|
1350
|
+
return node
|
|
1351
|
+
elif token.type == 'NUMBER':
|
|
1280
1352
|
self.consume()
|
|
1281
1353
|
val = token.value
|
|
1282
1354
|
if '.' in val:
|
|
@@ -1353,7 +1425,7 @@ class Parser:
|
|
|
1353
1425
|
expr = self.parse_expression()
|
|
1354
1426
|
self.consume('RPAREN')
|
|
1355
1427
|
return expr
|
|
1356
|
-
elif token.type == 'INPUT'
|
|
1428
|
+
elif token.type == 'INPUT':
|
|
1357
1429
|
is_tag = False
|
|
1358
1430
|
next_t = self.peek(1)
|
|
1359
1431
|
if next_t.type in ('ID', 'TYPE', 'STRING', 'NAME', 'VALUE', 'CLASS', 'STYLE', 'ONCLICK', 'SRC', 'HREF', 'ACTION', 'METHOD'):
|
|
@@ -1382,6 +1454,8 @@ class Parser:
|
|
|
1382
1454
|
node = UnaryOp('not', right)
|
|
1383
1455
|
node.line = op.line
|
|
1384
1456
|
return node
|
|
1457
|
+
elif token.type == 'ASK':
|
|
1458
|
+
return self.parse_factor_simple()
|
|
1385
1459
|
elif token.type == 'LPAREN':
|
|
1386
1460
|
self.consume('LPAREN')
|
|
1387
1461
|
node = self.parse_expression()
|
|
@@ -1445,8 +1519,6 @@ class Parser:
|
|
|
1445
1519
|
return self.parse_sum()
|
|
1446
1520
|
elif token.type == 'UPPER':
|
|
1447
1521
|
return self.parse_upper()
|
|
1448
|
-
elif token.type == 'NUMBERS':
|
|
1449
|
-
return self.parse_numbers_range()
|
|
1450
1522
|
elif token.type == 'DATE':
|
|
1451
1523
|
token = self.consume('DATE')
|
|
1452
1524
|
s = self.consume('STRING').value
|
|
@@ -1494,6 +1566,8 @@ class Parser:
|
|
|
1494
1566
|
return self._parse_natural_list()
|
|
1495
1567
|
elif self.peek(1).type == 'UNIQUE' and self.peek(2).type == 'SET' and self.peek(3).type == 'OF':
|
|
1496
1568
|
return self._parse_natural_set()
|
|
1569
|
+
if token.value == 'numbers' and self.peek(1).type == 'FROM':
|
|
1570
|
+
return self.parse_numbers_range()
|
|
1497
1571
|
self.consume()
|
|
1498
1572
|
instance_name = token.value
|
|
1499
1573
|
method_name = None
|
|
@@ -1606,6 +1680,7 @@ class Parser:
|
|
|
1606
1680
|
node.line = start_token.line
|
|
1607
1681
|
return node
|
|
1608
1682
|
start_token = self.consume('FOR')
|
|
1683
|
+
if self.check('EACH'): self.consume('EACH')
|
|
1609
1684
|
if self.check('ID') and self.peek(1).type == 'IN':
|
|
1610
1685
|
var_name = self.consume('ID').value
|
|
1611
1686
|
self.consume('IN')
|
|
@@ -1642,6 +1717,8 @@ class Parser:
|
|
|
1642
1717
|
count_expr = self.parse_expression()
|
|
1643
1718
|
self.consume('IN')
|
|
1644
1719
|
self.consume('RANGE')
|
|
1720
|
+
if self.check('COLON'):
|
|
1721
|
+
self.consume('COLON')
|
|
1645
1722
|
self.consume('NEWLINE')
|
|
1646
1723
|
self.consume('INDENT')
|
|
1647
1724
|
body = []
|
|
@@ -1704,11 +1781,39 @@ class Parser:
|
|
|
1704
1781
|
return left
|
|
1705
1782
|
def parse_comparison(self) -> Node:
|
|
1706
1783
|
left = self.parse_arithmetic()
|
|
1707
|
-
if self.peek().type in ('EQ', 'NEQ', 'GT', 'LT', 'GE', 'LE', 'IS', 'MATCHES'):
|
|
1784
|
+
if self.peek().type in ('EQ', 'NEQ', 'GT', 'LT', 'GE', 'LE', 'IS', 'MATCHES', 'GREATER', 'LESS', 'EQUAL', 'CONTAINS', 'EMPTY'):
|
|
1708
1785
|
op_token = self.consume()
|
|
1709
1786
|
op_val = op_token.value
|
|
1787
|
+
|
|
1788
|
+
# Handle "is greater/less/equal"
|
|
1710
1789
|
if op_token.type == 'IS':
|
|
1711
|
-
|
|
1790
|
+
if self.check('GREATER'):
|
|
1791
|
+
self.consume('GREATER'); self.consume('THAN')
|
|
1792
|
+
op_val = '>'
|
|
1793
|
+
elif self.check('LESS'):
|
|
1794
|
+
self.consume('LESS'); self.consume('THAN')
|
|
1795
|
+
op_val = '<'
|
|
1796
|
+
elif self.check('EQUAL'):
|
|
1797
|
+
self.consume('EQUAL'); self.consume('TO')
|
|
1798
|
+
op_val = '=='
|
|
1799
|
+
elif self.check('NOT'):
|
|
1800
|
+
self.consume('NOT'); self.consume('EQUAL'); self.consume('TO')
|
|
1801
|
+
op_val = '!='
|
|
1802
|
+
elif self.check('EMPTY'):
|
|
1803
|
+
self.consume('EMPTY')
|
|
1804
|
+
# is empty -> Call('empty', [left])
|
|
1805
|
+
node = Call('empty', [left])
|
|
1806
|
+
node.line = op_token.line
|
|
1807
|
+
return node
|
|
1808
|
+
else:
|
|
1809
|
+
op_val = '=='
|
|
1810
|
+
elif op_token.type == 'CONTAINS':
|
|
1811
|
+
# list contains item -> Call('contains', [list, item])
|
|
1812
|
+
right = self.parse_arithmetic()
|
|
1813
|
+
node = Call('contains', [left, right])
|
|
1814
|
+
node.line = op_token.line
|
|
1815
|
+
return node
|
|
1816
|
+
|
|
1712
1817
|
right = self.parse_arithmetic()
|
|
1713
1818
|
node = BinOp(left, op_val, right)
|
|
1714
1819
|
node.line = op_token.line
|
|
@@ -1718,17 +1823,39 @@ class Parser:
|
|
|
1718
1823
|
left = self.parse_term()
|
|
1719
1824
|
while self.peek().type in ('PLUS', 'MINUS'):
|
|
1720
1825
|
op_token = self.consume()
|
|
1826
|
+
op_val = op_token.value
|
|
1827
|
+
# Normalize to symbols
|
|
1828
|
+
if op_token.type == 'PLUS': op_val = '+'
|
|
1829
|
+
if op_token.type == 'MINUS': op_val = '-'
|
|
1830
|
+
|
|
1721
1831
|
right = self.parse_term()
|
|
1722
|
-
new_node = BinOp(left,
|
|
1832
|
+
new_node = BinOp(left, op_val, right)
|
|
1723
1833
|
new_node.line = op_token.line
|
|
1724
1834
|
left = new_node
|
|
1725
1835
|
return left
|
|
1726
1836
|
def parse_term(self) -> Node:
|
|
1727
1837
|
left = self.parse_factor()
|
|
1728
|
-
while self.peek().type in ('MUL', 'DIV', 'MOD'):
|
|
1838
|
+
while self.peek().type in ('MUL', 'DIV', 'MOD', 'TIMES'):
|
|
1839
|
+
# Disambiguate "repeat 3 TIMES" vs "3 TIMES 4"
|
|
1840
|
+
if self.peek().type == 'TIMES':
|
|
1841
|
+
next_tok = self.peek(1)
|
|
1842
|
+
if next_tok.type in ('COLON', 'NEWLINE'):
|
|
1843
|
+
break
|
|
1844
|
+
|
|
1729
1845
|
op_token = self.consume()
|
|
1846
|
+
op_val = op_token.value
|
|
1847
|
+
|
|
1848
|
+
# Normalize
|
|
1849
|
+
if op_token.type == 'MUL': op_val = '*'
|
|
1850
|
+
if op_token.type == 'TIMES': op_val = '*'
|
|
1851
|
+
if op_token.type == 'DIV':
|
|
1852
|
+
op_val = '/'
|
|
1853
|
+
if self.check('BY'): self.consume('BY') # Handle "divided by"
|
|
1854
|
+
|
|
1855
|
+
if op_token.type == 'MOD': op_val = '%'
|
|
1856
|
+
|
|
1730
1857
|
right = self.parse_factor()
|
|
1731
|
-
new_node = BinOp(left,
|
|
1858
|
+
new_node = BinOp(left, op_val, right)
|
|
1732
1859
|
new_node.line = op_token.line
|
|
1733
1860
|
left = new_node
|
|
1734
1861
|
return left
|
|
@@ -1960,134 +2087,157 @@ class Parser:
|
|
|
1960
2087
|
|
|
1961
2088
|
|
|
1962
2089
|
def parse_sum(self) -> Node:
|
|
1963
|
-
|
|
1964
2090
|
token = self.consume('SUM')
|
|
1965
|
-
|
|
1966
2091
|
self.consume('OF')
|
|
1967
|
-
|
|
1968
2092
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
if self.check('NUMBERS'):
|
|
1973
|
-
|
|
2093
|
+
# Check for 'numbers from ...' (contextual keyword 'numbers')
|
|
2094
|
+
if self.check('ID') and self.peek().value == 'numbers':
|
|
1974
2095
|
range_node = self.parse_numbers_range()
|
|
1975
|
-
|
|
1976
2096
|
# range_node is Call('range_list', ...)
|
|
1977
|
-
|
|
1978
2097
|
# We want Call('sum', [range_node])
|
|
1979
|
-
|
|
1980
2098
|
node = Call('sum', [range_node])
|
|
1981
|
-
|
|
1982
2099
|
node.line = token.line
|
|
1983
|
-
|
|
1984
2100
|
return node
|
|
1985
|
-
|
|
1986
2101
|
|
|
1987
|
-
|
|
1988
2102
|
expr = self.parse_expression()
|
|
1989
|
-
|
|
1990
2103
|
node = Call('sum', [expr])
|
|
1991
|
-
|
|
1992
2104
|
node.line = token.line
|
|
1993
|
-
|
|
1994
2105
|
return node
|
|
1995
2106
|
|
|
1996
2107
|
|
|
1997
2108
|
|
|
1998
2109
|
def parse_upper(self) -> Node:
|
|
1999
|
-
|
|
2000
2110
|
token = self.consume('UPPER')
|
|
2001
|
-
|
|
2002
2111
|
expr = self.parse_expression()
|
|
2003
|
-
|
|
2004
2112
|
only_letters = Boolean(False)
|
|
2005
2113
|
|
|
2006
|
-
if self.check('
|
|
2007
|
-
|
|
2008
|
-
self.
|
|
2009
|
-
|
|
2010
|
-
if self.check('LETTERS'):
|
|
2011
|
-
|
|
2012
|
-
self.consume('LETTERS')
|
|
2013
|
-
|
|
2114
|
+
if self.check('ID') and self.peek().value == 'only':
|
|
2115
|
+
self.consume() # consume 'only'
|
|
2116
|
+
if self.check('ID') and self.peek().value == 'letters':
|
|
2117
|
+
self.consume() # consume 'letters'
|
|
2014
2118
|
only_letters = Boolean(True)
|
|
2015
|
-
|
|
2016
2119
|
node = Call('upper', [expr, only_letters])
|
|
2017
|
-
|
|
2018
2120
|
node.line = token.line
|
|
2019
|
-
|
|
2020
2121
|
return node
|
|
2021
2122
|
|
|
2022
2123
|
|
|
2023
2124
|
|
|
2024
2125
|
def parse_numbers_range(self) -> Node:
|
|
2025
|
-
|
|
2026
|
-
token = self.
|
|
2027
|
-
|
|
2126
|
+
# Expect 'numbers' as ID
|
|
2127
|
+
token = self.peek()
|
|
2128
|
+
if self.check('ID') and self.peek().value == 'numbers':
|
|
2129
|
+
self.consume()
|
|
2130
|
+
else:
|
|
2131
|
+
# Should be 'numbers' but if called from parse_sum we assume check passed.
|
|
2132
|
+
# If called from Factor loop...
|
|
2133
|
+
pass
|
|
2134
|
+
|
|
2028
2135
|
self.consume('FROM')
|
|
2029
|
-
|
|
2030
2136
|
start = self.parse_expression()
|
|
2031
|
-
|
|
2032
2137
|
self.consume('TO')
|
|
2033
|
-
|
|
2034
2138
|
end = self.parse_expression()
|
|
2035
|
-
|
|
2036
2139
|
|
|
2037
|
-
|
|
2038
2140
|
condition = None
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
self.
|
|
2045
|
-
|
|
2046
|
-
if self.check('PRIME'):
|
|
2047
|
-
|
|
2048
|
-
self.consume('PRIME')
|
|
2049
|
-
|
|
2141
|
+
if self.check('ID') and self.peek().value == 'that':
|
|
2142
|
+
self.consume() # that
|
|
2143
|
+
if self.check('ID') and self.peek().value == 'are':
|
|
2144
|
+
self.consume() # are
|
|
2145
|
+
|
|
2146
|
+
if self.check('ID') and self.peek().value == 'prime':
|
|
2147
|
+
self.consume() # prime
|
|
2050
2148
|
condition = String('prime')
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
self.consume('DIGITS')
|
|
2055
|
-
|
|
2149
|
+
elif self.check('ID') and self.peek().value == 'digits':
|
|
2150
|
+
self.consume() # digits
|
|
2056
2151
|
condition = String('digits')
|
|
2057
|
-
|
|
2152
|
+
|
|
2058
2153
|
elif self.check('WHEN'):
|
|
2059
|
-
|
|
2060
2154
|
self.consume('WHEN')
|
|
2061
|
-
|
|
2062
2155
|
# 'when even' -> check for ID 'even' or expression?
|
|
2063
|
-
|
|
2064
2156
|
# User example: 'when even'. Implicit variable?
|
|
2065
|
-
|
|
2066
2157
|
# Let's verify repro: 'when even'
|
|
2067
|
-
|
|
2068
2158
|
if self.check('ID') and self.peek().value == 'even':
|
|
2069
|
-
|
|
2070
2159
|
self.consume()
|
|
2071
|
-
|
|
2072
2160
|
condition = String('even')
|
|
2073
|
-
|
|
2074
2161
|
elif self.check('ID') and self.peek().value == 'odd':
|
|
2075
|
-
|
|
2076
2162
|
self.consume()
|
|
2077
|
-
|
|
2078
2163
|
condition = String('odd')
|
|
2079
|
-
|
|
2080
2164
|
else:
|
|
2081
|
-
|
|
2082
2165
|
# TODO: handle generic expression filter if needed
|
|
2083
|
-
|
|
2084
2166
|
pass
|
|
2085
|
-
|
|
2086
2167
|
|
|
2087
|
-
|
|
2088
2168
|
node = Call('range_list', [start, end, condition if condition else Boolean(False)])
|
|
2169
|
+
node.line = token.line
|
|
2170
|
+
return node
|
|
2171
|
+
def parse_add_to_list(self) -> Node:
|
|
2172
|
+
token = self.consume('ADD')
|
|
2173
|
+
item = self.parse_expression()
|
|
2174
|
+
self.consume('TO')
|
|
2175
|
+
list_expr = self.parse_expression()
|
|
2176
|
+
self.consume('NEWLINE')
|
|
2177
|
+
node = Call('append', [list_expr, item])
|
|
2178
|
+
node.line = token.line
|
|
2179
|
+
return node
|
|
2089
2180
|
|
|
2181
|
+
def parse_remove_from_list(self) -> Node:
|
|
2182
|
+
token = self.consume('REMOVE')
|
|
2183
|
+
item = self.parse_expression()
|
|
2184
|
+
self.consume('FROM')
|
|
2185
|
+
list_expr = self.parse_expression()
|
|
2186
|
+
self.consume('NEWLINE')
|
|
2187
|
+
node = Call('remove', [list_expr, item])
|
|
2090
2188
|
node.line = token.line
|
|
2189
|
+
return node
|
|
2190
|
+
|
|
2191
|
+
def parse_wait(self) -> Node:
|
|
2192
|
+
token = self.consume('WAIT')
|
|
2193
|
+
value = self.parse_expression()
|
|
2194
|
+
if self.check('SECOND'): self.consume('SECOND')
|
|
2195
|
+
elif self.check('SECONDS'): self.consume('SECONDS') # Assuming 'SECONDS' token maps to SECOND?
|
|
2196
|
+
# Actually I need to check lexer mapping for MINUTES/SECONDS.
|
|
2197
|
+
# Lexer has: 'minute': 'MINUTE', 'minutes': 'MINUTE', 'second': 'SECOND', 'seconds': 'SECOND'
|
|
2198
|
+
|
|
2199
|
+
elif self.check('MINUTE'):
|
|
2200
|
+
self.consume('MINUTE')
|
|
2201
|
+
value = BinOp(value, '*', Number(60))
|
|
2202
|
+
|
|
2203
|
+
self.consume('NEWLINE')
|
|
2204
|
+
node = Call('wait', [value])
|
|
2205
|
+
node.line = token.line
|
|
2206
|
+
return node
|
|
2091
2207
|
|
|
2208
|
+
def parse_add_distinguish(self) -> Node:
|
|
2209
|
+
# Distinguish "ADD <expr> TO <list>" vs "ADD <component> ..."
|
|
2210
|
+
tok = self.peek(1)
|
|
2211
|
+
if tok.type in ('BUTTON', 'HEADING', 'PARAGRAPH', 'IMAGE', 'APP', 'PAGE', 'Use', 'INPUT', 'TEXT'):
|
|
2212
|
+
return self.parse_add_to()
|
|
2213
|
+
else:
|
|
2214
|
+
return self.parse_add_to_list()
|
|
2215
|
+
|
|
2216
|
+
def parse_make_assignment(self) -> Node:
|
|
2217
|
+
token = self.consume('MAKE')
|
|
2218
|
+
name = self.consume('ID').value
|
|
2219
|
+
if self.check('BE'): self.consume('BE')
|
|
2220
|
+
value = self.parse_expression()
|
|
2221
|
+
self.consume('NEWLINE')
|
|
2222
|
+
node = Assign(name, value)
|
|
2223
|
+
node.line = token.line
|
|
2224
|
+
return node
|
|
2225
|
+
|
|
2226
|
+
def parse_as_long_as(self) -> While:
|
|
2227
|
+
start_token = self.consume('AS')
|
|
2228
|
+
self.consume('LONG')
|
|
2229
|
+
self.consume('AS')
|
|
2230
|
+
condition = self.parse_expression()
|
|
2231
|
+
if self.check('COLON'): self.consume('COLON')
|
|
2232
|
+
self.consume('NEWLINE')
|
|
2233
|
+
self.consume('INDENT')
|
|
2234
|
+
body = []
|
|
2235
|
+
while not self.check('DEDENT') and not self.check('EOF'):
|
|
2236
|
+
while self.check('NEWLINE'): self.consume()
|
|
2237
|
+
if self.check('DEDENT'): break
|
|
2238
|
+
body.append(self.parse_statement())
|
|
2239
|
+
self.consume('DEDENT')
|
|
2240
|
+
node = While(condition, body)
|
|
2241
|
+
node.line = start_token.line
|
|
2092
2242
|
return node
|
|
2093
2243
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
shell_lite/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
2
|
+
shell_lite/ast_nodes.py,sha256=KGFlZmP21MJXo6uRYRJfo_R3BEspfDD8xMBtAVfUDgU,5873
|
|
3
|
+
shell_lite/cli.py,sha256=14Kq1ohSXS3p-xdh0DPi7eXskUtSX81huSyGhktoOMA,250
|
|
4
|
+
shell_lite/compiler.py,sha256=FcwDLbrjnMnH0VMzEeYPSqD3nIeFAwKIdvEDAT1e3GE,24984
|
|
5
|
+
shell_lite/interpreter.py,sha256=iyRlaCgLTpGQjeyn5hDm1_4BpztmBjH45QYwaDoOtBg,78408
|
|
6
|
+
shell_lite/lexer.py,sha256=TdU2QIfxjZqnKDUz_SBECNC74k4ZeQBpX27xFjZImEo,13441
|
|
7
|
+
shell_lite/main.py,sha256=xkUy2KHp8zpQ6yQ3TulwI86a-PX63-kqVGeaOf6lp8s,22791
|
|
8
|
+
shell_lite/parser.py,sha256=IznHJxopliPX3MsI-P9SzB5UOkBCJlxwatw9wxMzDK0,88211
|
|
9
|
+
shell_lite/runtime.py,sha256=pSjBeA1dTQ-a94q3FLdv9lqZurdd6MJmfhFGHhOoQEM,16057
|
|
10
|
+
shell_lite-0.4.5.dist-info/LICENSE,sha256=33eziKLPxbqGCqdHtEHAFe1KSOgqc0-jWUQmdgKq85Q,1092
|
|
11
|
+
shell_lite-0.4.5.dist-info/METADATA,sha256=FV-ta6IiRo6YdJ46ufi0yrF4Nec_9f13k2cMDj4aY4Q,2475
|
|
12
|
+
shell_lite-0.4.5.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
13
|
+
shell_lite-0.4.5.dist-info/entry_points.txt,sha256=tglL8tjyPIh1W85j6zFpNZjMpQe_xC-k-7BOhHLWfxc,45
|
|
14
|
+
shell_lite-0.4.5.dist-info/top_level.txt,sha256=hIln5ltrok_Mn3ijlQeqMFF6hHBHCyhzqCO7KL358cg,11
|
|
15
|
+
shell_lite-0.4.5.dist-info/RECORD,,
|
shell_lite/fix_nulls.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import sys
|
|
3
|
-
import glob
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
files = [
|
|
7
|
-
r'c:\Users\shrey\OneDrive\Desktop\oka\shell-lite\shell_lite\parser.py',
|
|
8
|
-
r'c:\Users\shrey\OneDrive\Desktop\oka\shell-lite\shell_lite\lexer.py',
|
|
9
|
-
r'c:\Users\shrey\OneDrive\Desktop\oka\shell-lite\shell_lite\interpreter.py',
|
|
10
|
-
r'c:\Users\shrey\OneDrive\Desktop\oka\shell-lite\shell_lite\main.py',
|
|
11
|
-
r'c:\Users\shrey\OneDrive\Desktop\oka\shell-lite\shell_lite\ast_nodes.py',
|
|
12
|
-
r'c:\Users\shrey\OneDrive\Desktop\oka\tests_suite\repro_issues.shl'
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
for path in files:
|
|
16
|
-
try:
|
|
17
|
-
with open(path, 'rb') as f:
|
|
18
|
-
content = f.read()
|
|
19
|
-
|
|
20
|
-
if b'\x00' in content:
|
|
21
|
-
print(f"Null bytes found in {path}! Fixing...")
|
|
22
|
-
new_content = content.replace(b'\x00', b'')
|
|
23
|
-
with open(path, 'wb') as f:
|
|
24
|
-
f.write(new_content)
|
|
25
|
-
print(f"Fixed {path}.")
|
|
26
|
-
else:
|
|
27
|
-
print(f"No null bytes in {path}.")
|
|
28
|
-
except Exception as e:
|
|
29
|
-
print(f"Error checking {path}: {e}")
|