sutra-dev 0.9.1__tar.gz → 0.9.2__tar.gz
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.
- {sutra_dev-0.9.1/sutra_dev.egg-info → sutra_dev-0.9.2}/PKG-INFO +4 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/pyproject.toml +11 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/__init__.py +1 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/__main__.py +38 -10
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen.py +33 -15
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen_base.py +11 -39
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen_pytorch.py +14 -10
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen_thrml.py +7 -5
- sutra_dev-0.9.2/sutra_compiler/embedding.py +142 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/lexer.py +10 -11
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/multi_process.py +1 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/parser.py +7 -7
- sutra_dev-0.9.2/sutra_compiler/repl.py +221 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/simplify.py +11 -26
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/axons.su +3 -3
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/bitwise.su +1 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/embed.su +3 -6
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/logic.su +2 -3
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/numbers.su +2 -2
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/rotation.su +2 -2
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/similarity.su +35 -15
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/vectors.su +0 -14
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib_loader.py +1 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/validator.py +50 -3
- {sutra_dev-0.9.1 → sutra_dev-0.9.2/sutra_dev.egg-info}/PKG-INFO +4 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/SOURCES.txt +6 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/requires.txt +4 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_keys.py +4 -4
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_project.py +3 -3
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_branchless_loop.py +1 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_cached_compile.py +3 -3
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_codegen.py +85 -85
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_codegen_pytorch.py +19 -18
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_dimension_audit.py +2 -2
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_dimension_audit_warning.py +1 -1
- sutra_dev-0.9.2/tests/test_embedding_provider.py +109 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_key_soundness.py +2 -2
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_termination.py +1 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_inliner.py +2 -2
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_list_ops.py +14 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_mini_wasm_machine.py +1 -1
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_multi_process_runtime.py +2 -2
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_ntm_ram.py +3 -3
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_optional_llm_model.py +2 -2
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_parser.py +5 -5
- sutra_dev-0.9.2/tests/test_repl.py +73 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_simplify.py +56 -56
- sutra_dev-0.9.2/tests/test_snap_diagnostic.py +143 -0
- sutra_dev-0.9.2/tests/test_terminal_string_decode.py +53 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_transcendentals.py +14 -63
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/LICENSE +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/README.md +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/setup.cfg +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/ast_nodes.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/axon_keys.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/cached_compile.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/diagnostics.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv_key_soundness.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv_obligation_checker.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv_poly_bound.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/inliner.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/loop_capture.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/loop_desugar.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/preeval.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/promise_desugar.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/review.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/simplify_egglog.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/bigint.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/javascript_object.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/math.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/memory.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/modulus.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/promises.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/strings.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/tensor.su +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/sutradb_embedded.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/tabulate.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/trace.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/workspace.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/dependency_links.txt +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/entry_points.txt +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/top_level.txt +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_arrow_functions.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_await_midfunction.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_await_substrate_pure.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_build.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_device_coherence.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_item_call_result.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_buffer_transcendentals.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_class_fields_runtime.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_codegen_thrml.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_corpus.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_equality_autograd.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fused_nn.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_general_checker.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_kleene_grid_exactness.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_poly_obligation_checker.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_public_api.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_worked_example.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_higher_order_functions.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_implicit_loop_desugar.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_int_dict.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_jvm_core.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_lexer.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_load_matrix.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_loop_capture.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_loop_function_decl.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_matrix_literal.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_measurement_claim_sweep.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_native_recursion.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_no_host_readout.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_preeval.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_process_pool_runtime.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_ram_direct_tensor.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_rotation_prewarm.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_simplify_egglog.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_simplify_egglog_cse.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_stdlib_loader.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_string_equality.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_substrate_leak_sweep.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_sutradb_embedded.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_tabulate.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_torch_compile_wrap.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_trainable_matrix.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_type_test_gap.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_vector_literal.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_wasm_core.py +0 -0
- {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sutra-dev
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Compiler for the Sutra programming language — tensor ops on frozen-LLM embedding vectors.
|
|
5
5
|
Author: Emma Leonhart
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -23,6 +23,9 @@ License-File: LICENSE
|
|
|
23
23
|
Requires-Dist: numpy>=1.26
|
|
24
24
|
Provides-Extra: runtime
|
|
25
25
|
Requires-Dist: torch>=2.1; extra == "runtime"
|
|
26
|
+
Provides-Extra: embed
|
|
27
|
+
Requires-Dist: sentence-transformers>=3.0; extra == "embed"
|
|
28
|
+
Requires-Dist: einops>=0.7; extra == "embed"
|
|
26
29
|
Provides-Extra: ts
|
|
27
30
|
Requires-Dist: sutra-from-ts>=0.1.0; extra == "ts"
|
|
28
31
|
Provides-Extra: fv
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sutra-dev"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
8
8
|
description = "Compiler for the Sutra programming language — tensor ops on frozen-LLM embedding vectors."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -44,6 +44,16 @@ dependencies = [
|
|
|
44
44
|
runtime = [
|
|
45
45
|
"torch>=2.1",
|
|
46
46
|
]
|
|
47
|
+
# In-process embedding backend. sentence-transformers loads the SAME frozen
|
|
48
|
+
# nomic-embed-text model HuggingFace serves, so semantic Sutra programs run with
|
|
49
|
+
# NO Ollama daemon — this is the default backend (sutra_compiler/embedding.py).
|
|
50
|
+
# einops is needed by nomic-bert's custom modeling code. Pure make_real / matrix
|
|
51
|
+
# / arithmetic programs embed nothing and need none of this.
|
|
52
|
+
# Install with: pip install sutra-dev[runtime,embed]
|
|
53
|
+
embed = [
|
|
54
|
+
"sentence-transformers>=3.0",
|
|
55
|
+
"einops>=0.7",
|
|
56
|
+
]
|
|
47
57
|
# TypeScript→Sutra transpiler. `pip install sutra-dev[ts]` pulls in
|
|
48
58
|
# the working ts2su CLI from the sister package, so the recommended
|
|
49
59
|
# way to get a complete Sutra dev environment for browser-layer work
|
|
@@ -28,7 +28,7 @@ The compiler is intentionally liberal where the spec is still open
|
|
|
28
28
|
forms and flags the clearly-forbidden ones.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
-
__version__ = "0.9.
|
|
31
|
+
__version__ = "0.9.2"
|
|
32
32
|
|
|
33
33
|
from .diagnostics import Diagnostic, DiagnosticLevel, DiagnosticBag
|
|
34
34
|
from .lexer import Lexer, Token, TokenKind
|
|
@@ -23,6 +23,7 @@ from typing import List
|
|
|
23
23
|
|
|
24
24
|
from . import __version__
|
|
25
25
|
from . import ast_nodes as ast
|
|
26
|
+
from .codegen_base import CodegenNotSupported
|
|
26
27
|
from .codegen_pytorch import translate_module as translate_pytorch
|
|
27
28
|
from .diagnostics import Diagnostic, DiagnosticLevel
|
|
28
29
|
from .lexer import Lexer
|
|
@@ -292,10 +293,20 @@ def _compile_to_python(path: str, *, runtime_dim: int,
|
|
|
292
293
|
tabulate_module(module)
|
|
293
294
|
if loop_T is None:
|
|
294
295
|
loop_T = _read_atman_loop_T(path) or 50
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
try:
|
|
297
|
+
return translate_pytorch(
|
|
298
|
+
module, runtime_dim=runtime_dim, runtime_seed=runtime_seed,
|
|
299
|
+
loop_max_iterations=loop_T,
|
|
300
|
+
)
|
|
301
|
+
except CodegenNotSupported as exc:
|
|
302
|
+
# The backend can't lower this construct (an unsupported builtin like
|
|
303
|
+
# `snap`, an unsupported node, etc.). `exc` already formats as
|
|
304
|
+
# `line:col: codegen: <message>`; prepend the file path so it reads
|
|
305
|
+
# like every other Sutra diagnostic (`file:line:col: …`) instead of
|
|
306
|
+
# surfacing to the user as an uncaught Python traceback. Single choke
|
|
307
|
+
# point: --run / --emit / runtime-viz all route through here.
|
|
308
|
+
print(f"{path}:{exc}", file=sys.stderr)
|
|
309
|
+
return None
|
|
299
310
|
|
|
300
311
|
|
|
301
312
|
def _run_execute(path: str, *, runtime_dim: int, runtime_seed: int,
|
|
@@ -323,17 +334,26 @@ def _run_execute(path: str, *, runtime_dim: int, runtime_seed: int,
|
|
|
323
334
|
|
|
324
335
|
|
|
325
336
|
def _decode_terminal_result(mod, result):
|
|
326
|
-
"""Terminal/output boundary: decode a
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
"""Terminal/output boundary: decode a `main()` result to a host value for
|
|
338
|
+
display. The language has no scalar-readout accessor (`real()` was removed —
|
|
339
|
+
substrate purity); the host reading the FINAL result for display is the one
|
|
340
|
+
external terminal boundary, the same as printing a returned value in any
|
|
341
|
+
runtime.
|
|
342
|
+
|
|
343
|
+
Both a String and a number-vector are 1-D tensors of length `_VSA.dim`; the
|
|
344
|
+
AXIS_STRING_FLAG tells them apart. The string check MUST come first: a String
|
|
345
|
+
stores its codepoints on synthetic axes and the real axis coincides with the
|
|
346
|
+
first codepoint, so a String read as a number prints that char's codepoint
|
|
347
|
+
(e.g. `return "hello world"` would print 104.0 = 'h'). A String decodes to its
|
|
348
|
+
Python text via the sanctioned `string_to_python` terminal decode; a
|
|
349
|
+
number-vector decodes to its real-axis value; anything else prints as-is."""
|
|
332
350
|
try:
|
|
333
351
|
import torch as _t
|
|
334
352
|
vsa = getattr(mod, "_VSA", None)
|
|
335
353
|
if (vsa is not None and isinstance(result, _t.Tensor)
|
|
336
354
|
and result.ndim == 1 and result.shape[0] == vsa.dim):
|
|
355
|
+
if vsa.is_string(result):
|
|
356
|
+
return vsa.string_to_python(result)
|
|
337
357
|
return float(result[vsa.semantic_dim + vsa.AXIS_REAL])
|
|
338
358
|
except Exception:
|
|
339
359
|
pass
|
|
@@ -483,6 +503,14 @@ def _emit_thrml(path: str) -> int:
|
|
|
483
503
|
|
|
484
504
|
|
|
485
505
|
def main(argv: List[str] | None = None) -> int:
|
|
506
|
+
# `sutrac repl` (or `--repl`) launches the interactive evaluator. Intercepted
|
|
507
|
+
# before argparse because the validator requires a `paths` positional and the
|
|
508
|
+
# REPL takes no file.
|
|
509
|
+
_args = argv if argv is not None else sys.argv[1:]
|
|
510
|
+
if _args and _args[0] in ("repl", "--repl"):
|
|
511
|
+
from .repl import main_repl
|
|
512
|
+
return main_repl()
|
|
513
|
+
|
|
486
514
|
parser = argparse.ArgumentParser(
|
|
487
515
|
prog="sutrac",
|
|
488
516
|
description="Validate Sutra (.su) source files.",
|
|
@@ -89,7 +89,7 @@ class Codegen(BaseCodegen):
|
|
|
89
89
|
self._semantic_dim = runtime_dim
|
|
90
90
|
self._synthetic_dim = (synthetic_dim if synthetic_dim is not None
|
|
91
91
|
else self.DEFAULT_SYNTHETIC_DIM)
|
|
92
|
-
# List of strings that appear in `
|
|
92
|
+
# List of strings that appear in `embed("...")` calls,
|
|
93
93
|
# populated by translate_module() between simplify and codegen.
|
|
94
94
|
# The codegen emits a batched Ollama pre-fetch at module init
|
|
95
95
|
# to replace N sequential HTTP round-trips with one call.
|
|
@@ -119,6 +119,18 @@ class Codegen(BaseCodegen):
|
|
|
119
119
|
"array_filter",
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
+
# Steering hints attached to the CodegenNotSupported error for
|
|
123
|
+
# spec'd-but-unimplemented builtins. `snap` is the cleanup primitive
|
|
124
|
+
# backed by an attractor circuit the substrate doesn't have yet;
|
|
125
|
+
# argmax_cosine against an explicit codebook is the implemented form
|
|
126
|
+
# the demos use today (tutorial 03). Keeps the rejection message from
|
|
127
|
+
# being a dead end.
|
|
128
|
+
_UNSUPPORTED_BUILTIN_HINTS = {
|
|
129
|
+
"snap": "use `argmax_cosine(query, [a, b, c])` to clean a vector up "
|
|
130
|
+
"against an explicit codebook; `snap` (attractor-circuit "
|
|
131
|
+
"cleanup) is a future substrate primitive",
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
def _char_literal_src(self, expr: ast.CharLiteral) -> str:
|
|
123
135
|
"""Lower `'a'` to a runtime make_char call with the code point."""
|
|
124
136
|
return f"_VSA.make_char({int(expr.value)})"
|
|
@@ -444,10 +456,12 @@ class Codegen(BaseCodegen):
|
|
|
444
456
|
callee = call.callee
|
|
445
457
|
if isinstance(callee, ast.Identifier):
|
|
446
458
|
if callee.name in self._UNSUPPORTED_BUILTINS:
|
|
459
|
+
hint = self._UNSUPPORTED_BUILTIN_HINTS.get(callee.name)
|
|
460
|
+
detail = f" — {hint}" if hint else ""
|
|
447
461
|
raise CodegenNotSupported(
|
|
448
462
|
call,
|
|
449
|
-
f"`{callee.name}` is not supported on the
|
|
450
|
-
f"
|
|
463
|
+
f"`{callee.name}` is not yet supported on the substrate "
|
|
464
|
+
f"(no runtime lowering on this backend){detail}",
|
|
451
465
|
)
|
|
452
466
|
if (isinstance(callee, ast.MemberAccess)
|
|
453
467
|
and callee.member in self._REMOVED_SCALAR_ACCESSORS):
|
|
@@ -512,7 +526,7 @@ class Codegen(BaseCodegen):
|
|
|
512
526
|
self._emit("# repeated bind/unbind with the same role O(d^2) lookup + matmul.")
|
|
513
527
|
self._emit("self._rot_cache = {}")
|
|
514
528
|
self._emit("# On-disk embedding cache. Second-and-later runs load every")
|
|
515
|
-
self._emit("# previously-seen
|
|
529
|
+
self._emit("# previously-seen embed(...) string from disk instead of")
|
|
516
530
|
self._emit("# hitting Ollama. Cache is keyed by (model, dim) so changing")
|
|
517
531
|
self._emit("# either invalidates cleanly (different cache file).")
|
|
518
532
|
self._emit("import os as _os")
|
|
@@ -524,9 +538,13 @@ class Codegen(BaseCodegen):
|
|
|
524
538
|
self._emit("_os.makedirs(self._cache_dir, exist_ok=True)")
|
|
525
539
|
self._emit("# Sanitize model name for use as filename.")
|
|
526
540
|
self._emit("_safe_model = llm_model.replace('/', '_').replace(':', '_')")
|
|
541
|
+
self._emit("# Backend-aware: in-process (transformers) and ollama realize the")
|
|
542
|
+
self._emit("# same model with slightly different geometry, so they must not")
|
|
543
|
+
self._emit("# share a cache file or one backend reads the other's vectors.")
|
|
544
|
+
self._emit("_emb_backend = _os.environ.get('SUTRA_EMBED_BACKEND', 'auto').strip().lower() or 'auto'")
|
|
527
545
|
self._emit("self._cache_path = _os.path.join(")
|
|
528
546
|
self._indent += 1
|
|
529
|
-
self._emit("self._cache_dir, f'{_safe_model}-d{self.dim}.npz')")
|
|
547
|
+
self._emit("self._cache_dir, f'{_safe_model}-d{self.dim}-{_emb_backend}.npz')")
|
|
530
548
|
self._indent -= 1
|
|
531
549
|
self._emit("self._load_disk_cache()")
|
|
532
550
|
self._emit("# Transcendental lookup tables — substrate-pure interpolation per")
|
|
@@ -641,9 +659,9 @@ class Codegen(BaseCodegen):
|
|
|
641
659
|
self._emit('starts at zero."""')
|
|
642
660
|
self._emit("if name not in self._codebook:")
|
|
643
661
|
self._indent += 1
|
|
644
|
-
self._emit("import
|
|
645
|
-
self._emit("r =
|
|
646
|
-
self._emit("v = _np.array(r[
|
|
662
|
+
self._emit("from sutra_compiler.embedding import embed_texts as _embed_texts")
|
|
663
|
+
self._emit("r = _embed_texts([name], self.llm_model)")
|
|
664
|
+
self._emit("v = _np.array(r[0], dtype=_np.float64)")
|
|
647
665
|
self._emit("# Mean-center. Raw LLM embeddings cluster in a cone (all-")
|
|
648
666
|
self._emit("# positive-ish); centering keeps rotation/bind algebra")
|
|
649
667
|
self._emit("# well-behaved.")
|
|
@@ -685,11 +703,11 @@ class Codegen(BaseCodegen):
|
|
|
685
703
|
self._indent += 1
|
|
686
704
|
self._emit("return")
|
|
687
705
|
self._indent -= 1
|
|
688
|
-
self._emit("import
|
|
689
|
-
self._emit("r =
|
|
706
|
+
self._emit("from sutra_compiler.embedding import embed_texts as _embed_texts")
|
|
707
|
+
self._emit("r = _embed_texts(missing, self.llm_model)")
|
|
690
708
|
self._emit("for i, name in enumerate(missing):")
|
|
691
709
|
self._indent += 1
|
|
692
|
-
self._emit("v = _np.array(r[
|
|
710
|
+
self._emit("v = _np.array(r[i], dtype=_np.float64)")
|
|
693
711
|
self._emit("v = v - _np.mean(v)")
|
|
694
712
|
self._emit("n = _np.linalg.norm(v)")
|
|
695
713
|
self._emit("if n > 0: v = v / n")
|
|
@@ -2146,7 +2164,7 @@ class Codegen(BaseCodegen):
|
|
|
2146
2164
|
f"seed={self.runtime_seed}, "
|
|
2147
2165
|
f"llm_model={self._llm_model!r})"
|
|
2148
2166
|
)
|
|
2149
|
-
# Batched pre-fetch of every
|
|
2167
|
+
# Batched pre-fetch of every embed("...") string argument
|
|
2150
2168
|
# the program uses. One Ollama round-trip instead of N sequential
|
|
2151
2169
|
# ones. Collected by the simplify pass (see translate_module).
|
|
2152
2170
|
if self._prefetch_strings:
|
|
@@ -2233,11 +2251,11 @@ def translate_module(module: ast.Module, **kwargs) -> str:
|
|
|
2233
2251
|
Runs the simplification pass over the AST before handing to the
|
|
2234
2252
|
codegen so identity rewrites (bundle(v) -> v, bundle flattening)
|
|
2235
2253
|
happen in source-to-source form rather than in the emitted
|
|
2236
|
-
Python. Also collects every `
|
|
2254
|
+
Python. Also collects every `embed("...")` string literal
|
|
2237
2255
|
so the codegen can emit a batched Ollama pre-fetch at module init
|
|
2238
2256
|
(N HTTP round-trips collapse into one batched embed call).
|
|
2239
2257
|
"""
|
|
2240
|
-
from .simplify import simplify_module,
|
|
2258
|
+
from .simplify import simplify_module, collect_embedded_strings
|
|
2241
2259
|
from .inliner import inline_stdlib_calls
|
|
2242
2260
|
from .promise_desugar import desugar_promises
|
|
2243
2261
|
from .loop_desugar import desugar_implicit_loops
|
|
@@ -2263,7 +2281,7 @@ def translate_module(module: ast.Module, **kwargs) -> str:
|
|
|
2263
2281
|
# absorption, which can fold parts of the inlined form.
|
|
2264
2282
|
inline_stdlib_calls(module)
|
|
2265
2283
|
simplify_module(module)
|
|
2266
|
-
strings =
|
|
2284
|
+
strings = collect_embedded_strings(module)
|
|
2267
2285
|
cg = Codegen(**kwargs)
|
|
2268
2286
|
cg._prefetch_strings = strings
|
|
2269
2287
|
cg._axon_keys_bound = bound_keys
|
|
@@ -95,10 +95,6 @@ class CodegenNotSupported(Exception):
|
|
|
95
95
|
# expression to emit. Keeping this as a single table means the list of
|
|
96
96
|
# supported builtins is easy to audit against `planning/sutra-spec/21-builtins.md`.
|
|
97
97
|
|
|
98
|
-
def _builtin_basis_vector(args: List[str]) -> str:
|
|
99
|
-
return f"_VSA.embed({args[0]})"
|
|
100
|
-
|
|
101
|
-
|
|
102
98
|
def _builtin_permutation_key(args: List[str]) -> str:
|
|
103
99
|
return f"_VSA.make_sign_flip_key({args[0]})"
|
|
104
100
|
|
|
@@ -245,27 +241,6 @@ def _builtin_realvec(args: List[str]) -> str:
|
|
|
245
241
|
return f"_VSA.realvec({args[0]})"
|
|
246
242
|
|
|
247
243
|
|
|
248
|
-
def _builtin_real_number(args: List[str]) -> str:
|
|
249
|
-
# Canonical-axis constructor: a scalar real number as an extended-
|
|
250
|
-
# state vector with x at synthetic[0], zeros elsewhere. Part of the
|
|
251
|
-
# int/float/complex shared-axis allocation — see project memory
|
|
252
|
-
# project_sutra_complex_numbers_first_class.md.
|
|
253
|
-
return f"_VSA.make_real({args[0]})"
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def _builtin_complex_number(args: List[str]) -> str:
|
|
257
|
-
# Canonical-axis constructor: a complex number with re at
|
|
258
|
-
# synthetic[0] and im at synthetic[1]. Sutra's first-class complex.
|
|
259
|
-
return f"_VSA.make_complex({args[0]}, {args[1]})"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def _builtin_truth_value(args: List[str]) -> str:
|
|
263
|
-
# Canonical-axis constructor: a scalar truth value at synthetic[2].
|
|
264
|
-
# Higher = more true; 0 = neither; negative = false-leaning. The
|
|
265
|
-
# axis is orthogonal to real/imag by construction.
|
|
266
|
-
return f"_VSA.make_truth({args[0]})"
|
|
267
|
-
|
|
268
|
-
|
|
269
244
|
def _builtin_ram_read(args: List[str]) -> str:
|
|
270
245
|
# RAM pointer read (planning/sutra-spec/ram-pointers.md). Bridges to
|
|
271
246
|
# the host-attached external memory device `_VSA.ram`: decode the
|
|
@@ -430,7 +405,8 @@ def _builtin_load_matrix(args: List[str]) -> str:
|
|
|
430
405
|
|
|
431
406
|
|
|
432
407
|
BUILTINS = {
|
|
433
|
-
|
|
408
|
+
# (`basis_vector` was removed 2026-06-23 — it was a pure alias for `embed`,
|
|
409
|
+
# which is the canonical spelling. See CLAUDE.md § "Deprecate aliases aggressively".)
|
|
434
410
|
"permutation_key": (_builtin_permutation_key, 1),
|
|
435
411
|
"identity_permutation": (_builtin_identity_permutation, 0),
|
|
436
412
|
"permute": (_builtin_permute, 2),
|
|
@@ -454,13 +430,10 @@ BUILTINS = {
|
|
|
454
430
|
"make_rotation": (_builtin_make_rotation, None), # 1-2 args
|
|
455
431
|
"compile_prototypes": (_builtin_compile_prototypes, 1),
|
|
456
432
|
"geometric_loop": (_builtin_geometric_loop, None), # 3-4 args
|
|
457
|
-
# Canonical-axis constructors
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
"real_number": (_builtin_real_number, 1),
|
|
462
|
-
"complex_number": (_builtin_complex_number, 2),
|
|
463
|
-
"truth_value": (_builtin_truth_value, 1),
|
|
433
|
+
# Canonical-axis constructors `make_real` / `make_complex` / `make_truth`
|
|
434
|
+
# resolve via the stdlib-intrinsic path (`_VSA.make_*`), not this table. The
|
|
435
|
+
# redundant `real_number` / `complex_number` / `truth_value` aliases were
|
|
436
|
+
# removed 2026-06-23 (CLAUDE.md § "Deprecate aliases aggressively").
|
|
464
437
|
# RAM pointers (planning/sutra-spec/ram-pointers.md): ramRead(ptr) ->
|
|
465
438
|
# _VSA.ram_read, ramWrite(ptr, data) -> _VSA.ram_write. Host attaches
|
|
466
439
|
# the external memory device as _VSA.ram. `await ramRead(ptr)` is the
|
|
@@ -1129,7 +1102,7 @@ class BaseCodegen:
|
|
|
1129
1102
|
# use a plain float zero as the placeholder; the numpy /
|
|
1130
1103
|
# pytorch backends' make_truth / make_complex paths are
|
|
1131
1104
|
# used by initialized declarations.
|
|
1132
|
-
if type_name in ("fuzzy", "bool", "int", "
|
|
1105
|
+
if type_name in ("fuzzy", "bool", "int", "number",
|
|
1133
1106
|
"trit", "complex"):
|
|
1134
1107
|
if decl.array_size is not None:
|
|
1135
1108
|
self._emit(f"{decl.name} = [0.0] * {decl.array_size}")
|
|
@@ -3025,10 +2998,9 @@ class BaseCodegen:
|
|
|
3025
2998
|
return None
|
|
3026
2999
|
|
|
3027
3000
|
_TRUTH_TYPES = frozenset({"bool", "fuzzy", "trit"})
|
|
3028
|
-
# `number` canonical
|
|
3029
|
-
# identically — see lexer.py PRIMITIVE_TYPE_NAMES).
|
|
3001
|
+
# `number` canonical (the `scalar` alias was removed 2026-06-23).
|
|
3030
3002
|
_NUMBER_TYPES = frozenset(
|
|
3031
|
-
{"int", "float", "complex", "number", "
|
|
3003
|
+
{"int", "float", "complex", "number", "char"}
|
|
3032
3004
|
)
|
|
3033
3005
|
# Synthetic-axis-encoded types: numbers and strings live in the
|
|
3034
3006
|
# synthetic block of the extended state vector. Per the user's
|
|
@@ -3045,7 +3017,7 @@ class BaseCodegen:
|
|
|
3045
3017
|
# The codegen already treats `("string","String","Character")` together
|
|
3046
3018
|
# for return-type handling; this set was the one place missing them.
|
|
3047
3019
|
_SYNTHETIC_AXIS_TYPES = frozenset(
|
|
3048
|
-
{"int", "float", "complex", "number",
|
|
3020
|
+
{"int", "float", "complex", "number",
|
|
3049
3021
|
"char", "Character", "string", "String"}
|
|
3050
3022
|
)
|
|
3051
3023
|
|
|
@@ -3697,7 +3669,7 @@ class BaseCodegen:
|
|
|
3697
3669
|
param_types = self._func_param_types.get(name)
|
|
3698
3670
|
# Axon role-key intrinsics: the `key` arg is a role NAME the
|
|
3699
3671
|
# runtime embeds into a basis vector (axons.su:
|
|
3700
|
-
# `axon_item(a,k) → unbind(
|
|
3672
|
+
# `axon_item(a,k) → unbind(embed(k), a)`), NOT string
|
|
3701
3673
|
# content. It must reach the runtime as a host str so
|
|
3702
3674
|
# axon_add / axon_item take their `isinstance(key, str)` →
|
|
3703
3675
|
# embed(key) branch — consistently with the member-access
|
|
@@ -338,9 +338,13 @@ class PyTorchCodegen(Codegen):
|
|
|
338
338
|
self._indent -= 1
|
|
339
339
|
self._emit("_os.makedirs(self._cache_dir, exist_ok=True)")
|
|
340
340
|
self._emit("_safe_model = llm_model.replace('/', '_').replace(':', '_')")
|
|
341
|
+
self._emit("# Backend-aware: in-process (transformers) and ollama realize the")
|
|
342
|
+
self._emit("# same model with slightly different geometry, so they must not")
|
|
343
|
+
self._emit("# share a cache file or one backend reads the other's vectors.")
|
|
344
|
+
self._emit("_emb_backend = _os.environ.get('SUTRA_EMBED_BACKEND', 'auto').strip().lower() or 'auto'")
|
|
341
345
|
self._emit("self._cache_path = _os.path.join(")
|
|
342
346
|
self._indent += 1
|
|
343
|
-
self._emit("self._cache_dir, f'{_safe_model}-d{self.dim}.pt')")
|
|
347
|
+
self._emit("self._cache_dir, f'{_safe_model}-d{self.dim}-{_emb_backend}.pt')")
|
|
344
348
|
self._indent -= 1
|
|
345
349
|
self._emit("self._load_disk_cache()")
|
|
346
350
|
self._emit("# Transcendental lookup codebooks — read by _lerp's crosstalk")
|
|
@@ -460,9 +464,9 @@ class PyTorchCodegen(Codegen):
|
|
|
460
464
|
"llm_model='nomic-embed-text' to embed semantic content; \""
|
|
461
465
|
" \"programs using only make_real / matrices / arithmetic "
|
|
462
466
|
"need no model.\")")
|
|
463
|
-
self._emit("import
|
|
464
|
-
self._emit("r =
|
|
465
|
-
self._emit("v = _torch.tensor(r[
|
|
467
|
+
self._emit("from sutra_compiler.embedding import embed_texts as _embed_texts")
|
|
468
|
+
self._emit("r = _embed_texts([name], self.llm_model)")
|
|
469
|
+
self._emit("v = _torch.tensor(r[0], dtype=self.dtype, device=self.device)")
|
|
466
470
|
self._emit("# Mean-center; raw LLM embeddings cluster in a cone and centering")
|
|
467
471
|
self._emit("# keeps rotation/bind algebra well-behaved.")
|
|
468
472
|
self._emit("v = v - _torch.mean(v)")
|
|
@@ -506,11 +510,11 @@ class PyTorchCodegen(Codegen):
|
|
|
506
510
|
" \"llm_model is 'none'. Pass llm_model='nomic-embed-text' "
|
|
507
511
|
"to embed semantic content; programs using only make_real / \""
|
|
508
512
|
" \"matrices / arithmetic need no model. Tried: \" + repr(missing))")
|
|
509
|
-
self._emit("import
|
|
510
|
-
self._emit("r =
|
|
513
|
+
self._emit("from sutra_compiler.embedding import embed_texts as _embed_texts")
|
|
514
|
+
self._emit("r = _embed_texts(missing, self.llm_model)")
|
|
511
515
|
self._emit("for i, name in enumerate(missing):")
|
|
512
516
|
self._indent += 1
|
|
513
|
-
self._emit("v = _torch.tensor(r[
|
|
517
|
+
self._emit("v = _torch.tensor(r[i], dtype=self.dtype, device=self.device)")
|
|
514
518
|
self._emit("v = v - _torch.mean(v)")
|
|
515
519
|
self._emit("n = _torch.linalg.norm(v)")
|
|
516
520
|
self._emit("if n > 0: v = v / n")
|
|
@@ -3778,7 +3782,7 @@ def translate_module(module: ast.Module, **kwargs) -> str:
|
|
|
3778
3782
|
the torch backend benefits from every algebraic rewrite and the
|
|
3779
3783
|
batched Ollama pre-fetch without duplicating that infrastructure.
|
|
3780
3784
|
"""
|
|
3781
|
-
from .simplify import simplify_module,
|
|
3785
|
+
from .simplify import simplify_module, collect_embedded_strings
|
|
3782
3786
|
from .inliner import inline_stdlib_calls
|
|
3783
3787
|
from .promise_desugar import desugar_promises
|
|
3784
3788
|
from .loop_desugar import desugar_implicit_loops
|
|
@@ -3799,7 +3803,7 @@ def translate_module(module: ast.Module, **kwargs) -> str:
|
|
|
3799
3803
|
# Inline stdlib calls — same pass as the CPU codegen uses.
|
|
3800
3804
|
inline_stdlib_calls(module)
|
|
3801
3805
|
simplify_module(module)
|
|
3802
|
-
strings =
|
|
3806
|
+
strings = collect_embedded_strings(module)
|
|
3803
3807
|
cg = PyTorchCodegen(**kwargs)
|
|
3804
3808
|
cg._prefetch_strings = strings
|
|
3805
3809
|
cg._axon_keys_bound = bound_keys
|
|
@@ -3808,7 +3812,7 @@ def translate_module(module: ast.Module, **kwargs) -> str:
|
|
|
3808
3812
|
# substrate audit 2026-06-19): a program that embeds no codebook strings
|
|
3809
3813
|
# (no basis_vector / embed) and binds no axon string-keys does not use the
|
|
3810
3814
|
# LLM semantic subspace at all, so an LLM-sized semantic_dim is paid for
|
|
3811
|
-
# nothing (matrices scale with dim^2).
|
|
3815
|
+
# nothing (matrices scale with dim^2). collect_embedded_strings covers
|
|
3812
3816
|
# basis_vector + embed; bound_keys/read_keys cover axon string-keys, so
|
|
3813
3817
|
# all-three-empty is a reliable "codebook unused" signal. Warn loudly but
|
|
3814
3818
|
# still compile (Sutra is opinionated, not authoritarian).
|
|
@@ -34,13 +34,15 @@ class ThrmlCodegenNotSupported(Exception):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _basis_atoms(module) -> dict:
|
|
37
|
-
"""Top-level `vector NAME =
|
|
37
|
+
"""Top-level `vector NAME = embed("...");` declarations → {name: index}.
|
|
38
|
+
|
|
39
|
+
`embed("...")` parses to an `EmbedExpr`.
|
|
40
|
+
"""
|
|
38
41
|
atoms: dict = {}
|
|
39
42
|
for item in module.items:
|
|
40
|
-
if (isinstance(item, ast.VarDecl) and item.initializer is not None
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
and item.initializer.callee.name == "basis_vector"):
|
|
43
|
+
if not (isinstance(item, ast.VarDecl) and item.initializer is not None):
|
|
44
|
+
continue
|
|
45
|
+
if isinstance(item.initializer, ast.EmbedExpr):
|
|
44
46
|
atoms[item.name] = len(atoms)
|
|
45
47
|
return atoms
|
|
46
48
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Host-side embedding provider — the frozen-LLM `embed()` boundary.
|
|
2
|
+
|
|
3
|
+
Sutra values live in a frozen LLM embedding space. Resolving a string to its
|
|
4
|
+
embedding is a **compile/IO-boundary** concern: it runs once at module init to
|
|
5
|
+
populate the runtime's `_codebook`, never on the substrate hot path. So this is
|
|
6
|
+
legitimately host code (numpy/torch tensor construction from a model's output),
|
|
7
|
+
not a substrate operation.
|
|
8
|
+
|
|
9
|
+
Why this module exists: until 2026-06-22 the only way to fetch an embedding was
|
|
10
|
+
the **Ollama daemon** — every emitted program did `import ollama; ollama.embed(...)`
|
|
11
|
+
with no fallback, so `pip install sutra-dev` could not run a single program until
|
|
12
|
+
the user also installed Ollama and pulled a model. nomic-embed-text is a *frozen*
|
|
13
|
+
off-the-shelf model that Sutra never customizes, so there is no reason to require
|
|
14
|
+
a separate server: we load the same model **in-process** (sentence-transformers /
|
|
15
|
+
HuggingFace) and embed directly. Ollama stays available as a backend for users who
|
|
16
|
+
already run it or who want its exact GGUF realization.
|
|
17
|
+
|
|
18
|
+
Backend selection (env var `SUTRA_EMBED_BACKEND`):
|
|
19
|
+
- "auto" (default) — try in-process transformers first; fall back to Ollama.
|
|
20
|
+
- "transformers" / "hf" — in-process only (raise if unavailable).
|
|
21
|
+
- "ollama" — Ollama daemon only (the pre-2026-06-22 behavior).
|
|
22
|
+
|
|
23
|
+
The two realizations are NOT numerically identical (Ollama runs a quantized GGUF;
|
|
24
|
+
mean cosine vs the in-process model ~0.88). They are two substrates for the same
|
|
25
|
+
model family. Per Sutra's design (a program's structure is substrate-independent;
|
|
26
|
+
the coordinates are not), a program should use ONE backend consistently — which is
|
|
27
|
+
what the per-process model cache below enforces.
|
|
28
|
+
|
|
29
|
+
`embed_texts` returns the **raw** model vectors. The codegen's `embed()` does the
|
|
30
|
+
Sutra post-processing (mean-center, L2-normalize, fit to the semantic block,
|
|
31
|
+
append the zero synthetic block) identically regardless of backend.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
from typing import List
|
|
37
|
+
|
|
38
|
+
# Short Sutra/Ollama model name -> HuggingFace repo id for the in-process load.
|
|
39
|
+
# These are the same frozen models, just loaded directly instead of via Ollama.
|
|
40
|
+
_HF_MODEL_IDS = {
|
|
41
|
+
"nomic-embed-text": "nomic-ai/nomic-embed-text-v1.5",
|
|
42
|
+
"all-minilm": "sentence-transformers/all-MiniLM-L6-v2",
|
|
43
|
+
"mxbai-embed-large": "mixedbread-ai/mxbai-embed-large-v1",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Per-process cache of loaded SentenceTransformer instances, keyed by HF id.
|
|
47
|
+
# Loading a model is seconds + hundreds of MB; reuse it across every program
|
|
48
|
+
# compiled in the same process (e.g. the smoke test compiles ~11 modules).
|
|
49
|
+
_ST_CACHE: dict = {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _hf_id(model: str) -> str:
|
|
53
|
+
"""Map a Sutra/Ollama model name to a HuggingFace repo id.
|
|
54
|
+
|
|
55
|
+
Unknown names pass through unchanged so a user can name any HF repo
|
|
56
|
+
directly in `// @embedding:` / atman.toml.
|
|
57
|
+
"""
|
|
58
|
+
return _HF_MODEL_IDS.get(model, model)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_st_model(model: str):
|
|
62
|
+
"""Lazily load + cache the in-process SentenceTransformer for `model`."""
|
|
63
|
+
hf_id = _hf_id(model)
|
|
64
|
+
cached = _ST_CACHE.get(hf_id)
|
|
65
|
+
if cached is not None:
|
|
66
|
+
return cached
|
|
67
|
+
# First load of this model in the process. The very first time on a machine
|
|
68
|
+
# this also downloads weights (hundreds of MB), which otherwise looks like a
|
|
69
|
+
# hang with no output. Announce it on stderr so it never corrupts --emit
|
|
70
|
+
# stdout. Suppressible with SUTRA_QUIET=1.
|
|
71
|
+
if os.environ.get("SUTRA_QUIET", "").strip() not in ("1", "true", "yes"):
|
|
72
|
+
import sys
|
|
73
|
+
|
|
74
|
+
print(
|
|
75
|
+
f"[sutra] loading embedding model '{hf_id}' in-process "
|
|
76
|
+
f"(first run downloads it, ~hundreds of MB; cached afterward). "
|
|
77
|
+
f"Set SUTRA_EMBED_BACKEND=ollama to use a daemon instead, or "
|
|
78
|
+
f"SUTRA_QUIET=1 to silence this.",
|
|
79
|
+
file=sys.stderr,
|
|
80
|
+
flush=True,
|
|
81
|
+
)
|
|
82
|
+
from sentence_transformers import SentenceTransformer
|
|
83
|
+
|
|
84
|
+
# nomic-bert ships custom modeling code, hence trust_remote_code.
|
|
85
|
+
st = SentenceTransformer(hf_id, trust_remote_code=True)
|
|
86
|
+
_ST_CACHE[hf_id] = st
|
|
87
|
+
return st
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _embed_transformers(names: List[str], model: str) -> List[List[float]]:
|
|
91
|
+
"""In-process embedding via sentence-transformers / HuggingFace."""
|
|
92
|
+
st = _get_st_model(model)
|
|
93
|
+
# No task prefix: Sutra embeds bare concepts, and no-prefix is the closest
|
|
94
|
+
# match to Ollama's behavior. `normalize_embeddings=False` -> raw vectors;
|
|
95
|
+
# the codegen's embed() applies Sutra's mean-center + normalize afterward.
|
|
96
|
+
vecs = st.encode(list(names), normalize_embeddings=False)
|
|
97
|
+
return [list(map(float, v)) for v in vecs]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _embed_ollama(names: List[str], model: str) -> List[List[float]]:
|
|
101
|
+
"""Embedding via the Ollama daemon (the pre-2026-06-22 path)."""
|
|
102
|
+
import ollama
|
|
103
|
+
|
|
104
|
+
r = ollama.embed(model=model, input=list(names))
|
|
105
|
+
return [list(map(float, e)) for e in r["embeddings"]]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def embed_texts(names: List[str], model: str) -> List[List[float]]:
|
|
109
|
+
"""Return raw frozen-LLM embeddings for `names` under `model`.
|
|
110
|
+
|
|
111
|
+
Backend chosen by `SUTRA_EMBED_BACKEND` (auto | transformers/hf | ollama).
|
|
112
|
+
"auto" prefers the in-process model and falls back to Ollama, so a plain
|
|
113
|
+
pip install works with neither a daemon nor a manual choice.
|
|
114
|
+
"""
|
|
115
|
+
if not names:
|
|
116
|
+
return []
|
|
117
|
+
backend = os.environ.get("SUTRA_EMBED_BACKEND", "auto").strip().lower()
|
|
118
|
+
|
|
119
|
+
if backend in ("transformers", "hf", "huggingface", "sentence-transformers"):
|
|
120
|
+
return _embed_transformers(names, model)
|
|
121
|
+
if backend == "ollama":
|
|
122
|
+
return _embed_ollama(names, model)
|
|
123
|
+
|
|
124
|
+
# auto: in-process first, Ollama as fallback.
|
|
125
|
+
try:
|
|
126
|
+
return _embed_transformers(names, model)
|
|
127
|
+
except Exception as in_proc_err:
|
|
128
|
+
try:
|
|
129
|
+
return _embed_ollama(names, model)
|
|
130
|
+
except Exception as ollama_err:
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
"Sutra could not obtain embeddings for "
|
|
133
|
+
f"{names[:3]}{'...' if len(names) > 3 else ''} under model "
|
|
134
|
+
f"{model!r}. In-process load failed ({type(in_proc_err).__name__}: "
|
|
135
|
+
f"{in_proc_err}); Ollama fallback failed ({type(ollama_err).__name__}: "
|
|
136
|
+
f"{ollama_err}). Install the in-process stack (`pip install "
|
|
137
|
+
"sentence-transformers`) or run an Ollama daemon with the model "
|
|
138
|
+
"pulled. Set SUTRA_EMBED_BACKEND to force one backend."
|
|
139
|
+
) from ollama_err
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
__all__ = ["embed_texts"]
|
|
@@ -243,10 +243,10 @@ KEYWORDS = {
|
|
|
243
243
|
"false": TokenKind.FALSE,
|
|
244
244
|
# `unknown` — the neutral point on the truth axis (0.0 between
|
|
245
245
|
# true and false). The first-class three-valued value, and a
|
|
246
|
-
# readability win over `trit t = 0`. `unk`
|
|
247
|
-
#
|
|
246
|
+
# readability win over `trit t = 0`. (The `unk` short-alias was
|
|
247
|
+
# removed 2026-06-23 — one canonical spelling, CLAUDE.md § "Deprecate
|
|
248
|
+
# aliases aggressively".)
|
|
248
249
|
"unknown": TokenKind.KW_UNKNOWN,
|
|
249
|
-
"unk": TokenKind.KW_UNKNOWN,
|
|
250
250
|
# `wait` — explicit deferred-initializer marker. Only legal in a
|
|
251
251
|
# var-decl initializer position (`int i = wait;`). Tells the
|
|
252
252
|
# compiler "I'm declaring this name now, an assignment will
|
|
@@ -276,14 +276,13 @@ KEYWORDS = {
|
|
|
276
276
|
# casing-drift checks, and so that the spec treats it as a primitive
|
|
277
277
|
# container alongside `tuple`.
|
|
278
278
|
PRIMITIVE_TYPE_NAMES = {
|
|
279
|
-
# `number` is the canonical name for a value on the number axis
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
279
|
+
# `number` is the canonical name for a value on the number axis (real /
|
|
280
|
+
# imaginary components of a d-dim vector). The `scalar` alias was REMOVED
|
|
281
|
+
# 2026-06-23 (Emma — the paper + replication package are no longer frozen,
|
|
282
|
+
# so the backward-compat reason is void; one canonical spelling, CLAUDE.md
|
|
283
|
+
# § "Deprecate aliases aggressively"). A `scalar` (0-d tensor) was never
|
|
284
|
+
# what a Sutra number is, so the name misled.
|
|
285
285
|
"number",
|
|
286
|
-
"scalar",
|
|
287
286
|
"vector",
|
|
288
287
|
"matrix",
|
|
289
288
|
"tuple",
|
|
@@ -329,7 +328,7 @@ _LOGIC_KEYWORD_NAMES = {
|
|
|
329
328
|
"nand": "nand", # binary
|
|
330
329
|
"xor": "xor", # binary
|
|
331
330
|
"xnor": "xnor", # binary
|
|
332
|
-
|
|
331
|
+
# (the `iff` alias for xnor was removed 2026-06-23 — one canonical spelling.)
|
|
333
332
|
}
|
|
334
333
|
|
|
335
334
|
# Contextual keywords: identifiers with special meaning in expressions
|