openpuppet-language 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 (40) hide show
  1. openpuppet_language-2.0.data/data/share/puppet/conformance/README.md +274 -0
  2. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json +238 -0
  3. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json +155 -0
  4. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json +248 -0
  5. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json +220 -0
  6. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json +133 -0
  7. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json +274 -0
  8. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json +117 -0
  9. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json +180 -0
  10. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json +39 -0
  11. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json +143 -0
  12. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py +11 -0
  13. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py +24 -0
  14. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py +11 -0
  15. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py +10 -0
  16. openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py +721 -0
  17. openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md +283 -0
  18. openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md +77 -0
  19. openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md +167 -0
  20. openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md +227 -0
  21. openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md +126 -0
  22. openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md +151 -0
  23. openpuppet_language-2.0.data/data/share/puppet/spec/README.md +73 -0
  24. openpuppet_language-2.0.dist-info/METADATA +50 -0
  25. openpuppet_language-2.0.dist-info/RECORD +40 -0
  26. openpuppet_language-2.0.dist-info/WHEEL +5 -0
  27. openpuppet_language-2.0.dist-info/entry_points.txt +2 -0
  28. openpuppet_language-2.0.dist-info/top_level.txt +1 -0
  29. puppet/__init__.py +19 -0
  30. puppet/adapter.py +112 -0
  31. puppet/capabilities.py +172 -0
  32. puppet/cli.py +132 -0
  33. puppet/diag.py +42 -0
  34. puppet/engine.py +1038 -0
  35. puppet/eval.py +243 -0
  36. puppet/ir.py +538 -0
  37. puppet/lang.py +755 -0
  38. puppet/raster_adapter.py +348 -0
  39. puppet/tk_adapter.py +374 -0
  40. puppet/vocab.py +147 -0
