revision-engine 0.2.2__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.
- revision_engine/__init__.py +3 -0
- revision_engine/anchors.py +163 -0
- revision_engine/build.py +268 -0
- revision_engine/cli.py +159 -0
- revision_engine/config.py +159 -0
- revision_engine/diagrams.py +108 -0
- revision_engine/errors.py +18 -0
- revision_engine/highlight.py +62 -0
- revision_engine/intrinsics/__init__.py +34 -0
- revision_engine/intrinsics/arm.yaml +22 -0
- revision_engine/intrinsics/intel.yaml +42 -0
- revision_engine/intrinsics/riscv.yaml +28 -0
- revision_engine/lesson.py +64 -0
- revision_engine/markdown_ext.py +285 -0
- revision_engine/narration.py +276 -0
- revision_engine/render.py +119 -0
- revision_engine/serve.py +153 -0
- revision_engine/sources.py +61 -0
- revision_engine/static/app.js +292 -0
- revision_engine/static/styles.css +160 -0
- revision_engine/templates/lesson.html.j2 +89 -0
- revision_engine/templates/redirect.html.j2 +5 -0
- revision_engine-0.2.2.dist-info/METADATA +134 -0
- revision_engine-0.2.2.dist-info/RECORD +27 -0
- revision_engine-0.2.2.dist-info/WHEEL +4 -0
- revision_engine-0.2.2.dist-info/entry_points.txt +3 -0
- revision_engine-0.2.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Load and validate revision.yaml and curriculum.yaml into frozen dataclasses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from revision_engine.errors import BuildError
|
|
13
|
+
|
|
14
|
+
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
15
|
+
HOSTS = ("github", "gitlab")
|
|
16
|
+
DEFAULT_VOICE = "en_US-lessac-medium"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class SiteConfig:
|
|
21
|
+
title: str
|
|
22
|
+
base_url: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class RepoConfig:
|
|
27
|
+
url: str
|
|
28
|
+
host: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ProjectConfig:
|
|
33
|
+
root: Path
|
|
34
|
+
site: SiteConfig
|
|
35
|
+
repo: RepoConfig
|
|
36
|
+
content: Path
|
|
37
|
+
output: Path
|
|
38
|
+
voice: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class LessonRef:
|
|
43
|
+
slug: str
|
|
44
|
+
title: str
|
|
45
|
+
path: Path
|
|
46
|
+
type: str = "lesson"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Chapter:
|
|
51
|
+
slug: str
|
|
52
|
+
title: str
|
|
53
|
+
lessons: tuple[LessonRef, ...]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Curriculum:
|
|
58
|
+
chapters: tuple[Chapter, ...]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_yaml(path: Path) -> Any:
|
|
62
|
+
if not path.is_file():
|
|
63
|
+
raise BuildError(f"{path.name} not found at {path}")
|
|
64
|
+
try:
|
|
65
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
66
|
+
except yaml.YAMLError as e:
|
|
67
|
+
raise BuildError(f"{path}: invalid YAML: {e}") from e
|
|
68
|
+
return {} if data is None else data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _mapping(data: Any, where: str, file: str) -> dict:
|
|
72
|
+
if not isinstance(data, dict):
|
|
73
|
+
raise BuildError(f"{file}: {where} must be a mapping")
|
|
74
|
+
return data
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _require(m: dict, key: str, where: str, file: str) -> Any:
|
|
78
|
+
if key not in m or m[key] is None:
|
|
79
|
+
raise BuildError(f"{file}: {where}.{key} is required")
|
|
80
|
+
return m[key]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _string(m: dict, key: str, where: str, file: str, default: str | None = None) -> str:
|
|
84
|
+
value = m.get(key, default) if default is not None else _require(m, key, where, file)
|
|
85
|
+
if not isinstance(value, str):
|
|
86
|
+
raise BuildError(f"{file}: {where}.{key} must be a string")
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _slug(value: Any, where: str, file: str) -> str:
|
|
91
|
+
if not isinstance(value, str) or not SLUG_RE.match(value):
|
|
92
|
+
raise BuildError(f"{file}: {where}: {value!r} is not a slug (lowercase letters, digits, hyphens)")
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_project_config(root: Path) -> ProjectConfig:
|
|
97
|
+
root = root.resolve()
|
|
98
|
+
file = "revision.yaml"
|
|
99
|
+
data = _mapping(_load_yaml(root / file), "top level", file)
|
|
100
|
+
|
|
101
|
+
site = _mapping(_require(data, "site", "top level", file), "site", file)
|
|
102
|
+
repo = _mapping(_require(data, "repo", "top level", file), "repo", file)
|
|
103
|
+
host = _string(repo, "host", "repo", file)
|
|
104
|
+
if host not in HOSTS:
|
|
105
|
+
raise BuildError(f"{file}: repo.host must be one of: {', '.join(HOSTS)} (got {host!r})")
|
|
106
|
+
|
|
107
|
+
narration = _mapping(data.get("narration") or {}, "narration", file)
|
|
108
|
+
|
|
109
|
+
return ProjectConfig(
|
|
110
|
+
root=root,
|
|
111
|
+
site=SiteConfig(
|
|
112
|
+
title=_string(site, "title", "site", file),
|
|
113
|
+
base_url=_string(site, "base_url", "site", file, ""),
|
|
114
|
+
),
|
|
115
|
+
repo=RepoConfig(url=_string(repo, "url", "repo", file), host=host),
|
|
116
|
+
content=root / _string(data, "content", "top level", file, "docs/revision"),
|
|
117
|
+
output=root / _string(data, "output", "top level", file, "public"),
|
|
118
|
+
voice=_string(narration, "voice", "narration", file, DEFAULT_VOICE),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_curriculum(cfg: ProjectConfig) -> Curriculum:
|
|
123
|
+
path = cfg.content / "curriculum.yaml"
|
|
124
|
+
file = path.relative_to(cfg.root).as_posix()
|
|
125
|
+
data = _mapping(_load_yaml(path), "top level", file)
|
|
126
|
+
raw_chapters = data.get("chapters")
|
|
127
|
+
if not isinstance(raw_chapters, list):
|
|
128
|
+
raise BuildError(f"{file}: chapters must be a list")
|
|
129
|
+
|
|
130
|
+
chapters: list[Chapter] = []
|
|
131
|
+
seen_chapters: set[str] = set()
|
|
132
|
+
for ci, raw in enumerate(raw_chapters):
|
|
133
|
+
where = f"chapters[{ci}]"
|
|
134
|
+
m = _mapping(raw, where, file)
|
|
135
|
+
slug = _slug(m.get("slug"), f"{where}.slug", file)
|
|
136
|
+
if slug in seen_chapters:
|
|
137
|
+
raise BuildError(f"{file}: duplicate chapter slug {slug!r}")
|
|
138
|
+
seen_chapters.add(slug)
|
|
139
|
+
raw_lessons = m.get("lessons")
|
|
140
|
+
if not isinstance(raw_lessons, list):
|
|
141
|
+
raise BuildError(f"{file}: {where}.lessons must be a list")
|
|
142
|
+
lessons: list[LessonRef] = []
|
|
143
|
+
seen_lessons: set[str] = set()
|
|
144
|
+
for li, lraw in enumerate(raw_lessons):
|
|
145
|
+
lwhere = f"{where}.lessons[{li}]"
|
|
146
|
+
lm = _mapping(lraw, lwhere, file)
|
|
147
|
+
lslug = _slug(lm.get("slug"), f"{lwhere}.slug", file)
|
|
148
|
+
if lslug in seen_lessons:
|
|
149
|
+
raise BuildError(f"{file}: duplicate lesson slug {lslug!r} in chapter {slug!r}")
|
|
150
|
+
seen_lessons.add(lslug)
|
|
151
|
+
ltype = _string(lm, "type", lwhere, file, "lesson")
|
|
152
|
+
if ltype != "lesson":
|
|
153
|
+
raise BuildError(f"{file}: {lwhere}.type: only 'lesson' is supported in this version")
|
|
154
|
+
lpath = (cfg.content / _string(lm, "path", lwhere, file)).resolve()
|
|
155
|
+
if not (lpath / "lesson.md").is_file():
|
|
156
|
+
raise BuildError(f"{file}: {lwhere}.path: {lpath / 'lesson.md'} not found")
|
|
157
|
+
lessons.append(LessonRef(slug=lslug, title=_string(lm, "title", lwhere, file), path=lpath, type=ltype))
|
|
158
|
+
chapters.append(Chapter(slug=slug, title=_string(m, "title", where, file), lessons=tuple(lessons)))
|
|
159
|
+
return Curriculum(chapters=tuple(chapters))
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Render Graphviz dot files to inline SVG and stamp node ids for the front end."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from revision_engine.errors import BuildError
|
|
12
|
+
|
|
13
|
+
_STRIP_RES = (
|
|
14
|
+
re.compile(r"<\?xml[^>]*\?>\s*"),
|
|
15
|
+
re.compile(r"<!DOCTYPE[^>]*>\s*"),
|
|
16
|
+
re.compile(r"<!--.*?-->\s*", re.S),
|
|
17
|
+
)
|
|
18
|
+
_NODE_RE = re.compile(r'<g id="([^"]+)" class="node"')
|
|
19
|
+
_SVG_OPEN_RE = re.compile(r"<svg\b")
|
|
20
|
+
# Quote-aware: render_dot unescapes ids, so a quoted attribute value may hold a raw '>'.
|
|
21
|
+
_G_OPEN_RE = re.compile(r'<g\b(?:[^>"]|"[^"]*")*>')
|
|
22
|
+
_CLASS_RE = re.compile(r'(?<![\w-])class="([^"]*)"')
|
|
23
|
+
_ANCHOR_ATTR_RE = re.compile(r'(?<![\w-])data-anchor="([^"]*)"')
|
|
24
|
+
_A_OPEN_RE = re.compile(r'<a\b(?:[^>"]|"[^"]*")*>')
|
|
25
|
+
_HAS_LINK_RE = re.compile(r'\s(?:xlink:)?href="')
|
|
26
|
+
_LINK_ATTR_RE = re.compile(r'\s(?:xlink:)?href="[^"]*"|\starget="[^"]*"')
|
|
27
|
+
_GROUP_KINDS = {"node", "edge", "cluster", "graph"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def graphviz_version() -> str | None:
|
|
31
|
+
if shutil.which("dot") is None:
|
|
32
|
+
return None
|
|
33
|
+
out = subprocess.run(["dot", "-V"], capture_output=True, text=True, check=False)
|
|
34
|
+
m = re.search(r"version\s+(\S+)", out.stderr + out.stdout)
|
|
35
|
+
return m.group(1) if m else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_dot(path: Path, where: str) -> str:
|
|
39
|
+
if shutil.which("dot") is None:
|
|
40
|
+
raise BuildError(f"{where}: graphviz 'dot' is not installed or not on PATH")
|
|
41
|
+
proc = subprocess.run(
|
|
42
|
+
["dot", "-Tsvg", "-Gbgcolor=transparent", str(path)],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
check=False,
|
|
47
|
+
)
|
|
48
|
+
if proc.returncode != 0:
|
|
49
|
+
raise BuildError(f"{where}: graphviz failed: {proc.stderr.strip()}")
|
|
50
|
+
svg = proc.stdout
|
|
51
|
+
for rx in _STRIP_RES:
|
|
52
|
+
svg = rx.sub("", svg)
|
|
53
|
+
svg = svg.strip()
|
|
54
|
+
|
|
55
|
+
def node(m: re.Match) -> str:
|
|
56
|
+
# Graphviz entity-escapes ids (a hyphen becomes -); restore the author's spelling.
|
|
57
|
+
name = html.unescape(m.group(1))
|
|
58
|
+
return f'<g id="{name}" class="node" data-node="{name}"'
|
|
59
|
+
|
|
60
|
+
svg = _NODE_RE.sub(node, svg)
|
|
61
|
+
svg = _SVG_OPEN_RE.sub('<svg class="rv-svg"', svg, count=1)
|
|
62
|
+
return svg
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def stamp_anchors(svg: str, anchor_names: set[str]) -> str:
|
|
66
|
+
def repl(m: re.Match) -> str:
|
|
67
|
+
node = m.group(1)
|
|
68
|
+
if node in anchor_names:
|
|
69
|
+
return f'<g id="{node}" class="node rv-svg-anchor" data-node="{node}" data-anchor="{node}"'
|
|
70
|
+
return m.group(0)
|
|
71
|
+
|
|
72
|
+
return re.sub(r'<g id="([^"]+)" class="node" data-node="[^"]+"', repl, svg)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def unlink_anchors(svg: str) -> tuple[str, list[str]]:
|
|
76
|
+
"""Strip links from anchored nodes: a click on an anchored node pins it, so a link inside is dead.
|
|
77
|
+
|
|
78
|
+
Returns the new SVG and the sorted names of the anchored nodes that held a link.
|
|
79
|
+
"""
|
|
80
|
+
if 'data-anchor="' not in svg:
|
|
81
|
+
return svg, []
|
|
82
|
+
groups = []
|
|
83
|
+
for m in _G_OPEN_RE.finditer(svg):
|
|
84
|
+
cls = _CLASS_RE.search(m.group(0))
|
|
85
|
+
if cls and _GROUP_KINDS.intersection(cls.group(1).split()):
|
|
86
|
+
groups.append(m)
|
|
87
|
+
ends = [g.start() for g in groups[1:]] + [len(svg)]
|
|
88
|
+
|
|
89
|
+
def unlink(m: re.Match) -> str:
|
|
90
|
+
tag = m.group(0)
|
|
91
|
+
return _LINK_ATTR_RE.sub("", tag) if _HAS_LINK_RE.search(tag) else tag
|
|
92
|
+
|
|
93
|
+
out: list[str] = []
|
|
94
|
+
names: list[str] = []
|
|
95
|
+
pos = 0
|
|
96
|
+
for g, end in zip(groups, ends):
|
|
97
|
+
anchor = _ANCHOR_ATTR_RE.search(g.group(0))
|
|
98
|
+
if anchor is None:
|
|
99
|
+
continue
|
|
100
|
+
span = svg[g.end() : end]
|
|
101
|
+
new = _A_OPEN_RE.sub(unlink, span)
|
|
102
|
+
if new != span:
|
|
103
|
+
names.append(anchor.group(1))
|
|
104
|
+
out.append(svg[pos : g.end()])
|
|
105
|
+
out.append(new)
|
|
106
|
+
pos = end
|
|
107
|
+
out.append(svg[pos:])
|
|
108
|
+
return "".join(out), sorted(set(names))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Error and warning types shared by every build stage."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BuildError(Exception):
|
|
5
|
+
"""A content or configuration problem that must stop the build.
|
|
6
|
+
|
|
7
|
+
The message always names the file and, where possible, the key path or line.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Report:
|
|
12
|
+
"""Collects non-fatal warnings during a build for a summary at the end."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.warnings: list[str] = []
|
|
16
|
+
|
|
17
|
+
def warn(self, message: str) -> None:
|
|
18
|
+
self.warnings.append(message)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Pygments highlighting emitted as one <div class="rv-line"> per source line."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from pygments import highlight
|
|
8
|
+
from pygments.formatters import HtmlFormatter
|
|
9
|
+
from pygments.lexers import TextLexer, get_lexer_for_filename
|
|
10
|
+
from pygments.util import ClassNotFound
|
|
11
|
+
|
|
12
|
+
from revision_engine.sources import SourceFile
|
|
13
|
+
|
|
14
|
+
# With linespans set, Pygments emits `<span id="L-n">…\n</span>` per line: the newline sits
|
|
15
|
+
# inside the span, before its close tag, and tokens never cross a line boundary.
|
|
16
|
+
_LINE_RE = re.compile(r'<span id="L-(\d+)">(.*?)\n</span>', re.S)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _lexer(path: str):
|
|
20
|
+
try:
|
|
21
|
+
return get_lexer_for_filename(path, stripnl=False, ensurenl=False)
|
|
22
|
+
except ClassNotFound:
|
|
23
|
+
return TextLexer(stripnl=False, ensurenl=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def highlight_source(source: SourceFile) -> str:
|
|
27
|
+
n = len(source.lines)
|
|
28
|
+
if n == 0:
|
|
29
|
+
return ""
|
|
30
|
+
formatter = HtmlFormatter(linespans="L", cssclass="rvhl", nowrap=False)
|
|
31
|
+
text = "\n".join(source.lines) + "\n"
|
|
32
|
+
html = highlight(text, _lexer(source.path), formatter)
|
|
33
|
+
inner = html[html.index("<pre>") + len("<pre>") : html.rindex("</pre>")]
|
|
34
|
+
matches = _LINE_RE.findall(inner)
|
|
35
|
+
if len(matches) != n:
|
|
36
|
+
raise AssertionError(f"highlighter produced {len(matches)} lines for {n} source lines in {source.path}")
|
|
37
|
+
|
|
38
|
+
out: list[str] = []
|
|
39
|
+
for i, (_, code) in enumerate(matches, start=1):
|
|
40
|
+
out.append(
|
|
41
|
+
f'<div class="rv-line" id="L{i}" data-line="{i}">'
|
|
42
|
+
f'<span class="rv-ln">{i}</span><span class="rv-code">{code}</span></div>'
|
|
43
|
+
)
|
|
44
|
+
return "\n".join(out) + "\n"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _style_defs(style: str, selector: str) -> str:
|
|
48
|
+
"""Pygments token rules under `selector`, without the container background rule."""
|
|
49
|
+
defs = HtmlFormatter(style=style).get_style_defs(selector)
|
|
50
|
+
kept = [line for line in defs.splitlines() if not re.match(re.escape(selector) + r"\s*\{", line)]
|
|
51
|
+
return "\n".join(kept)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def token_css() -> str:
|
|
55
|
+
light = _style_defs("default", ".rv-code")
|
|
56
|
+
dark_forced = _style_defs("github-dark", ':root[data-theme="dark"] .rv-code')
|
|
57
|
+
dark_auto = _style_defs("github-dark", ':root:not([data-theme="light"]) .rv-code')
|
|
58
|
+
return (
|
|
59
|
+
f"{light}\n"
|
|
60
|
+
f"@media (prefers-color-scheme: dark) {{\n{dark_auto}\n}}\n"
|
|
61
|
+
f"{dark_forced}\n"
|
|
62
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Map SIMD intrinsic names to vendor documentation URLs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from importlib import resources
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
_VENDORS = ("intel", "arm", "riscv")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@lru_cache(maxsize=1)
|
|
14
|
+
def _table() -> dict[str, str]:
|
|
15
|
+
table: dict[str, str] = {}
|
|
16
|
+
for vendor in _VENDORS:
|
|
17
|
+
text = resources.files(__package__).joinpath(f"{vendor}.yaml").read_text(encoding="utf-8")
|
|
18
|
+
data = yaml.safe_load(text)
|
|
19
|
+
for name in data.get("names") or ():
|
|
20
|
+
table[name] = data["url"].format(name=name)
|
|
21
|
+
# Vendors whose page can't be derived from the name carry one explicit URL per name.
|
|
22
|
+
for name, url in (data.get("urls") or {}).items():
|
|
23
|
+
table[str(name)] = str(url)
|
|
24
|
+
return table
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def vendors() -> tuple[str, ...]:
|
|
28
|
+
return _VENDORS
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def lookup(name: str) -> str | None:
|
|
32
|
+
if not name:
|
|
33
|
+
return None
|
|
34
|
+
return _table().get(name)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
url: "https://developer.arm.com/architectures/instruction-sets/intrinsics/{name}"
|
|
2
|
+
names:
|
|
3
|
+
- vld1q_f32
|
|
4
|
+
- vst1q_f32
|
|
5
|
+
- vld2q_f32
|
|
6
|
+
- vst2q_f32
|
|
7
|
+
- vld4q_f32
|
|
8
|
+
- vdupq_n_f32
|
|
9
|
+
- vmulq_f32
|
|
10
|
+
- vaddq_f32
|
|
11
|
+
- vsubq_f32
|
|
12
|
+
- vadd_f32
|
|
13
|
+
- vmlaq_f32
|
|
14
|
+
- vmlsq_f32
|
|
15
|
+
- vfmaq_f32
|
|
16
|
+
- vfmsq_f32
|
|
17
|
+
- vpadd_f32
|
|
18
|
+
- vget_low_f32
|
|
19
|
+
- vget_high_f32
|
|
20
|
+
- vgetq_lane_f32
|
|
21
|
+
- vget_lane_f32
|
|
22
|
+
- vaddvq_f32
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
url: "https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text={name}"
|
|
2
|
+
names:
|
|
3
|
+
- _mm_setzero_ps
|
|
4
|
+
- _mm_load_ps
|
|
5
|
+
- _mm_loadu_ps
|
|
6
|
+
- _mm_store_ps
|
|
7
|
+
- _mm_storeu_ps
|
|
8
|
+
- _mm_mul_ps
|
|
9
|
+
- _mm_add_ps
|
|
10
|
+
- _mm_add_ss
|
|
11
|
+
- _mm_addsub_ps
|
|
12
|
+
- _mm_or_ps
|
|
13
|
+
- _mm_moveldup_ps
|
|
14
|
+
- _mm_movehdup_ps
|
|
15
|
+
- _mm_hadd_ps
|
|
16
|
+
- _mm_dp_ps
|
|
17
|
+
- _mm_shuffle_ps
|
|
18
|
+
- _mm_movehl_ps
|
|
19
|
+
- _mm_cvtss_f32
|
|
20
|
+
- _mm256_setzero_ps
|
|
21
|
+
- _mm256_load_ps
|
|
22
|
+
- _mm256_loadu_ps
|
|
23
|
+
- _mm256_store_ps
|
|
24
|
+
- _mm256_storeu_ps
|
|
25
|
+
- _mm256_mul_ps
|
|
26
|
+
- _mm256_add_ps
|
|
27
|
+
- _mm256_addsub_ps
|
|
28
|
+
- _mm256_shuffle_ps
|
|
29
|
+
- _mm256_moveldup_ps
|
|
30
|
+
- _mm256_movehdup_ps
|
|
31
|
+
- _mm256_fmadd_ps
|
|
32
|
+
- _mm256_fmaddsub_ps
|
|
33
|
+
- _mm256_hadd_ps
|
|
34
|
+
- _mm256_extractf128_ps
|
|
35
|
+
- _mm256_castps256_ps128
|
|
36
|
+
- _mm512_setzero_ps
|
|
37
|
+
- _mm512_load_ps
|
|
38
|
+
- _mm512_loadu_ps
|
|
39
|
+
- _mm512_store_ps
|
|
40
|
+
- _mm512_storeu_ps
|
|
41
|
+
- _mm512_fmadd_ps
|
|
42
|
+
- _mm512_reduce_add_ps
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# RISC-V Vector C intrinsics, ratified v1.0 reference (riscv-non-isa/riscv-rvv-intrinsic-doc,
|
|
2
|
+
# tag v1.0-ratified). Each name links to the section that lists it as a prototype; the section
|
|
3
|
+
# cannot be derived from the name, so every row carries its full URL. Rows checked 2026-10-03.
|
|
4
|
+
urls:
|
|
5
|
+
__riscv_vfadd: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/04_vector_floating-point_intrinsics.html#overloaded-vector-single-width-floating-point-add-subtract"
|
|
6
|
+
__riscv_vfadd_tu: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/policy_funcs/overloaded_intrinsic_funcs/04_vector_floating-point_intrinsics.html#policy-variant-overloadedvector-single-width-floating-point-add-subtract"
|
|
7
|
+
__riscv_vfmacc: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/04_vector_floating-point_intrinsics.html#overloaded-vector-single-width-floating-point-fused-multiply-add"
|
|
8
|
+
__riscv_vfmacc_tu: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/policy_funcs/overloaded_intrinsic_funcs/04_vector_floating-point_intrinsics.html#policy-variant-overloadedvector-single-width-floating-point-fused-multiply-add"
|
|
9
|
+
__riscv_vfmul: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/04_vector_floating-point_intrinsics.html#overloaded-vector-single-width-floating-point-multiply-divide"
|
|
10
|
+
__riscv_vfmv_f: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/07_vector_permutation_intrinsics.html#overloaded-integer-scalar-move"
|
|
11
|
+
__riscv_vfmv_s_f_f32m1: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/07_vector_permutation_intrinsics.html#integer-scalar-move"
|
|
12
|
+
__riscv_vfmv_v_f_f32m2: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/04_vector_floating-point_intrinsics.html#vector-floating-point-move"
|
|
13
|
+
__riscv_vfmv_v_f_f32m8: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/04_vector_floating-point_intrinsics.html#vector-floating-point-move"
|
|
14
|
+
__riscv_vfneg: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/04_vector_floating-point_intrinsics.html#overloaded-vector-single-width-floating-point-add-subtract"
|
|
15
|
+
__riscv_vfnmsac: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/04_vector_floating-point_intrinsics.html#overloaded-vector-single-width-floating-point-fused-multiply-add"
|
|
16
|
+
__riscv_vfredusum: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/05_vector_reduction_operations.html#overloaded-vector-single-width-floating-point-reduction"
|
|
17
|
+
__riscv_vget_f32m1: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#overloaded-vector-extraction"
|
|
18
|
+
__riscv_vget_f32m2: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#overloaded-vector-extraction"
|
|
19
|
+
__riscv_vle32_v_f32m8: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/00_vector_loads_and_stores_intrinsics.html#vector-unit-stride-load"
|
|
20
|
+
__riscv_vle64_v_u64m4: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/00_vector_loads_and_stores_intrinsics.html#vector-unit-stride-load"
|
|
21
|
+
__riscv_vlseg2e32_v_f32m2x2: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/01_vector_loads_and_stores_segment_intrinsics.html#vector-unit-stride-segment-load"
|
|
22
|
+
__riscv_vnsrl: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/02_vector_integer_arithmetic_intrinsics.html#overloaded-vector-narrowing-integer-right-shift"
|
|
23
|
+
__riscv_vreinterpret_f32m2: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/overloaded_intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#overloaded-reinterpret-cast-conversion"
|
|
24
|
+
__riscv_vsetvl_e32m2: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#set-vl-and-vtype"
|
|
25
|
+
__riscv_vsetvl_e32m8: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#set-vl-and-vtype"
|
|
26
|
+
__riscv_vsetvlmax_e32m1: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#set-vl-to-vlmax-with-specific-vtype"
|
|
27
|
+
__riscv_vsetvlmax_e32m2: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#set-vl-to-vlmax-with-specific-vtype"
|
|
28
|
+
__riscv_vsetvlmax_e32m8: "https://docs.riscv.org/reference/vector-c-intrinsics/v1.0/intrinsic_funcs/08_miscellaneous_vector_utility_intrinsics.html#set-vl-to-vlmax-with-specific-vtype"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Parse lesson.md: YAML front matter plus a Markdown body."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from revision_engine.errors import BuildError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class LessonDoc:
|
|
15
|
+
title: str
|
|
16
|
+
summary: str
|
|
17
|
+
file: str
|
|
18
|
+
start: str | None
|
|
19
|
+
media: str | None
|
|
20
|
+
voice: str | None
|
|
21
|
+
body: str
|
|
22
|
+
body_line: int = 1 # 1-based line number in lesson.md where the body starts
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _opt_str(m: dict, key: str, where: str) -> str | None:
|
|
26
|
+
v = m.get(key)
|
|
27
|
+
if v is None:
|
|
28
|
+
return None
|
|
29
|
+
if not isinstance(v, str):
|
|
30
|
+
raise BuildError(f"{where}: front matter key {key!r} must be a string")
|
|
31
|
+
return v
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_lesson(path: Path, where: str) -> LessonDoc:
|
|
35
|
+
text = path.read_text(encoding="utf-8").replace("\r\n", "\n")
|
|
36
|
+
if not text.startswith("---\n"):
|
|
37
|
+
raise BuildError(f"{where}: front matter must start with '---' on line 1")
|
|
38
|
+
end = text.find("\n---\n", 4)
|
|
39
|
+
if end == -1:
|
|
40
|
+
if text.endswith("\n---"):
|
|
41
|
+
end = len(text) - 4
|
|
42
|
+
else:
|
|
43
|
+
raise BuildError(f"{where}: front matter is not closed by a '---' line")
|
|
44
|
+
try:
|
|
45
|
+
meta = yaml.safe_load(text[4:end]) or {}
|
|
46
|
+
except yaml.YAMLError as e:
|
|
47
|
+
raise BuildError(f"{where}: front matter is not valid YAML: {e}") from e
|
|
48
|
+
if not isinstance(meta, dict):
|
|
49
|
+
raise BuildError(f"{where}: front matter must be a mapping")
|
|
50
|
+
for key in ("title", "file"):
|
|
51
|
+
if not isinstance(meta.get(key), str) or not meta[key]:
|
|
52
|
+
raise BuildError(f"{where}: front matter key {key!r} is required")
|
|
53
|
+
body = text[end + 5 :]
|
|
54
|
+
body_line = text[: end + 5].count("\n") + 1
|
|
55
|
+
return LessonDoc(
|
|
56
|
+
title=meta["title"],
|
|
57
|
+
summary=_opt_str(meta, "summary", where) or "",
|
|
58
|
+
file=meta["file"].replace("\\", "/"),
|
|
59
|
+
start=_opt_str(meta, "start", where),
|
|
60
|
+
media=_opt_str(meta, "media", where),
|
|
61
|
+
voice=_opt_str(meta, "voice", where),
|
|
62
|
+
body=body,
|
|
63
|
+
body_line=body_line,
|
|
64
|
+
)
|