hython-lang 2.0.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.
- hython/__init__.py +15 -0
- hython/__main__.py +3 -0
- hython/bytecode.py +368 -0
- hython/cli.py +449 -0
- hython/compiler.py +870 -0
- hython/compiler_manager.py +227 -0
- hython/diagnostics.py +158 -0
- hython/environment.py +48 -0
- hython/exe_builder.py +330 -0
- hython/frontend.py +870 -0
- hython/hir.py +93 -0
- hython/importer.py +76 -0
- hython/package_manager.py +343 -0
- hython/phonetics.py +49 -0
- hython/runtime.py +110 -0
- hython/runtime_manager.py +83 -0
- hython/translator.py +244 -0
- hython/updater.py +93 -0
- hython/vm.py +2409 -0
- hython/vocabulary.py +132 -0
- hython_lang-2.0.0.dist-info/METADATA +327 -0
- hython_lang-2.0.0.dist-info/RECORD +26 -0
- hython_lang-2.0.0.dist-info/WHEEL +5 -0
- hython_lang-2.0.0.dist-info/entry_points.txt +2 -0
- hython_lang-2.0.0.dist-info/licenses/LICENSE +22 -0
- hython_lang-2.0.0.dist-info/top_level.txt +1 -0
hython/hir.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Hython Intermediate Representation and conservative optimizer."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import operator
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class HIRCode:
|
|
8
|
+
name: str
|
|
9
|
+
parameters: list[str]
|
|
10
|
+
constants: list[object] = field(default_factory=list)
|
|
11
|
+
instructions: list[list] = field(default_factory=list)
|
|
12
|
+
lines: list[int] = field(default_factory=list)
|
|
13
|
+
|
|
14
|
+
def to_dict(self) -> dict:
|
|
15
|
+
return {"name": self.name, "parameters": self.parameters, "constants": self.constants, "instructions": self.instructions,"lines":self.lines}
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_dict(cls,value:dict) -> "HIRCode":
|
|
19
|
+
return cls(value["name"],value["parameters"],value["constants"],value["instructions"],value.get("lines",[]))
|
|
20
|
+
|
|
21
|
+
_BINARY = {"ADD":operator.add,"SUB":operator.sub,"MUL":operator.mul,"DIV":operator.truediv,
|
|
22
|
+
"FLOORDIV":operator.floordiv,"MOD":operator.mod,"POW":operator.pow,
|
|
23
|
+
"EQ":operator.eq,"NE":operator.ne,"LT":operator.lt,"LE":operator.le,
|
|
24
|
+
"GT":operator.gt,"GE":operator.ge,"IS":operator.is_}
|
|
25
|
+
_UNARY = {"NEG":operator.neg,"POS":operator.pos,"NOT":operator.not_}
|
|
26
|
+
|
|
27
|
+
def optimize_hir(code: HIRCode) -> HIRCode:
|
|
28
|
+
"""Fold safe constants without changing instruction offsets."""
|
|
29
|
+
ins = code.instructions
|
|
30
|
+
for index in range(len(ins) - 2):
|
|
31
|
+
first, second, third = ins[index:index + 3]
|
|
32
|
+
if first[0] == second[0] == "CONST" and third[0] in _BINARY:
|
|
33
|
+
try:
|
|
34
|
+
value = _BINARY[third[0]](code.constants[first[1]], code.constants[second[1]])
|
|
35
|
+
except Exception:
|
|
36
|
+
continue
|
|
37
|
+
if isinstance(value, (type(None), bool, int, float, str)):
|
|
38
|
+
code.constants.append(value)
|
|
39
|
+
ins[index:index + 3] = [["CONST", len(code.constants)-1], ["NOP"], ["NOP"]]
|
|
40
|
+
for index in range(len(ins) - 1):
|
|
41
|
+
first, second = ins[index:index + 2]
|
|
42
|
+
if first[0] == "CONST" and second[0] in _UNARY:
|
|
43
|
+
try: value = _UNARY[second[0]](code.constants[first[1]])
|
|
44
|
+
except Exception: continue
|
|
45
|
+
code.constants.append(value)
|
|
46
|
+
ins[index:index + 2] = [["CONST", len(code.constants)-1], ["NOP"]]
|
|
47
|
+
for instruction in ins:
|
|
48
|
+
if instruction[0] in ("MAKE_FUNCTION","MAKE_CLASS"):
|
|
49
|
+
nested = instruction[1]["code"]
|
|
50
|
+
child = HIRCode.from_dict(nested)
|
|
51
|
+
optimized=optimize_hir(child).to_dict()
|
|
52
|
+
instruction[1]["code"]=optimized
|
|
53
|
+
elif instruction[0]=="TRY":
|
|
54
|
+
payload=instruction[1]
|
|
55
|
+
for key in ("body","else","finally"):
|
|
56
|
+
nested=payload.get(key)
|
|
57
|
+
if nested:
|
|
58
|
+
payload[key]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
59
|
+
for handler in payload["handlers"]:
|
|
60
|
+
nested=handler["code"]
|
|
61
|
+
handler["code"]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
62
|
+
elif instruction[0]=="COMPREHENSION":
|
|
63
|
+
payload=instruction[1]
|
|
64
|
+
for key in ("element","key","value"):
|
|
65
|
+
nested=payload.get(key)
|
|
66
|
+
if nested: payload[key]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
67
|
+
for clause in payload["clauses"]:
|
|
68
|
+
nested=clause["iter"]
|
|
69
|
+
clause["iter"]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
70
|
+
clause["filters"]=[optimize_hir(HIRCode.from_dict(n)).to_dict() for n in clause["filters"]]
|
|
71
|
+
elif instruction[0]=="WITH":
|
|
72
|
+
payload=instruction[1]; nested=payload["body"]
|
|
73
|
+
payload["body"]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
74
|
+
for manager in payload["managers"]:
|
|
75
|
+
nested=manager["code"]
|
|
76
|
+
manager["code"]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
77
|
+
elif instruction[0] in ("ASYNC_FOR","ASYNC_WITH"):
|
|
78
|
+
payload=instruction[1]
|
|
79
|
+
for key in ("iter","body","else"):
|
|
80
|
+
nested=payload.get(key)
|
|
81
|
+
if nested: payload[key]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
82
|
+
for manager in payload.get("managers",[]):
|
|
83
|
+
nested=manager["code"]; manager["code"]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
84
|
+
elif instruction[0]=="ANNOTATE_LAZY":
|
|
85
|
+
nested=instruction[1]["code"]; instruction[1]["code"]=optimize_hir(HIRCode.from_dict(nested)).to_dict()
|
|
86
|
+
return code
|
|
87
|
+
|
|
88
|
+
def format_hir(code: HIRCode) -> str:
|
|
89
|
+
lines = [f"HIR {code.name}({', '.join(code.parameters)})"]
|
|
90
|
+
for index, instruction in enumerate(code.instructions):
|
|
91
|
+
argument = " ".join(map(str, instruction[1:]))
|
|
92
|
+
lines.append(f"{index:04d} {instruction[0]:14} {argument}")
|
|
93
|
+
return "\n".join(lines)
|
hython/importer.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Import hook for loading ``.hy`` source modules and packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.abc
|
|
6
|
+
import importlib.machinery
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .translator import compile_hython
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HythonLoader(importlib.abc.SourceLoader):
|
|
15
|
+
"""Load a Hython source file as a normal Python module."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, fullname: str, path: Path) -> None:
|
|
18
|
+
self.fullname = fullname
|
|
19
|
+
self.path = path
|
|
20
|
+
|
|
21
|
+
def get_filename(self, fullname: str) -> str:
|
|
22
|
+
return str(self.path)
|
|
23
|
+
|
|
24
|
+
def get_data(self, path: str) -> bytes:
|
|
25
|
+
return Path(path).read_bytes()
|
|
26
|
+
|
|
27
|
+
def get_code(self, fullname: str):
|
|
28
|
+
source = self.path.read_text(encoding="utf-8-sig")
|
|
29
|
+
return compile_hython(source, str(self.path))
|
|
30
|
+
|
|
31
|
+
def set_data(self, path: str, data: bytes, *, _mode: int = 0o666) -> None:
|
|
32
|
+
# Do not create misleading CPython .pyc cache files for Hython sources.
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HythonFinder(importlib.abc.MetaPathFinder):
|
|
37
|
+
"""Find ``name.hy`` and ``name/__init__.hy`` on the import path."""
|
|
38
|
+
|
|
39
|
+
marker = "hython-finder-v1"
|
|
40
|
+
|
|
41
|
+
def find_spec(self, fullname: str, path=None, target=None):
|
|
42
|
+
name = fullname.rpartition(".")[2]
|
|
43
|
+
search_paths = sys.path if path is None else path
|
|
44
|
+
for entry in search_paths:
|
|
45
|
+
base = Path(entry or ".")
|
|
46
|
+
package_file = base / name / "__init__.hy"
|
|
47
|
+
if package_file.is_file():
|
|
48
|
+
loader = HythonLoader(fullname, package_file)
|
|
49
|
+
return importlib.util.spec_from_file_location(
|
|
50
|
+
fullname,
|
|
51
|
+
package_file,
|
|
52
|
+
loader=loader,
|
|
53
|
+
submodule_search_locations=[str(package_file.parent)],
|
|
54
|
+
)
|
|
55
|
+
module_file = base / f"{name}.hy"
|
|
56
|
+
if module_file.is_file():
|
|
57
|
+
return importlib.util.spec_from_file_location(
|
|
58
|
+
fullname, module_file, loader=HythonLoader(fullname, module_file)
|
|
59
|
+
)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def install_importer() -> HythonFinder:
|
|
64
|
+
"""Install the finder once, ahead of Python's normal path finder."""
|
|
65
|
+
for finder in sys.meta_path:
|
|
66
|
+
if isinstance(finder, HythonFinder):
|
|
67
|
+
return finder
|
|
68
|
+
finder = HythonFinder()
|
|
69
|
+
sys.meta_path.insert(0, finder)
|
|
70
|
+
return finder
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def uninstall_importer() -> None:
|
|
74
|
+
"""Remove all installed Hython finders (mainly useful for tests)."""
|
|
75
|
+
sys.meta_path[:] = [finder for finder in sys.meta_path if not isinstance(finder, HythonFinder)]
|
|
76
|
+
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Package installation and safe, static public-API dictionary generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import importlib.machinery
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import sysconfig
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .phonetics import pronounce_identifier
|
|
17
|
+
from .environment import is_frozen, package_store
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_DEEP_SCAN_SCRIPT = r"""
|
|
21
|
+
import importlib, inspect, json, sys
|
|
22
|
+
if sys.argv[2]:
|
|
23
|
+
sys.path.insert(0, sys.argv[2])
|
|
24
|
+
module = importlib.import_module(sys.argv[1])
|
|
25
|
+
names = set()
|
|
26
|
+
def visit(obj, depth=0):
|
|
27
|
+
try:
|
|
28
|
+
members = inspect.getmembers_static(obj)
|
|
29
|
+
except Exception:
|
|
30
|
+
return
|
|
31
|
+
for name, value in members:
|
|
32
|
+
if not name.startswith("_"):
|
|
33
|
+
names.add(name)
|
|
34
|
+
if depth == 0 and inspect.isclass(value):
|
|
35
|
+
visit(value, 1)
|
|
36
|
+
visit(module)
|
|
37
|
+
print("__HYTHON_API__" + json.dumps(sorted(names), ensure_ascii=False))
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_DICTIONARY_CACHE_KEY: tuple[tuple[str, int, int], ...] | None = None
|
|
41
|
+
_DICTIONARY_CACHE: dict[str, str] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def infer_module_name(spec: str) -> str:
|
|
45
|
+
"""Infer an import name from a PEP 508-style distribution specifier."""
|
|
46
|
+
match=re.match(r"\s*([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)",spec)
|
|
47
|
+
if not match or spec[match.end():match.end()+1] not in ("","[","<",">","=","!","~","@"," ","\t"):
|
|
48
|
+
raise ValueError("패키지 지정자에서 모듈명을 추론할 수 없습니다. --module을 지정하세요.")
|
|
49
|
+
return match.group(1).replace("-","_")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def modules_for_distribution(spec: str, fallback: str | None = None) -> list[str]:
|
|
53
|
+
"""Return every top-level import supplied by a distribution."""
|
|
54
|
+
distribution = infer_module_name(spec)
|
|
55
|
+
normalized = re.sub(r"[-_.]+", "-", distribution).lower()
|
|
56
|
+
modules = {
|
|
57
|
+
module
|
|
58
|
+
for module, distributions in importlib.metadata.packages_distributions().items()
|
|
59
|
+
if any(re.sub(r"[-_.]+", "-", item).lower() == normalized for item in distributions)
|
|
60
|
+
and module.isidentifier()
|
|
61
|
+
}
|
|
62
|
+
if fallback:
|
|
63
|
+
modules.add(fallback)
|
|
64
|
+
elif not modules:
|
|
65
|
+
modules.add(distribution)
|
|
66
|
+
return sorted(modules)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def dictionary_dir() -> Path:
|
|
70
|
+
root = Path(__import__("os").environ.get("HYTHON_HOME", Path.home() / ".hython")) / "dictionaries"
|
|
71
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
return root
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def install(package: str, *, upgrade: bool = False) -> None:
|
|
76
|
+
"""Install through the current interpreter; subprocess avoids pip internals."""
|
|
77
|
+
if is_frozen():
|
|
78
|
+
from .runtime_manager import find_manager
|
|
79
|
+
command=[find_manager(),"exec","-V:default","-m","pip","install","--target",str(package_store())]
|
|
80
|
+
else:
|
|
81
|
+
command = [sys.executable, "-m", "pip", "install"]
|
|
82
|
+
if upgrade:
|
|
83
|
+
command.append("--upgrade")
|
|
84
|
+
command.append(package)
|
|
85
|
+
subprocess.run(command, check=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def uninstall(package: str) -> None:
|
|
89
|
+
"""Remove a distribution through pip."""
|
|
90
|
+
if is_frozen():
|
|
91
|
+
from .runtime_manager import find_manager
|
|
92
|
+
env=__import__("os").environ.copy()
|
|
93
|
+
env["PYTHONPATH"]=str(package_store())+__import__("os").pathsep+env.get("PYTHONPATH","")
|
|
94
|
+
command=[find_manager(),"exec","-V:default","-m","pip","uninstall","-y",package]
|
|
95
|
+
subprocess.run(command,check=True,env=env)
|
|
96
|
+
else:
|
|
97
|
+
subprocess.run([sys.executable,"-m","pip","uninstall","-y",package],check=True)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _public_names(path: Path) -> set[str]:
|
|
101
|
+
names: set[str] = set()
|
|
102
|
+
try:
|
|
103
|
+
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
|
104
|
+
except (OSError, UnicodeError, SyntaxError):
|
|
105
|
+
return names
|
|
106
|
+
explicit_all: set[str] = set()
|
|
107
|
+
def collect_node(node: ast.AST) -> None:
|
|
108
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
109
|
+
if not node.name.startswith("_"):
|
|
110
|
+
names.add(node.name)
|
|
111
|
+
# Public methods and nested public API classes are part of the
|
|
112
|
+
# spelling surface too (for example tkinter's title/mainloop).
|
|
113
|
+
if isinstance(node, ast.ClassDef):
|
|
114
|
+
for child in node.body:
|
|
115
|
+
collect_node(child)
|
|
116
|
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
117
|
+
for alias in node.names:
|
|
118
|
+
exposed = alias.asname or alias.name.split(".")[0]
|
|
119
|
+
if exposed != "*" and not exposed.startswith("_"):
|
|
120
|
+
names.add(exposed)
|
|
121
|
+
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
122
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
|
123
|
+
for target in targets:
|
|
124
|
+
if isinstance(target, ast.Name) and not target.id.startswith("_"):
|
|
125
|
+
names.add(target.id)
|
|
126
|
+
if isinstance(node, ast.Assign) and any(
|
|
127
|
+
isinstance(target, ast.Name) and target.id == "__all__" for target in node.targets
|
|
128
|
+
) and isinstance(node.value, (ast.List, ast.Tuple)):
|
|
129
|
+
explicit_all.update(
|
|
130
|
+
item.value for item in node.value.elts
|
|
131
|
+
if isinstance(item, ast.Constant) and isinstance(item.value, str)
|
|
132
|
+
)
|
|
133
|
+
for node in tree.body:
|
|
134
|
+
collect_node(node)
|
|
135
|
+
return explicit_all or names
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _deep_public_names(module: str, timeout: int = 30) -> set[str]:
|
|
139
|
+
"""Inspect extension/dynamic exports in an isolated child interpreter."""
|
|
140
|
+
if is_frozen():
|
|
141
|
+
from .runtime_manager import find_manager
|
|
142
|
+
command = [
|
|
143
|
+
find_manager(), "exec", "-V:default", "-c", _DEEP_SCAN_SCRIPT,
|
|
144
|
+
module, str(package_store()),
|
|
145
|
+
]
|
|
146
|
+
else:
|
|
147
|
+
command = [sys.executable, "-I", "-c", _DEEP_SCAN_SCRIPT, module, ""]
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
command, text=True, capture_output=True, errors="replace", timeout=timeout
|
|
150
|
+
)
|
|
151
|
+
if result.returncode:
|
|
152
|
+
detail = (result.stderr or result.stdout).strip().splitlines()
|
|
153
|
+
raise RuntimeError(
|
|
154
|
+
"패키지 심층 API 분석 실패: " + (detail[-1] if detail else module)
|
|
155
|
+
)
|
|
156
|
+
marker = next(
|
|
157
|
+
(line.removeprefix("__HYTHON_API__") for line in result.stdout.splitlines()
|
|
158
|
+
if line.startswith("__HYTHON_API__")),
|
|
159
|
+
None,
|
|
160
|
+
)
|
|
161
|
+
if marker is None:
|
|
162
|
+
raise RuntimeError("패키지 심층 API 분석 결과를 읽을 수 없습니다.")
|
|
163
|
+
payload = json.loads(marker)
|
|
164
|
+
return {name for name in payload if isinstance(name, str)}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _unique_spellings(
|
|
168
|
+
names: set[str], reserved: dict[str, str] | None = None
|
|
169
|
+
) -> dict[str, str]:
|
|
170
|
+
"""Give every public API a deterministic, collision-free Hangul spelling."""
|
|
171
|
+
result: dict[str, str] = {}
|
|
172
|
+
occupied: dict[str, str] = dict(reserved or {})
|
|
173
|
+
for name in sorted(names, key=lambda item: (pronounce_identifier(item), item)):
|
|
174
|
+
base = pronounce_identifier(name)
|
|
175
|
+
spoken = base
|
|
176
|
+
suffix = 2
|
|
177
|
+
while spoken in occupied and occupied[spoken] != name:
|
|
178
|
+
spoken = f"{base}{suffix}"
|
|
179
|
+
suffix += 1
|
|
180
|
+
result[name] = spoken
|
|
181
|
+
occupied[spoken] = name
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def scan(module: str, *, deep: bool = False) -> Path:
|
|
186
|
+
"""Statically scan a module/package without importing or executing it."""
|
|
187
|
+
spec = _find_spec_static(module)
|
|
188
|
+
if spec is None:
|
|
189
|
+
raise ModuleNotFoundError(f"패키지를 찾을 수 없습니다: {module}")
|
|
190
|
+
files: list[Path] = []
|
|
191
|
+
if spec.submodule_search_locations:
|
|
192
|
+
for location in spec.submodule_search_locations:
|
|
193
|
+
files.extend(Path(location).rglob("*.py"))
|
|
194
|
+
elif spec.origin and spec.origin.endswith(".py"):
|
|
195
|
+
files.append(Path(spec.origin))
|
|
196
|
+
# The module spelling itself must be translatable in an import statement.
|
|
197
|
+
names: set[str] = {part for part in module.split(".") if not part.startswith("_")}
|
|
198
|
+
for file in files:
|
|
199
|
+
names.update(_public_names(file))
|
|
200
|
+
if deep:
|
|
201
|
+
names.update(_deep_public_names(module))
|
|
202
|
+
from .vocabulary import HYTHON_TO_PYTHON
|
|
203
|
+
python_to_hython = _unique_spellings(
|
|
204
|
+
names, load_dictionaries() | HYTHON_TO_PYTHON
|
|
205
|
+
)
|
|
206
|
+
payload = {
|
|
207
|
+
"format": 1,
|
|
208
|
+
"module": module,
|
|
209
|
+
"scan_mode": "deep" if deep else "static",
|
|
210
|
+
"distribution_version": _version_for(module),
|
|
211
|
+
"python_to_hython": python_to_hython,
|
|
212
|
+
}
|
|
213
|
+
output = dictionary_dir() / f"{module.replace('.', '__')}.json"
|
|
214
|
+
_atomic_json(output, payload)
|
|
215
|
+
return output
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def scan_standard_library() -> Path:
|
|
219
|
+
"""Build one collision-free dictionary for the active Python stdlib."""
|
|
220
|
+
root = Path(sysconfig.get_paths()["stdlib"])
|
|
221
|
+
names = {
|
|
222
|
+
name for name in getattr(sys, "stdlib_module_names", ())
|
|
223
|
+
if isinstance(name, str) and not name.startswith("_")
|
|
224
|
+
}
|
|
225
|
+
for file in root.rglob("*.py"):
|
|
226
|
+
lowered = {part.lower() for part in file.parts}
|
|
227
|
+
if "site-packages" in lowered or "dist-packages" in lowered:
|
|
228
|
+
continue
|
|
229
|
+
names.update(_public_names(file))
|
|
230
|
+
from .vocabulary import HYTHON_TO_PYTHON
|
|
231
|
+
spellings = _unique_spellings(names, load_dictionaries() | HYTHON_TO_PYTHON)
|
|
232
|
+
output = dictionary_dir() / "_stdlib.json"
|
|
233
|
+
_atomic_json(output, {
|
|
234
|
+
"format": 1,
|
|
235
|
+
"module": "__stdlib__",
|
|
236
|
+
"scan_mode": "stdlib",
|
|
237
|
+
"python_version": sys.version.split()[0],
|
|
238
|
+
"python_to_hython": spellings,
|
|
239
|
+
})
|
|
240
|
+
return output
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _atomic_json(output: Path, payload: dict) -> None:
|
|
244
|
+
handle, temporary = tempfile.mkstemp(prefix=output.name + ".", suffix=".tmp", dir=output.parent)
|
|
245
|
+
try:
|
|
246
|
+
with __import__("os").fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
|
|
247
|
+
json.dump(payload, stream, ensure_ascii=False, indent=2)
|
|
248
|
+
stream.write("\n")
|
|
249
|
+
stream.flush()
|
|
250
|
+
__import__("os").fsync(stream.fileno())
|
|
251
|
+
Path(temporary).replace(output)
|
|
252
|
+
except BaseException:
|
|
253
|
+
Path(temporary).unlink(missing_ok=True)
|
|
254
|
+
raise
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def remove_dictionary(module: str) -> bool:
|
|
258
|
+
path = dictionary_dir() / f"{module.replace('.', '__')}.json"
|
|
259
|
+
existed = path.exists()
|
|
260
|
+
path.unlink(missing_ok=True)
|
|
261
|
+
return existed
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def refresh_dictionaries() -> tuple[list[Path], list[Path]]:
|
|
265
|
+
"""Rescan installed modules and prune dictionaries whose packages disappeared."""
|
|
266
|
+
refreshed: list[Path] = []
|
|
267
|
+
removed: list[Path] = []
|
|
268
|
+
for path in sorted(dictionary_dir().glob("*.json")):
|
|
269
|
+
try:
|
|
270
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
271
|
+
module = payload["module"]
|
|
272
|
+
if not isinstance(module, str):
|
|
273
|
+
continue
|
|
274
|
+
except (OSError, UnicodeError, json.JSONDecodeError, KeyError):
|
|
275
|
+
continue
|
|
276
|
+
if module == "__stdlib__":
|
|
277
|
+
refreshed.append(scan_standard_library())
|
|
278
|
+
elif _find_spec_static(module) is None:
|
|
279
|
+
path.unlink(missing_ok=True)
|
|
280
|
+
removed.append(path)
|
|
281
|
+
else:
|
|
282
|
+
refreshed.append(scan(module, deep=payload.get("scan_mode") == "deep"))
|
|
283
|
+
return refreshed, removed
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _find_spec_static(module: str):
|
|
287
|
+
"""Resolve dotted modules without importing their parent packages."""
|
|
288
|
+
if not module or any(not part for part in module.split(".")):
|
|
289
|
+
return None
|
|
290
|
+
search_path = None
|
|
291
|
+
fullname = ""
|
|
292
|
+
spec = None
|
|
293
|
+
for part in module.split("."):
|
|
294
|
+
fullname = f"{fullname}.{part}" if fullname else part
|
|
295
|
+
spec = importlib.machinery.PathFinder.find_spec(fullname, search_path)
|
|
296
|
+
if spec is None:
|
|
297
|
+
return None
|
|
298
|
+
search_path = spec.submodule_search_locations
|
|
299
|
+
return spec
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _version_for(module: str) -> str | None:
|
|
303
|
+
packages = importlib.metadata.packages_distributions()
|
|
304
|
+
distributions = packages.get(module.split(".")[0], [])
|
|
305
|
+
if not distributions:
|
|
306
|
+
return None
|
|
307
|
+
try:
|
|
308
|
+
return importlib.metadata.version(distributions[0])
|
|
309
|
+
except importlib.metadata.PackageNotFoundError:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def load_dictionaries() -> dict[str, str]:
|
|
314
|
+
"""Load valid user dictionaries as Hython-to-Python mappings."""
|
|
315
|
+
global _DICTIONARY_CACHE_KEY, _DICTIONARY_CACHE
|
|
316
|
+
paths = sorted(dictionary_dir().glob("*.json"))
|
|
317
|
+
key = tuple(
|
|
318
|
+
(str(path), stat.st_mtime_ns, stat.st_size)
|
|
319
|
+
for path in paths
|
|
320
|
+
if (stat := path.stat())
|
|
321
|
+
)
|
|
322
|
+
if key == _DICTIONARY_CACHE_KEY:
|
|
323
|
+
return dict(_DICTIONARY_CACHE)
|
|
324
|
+
result: dict[str, str] = {}
|
|
325
|
+
for path in paths:
|
|
326
|
+
try:
|
|
327
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
328
|
+
entries = payload["python_to_hython"]
|
|
329
|
+
if not isinstance(entries, dict):
|
|
330
|
+
continue
|
|
331
|
+
for python_name, spoken in entries.items():
|
|
332
|
+
if isinstance(python_name, str) and isinstance(spoken, str):
|
|
333
|
+
result.setdefault(spoken, python_name)
|
|
334
|
+
except (OSError, UnicodeError, json.JSONDecodeError, KeyError):
|
|
335
|
+
continue
|
|
336
|
+
_DICTIONARY_CACHE_KEY = key
|
|
337
|
+
_DICTIONARY_CACHE = dict(result)
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def load_python_dictionaries() -> dict[str, str]:
|
|
342
|
+
"""Load valid user dictionaries as Python-to-Hython mappings."""
|
|
343
|
+
return {python: spoken for spoken, python in load_dictionaries().items()}
|
hython/phonetics.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Deterministic English-identifier to Hangul phonetic approximation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
# Longest fragments are applied first. This is deliberately deterministic: users
|
|
8
|
+
# may edit generated dictionaries when a package's preferred pronunciation differs.
|
|
9
|
+
_SOUNDS = {
|
|
10
|
+
"tion": "션", "sion": "전", "ough": "오", "ight": "아이트",
|
|
11
|
+
"ph": "프", "sh": "시", "ch": "치", "th": "스", "wh": "우",
|
|
12
|
+
"ck": "크", "qu": "쿠", "ng": "응", "ee": "이", "oo": "우",
|
|
13
|
+
"ai": "에이", "ay": "에이", "ea": "이", "ou": "아우", "ow": "오우",
|
|
14
|
+
"a": "아", "b": "브", "c": "크", "d": "드", "e": "에",
|
|
15
|
+
"f": "프", "g": "그", "h": "흐", "i": "이", "j": "제이",
|
|
16
|
+
"k": "크", "l": "르", "m": "므", "n": "느", "o": "오",
|
|
17
|
+
"p": "프", "q": "큐", "r": "르", "s": "스", "t": "트",
|
|
18
|
+
"u": "유", "v": "브", "w": "우", "x": "엑스", "y": "이",
|
|
19
|
+
"z": "즈",
|
|
20
|
+
}
|
|
21
|
+
_PARTS = sorted(_SOUNDS, key=len, reverse=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def pronounce_word(word: str) -> str:
|
|
25
|
+
"""Return a stable Hangul approximation for one ASCII word."""
|
|
26
|
+
result: list[str] = []
|
|
27
|
+
lowered = word.lower()
|
|
28
|
+
index = 0
|
|
29
|
+
while index < len(lowered):
|
|
30
|
+
if lowered[index].isdigit():
|
|
31
|
+
result.append(lowered[index])
|
|
32
|
+
index += 1
|
|
33
|
+
continue
|
|
34
|
+
for part in _PARTS:
|
|
35
|
+
if lowered.startswith(part, index):
|
|
36
|
+
result.append(_SOUNDS[part])
|
|
37
|
+
index += len(part)
|
|
38
|
+
break
|
|
39
|
+
else:
|
|
40
|
+
result.append(lowered[index])
|
|
41
|
+
index += 1
|
|
42
|
+
return "".join(result)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pronounce_identifier(name: str) -> str:
|
|
46
|
+
"""Pronounce snake_case and CamelCase identifiers while preserving underscores."""
|
|
47
|
+
expanded = re.sub(r"(?<=[A-Z])(?=[A-Z][a-z])", "_", name)
|
|
48
|
+
expanded = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", expanded)
|
|
49
|
+
return "_".join(pronounce_word(part) for part in expanded.split("_"))
|
hython/runtime.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Python runtime discovery and Hython syntax compatibility profiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import keyword
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
import sysconfig
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .phonetics import pronounce_identifier
|
|
14
|
+
from .translator import to_python
|
|
15
|
+
from .vocabulary import KEYWORDS
|
|
16
|
+
from .environment import display_executable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def profile_dir() -> Path:
|
|
20
|
+
path = Path(__import__("os").environ.get("HYTHON_HOME", Path.home() / ".hython")) / "runtimes"
|
|
21
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def inspect_runtime() -> dict:
|
|
26
|
+
"""Inspect the active interpreter instead of assuming a Python grammar version."""
|
|
27
|
+
hard = sorted(keyword.kwlist)
|
|
28
|
+
soft = sorted(getattr(keyword, "softkwlist", []))
|
|
29
|
+
known = set(KEYWORDS)
|
|
30
|
+
# Pattern wildcard `_` is intentionally identical in Hython.
|
|
31
|
+
discovered = [name for name in hard + soft if name not in known and name != "_"]
|
|
32
|
+
return {
|
|
33
|
+
"format": 1,
|
|
34
|
+
"implementation": platform.python_implementation(),
|
|
35
|
+
"version": platform.python_version(),
|
|
36
|
+
"version_info": list(sys.version_info[:3]),
|
|
37
|
+
"executable": str(display_executable()),
|
|
38
|
+
"platform": sysconfig.get_platform(),
|
|
39
|
+
"hard_keywords": hard,
|
|
40
|
+
"soft_keywords": soft,
|
|
41
|
+
"new_keywords": discovered,
|
|
42
|
+
"generated_spellings": {
|
|
43
|
+
name: pronounce_identifier(name) for name in discovered
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def sync_runtime() -> Path:
|
|
49
|
+
"""Persist a reproducible profile for the active Python runtime."""
|
|
50
|
+
profile = inspect_runtime()
|
|
51
|
+
output = profile_dir() / f"python-{profile['version']}.json"
|
|
52
|
+
_atomic_json(output, profile)
|
|
53
|
+
# Keep the entire standard-library API pronunciation layer matched to the
|
|
54
|
+
# selected Python version. This state lives outside the installed core.
|
|
55
|
+
from .package_manager import scan_standard_library
|
|
56
|
+
scan_standard_library()
|
|
57
|
+
return output
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _atomic_json(output: Path, payload: dict) -> None:
|
|
61
|
+
"""Replace generated state without exposing a half-written profile."""
|
|
62
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
handle, temporary = tempfile.mkstemp(prefix=output.name + ".", suffix=".tmp", dir=output.parent)
|
|
64
|
+
try:
|
|
65
|
+
with __import__("os").fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
|
|
66
|
+
json.dump(payload, stream, ensure_ascii=False, indent=2)
|
|
67
|
+
stream.write("\n")
|
|
68
|
+
stream.flush()
|
|
69
|
+
__import__("os").fsync(stream.fileno())
|
|
70
|
+
Path(temporary).replace(output)
|
|
71
|
+
except BaseException:
|
|
72
|
+
Path(temporary).unlink(missing_ok=True)
|
|
73
|
+
raise
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_runtime_spellings() -> dict[str, str]:
|
|
77
|
+
"""Return generated spellings for the active interpreter only."""
|
|
78
|
+
path = profile_dir() / f"python-{platform.python_version()}.json"
|
|
79
|
+
try:
|
|
80
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
81
|
+
entries = payload.get("generated_spellings", {})
|
|
82
|
+
if payload.get("format") != 1 or not isinstance(entries, dict):
|
|
83
|
+
return {}
|
|
84
|
+
return {
|
|
85
|
+
spoken: python_name for python_name, spoken in entries.items()
|
|
86
|
+
if isinstance(python_name, str) and isinstance(spoken, str)
|
|
87
|
+
}
|
|
88
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check_file(path: Path) -> tuple[bool, str | None]:
|
|
93
|
+
"""Compile a Hython file with the active interpreter without executing it."""
|
|
94
|
+
try:
|
|
95
|
+
source = path.read_text(encoding="utf-8-sig")
|
|
96
|
+
compile(to_python(source), str(path), "exec")
|
|
97
|
+
except (OSError, UnicodeError, SyntaxError) as exc:
|
|
98
|
+
return False, str(exc)
|
|
99
|
+
return True, None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check_tree(root: Path) -> list[tuple[Path, str]]:
|
|
103
|
+
"""Return syntax failures for all Hython files under a path."""
|
|
104
|
+
paths = [root] if root.is_file() else sorted(root.rglob("*.hy"))
|
|
105
|
+
failures: list[tuple[Path, str]] = []
|
|
106
|
+
for path in paths:
|
|
107
|
+
valid, error = check_file(path)
|
|
108
|
+
if not valid:
|
|
109
|
+
failures.append((path, error or "알 수 없는 오류"))
|
|
110
|
+
return failures
|