multilingualprogramming 0.2.0__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.
Files changed (61) hide show
  1. multilingualprogramming/__init__.py +74 -0
  2. multilingualprogramming/__main__.py +194 -0
  3. multilingualprogramming/codegen/__init__.py +12 -0
  4. multilingualprogramming/codegen/executor.py +215 -0
  5. multilingualprogramming/codegen/python_generator.py +592 -0
  6. multilingualprogramming/codegen/repl.py +489 -0
  7. multilingualprogramming/codegen/runtime_builtins.py +308 -0
  8. multilingualprogramming/core/__init__.py +12 -0
  9. multilingualprogramming/core/ir.py +29 -0
  10. multilingualprogramming/core/lowering.py +24 -0
  11. multilingualprogramming/datetime/__init__.py +11 -0
  12. multilingualprogramming/datetime/date_parser.py +190 -0
  13. multilingualprogramming/datetime/mp_date.py +210 -0
  14. multilingualprogramming/datetime/mp_datetime.py +153 -0
  15. multilingualprogramming/datetime/mp_time.py +147 -0
  16. multilingualprogramming/datetime/resource_loader.py +18 -0
  17. multilingualprogramming/exceptions.py +158 -0
  18. multilingualprogramming/imports.py +150 -0
  19. multilingualprogramming/keyword/__init__.py +13 -0
  20. multilingualprogramming/keyword/keyword_registry.py +249 -0
  21. multilingualprogramming/keyword/keyword_validator.py +59 -0
  22. multilingualprogramming/keyword/language_pack_validator.py +110 -0
  23. multilingualprogramming/lexer/__init__.py +11 -0
  24. multilingualprogramming/lexer/lexer.py +570 -0
  25. multilingualprogramming/lexer/source_reader.py +91 -0
  26. multilingualprogramming/lexer/token.py +54 -0
  27. multilingualprogramming/lexer/token_types.py +38 -0
  28. multilingualprogramming/numeral/__init__.py +11 -0
  29. multilingualprogramming/numeral/abstract_numeral.py +232 -0
  30. multilingualprogramming/numeral/complex_numeral.py +190 -0
  31. multilingualprogramming/numeral/fraction_numeral.py +165 -0
  32. multilingualprogramming/numeral/mp_numeral.py +243 -0
  33. multilingualprogramming/numeral/numeral_converter.py +151 -0
  34. multilingualprogramming/numeral/roman_numeral.py +301 -0
  35. multilingualprogramming/numeral/unicode_numeral.py +292 -0
  36. multilingualprogramming/parser/__init__.py +28 -0
  37. multilingualprogramming/parser/ast_nodes.py +459 -0
  38. multilingualprogramming/parser/ast_printer.py +677 -0
  39. multilingualprogramming/parser/error_messages.py +75 -0
  40. multilingualprogramming/parser/parser.py +1796 -0
  41. multilingualprogramming/parser/semantic_analyzer.py +689 -0
  42. multilingualprogramming/parser/surface_normalizer.py +282 -0
  43. multilingualprogramming/resources/datetime/eras.json +23 -0
  44. multilingualprogramming/resources/datetime/formats.json +32 -0
  45. multilingualprogramming/resources/datetime/months.json +150 -0
  46. multilingualprogramming/resources/datetime/weekdays.json +90 -0
  47. multilingualprogramming/resources/parser/error_messages.json +310 -0
  48. multilingualprogramming/resources/repl/commands.json +636 -0
  49. multilingualprogramming/resources/usm/builtins_aliases.json +731 -0
  50. multilingualprogramming/resources/usm/keywords.json +1063 -0
  51. multilingualprogramming/resources/usm/operators.json +532 -0
  52. multilingualprogramming/resources/usm/schema.json +34 -0
  53. multilingualprogramming/resources/usm/surface_patterns.json +1523 -0
  54. multilingualprogramming/unicode_string.py +140 -0
  55. multilingualprogramming/version.py +9 -0
  56. multilingualprogramming-0.2.0.dist-info/METADATA +350 -0
  57. multilingualprogramming-0.2.0.dist-info/RECORD +61 -0
  58. multilingualprogramming-0.2.0.dist-info/WHEEL +5 -0
  59. multilingualprogramming-0.2.0.dist-info/entry_points.txt +3 -0
  60. multilingualprogramming-0.2.0.dist-info/licenses/LICENSE +674 -0
  61. multilingualprogramming-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,489 @@
