uc-forth 0.1.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.
- uc_forth/__init__.py +78 -0
- uc_forth/__main__.py +3 -0
- uc_forth/builtins_catalog.py +80 -0
- uc_forth/catalog.py +265 -0
- uc_forth/catalog_primitives.py +144 -0
- uc_forth/cli.py +148 -0
- uc_forth/core/core.fth +73 -0
- uc_forth/core/init.fth +9 -0
- uc_forth/engine.py +101 -0
- uc_forth/env_primitives.py +55 -0
- uc_forth/fs_extra_primitives.py +163 -0
- uc_forth/http_primitives.py +160 -0
- uc_forth/inference.py +133 -0
- uc_forth/json_primitives.py +291 -0
- uc_forth/modules.py +23 -0
- uc_forth/os_primitives.py +230 -0
- uc_forth/primitives.py +571 -0
- uc_forth/require.py +132 -0
- uc_forth/session.py +135 -0
- uc_forth/string_extra_primitives.py +159 -0
- uc_forth/syscall.py +51 -0
- uc_forth/time_primitives.py +70 -0
- uc_forth/vm.py +402 -0
- uc_forth/web/index.html +597 -0
- uc_forth-0.1.0.dist-info/METADATA +150 -0
- uc_forth-0.1.0.dist-info/RECORD +28 -0
- uc_forth-0.1.0.dist-info/WHEEL +4 -0
- uc_forth-0.1.0.dist-info/entry_points.txt +2 -0
uc_forth/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""uc-forth — A Forth-based knowledge and execution kernel."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from .vm import VM, Word, WordType, Token, CatalogEntry, CatalogVersion, Fact
|
|
8
|
+
from .engine import Engine
|
|
9
|
+
from .primitives import register_primitives
|
|
10
|
+
from .os_primitives import register_core_os_primitives
|
|
11
|
+
from .syscall import register_syscall_primitives
|
|
12
|
+
from .require import register_require_primitives
|
|
13
|
+
from .builtins_catalog import register_builtin_catalog
|
|
14
|
+
from .modules import register_modules
|
|
15
|
+
|
|
16
|
+
# Must import catalog to ensure methods are patched onto VM
|
|
17
|
+
import uc_forth.catalog # noqa: F401
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register_kernel(vm: VM) -> None:
|
|
21
|
+
"""Register only the minimum kernel primitives and populate the module dict."""
|
|
22
|
+
register_primitives(vm)
|
|
23
|
+
register_core_os_primitives(vm)
|
|
24
|
+
register_syscall_primitives(vm)
|
|
25
|
+
register_require_primitives(vm)
|
|
26
|
+
register_builtin_catalog(vm)
|
|
27
|
+
register_modules(vm)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_all(vm: VM) -> None:
|
|
31
|
+
"""Register kernel primitives, populate modules, and load ALL modules."""
|
|
32
|
+
register_kernel(vm)
|
|
33
|
+
for module_name, loader in vm.modules.items():
|
|
34
|
+
module_key = f"module:{module_name}"
|
|
35
|
+
if module_key not in vm.loaded_files:
|
|
36
|
+
loader(vm)
|
|
37
|
+
vm.loaded_files[module_key] = True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_core(vm: VM) -> int:
|
|
41
|
+
"""Load core.fth and init.fth. Returns number of loaded definitions."""
|
|
42
|
+
loaded = 0
|
|
43
|
+
for filename in ("core.fth", "init.fth"):
|
|
44
|
+
# Search in multiple locations
|
|
45
|
+
candidates = [
|
|
46
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "core", filename),
|
|
47
|
+
os.path.join(os.path.dirname(__file__), "core", filename),
|
|
48
|
+
os.path.join("core", filename),
|
|
49
|
+
]
|
|
50
|
+
for path in candidates:
|
|
51
|
+
if os.path.isfile(path):
|
|
52
|
+
with open(path) as fh:
|
|
53
|
+
for line in fh:
|
|
54
|
+
line = line.strip()
|
|
55
|
+
if not line or line.startswith("\\"):
|
|
56
|
+
continue
|
|
57
|
+
result = vm.eval(line)
|
|
58
|
+
if not result.startswith("Error:"):
|
|
59
|
+
loaded += 1
|
|
60
|
+
break
|
|
61
|
+
return loaded
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_vm() -> VM:
|
|
65
|
+
"""Create a fully initialized VM with all primitives and core.fth loaded."""
|
|
66
|
+
vm = VM()
|
|
67
|
+
register_all(vm)
|
|
68
|
+
loaded = load_core(vm)
|
|
69
|
+
return vm
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"VM", "Word", "WordType", "Token",
|
|
74
|
+
"CatalogEntry", "CatalogVersion", "Fact",
|
|
75
|
+
"Engine",
|
|
76
|
+
"register_kernel", "register_all", "load_core", "create_vm",
|
|
77
|
+
"__version__",
|
|
78
|
+
]
|
uc_forth/__main__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Builtin catalog -- stack-effect docs for kernel primitives only."""
|
|
2
|
+
|
|
3
|
+
from uc_forth.vm import VM, CatalogEntry
|
|
4
|
+
|
|
5
|
+
# Must import catalog to ensure methods are patched onto VM
|
|
6
|
+
import uc_forth.catalog # noqa: F401
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_builtin_catalog(vm: VM) -> None:
|
|
10
|
+
"""Add stack-effect documentation for kernel primitives."""
|
|
11
|
+
entries = {
|
|
12
|
+
# Stack
|
|
13
|
+
"DUP": ("( a -- a a )", "Duplicate top of stack"),
|
|
14
|
+
"DROP": ("( a -- )", "Remove top of stack"),
|
|
15
|
+
"SWAP": ("( a b -- b a )", "Swap top two values"),
|
|
16
|
+
"DEPTH": ("( -- n )", "Number of items on stack"),
|
|
17
|
+
|
|
18
|
+
# Arithmetic
|
|
19
|
+
"+": ("( a b -- a+b )", "Addition"),
|
|
20
|
+
"-": ("( a b -- a-b )", "Subtraction"),
|
|
21
|
+
"*": ("( a b -- a*b )", "Multiplication"),
|
|
22
|
+
"/": ("( a b -- a/b )", "Integer division"),
|
|
23
|
+
"/MOD": ("( a b -- rem quot )", "Division with remainder"),
|
|
24
|
+
|
|
25
|
+
# Comparison
|
|
26
|
+
"=": ("( a b -- flag )", "Equal"),
|
|
27
|
+
"<": ("( a b -- flag )", "Less than"),
|
|
28
|
+
"0=": ("( a -- flag )", "Equal to zero"),
|
|
29
|
+
|
|
30
|
+
# Logic
|
|
31
|
+
"AND": ("( a b -- a&b )", "Bitwise AND"),
|
|
32
|
+
"OR": ("( a b -- a|b )", "Bitwise OR"),
|
|
33
|
+
"XOR": ("( a b -- a^b )", "Bitwise XOR"),
|
|
34
|
+
|
|
35
|
+
# Memory
|
|
36
|
+
"@": ("( addr -- val )", "Fetch value from address"),
|
|
37
|
+
"!": ("( val addr -- )", "Store value at address"),
|
|
38
|
+
"VARIABLE": ("( -- )", "Create a variable: VARIABLE name"),
|
|
39
|
+
"CONSTANT": ("( val -- )", "Create a constant: val CONSTANT name"),
|
|
40
|
+
"ALLOT": ("( n -- )", "Reserve n cells of memory"),
|
|
41
|
+
"HERE": ("( -- addr )", "Current memory pointer"),
|
|
42
|
+
|
|
43
|
+
# Return stack
|
|
44
|
+
">R": ("( val -- ) R:( -- val )", "Move to return stack"),
|
|
45
|
+
"R>": ("( -- val ) R:( val -- )", "Move from return stack"),
|
|
46
|
+
|
|
47
|
+
# I/O
|
|
48
|
+
".": ("( n -- )", "Print number"),
|
|
49
|
+
".S": ("( -- )", "Show stack contents"),
|
|
50
|
+
"EMIT": ("( char -- )", "Print character"),
|
|
51
|
+
|
|
52
|
+
# Compiler primitives for POSTPONE-based control structures
|
|
53
|
+
"POSTPONE": ("( -- )", "Compile semantics of next word (IMMEDIATE)"),
|
|
54
|
+
"BODY-HERE": ("( -- addr )", "Current compilation body position"),
|
|
55
|
+
"BODY,": ("( val -- )", "Append value to compilation body"),
|
|
56
|
+
"BODY!": ("( val addr -- )", "Store value at body position"),
|
|
57
|
+
"IMMEDIATE": ("( -- )", "Mark last defined word as immediate"),
|
|
58
|
+
"EXIT": ("( -- )", "Return from current word"),
|
|
59
|
+
"(DO)": ("( limit index -- )", "Runtime: push loop params to return stack"),
|
|
60
|
+
"(LOOP)": ("( -- flag )", "Runtime: increment index, push continue flag"),
|
|
61
|
+
"(+LOOP)": ("( step -- flag )", "Runtime: add step to index, push continue flag"),
|
|
62
|
+
"J": ("( -- index )", "Outer loop counter"),
|
|
63
|
+
"RECURSE": ("( -- )", "Call the word being defined"),
|
|
64
|
+
|
|
65
|
+
# Compiler
|
|
66
|
+
":": ("( -- )", "Begin word definition: : name ... ;"),
|
|
67
|
+
|
|
68
|
+
# System
|
|
69
|
+
"WORDS": ("( -- )", "List all defined words"),
|
|
70
|
+
"SEE": ("( -- )", "Show word definition: SEE name"),
|
|
71
|
+
".STACK": ("( -- )", "Display stack contents"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for name, (stack, description) in entries.items():
|
|
75
|
+
vm.set_catalog_entry(CatalogEntry(
|
|
76
|
+
name=name,
|
|
77
|
+
stack=stack,
|
|
78
|
+
description=description,
|
|
79
|
+
tags=["builtin"],
|
|
80
|
+
))
|
uc_forth/catalog.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Catalog methods for uc-forth VM — ported from Go catalog.go."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
from uc_forth.vm import VM, CatalogEntry, CatalogVersion
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _set_catalog_entry(self, entry: CatalogEntry) -> None:
|
|
12
|
+
"""Add or update a catalog entry with version tracking."""
|
|
13
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
14
|
+
key = entry.name.upper()
|
|
15
|
+
|
|
16
|
+
with self._mu:
|
|
17
|
+
existing = self.catalog.get(key)
|
|
18
|
+
if existing is not None:
|
|
19
|
+
# Save previous version to history
|
|
20
|
+
timestamp = existing.updated_at if existing.updated_at else existing.created
|
|
21
|
+
history_entry = CatalogVersion(
|
|
22
|
+
version=existing.version,
|
|
23
|
+
code=existing.code,
|
|
24
|
+
author=existing.author,
|
|
25
|
+
timestamp=timestamp,
|
|
26
|
+
)
|
|
27
|
+
entry.history = existing.history + [history_entry]
|
|
28
|
+
entry.version = existing.version + 1
|
|
29
|
+
# Preserve original created timestamp
|
|
30
|
+
if not entry.created:
|
|
31
|
+
entry.created = existing.created
|
|
32
|
+
else:
|
|
33
|
+
entry.version = 1
|
|
34
|
+
if not entry.created:
|
|
35
|
+
entry.created = now
|
|
36
|
+
|
|
37
|
+
entry.updated_at = now
|
|
38
|
+
self.catalog[key] = entry
|
|
39
|
+
|
|
40
|
+
self._auto_save()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_catalog_entry(self, name: str) -> CatalogEntry | None:
|
|
44
|
+
"""Retrieve a catalog entry."""
|
|
45
|
+
with self._mu:
|
|
46
|
+
return self.catalog.get(name.upper())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _search_by_tag(self, tag: str) -> list[CatalogEntry]:
|
|
50
|
+
"""Return all words matching a tag."""
|
|
51
|
+
with self._mu:
|
|
52
|
+
tag_lower = tag.lower()
|
|
53
|
+
results = []
|
|
54
|
+
for entry in self.catalog.values():
|
|
55
|
+
for entry_tag in entry.tags:
|
|
56
|
+
if entry_tag.lower() == tag_lower:
|
|
57
|
+
results.append(entry)
|
|
58
|
+
break
|
|
59
|
+
return results
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _search_by_name(self, substring: str) -> list[CatalogEntry]:
|
|
63
|
+
"""Return all entries where the name contains the substring (case-insensitive)."""
|
|
64
|
+
with self._mu:
|
|
65
|
+
sub_upper = substring.upper()
|
|
66
|
+
results = []
|
|
67
|
+
for entry in self.catalog.values():
|
|
68
|
+
if sub_upper in entry.name.upper():
|
|
69
|
+
results.append(entry)
|
|
70
|
+
return results
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _search_fuzzy(self, query: str) -> list[CatalogEntry]:
|
|
74
|
+
"""Search catalog by name, tags, and description. Scored by relevance."""
|
|
75
|
+
with self._mu:
|
|
76
|
+
query_lower = query.lower()
|
|
77
|
+
words = query_lower.split()
|
|
78
|
+
|
|
79
|
+
hits: list[tuple[CatalogEntry, int]] = []
|
|
80
|
+
|
|
81
|
+
for entry in self.catalog.values():
|
|
82
|
+
score = 0
|
|
83
|
+
name_lower = entry.name.lower()
|
|
84
|
+
desc_lower = entry.description.lower()
|
|
85
|
+
|
|
86
|
+
for word in words:
|
|
87
|
+
if name_lower == word:
|
|
88
|
+
score += 100
|
|
89
|
+
elif word in name_lower:
|
|
90
|
+
score += 50
|
|
91
|
+
|
|
92
|
+
for tag in entry.tags:
|
|
93
|
+
tag_lower = tag.lower()
|
|
94
|
+
if tag_lower == word:
|
|
95
|
+
score += 30
|
|
96
|
+
elif word in tag_lower:
|
|
97
|
+
score += 15
|
|
98
|
+
|
|
99
|
+
if word in desc_lower:
|
|
100
|
+
score += 10
|
|
101
|
+
|
|
102
|
+
if word in entry.stack.lower():
|
|
103
|
+
score += 5
|
|
104
|
+
|
|
105
|
+
if score > 0:
|
|
106
|
+
hits.append((entry, score))
|
|
107
|
+
|
|
108
|
+
hits.sort(key=lambda item: item[1], reverse=True)
|
|
109
|
+
return [entry for entry, _score in hits]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _all_catalog_entries(self) -> list[CatalogEntry]:
|
|
113
|
+
"""Return the full catalog."""
|
|
114
|
+
with self._mu:
|
|
115
|
+
return list(self.catalog.values())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _catalog_dump(self) -> bytes:
|
|
119
|
+
"""Export the catalog as JSON bytes."""
|
|
120
|
+
entries = self.all_catalog_entries()
|
|
121
|
+
data = {
|
|
122
|
+
"version": "1.0",
|
|
123
|
+
"exported": datetime.now(timezone.utc).isoformat(),
|
|
124
|
+
"word_count": len(entries),
|
|
125
|
+
"words": [_entry_to_dict(entry) for entry in entries],
|
|
126
|
+
}
|
|
127
|
+
return json.dumps(data, indent=2).encode("utf-8")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _catalog_load(self, data: bytes) -> None:
|
|
131
|
+
"""Import a catalog from JSON bytes."""
|
|
132
|
+
parsed = json.loads(data)
|
|
133
|
+
for word_data in parsed.get("words", []):
|
|
134
|
+
entry = _dict_to_entry(word_data)
|
|
135
|
+
self.set_catalog_entry(entry)
|
|
136
|
+
# Eval the code to define the word — but skip builtin words
|
|
137
|
+
# (those come from core.fth and modules, reloading them causes issues
|
|
138
|
+
# with IMMEDIATE words and compile-time stack manipulation)
|
|
139
|
+
if entry.code and entry.author != "forth":
|
|
140
|
+
self.eval(entry.code)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _catalog_save(self, filename: str) -> None:
|
|
144
|
+
"""Save the catalog to a file."""
|
|
145
|
+
data = self.catalog_dump()
|
|
146
|
+
with open(filename, "wb") as fh:
|
|
147
|
+
fh.write(data)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _catalog_load_file(self, filename: str) -> None:
|
|
151
|
+
"""Load a catalog from a file."""
|
|
152
|
+
with open(filename, "rb") as fh:
|
|
153
|
+
data = fh.read()
|
|
154
|
+
self.catalog_load(data)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _enable_auto_save(self, filename: str) -> None:
|
|
158
|
+
"""Activate automatic catalog persistence."""
|
|
159
|
+
directory = os.path.dirname(filename)
|
|
160
|
+
if directory:
|
|
161
|
+
try:
|
|
162
|
+
os.makedirs(directory, exist_ok=True)
|
|
163
|
+
except OSError as exc:
|
|
164
|
+
print(f"Warning: cannot create catalog directory {directory}: {exc}", file=sys.stderr)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
self.catalog_file = filename
|
|
168
|
+
self.auto_save_enabled = True
|
|
169
|
+
|
|
170
|
+
# Load existing catalog if the file exists
|
|
171
|
+
if os.path.isfile(filename):
|
|
172
|
+
try:
|
|
173
|
+
self.catalog_load_file(filename)
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
print(f"Warning: could not load catalog {filename}: {exc}", file=sys.stderr)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _auto_save(self) -> None:
|
|
179
|
+
"""Persist the catalog if auto-save is enabled."""
|
|
180
|
+
if not self.auto_save_enabled:
|
|
181
|
+
return
|
|
182
|
+
try:
|
|
183
|
+
self.catalog_save(self.catalog_file)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -- Serialization helpers --
|
|
189
|
+
|
|
190
|
+
def _entry_to_dict(entry: CatalogEntry) -> dict:
|
|
191
|
+
result = {
|
|
192
|
+
"name": entry.name,
|
|
193
|
+
"stack": entry.stack,
|
|
194
|
+
"description": entry.description,
|
|
195
|
+
"tags": entry.tags,
|
|
196
|
+
"version": entry.version,
|
|
197
|
+
}
|
|
198
|
+
if entry.code:
|
|
199
|
+
result["code"] = entry.code
|
|
200
|
+
if entry.depends:
|
|
201
|
+
result["depends"] = entry.depends
|
|
202
|
+
if entry.author:
|
|
203
|
+
result["author"] = entry.author
|
|
204
|
+
if entry.created:
|
|
205
|
+
result["created"] = entry.created
|
|
206
|
+
if entry.updated_at:
|
|
207
|
+
result["updated_at"] = entry.updated_at
|
|
208
|
+
if entry.test_in:
|
|
209
|
+
result["test_in"] = entry.test_in
|
|
210
|
+
if entry.test_out:
|
|
211
|
+
result["test_out"] = entry.test_out
|
|
212
|
+
if entry.history:
|
|
213
|
+
result["history"] = [
|
|
214
|
+
{
|
|
215
|
+
"version": hist.version,
|
|
216
|
+
"code": hist.code,
|
|
217
|
+
"author": hist.author,
|
|
218
|
+
"timestamp": hist.timestamp,
|
|
219
|
+
}
|
|
220
|
+
for hist in entry.history
|
|
221
|
+
]
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _dict_to_entry(data: dict) -> CatalogEntry:
|
|
226
|
+
history = []
|
|
227
|
+
for hist_data in data.get("history", []):
|
|
228
|
+
history.append(CatalogVersion(
|
|
229
|
+
version=hist_data.get("version", 0),
|
|
230
|
+
code=hist_data.get("code", ""),
|
|
231
|
+
author=hist_data.get("author", ""),
|
|
232
|
+
timestamp=hist_data.get("timestamp", ""),
|
|
233
|
+
))
|
|
234
|
+
|
|
235
|
+
return CatalogEntry(
|
|
236
|
+
name=data.get("name", ""),
|
|
237
|
+
code=data.get("code", ""),
|
|
238
|
+
stack=data.get("stack", ""),
|
|
239
|
+
description=data.get("description", ""),
|
|
240
|
+
tags=data.get("tags", []),
|
|
241
|
+
depends=data.get("depends", []),
|
|
242
|
+
author=data.get("author", ""),
|
|
243
|
+
created=data.get("created", ""),
|
|
244
|
+
test_in=data.get("test_in", ""),
|
|
245
|
+
test_out=data.get("test_out", ""),
|
|
246
|
+
version=data.get("version", 0),
|
|
247
|
+
updated_at=data.get("updated_at", ""),
|
|
248
|
+
history=history,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# -- Monkey-patch methods onto VM --
|
|
253
|
+
|
|
254
|
+
VM.set_catalog_entry = _set_catalog_entry
|
|
255
|
+
VM.get_catalog_entry = _get_catalog_entry
|
|
256
|
+
VM.search_by_tag = _search_by_tag
|
|
257
|
+
VM.search_by_name = _search_by_name
|
|
258
|
+
VM.search_fuzzy = _search_fuzzy
|
|
259
|
+
VM.all_catalog_entries = _all_catalog_entries
|
|
260
|
+
VM.catalog_dump = _catalog_dump
|
|
261
|
+
VM.catalog_load = _catalog_load
|
|
262
|
+
VM.catalog_save = _catalog_save
|
|
263
|
+
VM.catalog_load_file = _catalog_load_file
|
|
264
|
+
VM.enable_auto_save = _enable_auto_save
|
|
265
|
+
VM._auto_save = _auto_save
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Catalog introspection primitives -- ported from Go catalog_primitives.go."""
|
|
2
|
+
|
|
3
|
+
from uc_forth.vm import VM, CatalogEntry, Word, WordType
|
|
4
|
+
from uc_forth.primitives import prim
|
|
5
|
+
|
|
6
|
+
# Must import catalog to ensure methods are patched onto VM
|
|
7
|
+
import uc_forth.catalog # noqa: F401
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_catalog_entry(entry: CatalogEntry) -> str:
|
|
11
|
+
"""Format a catalog entry for display."""
|
|
12
|
+
parts = [f"Name: {entry.name}"]
|
|
13
|
+
|
|
14
|
+
word_type = "compiled" if entry.code else "primitive"
|
|
15
|
+
parts.append(f"Type: {word_type}")
|
|
16
|
+
|
|
17
|
+
if entry.stack:
|
|
18
|
+
parts.append(f"Stack: {entry.stack}")
|
|
19
|
+
if entry.description:
|
|
20
|
+
parts.append(f"Description: {entry.description}")
|
|
21
|
+
if entry.tags:
|
|
22
|
+
parts.append(f"Tags: {', '.join(entry.tags)}")
|
|
23
|
+
if entry.author:
|
|
24
|
+
parts.append(f"Author: {entry.author}")
|
|
25
|
+
|
|
26
|
+
return "\n".join(parts) + "\n"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_catalog_primitives(vm: VM) -> None:
|
|
30
|
+
"""Add catalog introspection words and their catalog entries to the VM."""
|
|
31
|
+
|
|
32
|
+
# CAN? ( search-str -- result-str )
|
|
33
|
+
def _can(vm: VM) -> None:
|
|
34
|
+
idx = vm.must_pop()
|
|
35
|
+
if vm.aborted:
|
|
36
|
+
return
|
|
37
|
+
search, ok = vm.get_string(idx)
|
|
38
|
+
if not ok:
|
|
39
|
+
vm.abort("invalid string reference")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
results = vm.search_fuzzy(search)
|
|
43
|
+
names = [entry.name for entry in results]
|
|
44
|
+
vm.push(vm.store_string("\n".join(names)))
|
|
45
|
+
|
|
46
|
+
prim(vm, "CAN?", _can)
|
|
47
|
+
|
|
48
|
+
# CAN-TAG? ( tag-str -- result-str )
|
|
49
|
+
def _can_tag(vm: VM) -> None:
|
|
50
|
+
idx = vm.must_pop()
|
|
51
|
+
if vm.aborted:
|
|
52
|
+
return
|
|
53
|
+
tag, ok = vm.get_string(idx)
|
|
54
|
+
if not ok:
|
|
55
|
+
vm.abort("invalid string reference")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
results = vm.search_by_tag(tag)
|
|
59
|
+
names = [entry.name for entry in results]
|
|
60
|
+
vm.push(vm.store_string("\n".join(names)))
|
|
61
|
+
|
|
62
|
+
prim(vm, "CAN-TAG?", _can_tag)
|
|
63
|
+
|
|
64
|
+
# ABOUT ( name-str -- )
|
|
65
|
+
def _about(vm: VM) -> None:
|
|
66
|
+
idx = vm.must_pop()
|
|
67
|
+
if vm.aborted:
|
|
68
|
+
return
|
|
69
|
+
name, ok = vm.get_string(idx)
|
|
70
|
+
if not ok:
|
|
71
|
+
vm.abort("invalid string reference")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
entry = vm.get_catalog_entry(name)
|
|
75
|
+
if entry is None:
|
|
76
|
+
vm.abort(f"no catalog entry for {name}")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
vm.write_string(format_catalog_entry(entry))
|
|
80
|
+
|
|
81
|
+
prim(vm, "ABOUT", _about)
|
|
82
|
+
|
|
83
|
+
# ABOUT>STR ( name-str -- info-str )
|
|
84
|
+
def _about_str(vm: VM) -> None:
|
|
85
|
+
idx = vm.must_pop()
|
|
86
|
+
if vm.aborted:
|
|
87
|
+
return
|
|
88
|
+
name, ok = vm.get_string(idx)
|
|
89
|
+
if not ok:
|
|
90
|
+
vm.abort("invalid string reference")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
entry = vm.get_catalog_entry(name)
|
|
94
|
+
if entry is None:
|
|
95
|
+
vm.abort(f"no catalog entry for {name}")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
vm.push(vm.store_string(format_catalog_entry(entry)))
|
|
99
|
+
|
|
100
|
+
prim(vm, "ABOUT>STR", _about_str)
|
|
101
|
+
|
|
102
|
+
# WORD-COUNT ( -- n )
|
|
103
|
+
def _word_count(vm: VM) -> None:
|
|
104
|
+
entries = vm.all_catalog_entries()
|
|
105
|
+
vm.push(len(entries))
|
|
106
|
+
|
|
107
|
+
prim(vm, "WORD-COUNT", _word_count)
|
|
108
|
+
|
|
109
|
+
# WORD-VERSION ( name-str -- n )
|
|
110
|
+
def _word_version(vm: VM) -> None:
|
|
111
|
+
idx = vm.must_pop()
|
|
112
|
+
if vm.aborted:
|
|
113
|
+
return
|
|
114
|
+
name, ok = vm.get_string(idx)
|
|
115
|
+
if not ok:
|
|
116
|
+
vm.abort("invalid string reference")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
entry = vm.get_catalog_entry(name)
|
|
120
|
+
if entry is None:
|
|
121
|
+
vm.abort(f"no catalog entry for {name}")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
vm.push(entry.version)
|
|
125
|
+
|
|
126
|
+
prim(vm, "WORD-VERSION", _word_version)
|
|
127
|
+
|
|
128
|
+
# Catalog entries
|
|
129
|
+
entries = {
|
|
130
|
+
"CAN?": ("( search-str -- result-str )", "Search catalog by tag or name substring, returns matching word names"),
|
|
131
|
+
"CAN-TAG?": ("( tag-str -- result-str )", "Search catalog by tag only"),
|
|
132
|
+
"ABOUT": ("( name-str -- )", "Print catalog info for a word"),
|
|
133
|
+
"ABOUT>STR": ("( name-str -- info-str )", "Return catalog info for a word as string"),
|
|
134
|
+
"WORD-COUNT": ("( -- n )", "Number of catalog entries"),
|
|
135
|
+
"WORD-VERSION": ("( name-str -- n )", "Get version number of a catalog entry"),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for name, (stack, description) in entries.items():
|
|
139
|
+
vm.set_catalog_entry(CatalogEntry(
|
|
140
|
+
name=name,
|
|
141
|
+
stack=stack,
|
|
142
|
+
description=description,
|
|
143
|
+
tags=["builtin", "catalog"],
|
|
144
|
+
))
|