sutra-dev 0.9.2__tar.gz → 0.9.3__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 (152) hide show
  1. {sutra_dev-0.9.2/sutra_dev.egg-info → sutra_dev-0.9.3}/PKG-INFO +1 -1
  2. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/pyproject.toml +1 -1
  3. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/__init__.py +1 -1
  4. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/__main__.py +35 -9
  5. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/codegen.py +1 -1
  6. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/codegen_base.py +243 -0
  7. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/codegen_pytorch.py +107 -9
  8. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/embedding.py +10 -2
  9. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/fv.py +24 -0
  10. sutra_dev-0.9.3/sutra_compiler/fv_loop_convergence.py +177 -0
  11. sutra_dev-0.9.3/sutra_compiler/fv_sampler_convergence.py +199 -0
  12. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/parser.py +31 -1
  13. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/repl.py +87 -5
  14. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/strings.su +6 -0
  15. sutra_dev-0.9.3/sutra_compiler/symbol_table.py +550 -0
  16. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/validator.py +250 -1
  17. {sutra_dev-0.9.2 → sutra_dev-0.9.3/sutra_dev.egg-info}/PKG-INFO +1 -1
  18. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_dev.egg-info/SOURCES.txt +21 -1
  19. sutra_dev-0.9.3/tests/test_arity_diagnostic.py +82 -0
  20. sutra_dev-0.9.3/tests/test_cast_codegen.py +183 -0
  21. sutra_dev-0.9.3/tests/test_fv_loop_convergence.py +164 -0
  22. sutra_dev-0.9.3/tests/test_fv_sampler_convergence.py +121 -0
  23. sutra_dev-0.9.3/tests/test_int_to_string.py +84 -0
  24. sutra_dev-0.9.3/tests/test_interp_string_codegen.py +86 -0
  25. sutra_dev-0.9.3/tests/test_make_string_idempotent.py +62 -0
  26. sutra_dev-0.9.3/tests/test_ntm_ram.py +492 -0
  27. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_parser.py +25 -0
  28. sutra_dev-0.9.3/tests/test_python_builtin_escape.py +66 -0
  29. sutra_dev-0.9.3/tests/test_repl.py +116 -0
  30. sutra_dev-0.9.3/tests/test_run_error_diagnostics.py +53 -0
  31. sutra_dev-0.9.3/tests/test_string_plus_concat.py +84 -0
  32. sutra_dev-0.9.3/tests/test_symbol_table.py +250 -0
  33. sutra_dev-0.9.3/tests/test_type_inference.py +105 -0
  34. sutra_dev-0.9.3/tests/test_unknown_function_diagnostic.py +100 -0
  35. sutra_dev-0.9.3/tests/test_unknown_type_diagnostic.py +89 -0
  36. sutra_dev-0.9.3/tests/test_unknown_variable_diagnostic.py +114 -0
  37. sutra_dev-0.9.3/tests/test_unsafe_override_codegen.py +42 -0
  38. sutra_dev-0.9.3/tests/test_wrong_arg_type_diagnostic.py +81 -0
  39. sutra_dev-0.9.2/tests/test_ntm_ram.py +0 -263
  40. sutra_dev-0.9.2/tests/test_repl.py +0 -73
  41. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/LICENSE +0 -0
  42. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/README.md +0 -0
  43. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/setup.cfg +0 -0
  44. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/ast_nodes.py +0 -0
  45. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/axon_keys.py +0 -0
  46. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/cached_compile.py +0 -0
  47. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/codegen_thrml.py +0 -0
  48. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/diagnostics.py +0 -0
  49. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/fv_key_soundness.py +0 -0
  50. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/fv_obligation_checker.py +0 -0
  51. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/fv_poly_bound.py +0 -0
  52. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/inliner.py +0 -0
  53. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/lexer.py +0 -0
  54. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/loop_capture.py +0 -0
  55. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/loop_desugar.py +0 -0
  56. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/multi_process.py +0 -0
  57. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/preeval.py +0 -0
  58. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/promise_desugar.py +0 -0
  59. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/review.py +0 -0
  60. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/simplify.py +0 -0
  61. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/simplify_egglog.py +0 -0
  62. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/axons.su +0 -0
  63. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/bigint.su +0 -0
  64. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/bitwise.su +0 -0
  65. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/embed.su +0 -0
  66. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/javascript_object.su +0 -0
  67. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
  68. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/logic.su +0 -0
  69. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/math.su +0 -0
  70. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/memory.su +0 -0
  71. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/modulus.su +0 -0
  72. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/numbers.su +0 -0
  73. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/promises.su +0 -0
  74. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/rotation.su +0 -0
  75. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/similarity.su +0 -0
  76. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/tensor.su +0 -0
  77. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib/vectors.su +0 -0
  78. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/stdlib_loader.py +0 -0
  79. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/sutradb_embedded.py +0 -0
  80. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/tabulate.py +0 -0
  81. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/trace.py +0 -0
  82. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_compiler/workspace.py +0 -0
  83. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_dev.egg-info/dependency_links.txt +0 -0
  84. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_dev.egg-info/entry_points.txt +0 -0
  85. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_dev.egg-info/requires.txt +0 -0
  86. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/sutra_dev.egg-info/top_level.txt +0 -0
  87. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_arrow_functions.py +0 -0
  88. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_await_midfunction.py +0 -0
  89. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_await_substrate_pure.py +0 -0
  90. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_axon_build.py +0 -0
  91. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_axon_device_coherence.py +0 -0
  92. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_axon_item_call_result.py +0 -0
  93. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_axon_keys.py +0 -0
  94. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_axon_project.py +0 -0
  95. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_branchless_loop.py +0 -0
  96. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_buffer_transcendentals.py +0 -0
  97. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_cached_compile.py +0 -0
  98. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_class_fields_runtime.py +0 -0
  99. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_codegen.py +0 -0
  100. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_codegen_pytorch.py +0 -0
  101. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_codegen_thrml.py +0 -0
  102. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_corpus.py +0 -0
  103. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_dimension_audit.py +0 -0
  104. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_dimension_audit_warning.py +0 -0
  105. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_embedding_provider.py +0 -0
  106. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_equality_autograd.py +0 -0
  107. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fused_nn.py +0 -0
  108. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fv_general_checker.py +0 -0
  109. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fv_key_soundness.py +0 -0
  110. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fv_kleene_grid_exactness.py +0 -0
  111. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fv_poly_obligation_checker.py +0 -0
  112. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fv_public_api.py +0 -0
  113. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fv_termination.py +0 -0
  114. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_fv_worked_example.py +0 -0
  115. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_higher_order_functions.py +0 -0
  116. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_implicit_loop_desugar.py +0 -0
  117. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_inliner.py +0 -0
  118. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_int_dict.py +0 -0
  119. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_jvm_core.py +0 -0
  120. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_lexer.py +0 -0
  121. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_list_ops.py +0 -0
  122. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_load_matrix.py +0 -0
  123. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_loop_capture.py +0 -0
  124. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_loop_function_decl.py +0 -0
  125. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_matrix_literal.py +0 -0
  126. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_measurement_claim_sweep.py +0 -0
  127. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_mini_wasm_machine.py +0 -0
  128. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_multi_process_runtime.py +0 -0
  129. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_native_recursion.py +0 -0
  130. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_no_host_readout.py +0 -0
  131. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_optional_llm_model.py +0 -0
  132. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_preeval.py +0 -0
  133. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_process_pool_runtime.py +0 -0
  134. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_ram_direct_tensor.py +0 -0
  135. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_rotation_prewarm.py +0 -0
  136. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_simplify.py +0 -0
  137. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_simplify_egglog.py +0 -0
  138. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_simplify_egglog_cse.py +0 -0
  139. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_snap_diagnostic.py +0 -0
  140. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_stdlib_loader.py +0 -0
  141. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_string_equality.py +0 -0
  142. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_substrate_leak_sweep.py +0 -0
  143. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_sutradb_embedded.py +0 -0
  144. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_tabulate.py +0 -0
  145. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_terminal_string_decode.py +0 -0
  146. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_torch_compile_wrap.py +0 -0
  147. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_trainable_matrix.py +0 -0
  148. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_transcendentals.py +0 -0
  149. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_type_test_gap.py +0 -0
  150. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_vector_literal.py +0 -0
  151. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/tests/test_wasm_core.py +0 -0
  152. {sutra_dev-0.9.2 → sutra_dev-0.9.3}/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.2