1
+ #
2
+ # SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
3
+ #
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ #
6
+
7
+ """
8
+ Interactive REPL (Read-Eval-Print Loop) for the multilingual programming
9
+ language.
10
+
11
+ Supports line-by-line and multi-line (block) input, persistent state
12
+ across interactions, expression auto-printing, bracket-aware continuation,
13
+ and REPL commands.
14
+ """
15
+
16
+ import io
17
+ import json
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from multilingualprogramming.lexer.lexer import Lexer
23
+ from multilingualprogramming.keyword.keyword_registry import KeywordRegistry
24
+ from multilingualprogramming.parser.parser import Parser
25
+ from multilingualprogramming.codegen.python_generator import PythonCodeGenerator
26
+ from multilingualprogramming.codegen.runtime_builtins import RuntimeBuiltins
27
+ from multilingualprogramming.imports import enable_multilingual_imports
28
+ from multilingualprogramming.exceptions import UnsupportedLanguageError
29
+ from multilingualprogramming.version import __version__
30
+
31
+
32
+ class REPL:
33
+ """
34
+ Interactive multilingual programming REPL.
35
+
36
+ Usage:
37
+ repl = REPL(language="fr")
38
+ repl.run() # starts interactive loop
39
+
40
+ Or programmatically:
41
+ repl = REPL(language="en")
42
+ output = repl.eval_line("let x = 42")
43
+ output = repl.eval_line("print(x)") # "42\\n"
44
+ """
45
+
46
+ _COMMAND_CATALOG = None
47
+ _OPERATOR_CATALOG = None
48
+
49
+ def __init__(self, language=None, show_python=False):
50
+ """
51
+ Initialize the REPL.
52
+
53
+ Args:
54
+ language: Source language code (e.g., "en", "fr").
55
+ If None, auto-detect from input.
56
+ show_python: If True, display generated Python before execution.
57
+ """
58
+ self.language = language
59
+ self.show_python = show_python
60
+ self._globals = {}
61
+ self._init_globals()
62
+
63
+ @classmethod
64
+ def _load_command_catalog(cls):
65
+ """Load command aliases and help text from resources."""
66
+ if cls._COMMAND_CATALOG is not None:
67
+ return cls._COMMAND_CATALOG
68
+
69
+ path = (
70
+ Path(__file__).resolve().parent.parent
71
+ / "resources" / "repl" / "commands.json"
72
+ )
73
+ with open(path, "r", encoding="utf-8-sig") as handle:
74
+ cls._COMMAND_CATALOG = json.load(handle)
75
+ return cls._COMMAND_CATALOG
76
+
77
+ def _language_code(self):
78
+ """Return normalized active language code."""
79
+ return (self.language or "en").lower()
80
+
81
+ @classmethod
82
+ def _load_operator_catalog(cls):
83
+ """Load operator and symbol metadata from resources."""
84
+ if cls._OPERATOR_CATALOG is not None:
85
+ return cls._OPERATOR_CATALOG
86
+
87
+ path = (
88
+ Path(__file__).resolve().parent.parent
89
+ / "resources" / "usm" / "operators.json"
90
+ )
91
+ with open(path, "r", encoding="utf-8-sig") as handle:
92
+ cls._OPERATOR_CATALOG = json.load(handle)
93
+ return cls._OPERATOR_CATALOG
94
+
95
+ def _aliases_for(self, canonical, lang):
96
+ """Return ordered aliases for a command in active language."""
97
+ catalog = self._load_command_catalog()
98
+ commands = catalog.get("commands", {})
99
+ default_lang = catalog.get("default_language", "en")
100
+ meta = commands.get(canonical, {})
101
+ aliases_by_lang = meta.get("aliases", {})
102
+
103
+ aliases = []
104
+ aliases.extend(aliases_by_lang.get(default_lang, []))
105
+ if lang != default_lang:
106
+ aliases.extend(aliases_by_lang.get(lang, []))
107
+
108
+ seen = set()
109
+ ordered = []
110
+ for alias in aliases:
111
+ key = alias.casefold()
112
+ if key not in seen:
113
+ seen.add(key)
114
+ ordered.append(alias)
115
+ return ordered
116
+
117
+ def _message(self, key, ui_lang, **kwargs):
118
+ """Get localized REPL message template from command catalog."""
119
+ catalog = self._load_command_catalog()
120
+ default_lang = catalog.get("default_language", "en")
121
+ messages = catalog.get("messages", {})
122
+ template = messages.get(key, {}).get(
123
+ ui_lang,
124
+ messages.get(key, {}).get(default_lang, ""),
125
+ )
126
+ return template.format(**kwargs) if template else ""
127
+
128
+ def _command_alias_map(self, lang):
129
+ """Map canonical command names to casefolded aliases."""
130
+ catalog = self._load_command_catalog()
131
+ commands = catalog.get("commands", {})
132
+ result = {}
133
+ for canonical in commands:
134
+ aliases = self._aliases_for(canonical, lang)
135
+ result[canonical] = {
136
+ canonical.casefold(), *(alias.casefold() for alias in aliases)
137
+ }
138
+ return result
139
+
140
+ def _print_help(self):
141
+ """Print localized REPL help from command catalog."""
142
+ catalog = self._load_command_catalog()
143
+ lang = self._language_code()
144
+ default_lang = catalog.get("default_language", "en")
145
+ commands = catalog.get("commands", {})
146
+
147
+ title = catalog.get("help_titles", {}).get(
148
+ lang,
149
+ catalog.get("help_titles", {}).get(default_lang, "REPL Commands:"),
150
+ )
151
+ order = catalog.get("help_order", list(commands.keys()))
152
+
153
+ print(title)
154
+ for canonical in order:
155
+ meta = commands.get(canonical, {})
156
+ aliases = self._aliases_for(canonical, lang)
157
+ display = aliases[-1] if aliases else canonical
158
+ arg_hint = meta.get("arg_hint", "")
159
+ description = meta.get("descriptions", {}).get(
160
+ lang,
161
+ meta.get("descriptions", {}).get(default_lang, ""),
162
+ )
163
+ print(f" :{display}{arg_hint} {description}")
164
+
165
+ def _resolve_listing_language(self, arg):
166
+ """Resolve and validate the language used for keyword/symbol listings."""
167
+ lang = arg.strip().lower() if arg.strip() else self._language_code()
168
+ registry = KeywordRegistry()
169
+ registry.check_language(lang)
170
+ return lang
171
+
172
+ def _print_keywords(self, arg):
173
+ """Print available keywords for a given language."""
174
+ active_lang = self._language_code()
175
+ try:
176
+ lang = self._resolve_listing_language(arg)
177
+ except UnsupportedLanguageError:
178
+ label = arg.strip() or self._language_code()
179
+ print(self._message("unsupported_language", active_lang, lang=label))
180
+ return
181
+
182
+ keywords = KeywordRegistry().get_all_keywords(lang)
183
+ print(self._message("keywords_title", active_lang, lang=lang, count=len(keywords)))
184
+ for concept_id, keyword in sorted(
185
+ keywords.items(),
186
+ key=lambda item: (item[1].casefold(), item[0]),
187
+ ):
188
+ print(f" {keyword} -> {concept_id}")
189
+
190
+ def _print_symbols(self, arg):
191
+ """Print available operator and symbol mappings."""
192
+ active_lang = self._language_code()
193
+ try:
194
+ lang = self._resolve_listing_language(arg)
195
+ except UnsupportedLanguageError:
196
+ label = arg.strip() or self._language_code()
197
+ print(self._message("unsupported_language", active_lang, lang=label))
198
+ return
199
+
200
+ catalog = self._load_operator_catalog()
201
+ default_lang = catalog.get("default_language", "en")
202
+ print(self._message("symbols_title", active_lang, lang=lang))
203
+ for category, operators in catalog.items():
204
+ if not isinstance(operators, dict):
205
+ continue
206
+ print(f"{category}:")
207
+ for name, meta in operators.items():
208
+ if not isinstance(meta, dict):
209
+ continue
210
+ symbols = meta.get("symbols", [])
211
+ unicode_alt = meta.get("unicode_alt", [])
212
+ pairs = meta.get("pairs", [])
213
+ if pairs:
214
+ primary = f"{pairs[0]} {pairs[1]}"
215
+ elif symbols:
216
+ primary = ", ".join(symbols)
217
+ else:
218
+ primary = ""
219
+
220
+ line = f" {name}: {primary}"
221
+ if unicode_alt:
222
+ line += f" | alt: {', '.join(unicode_alt)}"
223
+
224
+ descriptions = meta.get("description", {})
225
+ if isinstance(descriptions, dict):
226
+ desc = descriptions.get(lang, descriptions.get(default_lang))
227
+ if desc:
228
+ line += f" ({desc})"
229
+ print(line)
230
+
231
+ def _init_globals(self):
232
+ """Initialize the execution namespace with builtins."""
233
+ lang = self.language or "en"
234
+ builtins_ns = RuntimeBuiltins(lang).namespace()
235
+ self._globals.update(builtins_ns)
236
+
237
+ def eval_line(self, source):
238
+ """
239
+ Evaluate a single line or block of multilingual source code.
240
+
241
+ Args:
242
+ source: Source code string.
243
+
244
+ Returns:
245
+ Captured stdout output as a string, or error message.
246
+ """
247
+ if not source.strip():
248
+ return ""
249
+
250
+ try:
251
+ lexer = Lexer(source, language=self.language)
252
+ tokens = lexer.tokenize()
253
+ detected_lang = lexer.language or self.language or "en"
254
+
255
+ parser = Parser(tokens, source_language=detected_lang)
256
+ program = parser.parse()
257
+
258
+ generator = PythonCodeGenerator()
259
+ python_source = generator.generate(program)
260
+
261
+ if self.show_python:
262
+ return f"[Python] {python_source.strip()}\n" + self._exec(
263
+ python_source
264
+ )
265
+
266
+ return self._exec(python_source)
267
+
268
+ except Exception as exc:
269
+ return f"Error: {exc}\n"
270
+
271
+ def _exec(self, python_source):
272
+ """Execute generated Python and return captured output.
273
+
274
+ For single expressions, auto-prints the result (like Python REPL).
275
+ """
276
+ enable_multilingual_imports()
277
+
278
+ captured = io.StringIO()
279
+ old_stdout = sys.stdout
280
+ try:
281
+ sys.stdout = captured
282
+ try:
283
+ code = compile(python_source, "<repl>", "eval")
284
+ result = eval(code, self._globals) # pylint: disable=eval-used
285
+ if result is not None:
286
+ print(repr(result))
287
+ except SyntaxError:
288
+ code = compile(python_source, "<repl>", "exec")
289
+ exec(code, self._globals) # pylint: disable=exec-used
290
+ except Exception as exc:
291
+ captured.write(f"Error: {exc}\n")
292
+ finally:
293
+ sys.stdout = old_stdout
294
+ return captured.getvalue()
295
+
296
+ def _continuation_state(self, text):
297
+ """Return (open_brackets, has_unclosed_string) for continuation."""
298
+ count = 0
299
+ string_char = None
300
+ is_triple = False
301
+ i = 0
302
+ while i < len(text):
303
+ ch = text[i]
304
+
305
+ if string_char is not None:
306
+ if is_triple:
307
+ if text[i:i+3] == string_char * 3:
308
+ string_char = None
309
+ is_triple = False
310
+ i += 3
311
+ continue
312
+ else:
313
+ if ch == '\\':
314
+ i += 2
315
+ continue
316
+ if ch == string_char:
317
+ string_char = None
318
+ i += 1
319
+ continue
320
+ i += 1
321
+ continue
322
+
323
+ if ch == '#':
324
+ # Comment until end of line (outside strings)
325
+ while i < len(text) and text[i] != '\n':
326
+ i += 1
327
+ continue
328
+
329
+ if ch in ('"', "'"):
330
+ if text[i:i+3] == ch * 3:
331
+ string_char = ch
332
+ is_triple = True
333
+ i += 3
334
+ continue
335
+ string_char = ch
336
+ is_triple = False
337
+ i += 1
338
+ continue
339
+
340
+ if ch in ('(', '[', '{'):
341
+ count += 1
342
+ elif ch in (')', ']', '}'):
343
+ count -= 1
344
+
345
+ i += 1
346
+
347
+ return count, string_char is not None
348
+
349
+ def continuation_state(self, text):
350
+ """Public wrapper for continuation-state inspection."""
351
+ return self._continuation_state(text)
352
+
353
+ def _count_open_brackets(self, text):
354
+ """Count net open brackets in text."""
355
+ count, _has_unclosed_string = self._continuation_state(text)
356
+ return count
357
+
358
+ def _resolve_command(self, line):
359
+ """Resolve REPL command aliases to canonical command names."""
360
+ text = line.strip()
361
+ if not text:
362
+ return None, ""
363
+
364
+ if text.startswith(":"):
365
+ text = text[1:].lstrip()
366
+ if not text:
367
+ return None, ""
368
+
369
+ parts = text.split(None, 1)
370
+ cmd = parts[0].casefold()
371
+ arg = parts[1] if len(parts) > 1 else ""
372
+
373
+ alias_map = self._command_alias_map(self._language_code())
374
+ for canonical, words in alias_map.items():
375
+ if cmd in words:
376
+ return canonical, arg
377
+ return None, ""
378
+
379
+ def _handle_command(self, line):
380
+ """Handle REPL commands. Returns True if handled."""
381
+ cmd, arg = self._resolve_command(line)
382
+ if cmd is None:
383
+ return False
384
+
385
+ if cmd == "quit":
386
+ print("Bye!")
387
+ return "exit"
388
+ if cmd == "lang":
389
+ if arg:
390
+ self.language = arg.strip()
391
+ self._globals.clear()
392
+ self._init_globals()
393
+ print(f"Language switched to: {self.language}")
394
+ else:
395
+ print(f"Current language: {self.language or 'auto'}")
396
+ return True
397
+ if cmd == "python":
398
+ self.show_python = not self.show_python
399
+ state = "on" if self.show_python else "off"
400
+ print(f"Show Python: {state}")
401
+ return True
402
+ if cmd == "reset":
403
+ self._globals.clear()
404
+ self._init_globals()
405
+ print("State cleared.")
406
+ return True
407
+ if cmd == "help":
408
+ self._print_help()
409
+ return True
410
+ if cmd == "keywords":
411
+ self._print_keywords(arg)
412
+ return True
413
+ if cmd == "symbols":
414
+ self._print_symbols(arg)
415
+ return True
416
+ return False
417
+
418
+ def run(self):
419
+ """
420
+ Start the interactive REPL loop.
421
+
422
+ Reads from stdin, supports multi-line blocks (lines ending with ':'),
423
+ bracket continuation, and REPL commands.
424
+ Exits on EOF (Ctrl+D on Unix, Ctrl+Z then Enter on Windows) or Ctrl+C.
425
+ """
426
+ lang_label = self.language or "auto"
427
+ eof_hint = "Ctrl+Z then Enter" if os.name == "nt" else "Ctrl+D"
428
+ print(f"Multilingual Programming REPL v{__version__} "
429
+ f"[language={lang_label}]")
430
+ print(
431
+ f"Type ':help' for commands. Use ':quit' (or Ctrl+C) to exit. "
432
+ f"EOF key is terminal-dependent ({eof_hint}).\n"
433
+ )
434
+
435
+ while True:
436
+ try:
437
+ line = input(">>> ")
438
+ except (EOFError, KeyboardInterrupt):
439
+ print("\nBye!")
440
+ break
441
+
442
+ if line.strip() in ("exit", "quit", "\x04", "\x1a"):
443
+ print("Bye!")
444
+ break
445
+
446
+ result = self._handle_command(line)
447
+ if result == "exit":
448
+ break
449
+ if result:
450
+ continue
451
+
452
+ open_brackets, has_unclosed_string = self._continuation_state(line)
453
+ needs_block = line.rstrip().endswith(":")
454
+
455
+ if needs_block or open_brackets > 0 or has_unclosed_string:
456
+ block_lines = [line]
457
+ while True:
458
+ try:
459
+ cont = input("... ")
460
+ except (EOFError, KeyboardInterrupt):
461
+ print()
462
+ break
463
+ if cont.strip() in ("\x04", "\x1a"):
464
+ print("Bye!")
465
+ return
466
+ block_lines.append(cont)
467
+ full_text = "\n".join(block_lines)
468
+ open_brackets, has_unclosed_string = (
469
+ self._continuation_state(full_text)
470
+ )
471
+
472
+ if needs_block and cont.strip() == "" \
473
+ and open_brackets <= 0 and not has_unclosed_string:
474
+ break
475
+ if not needs_block and open_brackets <= 0 \
476
+ and not has_unclosed_string:
477
+ break
478
+ source = "\n".join(block_lines) + "\n"
479
+ else:
480
+ source = line + "\n"
481
+
482
+ output = self.eval_line(source)
483
+ if output:
484
+ sys.stdout.write(output)
485
+
486
+ def reset(self):
487
+ """Clear the REPL state (variables, functions, etc.)."""
488
+ self._globals.clear()
489
+ self._init_globals()