-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlatin.py
More file actions
1245 lines (1061 loc) · 55.8 KB
/
latin.py
File metadata and controls
1245 lines (1061 loc) · 55.8 KB
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
#!/usr/bin/env python3
"""
LATIN Interpreter - Enhanced Version
Latin Ain't This Insufferable Normally
Full-featured interpreter with loops, functions, and more arithmetic.
"""
import re
import sys
from typing import Dict, List, Optional, Tuple
class LatinExceptionThrown(Exception):
"""Raised when a LATIN exception should be thrown (e.g., division by zero)."""
def __init__(self, handler_line: int):
self.handler_line = handler_line
super().__init__()
class RomanNumeralParser:
"""Parse Roman numerals to integers and vice versa."""
ROMAN_VALUES = {
'M': 1000, 'D': 500, 'C': 100, 'L': 50,
'X': 10, 'V': 5, 'I': 1
}
@classmethod
def parse(cls, roman: str) -> Optional[int]:
"""Convert a Roman numeral string to an integer."""
if not roman:
return None
total = 0
prev_value = 0
for char in reversed(roman):
if char not in cls.ROMAN_VALUES:
return None
value = cls.ROMAN_VALUES[char]
if value < prev_value:
total -= value
else:
total += value
prev_value = value
return total if total > 0 else None
@classmethod
def to_roman(cls, num: int) -> str:
"""Convert an integer to Roman numeral string."""
if num <= 0:
return "NIHIL" # "nothing" in Latin
values = [
(1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),
(100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'),
(10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')
]
result = []
for value, numeral in values:
count = num // value
if count:
result.append(numeral * count)
num -= value * count
return ''.join(result)
class LatinDeclension:
"""Handle Latin noun declensions."""
# Expanded declension table
DECLENSIONS = {
# Second declension masculine
'NUMERUS': {'gen': 'NUMERI', 'acc': 'NUMERUM', 'dat': 'NUMERO', 'abl': 'NUMERO', 'voc': 'NUMERE'},
'PRIMUS': {'gen': 'PRIMI', 'acc': 'PRIMUM', 'dat': 'PRIMO', 'abl': 'PRIMO', 'voc': 'PRIME'},
'SECUNDUS': {'gen': 'SECUNDI', 'acc': 'SECUNDUM', 'dat': 'SECUNDO', 'abl': 'SECUNDO', 'voc': 'SECUNDE'},
'TERTIUS': {'gen': 'TERTII', 'acc': 'TERTIUM', 'dat': 'TERTIO', 'abl': 'TERTIO', 'voc': 'TERTIE'},
'QUARTUS': {'gen': 'QUARTI', 'acc': 'QUARTUM', 'dat': 'QUARTO', 'abl': 'QUARTO', 'voc': 'QUARTE'},
'QUINTUS': {'gen': 'QUINTI', 'acc': 'QUINTUM', 'dat': 'QUINTO', 'abl': 'QUINTO', 'voc': 'QUINTE'},
'MAXIMVS': {'gen': 'MAXIMI', 'acc': 'MAXIMVM', 'dat': 'MAXIMO', 'abl': 'MAXIMO', 'voc': 'MAXIME'},
'AMICUS': {'gen': 'AMICI', 'acc': 'AMICUM', 'dat': 'AMICO', 'abl': 'AMICO', 'voc': 'AMICE'},
'SERVUS': {'gen': 'SERVII', 'acc': 'SERVUM', 'dat': 'SERVO', 'abl': 'SERVO', 'voc': 'SERVE'},
'DOMINUS': {'gen': 'DOMINI', 'acc': 'DOMINUM', 'dat': 'DOMINO', 'abl': 'DOMINO', 'voc': 'DOMINE'},
'FILIUS': {'gen': 'FILII', 'acc': 'FILIUM', 'dat': 'FILIO', 'abl': 'FILIO', 'voc': 'FILI'},
'ANNUS': {'gen': 'ANNI', 'acc': 'ANNUM', 'dat': 'ANNO', 'abl': 'ANNO', 'voc': 'ANNE'},
'LIBER': {'gen': 'LIBRI', 'acc': 'LIBRUM', 'dat': 'LIBRO', 'abl': 'LIBRO', 'voc': 'LIBER'},
'VENTER': {'gen': 'VENTRIS', 'acc': 'VENTREM', 'dat': 'VENTRI', 'abl': 'VENTRE', 'voc': 'VENTER'},
'INDEX': {'gen': 'INDICIS', 'acc': 'INDICEM', 'dat': 'INDICI', 'abl': 'INDICE', 'voc': 'INDEX'},
'RESULTAT': {'gen': 'RESULTATI', 'acc': 'RESULTATUM', 'dat': 'RESULTATO', 'abl': 'RESULTATO', 'voc': 'RESULTAT'},
'ERROR': {'gen': 'ERRORIS', 'acc': 'ERROREM', 'dat': 'ERRORI', 'abl': 'ERRORE', 'voc': 'ERROR'},
'EXCEPTIO': {'gen': 'EXCEPTIONIS', 'acc': 'EXCEPTIONEM', 'dat': 'EXCEPTIONI', 'abl': 'EXCEPTIONE', 'voc': 'EXCEPTIO'},
# Second declension neuter
'BELLVM': {'gen': 'BELLI', 'acc': 'BELLVM', 'dat': 'BELLO', 'abl': 'BELLO', 'voc': 'BELLVM'},
'VERBVM': {'gen': 'VERBI', 'acc': 'VERBVM', 'dat': 'VERBO', 'abl': 'VERBO', 'voc': 'VERBVM'},
'DONVM': {'gen': 'DONI', 'acc': 'DONVM', 'dat': 'DONO', 'abl': 'DONO', 'voc': 'DONVM'},
'PRAEFIXVM': {'gen': 'PRAEFIXI', 'acc': 'PRAEFIXVM', 'dat': 'PRAEFIXO', 'abl': 'PRAEFIXO', 'voc': 'PRAEFIXVM'},
# First declension feminine
'PUELLA': {'gen': 'PUELLAE', 'acc': 'PUELLAM', 'dat': 'PUELLAE', 'abl': 'PUELLA', 'voc': 'PUELLA'},
'ROSA': {'gen': 'ROSAE', 'acc': 'ROSAM', 'dat': 'ROSAE', 'abl': 'ROSA', 'voc': 'ROSA'},
'AQUA': {'gen': 'AQUAE', 'acc': 'AQUAM', 'dat': 'AQUAE', 'abl': 'AQUA', 'voc': 'AQUA'},
'VITA': {'gen': 'VITAE', 'acc': 'VITAM', 'dat': 'VITAE', 'abl': 'VITA', 'voc': 'VITA'},
'TERRA': {'gen': 'TERRAE', 'acc': 'TERRAM', 'dat': 'TERRAE', 'abl': 'TERRA', 'voc': 'TERRA'},
'SUMMA': {'gen': 'SUMMAE', 'acc': 'SUMMAM', 'dat': 'SUMMAE', 'abl': 'SUMMA', 'voc': 'SUMMA'},
# Third declension
'REX': {'gen': 'REGIS', 'acc': 'REGEM', 'dat': 'REGI', 'abl': 'REGE', 'voc': 'REX'},
'CIVIS': {'gen': 'CIVIS', 'acc': 'CIVEM', 'dat': 'CIVI', 'abl': 'CIVE', 'voc': 'CIVIS'},
'CORPVS': {'gen': 'CORPORIS', 'acc': 'CORPVS', 'dat': 'CORPORI', 'abl': 'CORPORE', 'voc': 'CORPVS'},
'TEMPVS': {'gen': 'TEMPORIS', 'acc': 'TEMPVS', 'dat': 'TEMPORI', 'abl': 'TEMPORE', 'voc': 'TEMPVS'},
'ITER': {'gen': 'ITINERIS', 'acc': 'ITER', 'dat': 'ITINERI', 'abl': 'ITINERE', 'voc': 'ITER'},
'NOMEN': {'gen': 'NOMINIS', 'acc': 'NOMEN', 'dat': 'NOMINI', 'abl': 'NOMINE', 'voc': 'NOMEN'},
'AETAS': {'gen': 'AETATIS', 'acc': 'AETATEM', 'dat': 'AETATI', 'abl': 'AETATE', 'voc': 'AETAS'},
'SALVTATIO': {'gen': 'SALVTATIONIS', 'acc': 'SALVTATIONEM', 'dat': 'SALVTATIONI', 'abl': 'SALVTATIONE', 'voc': 'SALVTATIO'},
'INPUTVM': {'gen': 'INPUTI', 'acc': 'INPUTVM', 'dat': 'INPUTO', 'abl': 'INPUTO', 'voc': 'INPUTVM'},
'CONTINVA': {'gen': 'CONTINVAE', 'acc': 'CONTINVAM', 'dat': 'CONTINVAE', 'abl': 'CONTINVA', 'voc': 'CONTINVA'},
'RESPONSUM': {'gen': 'RESPONSI', 'acc': 'RESPONSUM', 'dat': 'RESPONSO', 'abl': 'RESPONSO', 'voc': 'RESPONSUM'},
# Fourth declension
'MANVS': {'gen': 'MANVS', 'acc': 'MANVM', 'dat': 'MANVI', 'abl': 'MANV', 'voc': 'MANVS'},
'GRADVS': {'gen': 'GRADVS', 'acc': 'GRADVM', 'dat': 'GRADVI', 'abl': 'GRADV', 'voc': 'GRADVS'},
# Fifth declension
'RES': {'gen': 'REI', 'acc': 'REM', 'dat': 'REI', 'abl': 'RE', 'voc': 'RES'},
'DIES': {'gen': 'DIEI', 'acc': 'DIEM', 'dat': 'DIEI', 'abl': 'DIE', 'voc': 'DIES'},
}
@classmethod
def get_nominative(cls, declined_form: str) -> Optional[str]:
"""Get the nominative form of a declined noun."""
if declined_form in cls.DECLENSIONS:
return declined_form
for nom, forms in cls.DECLENSIONS.items():
if declined_form in forms.values():
return nom
return None
@classmethod
def get_accusative(cls, nominative: str) -> Optional[str]:
"""Get the accusative form of a noun."""
return cls.DECLENSIONS.get(nominative, {}).get('acc')
@classmethod
def get_dative(cls, nominative: str) -> Optional[str]:
"""Get the dative form of a noun."""
return cls.DECLENSIONS.get(nominative, {}).get('dat')
@classmethod
def get_ablative(cls, nominative: str) -> Optional[str]:
"""Get the ablative form of a noun."""
return cls.DECLENSIONS.get(nominative, {}).get('abl')
@classmethod
def get_vocative(cls, nominative: str) -> Optional[str]:
"""Get the vocative form of a noun."""
return cls.DECLENSIONS.get(nominative, {}).get('voc')
@classmethod
def get_genitive(cls, nominative: str) -> Optional[str]:
"""Get the genitive form of a noun."""
return cls.DECLENSIONS.get(nominative, {}).get('gen')
class Tokenizer:
"""Tokenize LATIN source code."""
KEYWORDS = ['SIT', 'EST', 'SI', 'ALITER', 'FINIS', 'SCRIBE', 'LEGO', 'ADDE', 'DEME', 'AEQUAT',
'DUM', 'FAC', 'REDDO', 'VOCA', 'DVCE', 'MVLTIPLICA', 'MAIVS', 'MINOR', 'IVNGE',
'INCIPITCVM', 'FINITVRCVM', 'CONTINET', 'INDICEDE', 'IACE', 'CAPE', 'AVDI', 'NOTA']
def __init__(self, declared_vars: set):
self.declared_vars = declared_vars
def tokenize_line(self, line: str) -> List[Tuple[str, str]]:
"""Tokenize a single line of LATIN code."""
# Remove comments
if ';' in line:
line = line.split(';')[0]
line = line.strip()
if not line:
return []
tokens = []
pos = 0
while pos < len(line):
matched = False
# Try to match string literals (quoted text)
if line[pos] == '"':
end_quote = line.find('"', pos + 1)
if end_quote == -1:
raise RuntimeError(f"ERRATUM: unclosed string literal")
string_content = line[pos + 1:end_quote]
tokens.append(('STRING', string_content))
pos = end_quote + 1
continue
# Try to match keywords
for keyword in self.KEYWORDS:
if line[pos:].startswith(keyword):
tokens.append(('KEYWORD', keyword))
pos += len(keyword)
matched = True
break
if matched:
# Special handling for SIT - next token is a new variable name
if tokens[-1] == ('KEYWORD', 'SIT'):
remaining = line[pos:]
found_var = None
# First try to find in DECLENSIONS table
for nom in LatinDeclension.DECLENSIONS.keys():
if remaining.startswith(nom):
found_var = nom
break
if found_var:
tokens.append(('VARIABLE', found_var))
pos += len(found_var)
else:
# Variable not in declensions - parse as uppercase letters
end = pos
while end < len(line) and line[end].isupper():
end += 1
if end > pos:
var_name = line[pos:end]
tokens.append(('VARIABLE', var_name))
pos = end
continue
continue
# Try to match NIHIL (zero)
if line[pos:].startswith('NIHIL'):
tokens.append(('NUMBER', 0))
pos += 5
continue
# Try to match variable names first (to avoid M in MAXIMVS being parsed as 1000)
best_match = None
best_length = 0
for var in self.declared_vars:
# Check nominative
if line[pos:].startswith(var):
if len(var) > best_length:
best_match = ('VARIABLE', var)
best_length = len(var)
# Check accusative
acc = LatinDeclension.get_accusative(var)
if acc and line[pos:].startswith(acc):
if len(acc) > best_length:
best_match = ('VARIABLE', var)
best_length = len(acc)
# Check dative
dat = LatinDeclension.get_dative(var)
if dat and line[pos:].startswith(dat):
if len(dat) > best_length:
best_match = ('VARIABLE', var)
best_length = len(dat)
# Check ablative
abl = LatinDeclension.get_ablative(var)
if abl and line[pos:].startswith(abl):
if len(abl) > best_length:
best_match = ('VARIABLE', var)
best_length = len(abl)
# Check vocative
voc = LatinDeclension.get_vocative(var)
if voc and line[pos:].startswith(voc):
if len(voc) > best_length:
best_match = ('VARIABLE', var)
best_length = len(voc)
# Check genitive (for field access like NOMENSERVII)
gen = LatinDeclension.get_genitive(var)
if gen and line[pos:].startswith(gen):
if len(gen) > best_length:
best_match = ('GENITIVE', var) # Mark as genitive for field access
best_length = len(gen)
if best_match:
# Check if next token would be a keyword - if so, prefer shorter variable match
remaining_after = line[pos + best_length:]
keyword_after = any(remaining_after.startswith(kw) for kw in self.KEYWORDS)
if not keyword_after and best_length > 0:
# Try shorter matches to see if they would allow a keyword
var_name = best_match[1]
nom_len = len(var_name)
# If we matched a declined form, check if nominative + keyword would work
if best_length > nom_len and line[pos:].startswith(var_name):
remaining_with_nom = line[pos + nom_len:]
if any(remaining_with_nom.startswith(kw) for kw in self.KEYWORDS):
# Use nominative match instead
best_length = nom_len
tokens.append(best_match)
pos += best_length
continue
# Try to match Roman numerals (only if no variable matched)
roman_match = ''
temp_pos = pos
while temp_pos < len(line) and line[temp_pos] in 'MDCLXVI':
roman_match += line[temp_pos]
temp_pos += 1
if roman_match:
num = RomanNumeralParser.parse(roman_match)
if num is not None:
tokens.append(('NUMBER', num))
pos = temp_pos
continue
# Unknown token - error
raise RuntimeError(f"ERRATUM: '{line[pos:]}' non intellegitur")
return tokens
class LatinInterpreter:
"""Interpret and execute LATIN programs."""
def __init__(self, use_english_errors=False):
self.variables: Dict[str, any] = {} # Can hold int or str
self.declared_vars: set = set()
self.tokenizer = Tokenizer(self.declared_vars)
self.skip_execution = False
self.use_english_errors = use_english_errors
self.lines = []
self.line_index = 0
self.loop_starts = [] # Stack of (loop_start_line, depth_at_loop_start) tuples
self.block_depth = 0 # Current nesting depth of SI/DUM/ALITER blocks
self.functions = {} # {function_name: {'params': [], 'start_line': int, 'end_line': int}}
self.call_stack = [] # Stack of return addresses and saved variables
self.in_function_def = False # Track if we're currently defining a function
self.exception_handlers = [] # Stack of (exception_type, handler_line) tuples
self.current_exception = None # Currently thrown exception
self.exception_throw_line = None # Line where exception was thrown
self.skip_handler_pop = False # Don't pop handler when FINIS balances a skip
def error(self, latin_msg: str, english_msg: str):
"""Raise error in Latin or English based on settings."""
if self.use_english_errors:
raise RuntimeError(english_msg)
else:
raise RuntimeError(f"ERRATUM: {latin_msg}")
def run(self, source: str):
"""Execute LATIN source code."""
self.lines = source.split('\n')
self.line_index = 0
while self.line_index < len(self.lines):
line = self.lines[self.line_index].strip()
# Remove inline comments
if ';' in line:
line = line.split(';')[0].strip()
# Skip empty lines
if not line:
self.line_index += 1
continue
try:
jump = self.execute_line(line)
if jump is not None:
self.line_index = jump
else:
self.line_index += 1
except LatinExceptionThrown as e:
# Exception thrown during execution (e.g., division by zero)
# Jump to the handler
self.line_index = e.handler_line
except Exception as e:
print(f"Error on line {self.line_index + 1}: {e}", file=sys.stderr)
sys.exit(1)
def execute_line(self, line: str) -> Optional[int]:
"""Execute a single line. Returns new line number if jump, else None."""
tokens = self.tokenizer.tokenize_line(line)
if not tokens:
return None
# Handle FINIS (end block)
if tokens[0] == ('KEYWORD', 'FINIS'):
self.skip_execution = False
self.block_depth -= 1
# Pop exception handler if this closes a CAPE block (but not if we're just balancing a skip)
if self.skip_handler_pop:
# Just balancing a CAPE skip - don't pop handler
self.skip_handler_pop = False
elif self.exception_handlers and self.exception_handlers[-1][1] <= self.line_index:
self.exception_handlers.pop()
# If we just handled an exception, end execution (jump past all lines)
if self.exception_throw_line is not None:
self.exception_throw_line = None
self.current_exception = None
return len(self.lines) # Jump past end to terminate
# If this ends a loop (depth returns to loop start depth), jump back
if self.loop_starts and self.loop_starts[-1][1] == self.block_depth:
loop_start, _ = self.loop_starts.pop()
self.block_depth += 1 # Re-enter the loop
return loop_start
return None
# Skip execution if in false conditional or loop condition
if self.skip_execution:
return None
# Handle SIT (variable declaration)
if tokens[0] == ('KEYWORD', 'SIT'):
if len(tokens) != 2 or tokens[1][0] != 'VARIABLE':
self.error("Syntax incorrecta post SIT", "Invalid syntax after SIT")
var_name = tokens[1][1]
self.declared_vars.add(var_name)
self.variables[var_name] = 0 # Default to 0 for compatibility
# If variable not in DECLENSIONS, add it with automatic declension pattern
if var_name not in LatinDeclension.DECLENSIONS:
# Guess declension based on ending
if var_name.endswith('US'):
# Second declension masculine like NUMERUS
root = var_name[:-2]
LatinDeclension.DECLENSIONS[var_name] = {
'gen': root + 'I',
'acc': root + 'UM',
'dat': root + 'O',
'abl': root + 'O',
'voc': root + 'E'
}
elif var_name.endswith('OR'):
# Third declension like ADDITOR, ERROR
LatinDeclension.DECLENSIONS[var_name] = {
'gen': var_name + 'IS',
'acc': var_name + 'EM',
'dat': var_name + 'I',
'abl': var_name + 'E',
'voc': var_name
}
elif var_name.endswith('IO'):
# Third declension like EXCEPTIO
LatinDeclension.DECLENSIONS[var_name] = {
'gen': var_name + 'NIS',
'acc': var_name + 'NEM',
'dat': var_name + 'NI',
'abl': var_name + 'NE',
'voc': var_name
}
elif var_name.endswith('A'):
# First declension feminine like SUMMA
root = var_name[:-1]
LatinDeclension.DECLENSIONS[var_name] = {
'gen': root + 'AE',
'acc': root + 'AM',
'dat': root + 'AE',
'abl': root + 'A',
'voc': root + 'A'
}
elif var_name.endswith('VM') or var_name.endswith('UM'):
# Second declension neuter
root = var_name[:-2]
LatinDeclension.DECLENSIONS[var_name] = {
'gen': root + 'I',
'acc': var_name,
'dat': root + 'O',
'abl': root + 'O',
'voc': var_name
}
else:
# Default to second declension masculine pattern
LatinDeclension.DECLENSIONS[var_name] = {
'gen': var_name + 'I',
'acc': var_name + 'M',
'dat': var_name + 'O',
'abl': var_name + 'O',
'voc': var_name + 'E'
}
return None
# Handle SCRIBE (print)
if tokens[0] == ('KEYWORD', 'SCRIBE'):
# SCRIBE FIELDGENITIVE (print field of object)
if len(tokens) == 3 and tokens[1][0] == 'VARIABLE' and tokens[2][0] == 'GENITIVE':
field_name = tokens[1][1]
object_name = tokens[2][1]
if object_name not in self.variables:
self.error(f"'{object_name}' non declaratur", f"Object '{object_name}' not declared")
if not isinstance(self.variables[object_name], dict):
self.error(f"'{object_name}' non est structura", f"'{object_name}' is not a struct")
if field_name not in self.variables[object_name]:
self.error(f"Campus '{field_name}' in '{object_name}' non existit",
f"Field '{field_name}' in '{object_name}' does not exist")
value = self.variables[object_name][field_name]
if isinstance(value, int):
print(RomanNumeralParser.to_roman(value))
else:
print(value)
return None
if len(tokens) != 2:
self.error("Syntax incorrecta post SCRIBE", "Invalid syntax after SCRIBE")
if tokens[1][0] == 'STRING':
print(tokens[1][1])
elif tokens[1][0] == 'NUMBER':
print(RomanNumeralParser.to_roman(tokens[1][1]))
elif tokens[1][0] == 'VARIABLE':
var_name = tokens[1][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
value = self.variables[var_name]
if isinstance(value, int):
print(RomanNumeralParser.to_roman(value))
else:
print(value)
return None
# Handle AVDI (debug print - with DEBUG prefix)
if tokens[0] == ('KEYWORD', 'AVDI'):
if len(tokens) != 2:
self.error("Syntax incorrecta post AVDI", "Invalid syntax after AVDI")
print("[DEBUG] ", end="", file=sys.stderr)
if tokens[1][0] == 'STRING':
print(tokens[1][1], file=sys.stderr)
elif tokens[1][0] == 'NUMBER':
print(RomanNumeralParser.to_roman(tokens[1][1]), file=sys.stderr)
elif tokens[1][0] == 'VARIABLE':
var_name = tokens[1][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
value = self.variables[var_name]
if isinstance(value, int):
print(RomanNumeralParser.to_roman(value), file=sys.stderr)
else:
print(value, file=sys.stderr)
return None
# Handle NOTA (log print - with LOG prefix)
if tokens[0] == ('KEYWORD', 'NOTA'):
if len(tokens) != 2:
self.error("Syntax incorrecta post NOTA", "Invalid syntax after NOTA")
print("[LOG] ", end="", file=sys.stderr)
if tokens[1][0] == 'STRING':
print(tokens[1][1], file=sys.stderr)
elif tokens[1][0] == 'NUMBER':
print(RomanNumeralParser.to_roman(tokens[1][1]), file=sys.stderr)
elif tokens[1][0] == 'VARIABLE':
var_name = tokens[1][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
value = self.variables[var_name]
if isinstance(value, int):
print(RomanNumeralParser.to_roman(value), file=sys.stderr)
else:
print(value, file=sys.stderr)
return None
# Handle LEGO (read input)
if tokens[0] == ('KEYWORD', 'LEGO'):
if len(tokens) != 2:
self.error("Syntax incorrecta post LEGO", "Invalid syntax after LEGO")
if tokens[1][0] != 'VARIABLE':
self.error("LEGO requirit variabilem", "LEGO requires a variable")
var_name = tokens[1][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
# Read input from user
user_input = input().strip()
# Try to parse as Roman numeral first
value = RomanNumeralParser.parse(user_input)
if value is not None:
self.variables[var_name] = value
else:
# If not a valid Roman numeral, treat as string
# Remove quotes if user included them
if user_input.startswith('"') and user_input.endswith('"'):
user_input = user_input[1:-1]
self.variables[var_name] = user_input
return None
# Handle IACE (throw exception)
if tokens[0] == ('KEYWORD', 'IACE'):
# IACE ERROR "message" or IACE ERROR
if len(tokens) < 2:
self.error("IACE requirit nomen exceptionis", "IACE requires exception name")
if tokens[1][0] != 'VARIABLE':
self.error("IACE requirit nomen exceptionis in vocativo", "IACE requires exception name in vocative")
exception_name = tokens[1][1]
message = ""
if len(tokens) == 3 and tokens[2][0] == 'STRING':
message = tokens[2][1]
# Store exception info and throw line
self.current_exception = {'type': exception_name, 'message': message}
self.exception_throw_line = self.line_index + 1 # Continue after this line when done handling
# Look for exception handler
for handler_type, handler_line in reversed(self.exception_handlers):
if handler_type == exception_name:
# Jump to handler
return handler_line
# No handler found - raise error
if message:
self.error(f"{exception_name}: {message}", f"{exception_name}: {message}")
else:
self.error(exception_name, exception_name)
return None
# Handle CAPE (catch exception)
if tokens[0] == ('KEYWORD', 'CAPE'):
# CAPE ERROR
if len(tokens) != 2:
self.error("CAPE requirit nomen exceptionis", "CAPE requires exception name")
if tokens[1][0] != 'VARIABLE':
self.error("CAPE requirit nomen exceptionis in vocativo", "CAPE requires exception name in vocative")
exception_name = tokens[1][1]
# Register exception handler (will be active until FINIS)
self.exception_handlers.append((exception_name, self.line_index + 1))
self.block_depth += 1
# If we're entering the handler after an exception, clear it
if self.current_exception and self.current_exception['type'] == exception_name:
self.current_exception = None
else:
# Not handling an exception now, skip to FINIS
depth = 1
search_idx = self.line_index + 1
while search_idx < len(self.lines) and depth > 0:
search_line = self.lines[search_idx].strip()
if ';' in search_line:
search_line = search_line.split(';')[0].strip()
if search_line.startswith('CAPE') or search_line.startswith('SI') or search_line.startswith('DUM'):
depth += 1
elif search_line == 'FINIS':
depth -= 1
search_idx += 1
# Balance for FINIS
self.block_depth += 1
self.skip_handler_pop = True # Don't pop handler, we want it to stay active
return search_idx - 1
return None
# Handle FAC (function definition)
if tokens[0] == ('KEYWORD', 'FAC'):
# FAC FUNCTION_NAME PARAM1 PARAM2 ...
if len(tokens) < 2:
self.error("FAC requirit nomen functionis", "FAC requires function name")
if tokens[1][0] != 'VARIABLE':
self.error("FAC requirit nomen functionis validum", "FAC requires valid function name")
func_name = tokens[1][1]
params = []
# Collect parameters (all should be variables in dative case)
for i in range(2, len(tokens)):
if tokens[i][0] != 'VARIABLE':
self.error("Parametri debent esse variabiles", "Parameters must be variables")
params.append(tokens[i][1])
# Find the matching FINIS for this function
depth = 1
search_idx = self.line_index + 1
while search_idx < len(self.lines) and depth > 0:
search_line = self.lines[search_idx].strip()
if ';' in search_line:
search_line = search_line.split(';')[0].strip()
if search_line.startswith('FAC') or search_line.startswith('SI') or search_line.startswith('DUM'):
depth += 1
elif search_line == 'FINIS':
depth -= 1
search_idx += 1
# Store function definition
self.functions[func_name] = {
'params': params,
'start_line': self.line_index + 1,
'end_line': search_idx - 2 # -2 because search_idx is after FINIS
}
# Skip to FINIS (don't execute function body during definition)
self.block_depth += 1 # Balance for FINIS
return search_idx - 1
# Handle REDDO (return from function)
if tokens[0] == ('KEYWORD', 'REDDO'):
if len(tokens) != 2:
self.error("REDDO requirit valorem", "REDDO requires a value")
# Get return value
if tokens[1][0] == 'NUMBER':
return_value = tokens[1][1]
elif tokens[1][0] == 'STRING':
return_value = tokens[1][1]
elif tokens[1][0] == 'VARIABLE':
var_name = tokens[1][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
return_value = self.variables[var_name]
else:
self.error("REDDO requirit numerum, textum, aut variabilem", "REDDO requires number, string, or variable")
# Pop call stack and restore context
if not self.call_stack:
self.error("REDDO extra functionem", "REDDO outside function")
call_info = self.call_stack.pop()
return_line = call_info['return_line']
saved_vars = call_info['saved_vars']
# If there was a calling variable, assign the return value to it
calling_var = self.variables.get('__CALLING_VAR__')
# Restore variables (remove local params, restore globals)
self.variables = saved_vars.copy()
# Assign return value to calling variable
if calling_var:
self.variables[calling_var] = return_value
# Jump back to caller (next line after the call)
return return_line + 1
# Handle DUM (while loop)
if tokens[0] == ('KEYWORD', 'DUM'):
# DUM VARIABLE COMPARISON VALUE
if len(tokens) != 4:
self.error("Syntax incorrecta in DUM", "Invalid DUM syntax")
if tokens[1][0] != 'VARIABLE':
self.error("DUM requirit variabilem", "DUM requires variable")
if tokens[2][1] not in ['AEQUAT', 'MAIVS', 'MINOR']:
self.error("DUM requirit AEQUAT, MAIVS, aut MINOR", "DUM requires AEQUAT, MAIVS, or MINOR")
var_name = tokens[1][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
left_value = self.variables[var_name]
if tokens[3][0] == 'NUMBER':
right_value = tokens[3][1]
elif tokens[3][0] == 'VARIABLE':
right_var = tokens[3][1]
if right_var not in self.variables:
self.error(f"'{right_var}' non declaratur", f"Variable '{right_var}' not declared")
right_value = self.variables[right_var]
else:
self.error("DUM requirit numerum aut variabilem", "DUM requires number or variable")
# Evaluate condition
condition_met = False
if tokens[2][1] == 'AEQUAT':
condition_met = (left_value == right_value)
elif tokens[2][1] == 'MAIVS':
condition_met = (left_value > right_value)
elif tokens[2][1] == 'MINOR':
condition_met = (left_value < right_value)
# If condition is false, skip to FINIS
if not condition_met:
depth = 1
search_idx = self.line_index + 1
while search_idx < len(self.lines) and depth > 0:
search_line = self.lines[search_idx].strip()
if ';' in search_line:
search_line = search_line.split(';')[0].strip()
if search_line.startswith('DUM') or search_line.startswith('SI'):
depth += 1
elif search_line == 'FINIS':
depth -= 1
search_idx += 1
return search_idx - 1
# Otherwise continue into loop and remember start position with current depth
self.loop_starts.append((self.line_index, self.block_depth))
self.block_depth += 1
return None
# Handle ALITER (else)
if tokens[0] == ('KEYWORD', 'ALITER'):
# When we reach ALITER, it means the SI condition was true
# So we need to skip to FINIS
depth = 1
search_idx = self.line_index + 1
while search_idx < len(self.lines) and depth > 0:
search_line = self.lines[search_idx].strip()
if ';' in search_line:
search_line = search_line.split(';')[0].strip()
if search_line.startswith('SI') or search_line.startswith('DUM'):
depth += 1
elif search_line == 'FINIS':
depth -= 1
search_idx += 1
return search_idx - 1
# Handle SI (conditional)
if tokens[0] == ('KEYWORD', 'SI'):
# Parse: SI VARIABLE AEQUAT NUMBER/VARIABLE/STRING
# Or: SI VARIABLE MAIVS/MINOR NUMBER/VARIABLE
if len(tokens) != 4:
self.error("Syntax incorrecta in SI", "Invalid SI syntax")
if tokens[1][0] != 'VARIABLE':
self.error("SI requirit variabilem", "SI requires variable")
if tokens[2][1] not in ['AEQUAT', 'MAIVS', 'MINOR']:
self.error("SI requirit AEQUAT, MAIVS, aut MINOR", "SI requires AEQUAT, MAIVS, or MINOR")
var_name = tokens[1][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
left_value = self.variables[var_name]
if tokens[3][0] == 'STRING':
right_value = tokens[3][1]
elif tokens[3][0] == 'NUMBER':
right_value = tokens[3][1]
elif tokens[3][0] == 'VARIABLE':
right_var = tokens[3][1]
if right_var not in self.variables:
self.error(f"'{right_var}' non declaratur", f"Variable '{right_var}' not declared")
right_value = self.variables[right_var]
else:
self.error("SI requirit numerum, textum, aut variabilem", "SI requires number, string, or variable")
# Evaluate condition
condition_met = False
if tokens[2][1] == 'AEQUAT':
condition_met = (left_value == right_value)
elif tokens[2][1] == 'MAIVS':
# Only works with numbers
if not isinstance(left_value, int) or not isinstance(right_value, int):
self.error("MAIVS requirit numeros", "MAIVS requires numbers")
condition_met = (left_value > right_value)
elif tokens[2][1] == 'MINOR':
# Only works with numbers
if not isinstance(left_value, int) or not isinstance(right_value, int):
self.error("MINOR requirit numeros", "MINOR requires numbers")
condition_met = (left_value < right_value)
if not condition_met:
# Skip to ALITER or FINIS (don't change depth since we're not entering the block)
depth = 1
search_idx = self.line_index + 1
while search_idx < len(self.lines) and depth > 0:
search_line = self.lines[search_idx].strip()
if ';' in search_line:
search_line = search_line.split(';')[0].strip()
if search_line.startswith('SI') or search_line.startswith('DUM'):
depth += 1
elif search_line == 'ALITER' and depth == 1:
# Found ALITER at same depth - jump past it to continue with else block
# Increment block_depth since we're entering the ALITER block
self.block_depth += 1
return search_idx + 1
elif search_line == 'FINIS':
depth -= 1
search_idx += 1
# Jumped to FINIS - it will handle decrementing depth, so pre-increment to balance
self.block_depth += 1
return search_idx - 1
# Condition is true, enter SI block
self.block_depth += 1
return None
# Handle field assignment (FIELDGENITIVE EST ...)
# Example: NOMENSERVII EST "Marcus" (name of servant is Marcus)
if len(tokens) >= 3 and tokens[0][0] == 'VARIABLE' and tokens[1][0] == 'GENITIVE' and tokens[2] == ('KEYWORD', 'EST'):
field_name = tokens[0][1] # NOMEN
object_name = tokens[1][1] # SERVUS
if object_name not in self.variables:
self.error(f"'{object_name}' non declaratur", f"Object '{object_name}' not declared")
# Ensure the object is a dictionary (struct)
if not isinstance(self.variables[object_name], dict):
# Initialize as dict if it's not already
self.variables[object_name] = {}
# Field assignment: FIELDGENITIVE EST STRING
if len(tokens) == 4 and tokens[3][0] == 'STRING':
self.variables[object_name][field_name] = tokens[3][1]
return None
# Field assignment: FIELDGENITIVE EST NUMBER
if len(tokens) == 4 and tokens[3][0] == 'NUMBER':
self.variables[object_name][field_name] = tokens[3][1]
return None
# Field assignment: FIELDGENITIVE EST VARIABLE
if len(tokens) == 4 and tokens[3][0] == 'VARIABLE':
source_var = tokens[3][1]
if source_var not in self.variables:
self.error(f"'{source_var}' non declaratur", f"Variable '{source_var}' not declared")
self.variables[object_name][field_name] = self.variables[source_var]
return None
# Field assignment: FIELDGENITIVE EST FIELD2GENITIVE2 (copy from another field)
if len(tokens) == 5 and tokens[3][0] == 'VARIABLE' and tokens[4][0] == 'GENITIVE':
source_field = tokens[3][1]
source_object = tokens[4][1]
if source_object not in self.variables:
self.error(f"'{source_object}' non declaratur", f"Object '{source_object}' not declared")
if not isinstance(self.variables[source_object], dict):
self.error(f"'{source_object}' non est structura", f"'{source_object}' is not a struct")
if source_field not in self.variables[source_object]:
self.error(f"Campus '{source_field}' in '{source_object}' non existit",
f"Field '{source_field}' in '{source_object}' does not exist")
self.variables[object_name][field_name] = self.variables[source_object][source_field]
return None
# Handle assignment (VARIABLE EST ...)
if len(tokens) >= 3 and tokens[0][0] == 'VARIABLE' and tokens[1] == ('KEYWORD', 'EST'):
var_name = tokens[0][1]
if var_name not in self.variables:
self.error(f"'{var_name}' non declaratur", f"Variable '{var_name}' not declared")
# Simple assignment: VARIABLE EST STRING
if len(tokens) == 3 and tokens[2][0] == 'STRING':
self.variables[var_name] = tokens[2][1]
return None
# Simple assignment: VARIABLE EST NUMBER
if len(tokens) == 3 and tokens[2][0] == 'NUMBER':
self.variables[var_name] = tokens[2][1]
return None
# VARIABLE EST VARIABLE
if len(tokens) == 3 and tokens[2][0] == 'VARIABLE':
source_var = tokens[2][1]
if source_var not in self.variables:
self.error(f"'{source_var}' non declaratur", f"Variable '{source_var}' not declared")
self.variables[var_name] = self.variables[source_var]
return None
# VARIABLE EST IVNGE ... (string concatenation)
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'IVNGE'):
result = self.evaluate_concatenation(tokens[3:])
self.variables[var_name] = result
return None
# VARIABLE EST INCIPITCVM ... (string startswith)
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'INCIPITCVM'):
result = self.evaluate_string_operation('INCIPITCVM', tokens[3:])
self.variables[var_name] = result
return None
# VARIABLE EST FINITVRCVM ... (string endswith)
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'FINITVRCVM'):
result = self.evaluate_string_operation('FINITVRCVM', tokens[3:])
self.variables[var_name] = result
return None
# VARIABLE EST CONTINET ... (string contains)
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'CONTINET'):
result = self.evaluate_string_operation('CONTINET', tokens[3:])
self.variables[var_name] = result
return None
# VARIABLE EST INDICEDE ... (string indexof)
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'INDICEDE'):
result = self.evaluate_string_operation('INDICEDE', tokens[3:])
self.variables[var_name] = result
return None
# VARIABLE EST ADDE ...
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'ADDE'):
result = self.evaluate_operation('ADDE', tokens[3:])
self.variables[var_name] = result
return None
# VARIABLE EST DEME ...
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'DEME'):
result = self.evaluate_operation('DEME', tokens[3:])
self.variables[var_name] = result
return None
# VARIABLE EST MVLTIPLICA ...
if len(tokens) >= 4 and tokens[2] == ('KEYWORD', 'MVLTIPLICA'):