3
+ Version: 0.9.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sutra-dev"
7
- version = "0.9.2"
7
+ version = "0.9.3"
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"
@@ -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.2"
31
+ __version__ = "0.9.3"
32
32
 
33
33
  from .diagnostics import Diagnostic, DiagnosticLevel, DiagnosticBag
34
34
  from .lexer import Lexer, Token, TokenKind
@@ -325,11 +325,24 @@ def _run_execute(path: str, *, runtime_dim: int, runtime_seed: int,
325
325
  return 1
326
326
  mod = types.ModuleType("_sutra_run")
327
327
  mod.__file__ = f"<generated from {path}>"
328
- exec(compile(py_src, mod.__file__, "exec"), mod.__dict__)
329
- if hasattr(mod, "main") and callable(mod.main):
330
- result = mod.main()
331
- if result is not None:
332
- print(_decode_terminal_result(mod, result))
328
+ try:
329
+ exec(compile(py_src, mod.__file__, "exec"), mod.__dict__)
330
+ if hasattr(mod, "main") and callable(mod.main):
331
+ result = mod.main()
332
+ if result is not None:
333
+ print(_decode_terminal_result(mod, result))
334
+ else:
335
+ # The substrate program is the body of main(); a file without one
336
+ # has nothing to run. Say so rather than exiting silently with no
337
+ # output (a newcomer otherwise can't tell why nothing happened).
338
+ print(f"{path}: no main() found - nothing to run", file=sys.stderr)
339
+ except Exception as exc:
340
+ # A runtime error in the generated module — e.g. a type mismatch the v0.1
341
+ # validator can't catch yet — should read like a Sutra diagnostic, not an
342
+ # uncaught Python traceback. KeyboardInterrupt / SystemExit are not caught
343
+ # (they subclass BaseException, not Exception), so Ctrl-C still works.
344
+ print(f"{path}: runtime error: {type(exc).__name__}: {exc}", file=sys.stderr)
345
+ return 1
333
346
  return 0
334
347
 
335
348
 
@@ -387,7 +400,7 @@ def _run_viz(path: str, *, runtime_dim: int, runtime_seed: int,
387
400
  # Inject tracing shim: after the _VSA = _TorchVSA(...) line,
388
401
  # wrap every method with a tracing version.
389
402
  shim = '''
390
- # ── Tracing shim (injected by --run-viz) ──
403
+ # -- Tracing shim (injected by --run-viz) --
391
404
  _orig_embed = _VSA.embed
392
405
  _orig_bind = _VSA.bind
393
406
  _orig_unbind = _VSA.unbind
@@ -417,7 +430,7 @@ _VSA.embed = _traced_embed
417
430
  _VSA.bind = _traced_bind
418
431
  _VSA.unbind = _traced_unbind
419
432
  _VSA.bundle = _traced_bundle
420
- # ── End tracing shim ──
433
+ # -- End tracing shim --
421
434
  '''
422
435
  # Find the _VSA = _TorchVSA(...) line and inject after it
423
436
  lines = py_src.split('\n')
@@ -514,6 +527,19 @@ def main(argv: List[str] | None = None) -> int:
514
527
  parser = argparse.ArgumentParser(
515
528
  prog="sutrac",
516
529
  description="Validate Sutra (.su) source files.",
530
+ epilog=(
531
+ "interactive REPL:\n"
532
+ " sutrac repl Launch the interactive evaluator (takes no\n"
533
+ " file). Type an expression to see its value;\n"
534
+ " declarations accumulate as session state.\n"
535
+ "\n"
536
+ "examples:\n"
537
+ " sutrac file.su Validate a source file.\n"
538
+ " sutrac --run file.su Compile and execute (needs torch).\n"
539
+ " sutrac --emit file.su Print the generated PyTorch module.\n"
540
+ " sutrac repl Explore interactively."
541
+ ),
542
+ formatter_class=argparse.RawDescriptionHelpFormatter,
517
543
  )
518
544
  parser.add_argument(
519
545
  "paths",
@@ -541,7 +567,7 @@ def main(argv: List[str] | None = None) -> int:
541
567
  help=(
542
568
  "Compile the first input file to self-contained torch Python and "
543
569
  "print it to stdout. Picks CUDA at module init if available; "
544
- "falls back to CPU otherwise. This is the one main codegen target "
570
+ "falls back to CPU otherwise. This is the one main codegen target - "
545
571
  "PyTorch is the runtime and the tensor-op library Sutra compiles "
546
572
  "against."
547
573
  ),
@@ -564,7 +590,7 @@ def main(argv: List[str] | None = None) -> int:
564
590
  "stdout. The default PyTorch path (--emit/--run) is untouched; this is "
565
591
  "the Extropic thermodynamic-sampling compile target (queue.md approach "
566
592
  "G). Requires jax + the thrml submodule. Lowering coverage is the "
567
- "validated subset only see codegen_thrml.py."
593
+ "validated subset only - see codegen_thrml.py."
568
594
  ),
569
595
  )
570
596
  parser.add_argument(
@@ -1700,7 +1700,7 @@ class Codegen(BaseCodegen):
1700
1700
  self._emit("distinguish `'a'` (97 with flag) from `97` (97 without).")
1701
1701
  self._emit("Arithmetic on chars works the same as on ints — both")
1702
1702
  self._emit("live on the number axis. Downstream code that cares")
1703
- self._emit("about the distinction can read the flag via `is_char`.")
1703
+ self._emit("about the distinction can read the flag via `is_string`.")
1704
1704
  self._emit('"""')
1705
1705
  self._emit("v = _np.zeros(self.dim, dtype=_np.float64)")
1706
1706
  self._emit("v[self.semantic_dim + self.AXIS_REAL] = float(codepoint)")
@@ -3021,6 +3021,30 @@ class BaseCodegen:
3021
3021
  "char", "Character", "string", "String"}
3022
3022
  )