@@ -0,0 +1,40 @@
1
+ openpuppet_language-2.0.data/data/share/puppet/conformance/README.md,sha256=c0YLKLgVu05PgkP7UZGQCraonbJBt1C1QBpVR4ZlsuI,17495
2
+ openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py,sha256=5psF1f4JaqUTYHpoJivR4eGGrETokHGjOvlx1Rsv7X0,32303
3
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json,sha256=tbxo15lbtL5B1bPJcrjQeNz0RiAh7YQenJu8fln5RBs,6887
4
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json,sha256=V8c3HjW0u3NQcNAbIXDr-ZmzZljLic29PaZxq7lp9Cw,5008
5
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json,sha256=eSe_60W8gpZUwidDjc4679DvVLlvUAn8-F30sXSB2zU,6668
6
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json,sha256=Pxa3cIcUhQDIQUVQ_B5lEaY0QHiRnZXrWXuwL4FH3qU,7556
7
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json,sha256=Skk2HHzAjTOupRzrEg2IXnD4Iwna6eOX5UuPUw5hlwI,3939
8
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json,sha256=Ikub0GSSR6OQZTZd63OBsmcB-gI-tXrWAlrPII6w05I,8042
9
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json,sha256=OJ66wqXoQiuUpB4NgfykMJ3mg0_zbM9vgndTKrZj5QA,3024
10
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json,sha256=QjMcJO7Kn4E0j7ba_Rd_nfbexOXZUR7tQygyz1mFwYo,5957
11
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json,sha256=LZfCGt5exnVogvFk4qsraN14mYoqr94pQh_f3-5MNbc,973
12
+ openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json,sha256=6eMLhtvdbhb9raZ4h9AKkMsw6ms4P3n9ebEaPwvXTmY,4590
13
+ openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py,sha256=CLcqL_fWmpqfmLvKoP1fJGl1BX78meXPVBlYYbCUt_I,306
14
+ openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py,sha256=iMMUJF7Qslhc1ef_hZ5cwaVKMQdysQGnizWl1v01Sn0,675
15
+ openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py,sha256=Km57Ix7ZuqvTQpjKcCC6BYVtNrBmf8uc-tEefd5fyo4,316
16
+ openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py,sha256=qBa4U4d_tYg_27ryevra6UUziFI9tUWA4dERCvXUWy0,215
17
+ openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md,sha256=ax5nNySdeXvybtKIphWPs8wpvVaM6rp3tFfPDFARrCc,13749
18
+ openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md,sha256=8Fjx8Wwl6kpwmVB5Y16A7SLOFQ-AHTnWPiZryasFNvQ,4124
19
+ openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md,sha256=Ljc9TA28WnfBPYkz_3URr-PbJFlMDYunfps5ThKzfac,9813
20
+ openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md,sha256=36Ao600pdHXQmPJ7n3_pPaLGrAp6eLaDxXJlSljbTZQ,9809
21
+ openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md,sha256=A9nYzVxItMvEeizr6gM_EKIbymB1suKIitfcPPpjLAw,9507
22
+ openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md,sha256=Wpa5V3VfYGH7CbyuRzf6N7LdyxtsTKus4mTdGHRsU1c,8746
23
+ openpuppet_language-2.0.data/data/share/puppet/spec/README.md,sha256=GoSflg7wpY-hQ_fI0pGzP9kDRZ5spJgMGfXxrW0l4_U,5297
24
+ puppet/__init__.py,sha256=q4kHbcONrhkCj-u2MGOczy3mm0OvA9ni5auLEs2WKqE,639
25
+ puppet/adapter.py,sha256=WC_d2bIQtSO9h-KERjgrQyXC16i3lqPOz9diFCcI6Ns,4161
26
+ puppet/capabilities.py,sha256=wEnUJLT8-S7xXuNBAKWj1ItFhiFFhRBVDfQpBqLnyx4,7005
27
+ puppet/cli.py,sha256=5jGHDKZOgTR1fbxCbramiDuZO5faxctkHiEzV5xN1Pg,4681
28
+ puppet/diag.py,sha256=bWhP0BjBDlBr9Ywj_Xhxm05VU9dLrk3ioRaLdc8sJuQ,1200
29
+ puppet/engine.py,sha256=uyAfGzgHKZ0y5x3RoT6gvGjk2iuK4I2k4nKWtwe_9Sc,46198
30
+ puppet/eval.py,sha256=FC-l4iUBDbU2LTXb-30n4kbvjYTMq_xC45jgPQZB0dc,8673
31
+ puppet/ir.py,sha256=cP3M5TcLwpxf_X13B4lmU2OKRSzIzbBfrQwL2TGChfk,21452
32
+ puppet/lang.py,sha256=KJ6CC4tYgp0J1irNdauYqK4xNqRKt2wY88MW0U0HssY,23420
33
+ puppet/raster_adapter.py,sha256=btxTENjdZNU9pdwiu0J5fmKpYmLr7fFwlEkvoyi8z9I,15609
34
+ puppet/tk_adapter.py,sha256=JhStuobJu-oH6wNNQWGfuWIv2tfPabpZV4oyQtJnY44,16180
35
+ puppet/vocab.py,sha256=-mX92BjkGqAyOZbR7A9MyjQf78O1CDBcrbC6s5ifMB0,5506
36
+ openpuppet_language-2.0.dist-info/METADATA,sha256=FQK1r413FXZCScqecPAlMi-Nh6NmsXw525KmMSM_PIM,2047
37
+ openpuppet_language-2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
38
+ openpuppet_language-2.0.dist-info/entry_points.txt,sha256=T3PaN03V-_GiGTrMawIEh2J5OjWe54N2NpLcvwYy6vI,43
39
+ openpuppet_language-2.0.dist-info/top_level.txt,sha256=O6DGsH-AdDl1k7C2RtsmmtnKBX8kjSgIvhUy3pUiCSY,7
40
+ openpuppet_language-2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ puppet = puppet.cli:main
@@ -0,0 +1 @@
1
+ puppet
puppet/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Puppet 语言标准实现。
2
+
3
+ 本包是 `spec/` 下语言规范的**参考实现**:解析、校验、IR、执行语义。
4
+ 它不包含任何渲染实现;渲染由下游软件(puppethub)按渲染契约提供。
5
+ """
6
+
7
+ from .capabilities import capability, contract_of, load_capability_module
8
+ from .diag import Diagnostic, ERROR, INFO, WARNING
9
+ from .engine import Engine
10
+ from .ir import Program, new_program, validate
11
+
12
+ SPEC_VERSION = "2.0"
13
+
14
+ __all__ = [
15
+ "Diagnostic", "ERROR", "INFO", "WARNING",
16
+ "Engine", "Program", "new_program", "validate",
17
+ "capability", "contract_of", "load_capability_module",
18
+ "SPEC_VERSION",
19
+ ]
puppet/adapter.py ADDED
@@ -0,0 +1,112 @@
1
+ """conformance 适配器:行分隔 JSON over stdio(见 `conformance/README.md` 第 4 节)。
2
+
3
+ 这个文件是"标准实现"与"外部测试者"之间的边界;任何语言都可以实现同一协议。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import sys
10
+
11
+ from . import vocab
12
+ from .engine import Engine
13
+
14
+
15
+ def describe_rendering() -> dict:
16
+ """参考渲染器的能力声明(启动握手 `hello` 的响应体)。
17
+
18
+ 本实现是**无渲染器的语义引擎**:词汇全支持,但几何与截图**不提供**
19
+ (二者都必须走可见降级);可无 GUI 驱动。
20
+ - 数组 = 声明支持的子集;`null` = 支持全部标准(本实现选择显式列全)。
21
+ - `geometry` / `snapshot` / `headless` 是布尔观测/驱动能力。
22
+ """
23
+ return {
24
+ "controls": list(vocab.NODE_TYPES),
25
+ "attributes": sorted(vocab.ALL_ATTRS),
26
+ "animations": sorted(vocab.ANIMATABLE),
27
+ "icons": sorted(vocab.CORE_ICONS),
28
+ "geometry": False,
29
+ "snapshot": False,
30
+ "interaction": False,
31
+ "headless": True,
32
+ "notes": "参考语义引擎:无渲染器;几何、截图与用户动作投递均按规范可见降级。"
33
+ "conformance 可用 load.renderGeometry / load.rendering 注入测试替身。",
34
+ }
35
+
36
+
37
+ def handle(engine: Engine, request: dict) -> dict:
38
+ op = request.get("op")
39
+ if op == "hello":
40
+ return {"protocol": "1", "rendering": describe_rendering()}
41
+ if op == "load":
42
+ diags = engine.load(
43
+ request.get("program", []),
44
+ capabilities=request.get("capabilities"),
45
+ limits=request.get("limits"),
46
+ seed_state=request.get("seedState"),
47
+ render_geometry=request.get("renderGeometry"),
48
+ rendering=request.get("rendering"),
49
+ capability_modules=request.get("capabilityModules"),
50
+ )
51
+ return {"diagnostics": [d.to_dict() for d in diags]}
52
+ if op == "send":
53
+ diags = engine.apply_batch(request.get("batch", []))
54
+ return {"diagnostics": [d.to_dict() for d in diags]}
55
+ if op == "fire":
56
+ diags = engine.fire(request.get("target", ""), request.get("event", ""),
57
+ request.get("row"), request.get("value"))
58
+ return {"diagnostics": [d.to_dict() for d in diags]}
59
+ if op == "restart":
60
+ diags = engine.restart()
61
+ return {"diagnostics": [d.to_dict() for d in diags]}
62
+ if op == "observe":
63
+ return engine.observe()
64
+ if op == "interact":
65
+ return engine.deliver_interaction(request.get("target", ""),
66
+ request.get("action", ""),
67
+ request.get("value"))
68
+ if op == "snapshot":
69
+ return engine.snapshot()
70
+ if op == "quit":
71
+ return {"ok": True}
72
+ return {"error": "未知操作 %r" % op}
73
+
74
+
75
+ def _emit(payload: dict) -> None:
76
+ """输出纯 ASCII 的行分隔 JSON。
77
+
78
+ 刻意用 `ensure_ascii=True`:协议不依赖子进程的标准输出编码,
79
+ 在 Windows 上尤为关键(默认可能是 GBK)。
80
+ """
81
+ sys.stdout.write(json.dumps(payload, ensure_ascii=True) + "\n")
82
+ sys.stdout.flush()
83
+
84
+
85
+ def main() -> int:
86
+ try:
87
+ sys.stdin.reconfigure(encoding="utf-8", errors="replace")
88
+ sys.stdout.reconfigure(encoding="ascii", errors="backslashreplace")
89
+ except Exception: # noqa: BLE001 - 老环境没有 reconfigure 也不致命
90
+ pass
91
+ engine = Engine()
92
+ for line in sys.stdin:
93
+ line = line.strip()
94
+ if not line:
95
+ continue
96
+ try:
97
+ request = json.loads(line)
98
+ except json.JSONDecodeError as ex:
99
+ _emit({"error": "请求不是合法 JSON:%s" % ex})
100
+ continue
101
+ try:
102
+ response = handle(engine, request)
103
+ except Exception as ex: # noqa: BLE001 - 适配器绝不静默:错误原样回传
104
+ response = {"error": "%s: %s" % (type(ex).__name__, ex)}
105
+ _emit(response)
106
+ if request.get("op") == "quit":
107
+ break
108
+ return 0
109
+
110
+
111
+ if __name__ == "__main__":
112
+ raise SystemExit(main())
puppet/capabilities.py ADDED
@@ -0,0 +1,172 @@
1
+ """能力(工具)的契约提取与真实加载(规范 `03-semantics.md` 第 4 节)。
2
+
3
+ 三条硬性义务,缺一即违规:
4
+
5
+ - **说明文本非空**:docstring 是 LLM 发现工具的唯一说明书(`03` 第 4.5 节),
6
+ 缺失即 **错误** `CAP_NO_DOC`,且该能力**不注册**。
7
+ - **依赖显式声明 + 扫描兜底 + 不一致必须报警**:模块级 `REQUIRES` 与源码实际 import
8
+ 不一致时 **警告** `CAP_DEPS_MISMATCH`——否则部署机上才缺包(v1 的教训)。
9
+ - **返回值可序列化**:由引擎校验(`CALL_RESULT`);`async` 函数直接 await,
10
+ 同步函数走线程池,超时 / 取消由槽负责。
11
+
12
+ 测试替身(conformance 的 `load.capabilities`)与真实模块(`load.capabilityModules`)
13
+ 走**同一份契约形状**,因此行为用例对二者同样适用。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import ast
19
+ import importlib.util
20
+ import inspect
21
+ import os
22
+ from typing import Any, Dict, List, Optional, Tuple
23
+
24
+ from .diag import ERROR, INFO, WARNING, Diagnostic
25
+
26
+ MARKER = "__puppet_capability__"
27
+
28
+ _PY_TYPES = {"str": str, "int": int, "float": float, "bool": bool,
29
+ "list": list, "dict": dict, "any": object}
30
+
31
+
32
+ def capability(fn=None, *, name: Optional[str] = None, returns: Any = None,
33
+ requires: Optional[List[str]] = None):
34
+ """把一个函数标记为能力。
35
+
36
+ ```python
37
+ @capability()
38
+ def compress(path: str, level: int = 6) -> dict:
39
+ \"\"\"压缩文件,返回 {"bytes": <大小>}。\"\"\"
40
+ ```
41
+
42
+ `requires` 是**本能力**额外需要的依赖;模块级 `REQUIRES` 覆盖全模块。
43
+ """
44
+ def wrap(func):
45
+ setattr(func, MARKER, {"name": name or func.__name__,
46
+ "returns": returns, "requires": requires})
47
+ return func
48
+ return wrap if fn is None else wrap(fn)
49
+
50
+
51
+ def type_name(annotation: Any) -> str:
52
+ """把类型注解归一化成规范里的类型名(`str` / `int` / …)。"""
53
+ if annotation is inspect.Parameter.empty or annotation is None:
54
+ return "any"
55
+ if isinstance(annotation, str):
56
+ return annotation.strip() or "any"
57
+ return getattr(annotation, "__name__", "any")
58
+
59
+
60
+ def contract_of(func) -> Optional[Dict[str, Any]]:
61
+ """从签名与类型注解提取契约;非能力函数返回 `None`。"""
62
+ marker = getattr(func, MARKER, None)
63
+ if marker is None:
64
+ return None
65
+ try:
66
+ sig = inspect.signature(func)
67
+ except (TypeError, ValueError): # 不可 introspect 的可调用对象
68
+ params = []
69
+ else:
70
+ params = []
71
+ for pname, param in sig.parameters.items():
72
+ if pname in ("self", "cls"):
73
+ continue
74
+ params.append({
75
+ "name": pname,
76
+ "type": type_name(param.annotation),
77
+ "required": param.default is inspect.Parameter.empty,
78
+ **({} if param.default is inspect.Parameter.empty
79
+ else {"default": param.default}),
80
+ })
81
+ doc = (func.__doc__ or "").strip()
82
+ return {
83
+ "name": marker.get("name") or func.__name__,
84
+ "params": params,
85
+ "returns": type_name(marker.get("returns"))
86
+ if marker.get("returns") is not None
87
+ else type_name(getattr(func, "__annotations__", {}).get("return")),
88
+ "doc": doc,
89
+ "requires": list(marker.get("requires") or []),
90
+ "callable": func,
91
+ "behavior": "callable",
92
+ }
93
+
94
+
95
+ def type_matches(declared: str, value: Any) -> bool:
96
+ """按契约声明的类型判定实参;`any` 或未声明一律接受。"""
97
+ expected = _PY_TYPES.get(str(declared).lower())
98
+ if expected is None or expected is object:
99
+ return True
100
+ if expected is float and isinstance(value, int) and not isinstance(value, bool):
101
+ return True # 整数提升为浮点(规范 04 的隐式转换)
102
+ if expected is int and isinstance(value, bool):
103
+ return False # 布尔不参与数值比较
104
+ return isinstance(value, expected)
105
+
106
+
107
+ def _imported_modules(path: str) -> List[str]:
108
+ """扫描源码的顶层 import(兜底:动态 import 扫不到,故仍要求显式声明)。"""
109
+ try:
110
+ with open(path, "r", encoding="utf-8") as fh:
111
+ tree = ast.parse(fh.read(), filename=path)
112
+ except (OSError, SyntaxError):
113
+ return []
114
+ found = set()
115
+ for node in tree.body:
116
+ if isinstance(node, ast.Import):
117
+ for alias in node.names:
118
+ found.add(alias.name.split(".")[0])
119
+ elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
120
+ found.add(node.module.split(".")[0])
121
+ return sorted(found)
122
+
123
+
124
+ def load_capability_module(path: str) -> Tuple[Dict[str, dict], List[Diagnostic]]:
125
+ """加载一个能力模块,返回 `(契约表, 诊断)`。
126
+
127
+ 失败(找不到文件 / 导入报错)**必须可见**:`CAP_IMPORT` 错误,不静默跳过。
128
+ """
129
+ diags: List[Diagnostic] = []
130
+ name = os.path.splitext(os.path.basename(path))[0]
131
+ if not os.path.isfile(path):
132
+ diags.append(Diagnostic("CAP_IMPORT", ERROR, "能力模块不存在:%s" % path))
133
+ return {}, diags
134
+ spec = importlib.util.spec_from_file_location("puppet_capability_" + name, path)
135
+ if spec is None or spec.loader is None:
136
+ diags.append(Diagnostic("CAP_IMPORT", ERROR, "能力模块无法加载:%s" % path))
137
+ return {}, diags
138
+ module = importlib.util.module_from_spec(spec)
139
+ try:
140
+ spec.loader.exec_module(module)
141
+ except Exception as ex: # noqa: BLE001 - 一律转为可见失败
142
+ diags.append(Diagnostic("CAP_IMPORT", ERROR,
143
+ "能力模块导入失败(%s):%s: %s"
144
+ % (name, type(ex).__name__, ex)))
145
+ return {}, diags
146
+
147
+ # 依赖核对:显式声明 vs 实际 import
148
+ declared = list(getattr(module, "REQUIRES", []) or [])
149
+ scanned = _imported_modules(path)
150
+ if declared and sorted(set(declared)) != scanned:
151
+ diags.append(Diagnostic(
152
+ "CAP_DEPS_MISMATCH", WARNING,
153
+ "能力模块 %s 的依赖声明与实际 import 不一致:声明 %s,实际 %s"
154
+ % (name, sorted(set(declared)), scanned)))
155
+
156
+ contracts: Dict[str, dict] = {}
157
+ for attr in vars(module).values():
158
+ contract = contract_of(attr)
159
+ if contract is None:
160
+ continue
161
+ if not contract["doc"]:
162
+ diags.append(Diagnostic(
163
+ "CAP_NO_DOC", ERROR,
164
+ "能力 %s 没有说明文本(docstring 是工具的唯一说明书),不予注册"
165
+ % contract["name"]))
166
+ continue
167
+ if contract["name"] in contracts:
168
+ diags.append(Diagnostic(
169
+ "CALL_UNKNOWN", ERROR, "能力 %s 重复定义" % contract["name"]))
170
+ continue
171
+ contracts[contract["name"]] = contract
172
+ return contracts, diags
puppet/cli.py ADDED
@@ -0,0 +1,132 @@
1
+ """`puppet` 命令:标准工具(不需要 app 即可运行)。
2
+
3
+ 子命令:`spec`(规范位置)、`validate`(静态校验)、`conformance`(跑合规用例)。
4
+
5
+ 注意:`validate` 只做静态校验——解析与 IR 校验,不起运行时。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import os
12
+ import subprocess
13
+ import sys
14
+
15
+ from . import SPEC_VERSION
16
+ from .engine import Engine
17
+ from .ir import apply_stmt, new_program, validate
18
+ from .lang import parse_program
19
+
20
+ def _assets_dir() -> str:
21
+ """定位随发行包分发的 `spec/` 与 `conformance/`。
22
+
23
+ 开发态(源码树 / `pip install -e`):仓库根。
24
+ 安装态:setuptools 的 data-files 落在 `<prefix>/share/puppet`。
25
+ 可用环境变量 `PUPPET_ASSETS` 覆盖(下游打包可自行安置资产)。
26
+ """
27
+ override = os.environ.get("PUPPET_ASSETS")
28
+ if override:
29
+ return override
30
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
31
+ if os.path.isdir(os.path.join(root, "spec")):
32
+ return root
33
+ installed = os.path.join(sys.prefix, "share", "puppet")
34
+ if os.path.isdir(os.path.join(installed, "spec")):
35
+ return installed
36
+ return root
37
+
38
+
39
+ ROOT = _assets_dir()
40
+
41
+
42
+ def cmd_spec(_args) -> int:
43
+ path = os.path.join(ROOT, "spec")
44
+ if not os.path.isdir(path):
45
+ print("找不到规范目录:%s(发行包安装不完整?)" % path, file=sys.stderr)
46
+ return 1
47
+ print("规范版本:%s" % SPEC_VERSION)
48
+ print(path)
49
+ return 0
50
+
51
+
52
+ def _validate_target(path: str):
53
+ with open(path, "r", encoding="utf-8") as fh:
54
+ lines = fh.read().splitlines()
55
+ program = new_program()
56
+ diags = []
57
+ stmts, pdiags = parse_program(lines)
58
+ diags += pdiags
59
+ for stmt in stmts:
60
+ apply_stmt(program, stmt, diags)
61
+ diags += validate(program)
62
+ return diags
63
+
64
+
65
+ def cmd_validate(args) -> int:
66
+ if not os.path.exists(args.path):
67
+ print("路径不存在:%s" % args.path)
68
+ return 1
69
+ if os.path.isdir(args.path):
70
+ targets = [os.path.join(args.path, name) for name in sorted(os.listdir(args.path))
71
+ if name.endswith(".puppet")]
72
+ if not targets:
73
+ print("目录里没有 .puppet 文件:%s" % args.path)
74
+ return 1
75
+ else:
76
+ targets = [args.path]
77
+
78
+ total_diags = total_errors = 0
79
+ for target in targets:
80
+ diags = _validate_target(target)
81
+ total_diags += len(diags)
82
+ total_errors += sum(1 for d in diags if d.level == "error")
83
+ if not diags:
84
+ print("%s:零诊断" % os.path.basename(target))
85
+ continue
86
+ print("%s:" % os.path.basename(target))
87
+ for diag in diags:
88
+ mark = {"error": "错误", "warning": "警告", "info": "信息"}[diag.level]
89
+ location = ("第 %d 行" % diag.line) if diag.line else "全局"
90
+ print(" %s %s %s: %s" % (location, mark, diag.code, diag.message))
91
+ print("合计 %d 条诊断,其中错误 %d 条" % (total_diags, total_errors))
92
+ if args.strict:
93
+ return 1 if total_diags else 0
94
+ return 1 if total_errors else 0
95
+
96
+
97
+ def cmd_conformance(args) -> int:
98
+ runner = os.path.join(ROOT, "conformance", "runner.py")
99
+ if not os.path.isfile(runner):
100
+ print("找不到 conformance 运行器:%s(发行包安装不完整?)" % runner,
101
+ file=sys.stderr)
102
+ return 1
103
+ module = "puppet.tk_adapter" if args.tk else "puppet.adapter"
104
+ cmd = [sys.executable, runner, "--impl", '"%s" -m %s' % (sys.executable, module)]
105
+ if args.filter:
106
+ cmd += ["--filter", args.filter]
107
+ return subprocess.call(cmd, cwd=ROOT)
108
+
109
+
110
+ def main(argv=None) -> int:
111
+ ap = argparse.ArgumentParser(prog="puppet", description="Puppet 语言标准工具")
112
+ sub = ap.add_subparsers(dest="cmd", required=True)
113
+
114
+ sub.add_parser("spec", help="显示规范版本与位置").set_defaults(func=cmd_spec)
115
+
116
+ p_val = sub.add_parser("validate", help="静态校验 .puppet 文件或目录")
117
+ p_val.add_argument("path", help="文件路径,或包含 .puppet 的目录")
118
+ p_val.add_argument("--strict", action="store_true", help="有任何诊断即视为失败")
119
+ p_val.set_defaults(func=cmd_validate)
120
+
121
+ p_conf = sub.add_parser("conformance", help="运行合规用例集")
122
+ p_conf.add_argument("--filter", default="")
123
+ p_conf.add_argument("--tk", action="store_true",
124
+ help="用 Tk 渲染器适配器跑(真实几何;需要显示器)")
125
+ p_conf.set_defaults(func=cmd_conformance)
126
+
127
+ args = ap.parse_args(argv)
128
+ return args.func(args)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ raise SystemExit(main())
puppet/diag.py ADDED
@@ -0,0 +1,42 @@
1
+ """诊断:级别、形状、码。
2
+
3
+ 码表与 `spec/06-diagnostics.md` 一致;此处只定义形状,码以字符串常量使用。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ ERROR = "error"
11
+ WARNING = "warning"
12
+ INFO = "info"
13
+
14
+
15
+ @dataclass
16
+ class Diagnostic:
17
+ code: str
18
+ level: str
19
+ message: str
20
+ line: int = 0
21
+ col: int = 0
22
+ suggest: str = ""
23
+ # 仅 DEGRADED_FEATURE 使用:被降级的那个特性(`geometry` / `snapshot` /
24
+ # `control:<类型>` / `attr:<名>` / `animation:<名>` / `icon:<名>`)。
25
+ # conformance 的"声明即 oracle"按它比对,故必须机读。
26
+ feature: str = ""
27
+
28
+ def to_dict(self):
29
+ out = {"code": self.code, "level": self.level, "message": self.message}
30
+ if self.line:
31
+ out["line"] = self.line
32
+ if self.col:
33
+ out["col"] = self.col
34
+ if self.suggest:
35
+ out["suggest"] = self.suggest
36
+ if self.feature:
37
+ out["feature"] = self.feature
38
+ return out
39
+
40
+ def key(self):
41
+ """用于"本步是否新增"的判据(不含行号:行号随批次变化)。"""
42
+ return (self.code, self.message, self.level)