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.
Files changed (130) hide show
  1. {sutra_dev-0.9.1/sutra_dev.egg-info → sutra_dev-0.9.2}/PKG-INFO +4 -1
  2. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/pyproject.toml +11 -1
  3. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/__init__.py +1 -1
  4. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/__main__.py +38 -10
  5. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen.py +33 -15
  6. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen_base.py +11 -39
  7. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen_pytorch.py +14 -10
  8. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/codegen_thrml.py +7 -5
  9. sutra_dev-0.9.2/sutra_compiler/embedding.py +142 -0
  10. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/lexer.py +10 -11
  11. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/multi_process.py +1 -1
  12. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/parser.py +7 -7
  13. sutra_dev-0.9.2/sutra_compiler/repl.py +221 -0
  14. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/simplify.py +11 -26
  15. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/axons.su +3 -3
  16. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/bitwise.su +1 -1
  17. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/embed.su +3 -6
  18. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/logic.su +2 -3
  19. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/numbers.su +2 -2
  20. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/rotation.su +2 -2
  21. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/similarity.su +35 -15
  22. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/vectors.su +0 -14
  23. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib_loader.py +1 -1
  24. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/validator.py +50 -3
  25. {sutra_dev-0.9.1 → sutra_dev-0.9.2/sutra_dev.egg-info}/PKG-INFO +4 -1
  26. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/SOURCES.txt +6 -0
  27. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/requires.txt +4 -0
  28. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_keys.py +4 -4
  29. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_project.py +3 -3
  30. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_branchless_loop.py +1 -1
  31. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_cached_compile.py +3 -3
  32. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_codegen.py +85 -85
  33. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_codegen_pytorch.py +19 -18
  34. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_dimension_audit.py +2 -2
  35. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_dimension_audit_warning.py +1 -1
  36. sutra_dev-0.9.2/tests/test_embedding_provider.py +109 -0
  37. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_key_soundness.py +2 -2
  38. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_termination.py +1 -1
  39. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_inliner.py +2 -2
  40. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_list_ops.py +14 -0
  41. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_mini_wasm_machine.py +1 -1
  42. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_multi_process_runtime.py +2 -2
  43. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_ntm_ram.py +3 -3
  44. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_optional_llm_model.py +2 -2
  45. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_parser.py +5 -5
  46. sutra_dev-0.9.2/tests/test_repl.py +73 -0
  47. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_simplify.py +56 -56
  48. sutra_dev-0.9.2/tests/test_snap_diagnostic.py +143 -0
  49. sutra_dev-0.9.2/tests/test_terminal_string_decode.py +53 -0
  50. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_transcendentals.py +14 -63
  51. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/LICENSE +0 -0
  52. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/README.md +0 -0
  53. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/setup.cfg +0 -0
  54. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/ast_nodes.py +0 -0
  55. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/axon_keys.py +0 -0
  56. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/cached_compile.py +0 -0
  57. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/diagnostics.py +0 -0
  58. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv.py +0 -0
  59. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv_key_soundness.py +0 -0
  60. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv_obligation_checker.py +0 -0
  61. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/fv_poly_bound.py +0 -0
  62. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/inliner.py +0 -0
  63. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/loop_capture.py +0 -0
  64. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/loop_desugar.py +0 -0
  65. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/preeval.py +0 -0
  66. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/promise_desugar.py +0 -0
  67. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/review.py +0 -0
  68. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/simplify_egglog.py +0 -0
  69. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/bigint.su +0 -0
  70. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/javascript_object.su +0 -0
  71. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
  72. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/math.su +0 -0
  73. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/memory.su +0 -0
  74. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/modulus.su +0 -0
  75. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/promises.su +0 -0
  76. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/strings.su +0 -0
  77. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/stdlib/tensor.su +0 -0
  78. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/sutradb_embedded.py +0 -0
  79. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/tabulate.py +0 -0
  80. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/trace.py +0 -0
  81. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_compiler/workspace.py +0 -0
  82. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/dependency_links.txt +0 -0
  83. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/entry_points.txt +0 -0
  84. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/sutra_dev.egg-info/top_level.txt +0 -0
  85. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_arrow_functions.py +0 -0
  86. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_await_midfunction.py +0 -0
  87. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_await_substrate_pure.py +0 -0
  88. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_build.py +0 -0
  89. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_device_coherence.py +0 -0
  90. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_axon_item_call_result.py +0 -0
  91. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_buffer_transcendentals.py +0 -0
  92. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_class_fields_runtime.py +0 -0
  93. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_codegen_thrml.py +0 -0
  94. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_corpus.py +0 -0
  95. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_equality_autograd.py +0 -0
  96. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fused_nn.py +0 -0
  97. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_general_checker.py +0 -0
  98. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_kleene_grid_exactness.py +0 -0
  99. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_poly_obligation_checker.py +0 -0
  100. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_public_api.py +0 -0
  101. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_fv_worked_example.py +0 -0
  102. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_higher_order_functions.py +0 -0
  103. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_implicit_loop_desugar.py +0 -0
  104. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_int_dict.py +0 -0
  105. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_jvm_core.py +0 -0
  106. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_lexer.py +0 -0
  107. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_load_matrix.py +0 -0
  108. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_loop_capture.py +0 -0
  109. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_loop_function_decl.py +0 -0
  110. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_matrix_literal.py +0 -0
  111. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_measurement_claim_sweep.py +0 -0
  112. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_native_recursion.py +0 -0
  113. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_no_host_readout.py +0 -0
  114. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_preeval.py +0 -0
  115. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_process_pool_runtime.py +0 -0
  116. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_ram_direct_tensor.py +0 -0
  117. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_rotation_prewarm.py +0 -0
  118. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_simplify_egglog.py +0 -0
  119. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_simplify_egglog_cse.py +0 -0
  120. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_stdlib_loader.py +0 -0
  121. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_string_equality.py +0 -0
  122. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_substrate_leak_sweep.py +0 -0
  123. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_sutradb_embedded.py +0 -0
  124. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_tabulate.py +0 -0
  125. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_torch_compile_wrap.py +0 -0
  126. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_trainable_matrix.py +0 -0
  127. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_type_test_gap.py +0 -0
  128. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_vector_literal.py +0 -0
  129. {sutra_dev-0.9.1 → sutra_dev-0.9.2}/tests/test_wasm_core.py +0 -0
  130. {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.1
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.1"
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.1"
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
- return translate_pytorch(
296
- module, runtime_dim=runtime_dim, runtime_seed=runtime_seed,
297
- loop_max_iterations=loop_T,
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 number-vector `main()` result to its
327
- real-axis value for display. The language itself has no scalar-readout
328
- accessor (`real()` was removed — substrate purity); the host reading the
329
- FINAL result for display is the one external terminal boundary, the same as
330
- printing a returned value in any runtime. A number-vector is a 1-D tensor of
331
- length `_VSA.dim`; anything else (string, int, already-decoded) prints as-is."""
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 `basis_vector("...")` calls,
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 pure-numpy "
450
- f"substrate (no cleanup circuit at runtime)",
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 basis_vector(...) string from disk instead of")
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 ollama")
645
- self._emit("r = ollama.embed(model=self.llm_model, input=name)")
646
- self._emit("v = _np.array(r['embeddings'][0], dtype=_np.float64)")
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 ollama")
689
- self._emit("r = ollama.embed(model=self.llm_model, input=missing)")
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['embeddings'][i], dtype=_np.float64)")
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 basis_vector("...") string argument
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 `basis_vector("...")` string literal
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, collect_basis_vector_strings
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 = collect_basis_vector_strings(module)
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
- "basis_vector": (_builtin_basis_vector, 1),
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. Lower to _VSA.make_real / make_complex /
458
- # make_truth runtime methods provided by the _VSA runtime class.
459
- # A backend that doesn't implement them will fail at runtime with a
460
- # clear AttributeError.
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", "scalar", "number",
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; `scalar` deprecated alias (both classify
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", "scalar", "char"}
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", "scalar",
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(basis_vector(k), a)`), NOT string
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 ollama")
464
- self._emit("r = ollama.embed(model=self.llm_model, input=name)")
465
- self._emit("v = _torch.tensor(r['embeddings'][0], dtype=self.dtype, device=self.device)")
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 ollama")
510
- self._emit("r = ollama.embed(model=self.llm_model, input=missing)")
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['embeddings'][i], dtype=self.dtype, device=self.device)")
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, collect_basis_vector_strings
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 = collect_basis_vector_strings(module)
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). collect_basis_vector_strings covers
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 = basis_vector("...");` declarations → {name: index}."""
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
- and isinstance(item.initializer, ast.Call)
42
- and isinstance(item.initializer.callee, ast.Identifier)
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` is a short alias
247
- # that gets the same token both forms are fine to write.
246
+ # readability win over `trit t = 0`. (The `unk` short-alias was
247
+ # removed 2026-06-23one 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
- # (real / imaginary components of a d-dim vector). `scalar` is a
281
- # DEPRECATED ALIAS kept for backward compatibility with existing
282
- # `scalar` programs a `scalar` is a 0-d tensor, which is
283
- # conceptually NOT what a Sutra number is, so the name misleads.
284
- # New code uses `number`.
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
- "iff": "xnor", # binary, alias for xnor
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