3023
3023
 
3024
+ #: Types whose values are substrate Strings (text family).
3025
+ _TEXT_TYPES = frozenset({"string", "String", "char", "Character"})
3026
+
3027
+ def _is_text_expr(self, expr: ast.Expr) -> bool:
3028
+ """True iff expr is provably a text (String) value at compile
3029
+ time. Conservative — unknown falls through to the numeric /
3030
+ element-wise `+` paths, same posture as _is_number_expr."""
3031
+ if isinstance(expr, (ast.StringLiteral, ast.CharLiteral,
3032
+ ast.InterpolatedString)):
3033
+ return True
3034
+ if isinstance(expr, ast.Identifier):
3035
+ return self._var_type.get(expr.name) in self._TEXT_TYPES
3036
+ if isinstance(expr, ast.Parenthesized):
3037
+ return self._is_text_expr(expr.inner)
3038
+ if isinstance(expr, (ast.CastExpr, ast.UnsafeCastExpr)):
3039
+ return (expr.target_type is not None
3040
+ and expr.target_type.name in self._TEXT_TYPES)
3041
+ if isinstance(expr, ast.BinaryOp) and expr.op == "+":
3042
+ return (self._is_text_expr(expr.left)
3043
+ and self._is_text_expr(expr.right))
3044
+ if isinstance(expr, ast.Call):
3045
+ return self._resolved_return_type(expr.callee) in self._TEXT_TYPES
3046
+ return False
3047
+
3024
3048
  def _is_synthetic_axis_expr(self, expr: ast.Expr) -> bool:
