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 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,3 @@
1
+ """Allow running with `python -m uc_forth`."""
2
+ from .cli import main
3
+ main()
@@ -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
+ ))