3025
3049
  """True iff expr is provably a synthetic-axis-encoded value at
3026
3050
  compile time — int, float, complex, scalar, char, or string.
@@ -3380,6 +3404,21 @@ class BaseCodegen:
3380
3404
  )
3381
3405
  if user_op_name is not None:
3382
3406
  return f"{user_op_name}({left}, {right})"
3407
+ # Text concatenation: `+` with BOTH operands provably text
3408
+ # dispatches to the substrate string_concat (round-17 audit
3409
+ # fix). Without this, lowercase-`string`-typed operands fell
3410
+ # through to ELEMENT-WISE VECTOR ADD — 'ab' + 'cd' silently
3411
+ # summed codepoint axes into garbage — and two string
3412
+ # literals crashed at runtime (host strs reached
3413
+ # string_concat). The String CLASS `operator +` still wins
3414
+ # above for class-typed operands. Literals re-translate with
3415
+ # dest_type="string" so they cross as substrate Strings.
3416
+ if (expr.op == "+" and self.supports_string_runtime
3417
+ and self._is_text_expr(expr.left)
3418
+ and self._is_text_expr(expr.right)):
3419
+ lt = self._translate_expr(expr.left, dest_type="string")
3420
+ rt = self._translate_expr(expr.right, dest_type="string")
3421
+ return f"_VSA.string_concat({lt}, {rt})"
3383
3422
  # Logical operators dispatch through the substrate so they
3384
3423
  # work uniformly on bool / fuzzy / trit / truth-axis-vector
3385
3424
  # inputs. Zadeh fuzzy logic — min for AND, max for OR — on
@@ -3589,10 +3628,214 @@ class BaseCodegen:
3589
3628
  return self._embed_expr_src(expr)
3590
3629
  if isinstance(expr, ast.DefuzzyExpr):
3591
3630
  return self._defuzzy_expr_src(expr)
3631
+ if isinstance(expr, ast.InterpolatedString):
3632
+ # `$"hello {s}!"` desugars to a make_string / string_concat
3633
+ # chain — every piece a substrate String, every join a
3634
+ # substrate op (strings.md). Only STRING-TYPED interpolants
3635
+ # are lowerable today: a number/fuzzy interpolant needs the
3636
+ # substrate number→string formatter, which is not built, so
3637
+ # those reject with a steer (same wall as the text casts).
3638
+ if not self.supports_string_runtime:
3639
+ raise CodegenNotSupported(
3640
+ expr,
3641
+ "interpolated strings need the substrate String "
3642
+ "runtime; use the PyTorch backend (codegen_pytorch)."
3643
+ )
3644
+ pieces: list[str] = []
3645
+ for part in expr.parts:
3646
+ if isinstance(part, str):
3647
+ if part:
3648
+ pieces.append(f"_VSA.make_string({part!r})")
3649
+ continue
3650
+ part_t = self._infer_cast_operand_type(part)
3651
+ if self._CAST_FAMILY.get(part_t) == "text":
3652
+ pieces.append(
3653
+ self._translate_expr(part, dest_type="string"))
3654
+ continue
3655
+ if part_t == "int":
3656
+ # strings.md § Integer formatting: the substrate
3657
+ # int→string formatter. int ONLY — a fractional
3658
+ # `number` rendering as its rounded integer would
3659
+ # silently lie, so it keeps rejecting below.
3660
+ pieces.append(
3661
+ f"_VSA.int_to_string("
3662
+ f"{self._translate_expr(part)})")
3663
+ continue
3664
+ if part_t is None:
3665
+ raise CodegenNotSupported(
3666
+ part,
3667
+ "interpolated expression must be string- or "
3668
+ "int-typed and statically inferable — assign it "
3669
+ "to a `string` (or `int`) variable first, or "
3670
+ "build the text with make_string / string_concat."
3671
+ )
3672
+ raise CodegenNotSupported(
3673
+ part,
3674
+ f"cannot interpolate a `{part_t}` value: only string- "
3675
+ "and int-typed interpolants lower (int_to_string is "
3676
+ "integer-shaped; fractional rendering is not built). "
3677
+ "For an integer value, declare it `int`; otherwise "
3678
+ "build the text with make_string / string_concat."
3679
+ )
3680
+ if not pieces:
3681
+ return "_VSA.make_string('')"
3682
+ src = pieces[0]
3683
+ for nxt in pieces[1:]:
3684
+ src = f"_VSA.string_concat({src}, {nxt})"
3685
+ return src
3686
+ if isinstance(expr, ast.UnsafeCastExpr):
3687
+ # types.md § "Casting — relabeling, not transformation":
3688
+ # unsafeCast<T>(x) is ALWAYS the pure relabel — the value
3689
+ # crosses unchanged and only the static type changes. It
3690
+ # never axis-moves; `(fuzzy) n` converts, `unsafeCast<fuzzy>(n)`
3691
+ # reinterprets (truth reading of a real-axis value is 0).
3692
+ return self._translate_expr(expr.expr)
3693
+ if isinstance(expr, ast.UnsafeOverrideExpr):
3694
+ # `unsafeOverride(v)` suppresses a type-check at the call
3695
+ # site and does not change the value — the runtime lowering
3696
+ # is the pure passthrough (same class as unsafeCast, minus
3697
+ # the type argument). Round-19 audit: was the last special
3698
+ # call form with no codegen.
3699
+ return self._translate_expr(expr.expr)
3700
+ if isinstance(expr, ast.CastExpr):
3701
+ return self._translate_cast(expr)
3592
3702
  raise CodegenNotSupported(
3593
3703
  expr, f"unsupported expression: {type(expr).__name__}"
3594
3704
  )
3595
3705
 
3706
+ # ---- cast lowering (types.md § "Casting — relabeling, not
3707
+ # transformation") -------------------------------------------------
3708
+ #
3709
+ # The default cast is a NO-OP relabel: every Sutra value is already a
3710
+ # d-dim substrate vector (or a 0-d number at the entry boundary), and
3711
+ # downstream ops project the axes they need, so relabeling is free.
3712
+ # The ONE genuine axis-move pair is numeric↔truth: a number carries
3713
+ # its value on AXIS_REAL, a fuzzy/bool/trit on AXIS_TRUTH — a pure
3714
+ # relabel would strand the value on the wrong axis and every read
3715
+ # would see 0 (neutral). Text is walled off: number→string needs the
3716
+ # unbuilt substrate formatter; string→vector is the spec's "embedding
3717
+ # cast" whose real implementation is `embed()` at the entry boundary.
3718
+
3719
+ #: Sutra type name → cast family. Types not in the map (user classes,
3720
+ #: generics) relabel as "geometric" — the spec's free no-op default.
3721
+ _CAST_FAMILY = {
3722
+ "int": "number", "number": "number",
3723
+ "fuzzy": "truth", "bool": "truth", "trit": "truth",
3724
+ "complex": "geometric", "vector": "geometric", "role": "geometric",
3725
+ "matrix": "geometric",
3726
+ "string": "text", "String": "text",
3727
+ "char": "text", "Character": "text",
3728
+ }
3729
+
3730
+ #: Whether this backend's runtime carries the numeric↔truth cast
3731
+ #: helpers (cast_number_to_truth / cast_truth_to_number / _cnum).
3732
+ #: The deprecated numpy backend does not; it keeps rejecting casts.
3733
+ supports_cast_lowering = False
3734
+
3735
+ #: Whether this backend's runtime carries the substrate String ops
3736
+ #: (make_string / string_concat). Gates InterpolatedString lowering;
3737
+ #: the deprecated numpy backend has no String runtime.
3738
+ supports_string_runtime = False
3739
+
3740
+ def _infer_cast_operand_type(self, expr: ast.Expr) -> str | None:
3741
+ """Conservative static type of a cast operand: literals, typed
3742
+ locals/params (via _var_type), nested casts, parenthesized
3743
+ exprs. None when unknown — the caller decides whether the
3744
+ target makes the relabel/axis-move distinction load-bearing."""
3745
+ from .symbol_table import _LITERAL_TYPES
3746
+ kind = type(expr).__name__
3747
+ if kind in _LITERAL_TYPES:
3748
+ return _LITERAL_TYPES[kind]
3749
+ if isinstance(expr, ast.Identifier):
3750
+ return self._var_type.get(expr.name)
3751
+ if isinstance(expr, (ast.CastExpr, ast.UnsafeCastExpr)):
3752
+ return expr.target_type.name if expr.target_type else None
3753
+ if type(expr).__name__ == "Parenthesized" and hasattr(expr, "inner"):
3754
+ return self._infer_cast_operand_type(expr.inner)
3755
+ if isinstance(expr, ast.BinaryOp):
3756
+ if expr.op in ("==", "!=", "<", ">", "<=", ">=", "&&", "||"):
3757
+ return "bool"
3758
+ if expr.op in ("+", "-", "*", "/", "%"):
3759
+ lt = self._infer_cast_operand_type(expr.left)
3760
+ rt = self._infer_cast_operand_type(expr.right)
3761
+ if (self._CAST_FAMILY.get(lt) == "number"
3762
+ and self._CAST_FAMILY.get(rt) == "number"):
3763
+ return "number"
3764
+ return None
3765
+
3766
+ def _translate_cast(self, expr: ast.CastExpr) -> str:
3767
+ if not self.supports_cast_lowering:
3768
+ raise CodegenNotSupported(
3769
+ expr,
3770
+ "`(Type) expr` casts are not supported by this backend; "
3771
+ "use the PyTorch backend (codegen_pytorch)."
3772
+ )
3773
+ dst = expr.target_type.name if expr.target_type else None
3774
+ dst_family = self._CAST_FAMILY.get(dst, "geometric")
3775
+ src = self._infer_cast_operand_type(expr.expr)
3776
+ src_family = self._CAST_FAMILY.get(src, "geometric") if src else None
3777
+
3778
+ # --- text walls -------------------------------------------------
3779
+ if dst_family == "text" and src_family != "text":
3780
+ if src == "int":
3781
+ # strings.md § Integer formatting: `(string) n` on an
3782
+ # int is the integer formatter — the one number→text
3783
+ # cast with a real substrate implementation.
3784
+ return (f"_VSA.int_to_string("
3785
+ f"{self._translate_expr(expr.expr)})")
3786
+ raise CodegenNotSupported(
3787
+ expr,
3788
+ f"cannot cast to `{dst}`: only `int` values format "
3789
+ "(int_to_string — integer-shaped; fractional rendering is "
3790
+ "not built). For a literal, use `make_string(\"...\")`; "
3791
+ "join strings with `string_concat`."
3792
+ )
3793
+ if src_family == "text" and dst_family != "text":
3794
+ if dst_family == "geometric":
3795
+ raise CodegenNotSupported(
3796
+ expr,
3797
+ f"cannot cast a string to `{dst}`: that is the embedding "
3798
+ "cast, and its implementation is `embed(...)` at the "
3799
+ "program boundary — a String vector encodes codepoints, "
3800
+ "not semantics."
3801
+ )
3802
+ raise CodegenNotSupported(
3803
+ expr,
3804
+ f"cannot cast a string to `{dst}`: a String vector encodes "
3805
+ "packed codepoints, so its numeric axes are not a number. "
3806
+ "Use the string operations (string_length, char_at, ...) "
3807
+ "instead."
3808
+ )
3809
+
3810
+ inner = self._translate_expr(expr.expr)
3811
+
3812
+ # --- same family / text→text: the free relabel --------------------
3813
+ if src_family == dst_family:
3814
+ return inner
3815
+
3816
+ # --- the one genuine axis-move pair -------------------------------
3817
+ if src_family == "number" and dst_family == "truth":
3818
+ return f"_VSA.cast_number_to_truth({inner})"
3819
+ if src_family == "truth" and dst_family == "number":
3820
+ return f"_VSA.cast_truth_to_number({inner})"
3821
+
3822
+ # --- unknown source: only reject when the move/relabel choice
3823
+ # --- is load-bearing (truth/number targets read a single axis) ----
3824
+ if src_family is None and dst_family in ("truth", "number"):
3825
+ raise CodegenNotSupported(
3826
+ expr,
3827
+ f"cannot cast to `{dst}` here: the operand's static type "
3828
+ "can't be inferred, and truth/number casts need it to pick "
3829
+ "between relabel and axis-move. Assign the operand to a "
3830
+ "typed variable first."
3831
+ )
3832
+
3833
+ # --- every remaining pair relabels; _cnum canonicalizes a 0-d
3834
+ # --- number at the entry boundary and passes vectors through ------
3835
+ if src_family == "number" or src is None:
3836
+ return f"_VSA._cnum({inner})"
3837
+ return inner
3838
+
3596
3839
  def _embed_expr_src(self, expr: ast.EmbedExpr) -> str:
3597
3840
  """Override point for per-backend `embed(<expr>)` lowering.
3598
3841
 
@@ -49,6 +49,15 @@ class PyTorchCodegen(Codegen):
49
49
  _as_truth_vector.
50
50
  """
51
51
 
52
+ # The torch runtime carries the numeric↔truth cast helpers
53
+ # (cast_number_to_truth / cast_truth_to_number / _cnum), so `(Type)`
54
+ # cast lowering is live on this backend (types.md § Casting).
55
+ supports_cast_lowering = True
56
+
57
+ # The torch runtime carries the substrate String ops (make_string /
58
+ # string_concat), so InterpolatedString lowering is live here.
59
+ supports_string_runtime = True
60
+
52
61
  # The numpy `Codegen` lists the immutable list ops (array_concat /
53
62
  # array_map / array_filter) as unsupported — it has no runtime methods
54
63
  # for them. The PyTorch runtime DOES implement them (Emma 2026-06-20),
@@ -2772,14 +2781,8 @@ class PyTorchCodegen(Codegen):
2772
2781
  self._emit("return self.make_string(chr(int(codepoint)))")
2773
2782
  self._indent -= 1
2774
2783
  self._emit()
2775
- self._emit("def is_char(self, v):")
2776
- self._indent += 1
2777
- self._emit('"""True iff v is a String value (kept as `is_char` for')
2778
- self._emit('backward-compat with code that pre-dated the rename to')
2779
- self._emit('AXIS_STRING_FLAG; new code should use is_string)."""')
2780
- self._emit("return bool(v[self.semantic_dim + self.AXIS_STRING_FLAG].item() >= 0.5)")
2781
- self._indent -= 1
2782
- self._emit()
2784
+ # `is_char` (a pure alias of is_string) retired 2026-07-08 —
2785
+ # zero call sites; CLAUDE.md § "Deprecate aliases aggressively".
2783
2786
  self._emit("# ---- String runtime methods ----")
2784
2787
  self._emit("# Encoding: AXIS_STRING_FLAG marks the vector as a String.")
2785
2788
  self._emit("# Characters pack into the synthetic axes — char[0] at")
@@ -2828,7 +2831,16 @@ class PyTorchCodegen(Codegen):
2828
2831
  self._emit()
2829
2832
  self._emit("def make_string(self, s):")
2830
2833
  self._indent += 1
2831
- self._emit('"""Construct a String value from a Python str."""')
2834
+ self._emit('"""Construct a String value from a Python str. IDEMPOTENT on an')
2835
+ self._emit('already-String vector: the type-coercion pass can wrap a String-typed')
2836
+ self._emit("initializer in make_string even when it is already a String (e.g.")
2837
+ self._emit('`String g = make_string(\"hi\")`), so a double make_string must return')
2838
+ self._emit('the value unchanged, not `str(tensor)`-encode its repr as text."""')
2839
+ self._emit("if _torch.is_tensor(s) and s.ndim >= 1 and "
2840
+ "s.shape[-1] == self.dim and bool(self.is_string(s)):")
2841
+ self._indent += 1
2842
+ self._emit("return s")
2843
+ self._indent -= 1
2832
2844
  self._emit("if not isinstance(s, str):")
2833
2845
  self._indent += 1
2834
2846
  self._emit("s = str(s)")
@@ -3257,6 +3269,57 @@ class PyTorchCodegen(Codegen):
3257
3269
  self._emit("return v")
3258
3270
  self._indent -= 1
3259
3271
  self._emit()
3272
+ self._emit("def _i2s_pows(self):")
3273
+ self._indent += 1
3274
+ self._emit('"""Cached power-of-10 table for int_to_string digit')
3275
+ self._emit('extraction. Length = the dtype exactness bound (7 digits')
3276
+ self._emit('float32, 15 float64) — beyond it the input integer is')
3277
+ self._emit('already inexact in the dtype."""')
3278
+ self._emit("if not hasattr(self, '_i2s_pows_cache') or self._i2s_pows_cache is None:")
3279
+ self._indent += 1
3280
+ self._emit("D = 15 if self.dtype == _torch.float64 else 7")
3281
+ self._emit("self._i2s_pows_cache = 10.0 ** _torch.arange("
3282
+ "D, dtype=self.dtype, device=self.device)")
3283
+ self._indent -= 1
3284
+ self._emit("return self._i2s_pows_cache")
3285
+ self._indent -= 1
3286
+ self._emit()
3287
+ self._emit("def int_to_string(self, x):")
3288
+ self._indent += 1
3289
+ self._emit('"""Render an integer NUMBER as a substrate String — the')
3290
+ self._emit("number->string formatter (strings.md § Integer formatting).")
3291
+ self._emit("Digit extraction is MOD-FREE (Math.mod is banned; measured")
3292
+ self._emit("vector-collapse): digit_k = floor(a/10^k) - 10*floor(a/10^(k+1)).")
3293
+ self._emit("Leading zeros gate on the quotient-significance mask (0 renders")
3294
+ self._emit("'0'); negatives gate codepoint 45 into slot 0 and shift digits")
3295
+ self._emit("right by one — a gather by shifted index, the same VSA-native")
3296
+ self._emit("permutation string_concat uses. round() first: the INT contract.")
3297
+ self._emit("Exact within the dtype bound (7 digits float32 / 15 float64);")
3298
+ self._emit('beyond it output is valid-but-unspecified (input already inexact)."""')
3299
+ self._emit("a0 = self._scalar(x)")
3300
+ self._emit("neg = (a0 < 0).to(self.dtype)")
3301
+ self._emit("a = _torch.round(_torch.abs(a0))")
3302
+ self._emit("pows = self._i2s_pows()")
3303
+ self._emit("D = pows.shape[0]")
3304
+ self._emit("q = _torch.floor(a / pows)")
3305
+ self._emit("qn = _torch.floor(a / (pows * 10.0))")
3306
+ self._emit("digits = q - 10.0 * qn")
3307
+ self._emit("nd = _torch.clamp((q > 0).to(self.dtype).sum(), min=1.0)")
3308
+ self._emit("ax = self._str_axes()")
3309
+ self._emit("n = ax.shape[0]")
3310
+ self._emit("idx = _torch.arange(n, dtype=self.dtype, device=self.device)")
3311
+ self._emit("place = nd - 1.0 - (idx - neg)")
3312
+ self._emit("valid = ((place >= 0) & (place < D) & (idx - neg >= 0))"
3313
+ ".to(self.dtype)")
3314
+ self._emit("gath = digits[place.clamp(0, D - 1).long()]")
3315
+ self._emit("out_cps = valid * (48.0 + gath) + "
3316
+ "(idx == 0).to(self.dtype) * neg * 45.0")
3317
+ self._emit("v = _torch.zeros(self.dim, dtype=self.dtype, device=self.device)")
3318
+ self._emit("v[self.semantic_dim + self.AXIS_STRING_FLAG] = 1.0")
3319
+ self._emit("v = v.index_copy(0, ax, out_cps)")
3320
+ self._emit("return v")
3321
+ self._indent -= 1
3322
+ self._emit()
3260
3323
  self._emit("def string_to_python(self, v):")
3261
3324
  self._indent += 1
3262
3325
  self._emit('"""Decode a String value back to a Python str. This is the')
@@ -3459,6 +3522,41 @@ class PyTorchCodegen(Codegen):
3459
3522
  self._emit("return self._t_from_r_cache")
3460
3523
  self._indent -= 1
3461
3524
  self._emit()
3525
+ self._emit("def _real_from_truth(self):")
3526
+ self._indent += 1
3527
+ self._emit('"""Matrix moving the truth-axis entry to the real axis')
3528
+ self._emit('(cached; the inverse-direction mate of _truth_from_real)."""')
3529
+ self._emit("if not hasattr(self, '_r_from_t_cache') or self._r_from_t_cache is None:")
3530
+ self._indent += 1
3531
+ self._emit("M = _torch.zeros((self.dim, self.dim), dtype=self.dtype, device=self.device)")
3532
+ self._emit("M[self.semantic_dim + self.AXIS_REAL,")
3533
+ self._indent += 1
3534
+ self._emit("self.semantic_dim + self.AXIS_TRUTH] = 1.0")
3535
+ self._indent -= 1
3536
+ self._emit("self._r_from_t_cache = M")
3537
+ self._indent -= 1
3538
+ self._emit("return self._r_from_t_cache")
3539
+ self._indent -= 1
3540
+ self._emit()
3541
+ self._emit("def cast_number_to_truth(self, x):")
3542
+ self._indent += 1
3543
+ self._emit('"""`(fuzzy|bool|trit) NUMBER` — the numeric→truth cast')
3544
+ self._emit("(types.md § Casting): the one cast pair that genuinely")
3545
+ self._emit("moves an axis. _cnum lifts a 0-d/host number onto AXIS_REAL")
3546
+ self._emit("(entry boundary), then a cached permutation-style matmul")
3547
+ self._emit('moves that entry to AXIS_TRUTH. Pure tensor ops throughout."""')
3548
+ self._emit("return self._truth_from_real() @ self._cnum(x)")
3549
+ self._indent -= 1
3550
+ self._emit()
3551
+ self._emit("def cast_truth_to_number(self, x):")
3552
+ self._indent += 1
3553
+ self._emit('"""`(number|int) TRUTH` — the truth→numeric cast: moves the')
3554
+ self._emit("AXIS_TRUTH entry to AXIS_REAL by the cached inverse-direction")
3555
+ self._emit('matmul. Truth values are canonical d-dim vectors (make_truth),')
3556
+ self._emit('which _cnum passes through unchanged."""')
3557
+ self._emit("return self._real_from_truth() @ self._cnum(x)")
3558
+ self._indent -= 1
3559
+ self._emit()
3462
3560
  self._emit("CMP_SLOPE = 100.0")
3463
3561
  self._emit()
3464
3562
  self._emit("def gt(self, a, b):")
@@ -79,10 +79,18 @@ def _get_st_model(model: str):
79
79
  file=sys.stderr,
80
80
  flush=True,
81
81
  )
82
+ import contextlib
83
+ import sys
84
+
82
85
  from sentence_transformers import SentenceTransformer
83
86
 
84
- # nomic-bert ships custom modeling code, hence trust_remote_code.
85
- st = SentenceTransformer(hf_id, trust_remote_code=True)
87
+ # nomic-bert ships custom modeling code, hence trust_remote_code. That load
88
+ # (and sentence-transformers itself) prints framework chatter to stdout —
89
+ # e.g. "<All keys matched successfully>" from the state-dict load — which
90
+ # would pollute a program's stdout (main()'s output, or --emit). Route any
91
+ # such load-time output to stderr, where our own notice already goes.
92
+ with contextlib.redirect_stdout(sys.stderr):
93
+ st = SentenceTransformer(hf_id, trust_remote_code=True)
86
94
  _ST_CACHE[hf_id] = st
87
95
  return st
88
96
 
@@ -37,7 +37,20 @@ from .fv_obligation_checker import (
37
37
  range_sound_by_composition,
38
38
  reduces_to_same_graph,
39
39
  )
40
+ from .fv_loop_convergence import (
41
+ ASYMPTOTICALLY_STABLE,
42
+ MARGINALLY_STABLE,
43
+ UNSTABLE,
44
+ LoopConvergenceReport,
45
+ analyze_loop_recurrence,
46
+ )
40
47
  from .fv_poly_bound import RangeBound, bound_polynomial_over_box
48
+ from .fv_sampler_convergence import (
49
+ SamplerConvergenceReport,
50
+ analyze_sampler_convergence,
51
+ integrate_master_equation,
52
+ measured_decay_rate,
53
+ )
41
54
 
42
55
  __all__ = [
43
56
  # polynomial range bounding (the §3.2 branch-range obligation)
@@ -52,4 +65,15 @@ __all__ = [
52
65
  # extraction + the verifiable-fragment boundary
53
66
  "extract_truth_polynomial",
54
67
  "NonPolynomialResidual",
68
+ # loop-convergence obligation via the Z-transform poles (the §3.3 linear core)
69
+ "analyze_loop_recurrence",
70
+ "LoopConvergenceReport",
71
+ "ASYMPTOTICALLY_STABLE",
72
+ "MARGINALLY_STABLE",
73
+ "UNSTABLE",
74
+ # continuous-time convergence of the probabilistic sampler (the §7 SDE angle)
75
+ "analyze_sampler_convergence",
76
+ "SamplerConvergenceReport",
77
+ "integrate_master_equation",
78
+ "measured_decay_rate",
55
79
  ]