diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index b2475da521..5c5f36db7e 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -167,6 +167,7 @@ Traps from this tick (ffmpeg on the lab): - lavfi rejects `d=.900`. Print the duration as `$((m/1000)).$(printf %03d $((m%1000)))`. - `pkill -f` on the loop script leaves its child renders running. Find them with `ps -eo pid,args | grep '[s]h xf'` and kill them by pid. - Through `fs-lab.sh`, `pkill -f 'why_seal'` also matches the ssh command line that carries it and kills your own session; the output comes back empty. Bracket the first letter (`pkill -f '[w]hy_seal'`) so the pattern does not match itself. A killed session leaves the detached ratchets it started running, so several end up writing one log; list them with `ps -eo pid,etimes,args | grep '[c]orpus-only'` before you read a verdict. +- **A new spec needs its own row in `docs/reports/t27b_expectations.json`**, or `t27b-native-ratchet` fails with UNLISTED. The issue gate still auto-merges the PR red: #10784 landed that way and left five unlisted specs for #10792 to carry. The lab is x86_64, and `t27b test` there ends with "the t27b JIT runs only on arm64". The front end still runs first: `t27b test --check` prints every blocker before that line. Run a blocked control next to your spec; `specs/t27f/frame_shard.t27` prints `unsupported construct ExprCall(rejected fn)`. If your spec reaches the JIT line with no blocker, and `t27c test-report` passes it, write a pass row. The PR's own arm64 run confirms or refutes the row. Keep the PR a draft until the row is in. Never guess "pass" without that check: a `[N]T` or `ExprCall` blocker turns a guessed row into a regression. Measured on glyph57, idea_factory and petal_menu (#10792). **Holding the address when the voice runs long (t27#10629).** If an overlay must stay on screen for a minimum time and must also stay off a crossfade, then the crossfade has to move; the overlay cannot. Work out in frames the latest cut that still leaves the last scene its minimum: cut 1 is capped at `BEAT_CUT1_LAST_MS` = 13249. Prove it is the latest with a one-ms-later control. Only the voices the reel speeds up are affected, so the `cmp` diff is limited to those voices and to crossfade 1's offset. The cost is real: the crossfade starts up to 184 ms under the task's last word, and lesson 1's measured voice (1300/4200/6800) is one of the affected voices. Write a test that bounds that overlap. @@ -399,13 +400,15 @@ Measured at 355d44a39: each still has 9 to 14 flat colours, and the PNGs are 4 t ## Application: idea factory, the OS particle design system (t27#10771) The owner wants one factory that unpacks any idea in any style preset, with every element made -of particles, and runnable in the OS itself and on FPGA. Two specs do this: +of particles, and runnable in the OS itself and on FPGA. Two specs do this. TRI CLAW's card for +them is `specs/skills/t27-idea-factory.t27` (t27#10796). If you add a preset, also add it to the +card's `PRESETS`; its lab check compares that list to `PRESET_NAMES` byte for byte. - **`specs/t27f/particle_ds.t27` is the design system.** A preset is a column in integer tables: `BG`, `COL_A/B/C`, `RADIUS`, `GAIN`, `SPRING`, `DAMP`, `LAW`, `FORCE`, `KICK`, `CARD`. The five are noir, neon, trit, paper and rain. **Add a preset by adding a column, never a branch in code.** - **`specs/t27f/idea_factory.t27` is the factory**, `(IDEA, PRESET) -> RGB24 frames`. It wraps - (at most 9 characters per line, 4 lines per scene, `|` cuts a scene), lays out in the 3x5 font, + (at most 9 characters per line, 4 lines per scene, `|` cuts a scene), lays out in glyph57's 5x7 font, assigns particles to glyph sub-slots and the card border, and closes on `CTA`. **Keep it FPGA-shaped.** Use Q8 fixed point. The per-step and per-pixel paths use only add, @@ -442,6 +445,28 @@ particles never settled into letters. - **Base the branch on master's tip before the PR.** If the base PR (R3) merged first, reset the branch onto `origin/master`. Untracked new specs survive the reset. +### Russian text: UTF-8 and the 5x7 font (t27#10788) + +`specs/t27f/glyph57.t27` gives the factory Cyrillic. It decodes UTF-8 (`cp_len`, `cp_at`, +`cp_count`), folds lower case to capitals, and stores a 5x7 font. The font holds ASCII 32..95 +and only the 21 Cyrillic capitals that have no Latin twin. The 11 look-alikes (A, Ve, Ie, Ka, Em, +En, O, Er, Es, Te, Ha) and Yo reuse the Latin glyph through the 32-entry `CYR_MAP`. + +- **Write non-ASCII text as a `u8` array, never as `"\xNN"`.** L3 keeps sources ASCII. The t27c + lexer lowers only `\n`, `\t`, `\\` and `\"`, and keeps `\xD0` as four characters, so a + `\xNN` test fails with no error. Use `pub const RU_IDEA : [80]u8 = [208, 167, ...];` and pass + `RU_IDEA[0..80]` to a `[]const u8` parameter. `specs/tri/utils/terminal.t27` records the same + trap for `\x1b`. +- **Count widths in characters, keep positions in bytes.** Wrapping compares `cp_count` with + `MAXC`. The line start and end stay byte offsets, and the layout loop steps by + `bi = bi + cp_len(t, bi)`. An ASCII idea's numbers do not change, so the old tests still pin + the old layout. +- **Guard the font with a duplicate-bitmap test.** Every pair of stored glyphs must differ, and + every look-alike must light exactly its Latin twin's pixels. +- **Render a Russian idea without touching the committed constant.** In the lab copy, `sed` + `reel_frames(IDEA)` and `reel_cell(IDEA,` to use `RU_IDEA[0..80]`, next to the `PRESET` and + module renames. + ## Check the whole corpus for import cycles (t27#10693) dependency-cruiser's `no-circular` and madge's `--circular` list every module on a cycle. diff --git a/.trinity/seals/skills_skill_t27_idea_factory.json b/.trinity/seals/skills_skill_t27_idea_factory.json new file mode 100644 index 0000000000..c85f6ea82c --- /dev/null +++ b/.trinity/seals/skills_skill_t27_idea_factory.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+355d44a39", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:db7607f38e760d8103bdb84a2539cdf00ca9d0e2f504d7e987f4a0c1e9d1b9f9", + "gen_hash_rust": "sha256:1f13dfc7d91fe679b5511fff2f871e621212826a23687bc0aaa2c8336c1e2454", + "gen_hash_verilog": "sha256:b600fb19813e02194e19d43f9b0443bd90fa85fcc16d99d6e45a90f5c97765a3", + "gen_hash_zig": "sha256:c44654ca106154eb1a44686bf49866c185fd7462e673e4665e3923f7e5ef8ba4", + "module": "skill_t27_idea_factory", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T03:00:36Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:405d8fcf7e0f8347a76fa785818ed759cae128272523c8fabab5f0e2114286a9", + "spec_path": "specs/skills/t27-idea-factory.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 2, + "total": 2, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_glyph57.json b/.trinity/seals/t27f_glyph57.json new file mode 100644 index 0000000000..e7712fea8d --- /dev/null +++ b/.trinity/seals/t27f_glyph57.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+355d44a39", + "closure": [ + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:7b616c3b258394696d96adfd9f33a21a788063f0fcc37c591d2982b9e5c44645", + "gen_hash_rust": "sha256:cfe5802136fe85e748ec9880d796a0ddb5ee6fe69a4de466dc544f373aa1bfab", + "gen_hash_verilog": "sha256:bc6fce86cf24c30a17e3d29e3afebdd28d1aed88c760196a055aec250d2a549e", + "gen_hash_zig": "sha256:d57c0bf3b612b865a9df137514ae0e66790196114c7111977ca43d795f67c0e4", + "module": "glyph57", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T02:41:07Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:75e98adfcf86a4070c03154e29cf533833c618a1252652f2c20bf91b8425e6cd", + "spec_path": "specs/t27f/glyph57.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 7, + "total": 7, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_idea_factory.json b/.trinity/seals/t27f_idea_factory.json index fff853f045..4f321c9bb3 100644 --- a/.trinity/seals/t27f_idea_factory.json +++ b/.trinity/seals/t27f_idea_factory.json @@ -1,6 +1,10 @@ { "built_by": "t27c-bootstrap@0.5.2+355d44a39", "closure": [ + { + "spec": "specs/t27f/glyph57.t27", + "spec_hash": "sha256:75e98adfcf86a4070c03154e29cf533833c618a1252652f2c20bf91b8425e6cd" + }, { "spec": "specs/t27f/particle_ds.t27", "spec_hash": "sha256:00e84a7492536bf54ac69aa0cd852dea4727477acee12538f9c4b145c3d9c8f3" @@ -11,23 +15,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:49e275c87108c8621d521226b89afb5f6c6a39f74de8e8bba502fbd1e39ad843", - "gen_hash_rust": "sha256:7d03e6e99a15344a1c8272b7f96ffc8ddcbfdb1cf44ab7f70f7a0e37847529bc", - "gen_hash_verilog": "sha256:0f761cb6e48d10ac555e51b8e5722eb8b02074a87ca2055aaec9241754140c49", - "gen_hash_zig": "sha256:0730d911d7047dc707075d16c8ff04afe222f4422a24344eff10b90e72edfdf0", + "gen_hash_c": "sha256:6a6ffcf30791e08b93221c1067abfc7e053f13c23dff90de4d9f7c9e23461479", + "gen_hash_rust": "sha256:d64150e24319f74e2b9413d45cfbd96a7d8bbc0df9d51c9fac0b68c8837b2efb", + "gen_hash_verilog": "sha256:387961ee44ce4ce7f32d4ccb23a4ac58ac6ef688db3b4c8aef38d1c60e41cc18", + "gen_hash_zig": "sha256:115d3f8ff262b6fd65b59d760aeb7eff66500b9f77f56577fb22233fefaa65e7", "module": "idea_factory", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T02:25:39Z", + "sealed_at": "2026-10-19T02:41:09Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:34f59b06d780be9cbbae1b0577e7c96ed5b6b7d2451e61ada1b1dde2fd58b6b8", + "spec_hash": "sha256:249909fbd1679b488be14be323c0079735a55895560423934c21daf28c824db9", "spec_path": "specs/t27f/idea_factory.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 8, - "total": 8, + "passed": 10, + "total": 10, "vacuous": 0 }, "toolchain": { diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index c65d3ed1de..627b1a5f22 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -12,7 +12,7 @@ "reference-bug": "the reference passes it for the wrong reason; t27b is right not to", "n/a": "outside what t27b is for (say why in the PR that sets it)" }, - "max_not_pass": 139, + "max_not_pass": 129, "entries": [ {"path": "specs/a/b_c.t27", "t27b": "pass"}, {"path": "specs/a_b/c.t27", "t27b": "pass"}, @@ -46,9 +46,13 @@ {"path": "specs/analog/bench.t27", "t27b": "pass"}, {"path": "specs/analog/linear.t27", "t27b": "pass"}, {"path": "specs/analog/linear_zoh.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, + {"path": "specs/analog/mos_psp103.t27", "t27b": "pass"}, + {"path": "specs/analog/mos_psp103_data.t27", "t27b": "pass"}, {"path": "specs/analog/mos_table.t27", "t27b": "pass"}, {"path": "specs/analog/sar_cdac.t27", "t27b": "pass"}, {"path": "specs/analog/sar_comparator.t27", "t27b": "pass"}, + {"path": "specs/analog/sar_logic.t27", "t27b": "pass"}, + {"path": "specs/analog/sar_switch.t27", "t27b": "pass"}, {"path": "specs/api/c_abi.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_context.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_loop.t27", "t27b": "pass"}, @@ -241,6 +245,7 @@ {"path": "specs/compiler/rust_optional_returns.t27", "t27b": "pass"}, {"path": "specs/compiler/rust_tail_returns.t27", "t27b": "pass"}, {"path": "specs/compiler/rust_value_parameters.t27", "t27b": "pass"}, + {"path": "specs/compiler/string_escapes.t27", "t27b": "pass"}, {"path": "specs/compiler/test_report_exit.t27", "t27b": "pass"}, {"path": "specs/compiler/theory/isa_round_trip.t27", "t27b": "pass"}, {"path": "specs/compiler/theory/reproducible.t27", "t27b": "pass"}, @@ -339,6 +344,7 @@ {"path": "specs/effects/replay_oracle.t27", "t27b": "pass"}, {"path": "specs/file/schema.t27", "t27b": "pass"}, {"path": "specs/fpga/adapter.t27", "t27b": "pass"}, + {"path": "specs/fpga/analog_cell_ax7203.t27", "t27b": "pass"}, {"path": "specs/fpga/apb_bridge.t27", "t27b": "pass"}, {"path": "specs/fpga/assembler.t27", "t27b": "pass"}, {"path": "specs/fpga/axi4.t27", "t27b": "pass"}, @@ -455,7 +461,7 @@ {"path": "specs/hosting/sybil.t27", "t27b": "pass"}, {"path": "specs/hosting/toolchain.t27", "t27b": "pass"}, {"path": "specs/hosting/topology.t27", "t27b": "pass"}, - {"path": "specs/hslm/forward_pass.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/hslm/forward_pass.t27", "t27b": "pass"}, {"path": "specs/i18n/agents-ru.t27", "t27b": "pass"}, {"path": "specs/i18n/blog-ru.t27", "t27b": "pass"}, {"path": "specs/i18n/docs-ru.t27", "t27b": "pass"}, @@ -464,11 +470,12 @@ {"path": "specs/igla/coder/benchmark.t27", "t27b": "blocked", "blocker": "ExprIdentifier(undeclared)", "reason": "unimplemented"}, {"path": "specs/igla/coder/dataset.t27", "t27b": "blocked", "blocker": "ExprIdentifier(undeclared)", "reason": "unimplemented"}, {"path": "specs/igla/coder/eval.t27", "t27b": "blocked", "blocker": "ExprIdentifier(undeclared)", "reason": "unimplemented"}, - {"path": "specs/igla/coder/kill_rule.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/igla/coder/kill_rule.t27", "t27b": "pass"}, {"path": "specs/igla/coder/pipeline.t27", "t27b": "blocked", "blocker": "ConstDecl(slice)", "reason": "unimplemented"}, {"path": "specs/igla/coder/preflight.t27", "t27b": "pass"}, {"path": "specs/igla/coder/prm.t27", "t27b": "pass"}, {"path": "specs/igla/coder/ternary_gemm_gpu.t27", "t27b": "pass"}, + {"path": "specs/igla/coder/ternary_gemm_ptx.t27", "t27b": "pass"}, {"path": "specs/igla/coder/tokenizer.t27", "t27b": "pass"}, {"path": "specs/igla/coder/training.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/igla/coder/weights.t27", "t27b": "blocked", "blocker": "ConstDecl(slice)", "reason": "unimplemented"}, @@ -543,32 +550,33 @@ {"path": "specs/ml/arena_heap.t27", "t27b": "pass"}, {"path": "specs/ml/file_io_contract.t27", "t27b": "pass"}, {"path": "specs/ml/host_io_model.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/ml/hslm_corpus.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/ml/igla_champion_capsule.t27", "t27b": "blocked", "blocker": "ExprIdentifier(undeclared)", "reason": "unimplemented"}, {"path": "specs/ml/layers/avgpool2d_layer.t27", "t27b": "pass"}, - {"path": "specs/ml/layers/batchnorm_layer.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/ml/layers/batchnorm_layer.t27", "t27b": "pass"}, {"path": "specs/ml/layers/conv2d_layer.t27", "t27b": "pass"}, {"path": "specs/ml/layers/dense_layer.t27", "t27b": "pass"}, {"path": "specs/ml/layers/dropout_layer.t27", "t27b": "pass"}, - {"path": "specs/ml/layers/embedding_layer.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/ml/layers/embedding_layer.t27", "t27b": "pass"}, {"path": "specs/ml/layers/flatten_layer.t27", "t27b": "pass"}, {"path": "specs/ml/layers/layernorm_layer.t27", "t27b": "pass"}, {"path": "specs/ml/layers/maxpool2d_layer.t27", "t27b": "pass"}, {"path": "specs/ml/layers/residual_connection.t27", "t27b": "pass"}, {"path": "specs/ml/loss/binary_crossentropy_loss.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, - {"path": "specs/ml/loss/contrastive_loss.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/ml/loss/contrastive_loss.t27", "t27b": "pass"}, {"path": "specs/ml/loss/cross_entropy_loss.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/ml/loss/huber_loss.t27", "t27b": "pass"}, {"path": "specs/ml/loss/kl_divergence.t27", "t27b": "pass"}, {"path": "specs/ml/loss/mse_loss.t27", "t27b": "pass"}, - {"path": "specs/ml/optimizer/adagrad.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, - {"path": "specs/ml/optimizer/adam.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, - {"path": "specs/ml/optimizer/adamw.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, - {"path": "specs/ml/optimizer/lamb.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/ml/optimizer/adagrad.t27", "t27b": "pass"}, + {"path": "specs/ml/optimizer/adam.t27", "t27b": "pass"}, + {"path": "specs/ml/optimizer/adamw.t27", "t27b": "pass"}, + {"path": "specs/ml/optimizer/lamb.t27", "t27b": "pass"}, {"path": "specs/ml/optimizer/race_config.t27", "t27b": "blocked", "blocker": "ExprIdentifier(undeclared)", "reason": "unimplemented"}, {"path": "specs/ml/optimizer/rmsprop.t27", "t27b": "pass"}, {"path": "specs/ml/optimizer/sgd.t27", "t27b": "pass"}, {"path": "specs/ml/optimizer/sgd_momentum.t27", "t27b": "blocked", "blocker": "ExprIdentifier(undeclared)", "reason": "unimplemented"}, - {"path": "specs/ml/pathway/mlp.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/ml/pathway/mlp.t27", "t27b": "blocked", "blocker": "ExprArrayLiteral(repeat count)", "reason": "unimplemented"}, {"path": "specs/ml/recurrent/attention_mechanism.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/ml/recurrent/bilstm.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/ml/recurrent/gru_cell.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, @@ -577,7 +585,7 @@ {"path": "specs/ml/recurrent/rnn_cell.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/ml/recurrent/self_attention.t27", "t27b": "pass"}, {"path": "specs/ml/recurrent/seq2seq.t27", "t27b": "pass"}, - {"path": "specs/ml/rl/advantage_estimator.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/ml/rl/advantage_estimator.t27", "t27b": "pass"}, {"path": "specs/ml/rl/dqn.t27", "t27b": "pass"}, {"path": "specs/ml/rl/dqn_target_network.t27", "t27b": "pass"}, {"path": "specs/ml/rl/ppo_actor.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, @@ -605,11 +613,12 @@ {"path": "specs/network/quorum.t27", "t27b": "pass"}, {"path": "specs/network/usage.t27", "t27b": "pass"}, {"path": "specs/nn/attention.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, - {"path": "specs/nn/gla.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/nn/gla.t27", "t27b": "blocked", "blocker": "ConstDecl", "reason": "unimplemented"}, {"path": "specs/nn/hslm.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/nn/hslm_backward.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, {"path": "specs/nn/hslm_data.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, - {"path": "specs/nn/ternary_ste.t27", "t27b": "blocked", "blocker": "ExprCall(undeclared fn)", "reason": "unimplemented"}, + {"path": "specs/nn/hslm_tokenizer.t27", "t27b": "pass"}, + {"path": "specs/nn/ternary_ste.t27", "t27b": "pass"}, {"path": "specs/numeric/bnf1024.t27", "t27b": "pass"}, {"path": "specs/numeric/bnf128.t27", "t27b": "pass"}, {"path": "specs/numeric/bnf16.t27", "t27b": "pass"}, @@ -682,6 +691,7 @@ {"path": "specs/policy/own_language.t27", "t27b": "pass"}, {"path": "specs/policy/pr_budget.t27", "t27b": "pass"}, {"path": "specs/policy/required_contexts.t27", "t27b": "pass"}, + {"path": "specs/policy/secret_gate.t27", "t27b": "pass"}, {"path": "specs/policy/text_lines.t27", "t27b": "pass"}, {"path": "specs/port/bootstrap/src/bitnet_irq.t27", "t27b": "pass"}, {"path": "specs/port/bootstrap/src/bridge.t27", "t27b": "pass"}, @@ -1135,7 +1145,9 @@ {"path": "specs/port/trinity/src/wasm_stubs/auto_shard_stub.t27", "t27b": "pass"}, {"path": "specs/port/trios-railway/bin/seed-agent/src/main.t27", "t27b": "pass"}, {"path": "specs/port/trios-railway/crates/trios-igla-race/src/asha.t27", "t27b": "pass"}, + {"path": "specs/port/trios-railway/crates/trios-igla-race/src/bin/debug_queue.t27", "t27b": "pass"}, {"path": "specs/port/trios-railway/crates/trios-igla-race/src/bin/reset_queue.t27", "t27b": "pass"}, + {"path": "specs/port/trios-railway/crates/trios-igla-race/src/bin/smoke_agent.t27", "t27b": "pass"}, {"path": "specs/port/trios-railway/crates/trios-igla-race/src/ema.t27", "t27b": "pass"}, {"path": "specs/port/trios-railway/crates/trios-igla-race/src/invariants.t27", "t27b": "pass"}, {"path": "specs/port/trios-railway/crates/trios-igla-race/src/race.t27", "t27b": "pass"}, @@ -1456,6 +1468,7 @@ {"path": "specs/server/session.t27", "t27b": "pass"}, {"path": "specs/server/sse.t27", "t27b": "blocked", "blocker": "type (alias)", "reason": "unimplemented"}, {"path": "specs/skills/t27-deps-card.t27", "t27b": "pass"}, + {"path": "specs/skills/t27-idea-factory.t27", "t27b": "pass"}, {"path": "specs/skills/t27-issue-shape.t27", "t27b": "pass"}, {"path": "specs/skills/t27-measure-corpus.t27", "t27b": "pass"}, {"path": "specs/skills/t27-phi-loop.t27", "t27b": "pass"}, @@ -1501,12 +1514,16 @@ {"path": "specs/t27f/frame_cache.t27", "t27b": "pass"}, {"path": "specs/t27f/frame_shard.t27", "t27b": "blocked", "blocker": "type [N]T", "reason": "unimplemented"}, {"path": "specs/t27f/frontend.t27", "t27b": "pass"}, + {"path": "specs/t27f/glyph57.t27", "t27b": "pass"}, {"path": "specs/t27f/hot.t27", "t27b": "pass"}, + {"path": "specs/t27f/idea_factory.t27", "t27b": "pass"}, {"path": "specs/t27f/lesson_reel.t27", "t27b": "blocked", "blocker": "type [N]T", "reason": "unimplemented"}, {"path": "specs/t27f/outfit_atlas.t27", "t27b": "pass"}, + {"path": "specs/t27f/particle_ds.t27", "t27b": "pass"}, {"path": "specs/t27f/particle_engine.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, {"path": "specs/t27f/particle_field.t27", "t27b": "pass"}, {"path": "specs/t27f/pbr_sphere.t27", "t27b": "pass"}, + {"path": "specs/t27f/petal_menu.t27", "t27b": "pass"}, {"path": "specs/t27f/quest.t27", "t27b": "pass"}, {"path": "specs/t27f/reel.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_beats.t27", "t27b": "pass"}, @@ -1977,7 +1994,7 @@ {"path": "specs/tri/utils/terminal.t27", "t27b": "blocked", "blocker": "ExprCall(@import)", "reason": "unimplemented"}, {"path": "specs/tri/utils/text.t27", "t27b": "pass"}, {"path": "specs/tri/utils/time.t27", "t27b": "blocked", "blocker": "ExprCall(@divFloor)", "reason": "unimplemented"}, - {"path": "specs/tri/utils/utf8.t27", "t27b": "blocked", "blocker": "ExprCall(@truncate)", "reason": "unimplemented"}, + {"path": "specs/tri/utils/utf8.t27", "t27b": "pass"}, {"path": "specs/tri/utils/version.t27", "t27b": "pass"}, {"path": "specs/trinet/eth_top_ax7203.t27", "t27b": "pass"}, {"path": "specs/trinet/etx.t27", "t27b": "pass"}, diff --git a/specs/skills/t27-idea-factory.t27 b/specs/skills/t27-idea-factory.t27 new file mode 100644 index 0000000000..a4f4cedba1 --- /dev/null +++ b/specs/skills/t27-idea-factory.t27 @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/skills/t27-idea-factory.t27 -- skill t27/idea-factory +; Source of truth for the skill card on t27.ai (#/skills?skill=t27/idea-factory). +; How TRI CLAW unpacks any idea in any style preset of the particle design system: the input +; is (idea, preset); the idea is UTF-8 text, a `|` cuts a scene, lines wrap at 9 characters +; and 4 lines a scene, and the closing scene is the address T27.AI. Every letter, card and +; glow on screen is a particle that springs to a target in glyph57's 5x7 Latin and Cyrillic +; font. The output is 1080x1920 frames at 30 fps, 72 frames a scene. +; The law lives once, in specs/t27f/idea_factory.t27 (wrap, layout, assign), in +; specs/t27f/glyph57.t27 (UTF-8 and the font) and in specs/t27f/particle_ds.t27 (presets, +; laws, tone); this card points at them and copies none. PRESETS below is a name list for +; the agent, in the order of particle_ds.PRESET_NAMES; cards import nothing, so the lab +; check compares the two byte for byte. +; A Russian idea is a u8 array of its UTF-8 bytes (idea_factory.RU_IDEA): t27c keeps +; "\xNN" in a string as four characters, and L3 keeps sources ASCII. +; NOT CLAIMED: no voice or music (lesson_reel mode 5 has those), capitals only (lower case +; folds), and the Verilog is emitted, not synthesised. There is no SKILL.md behind it yet, +; so the card is spec-only (see "Witness labels" in specs/skills/README.md). Issues +; gHashTag/t27#10771, gHashTag/t27#10788 and gHashTag/t27#10796, epic gHashTag/t27#10560. +; The JSON the site serves is generated from this file by the real compiler; +; nothing below is parsed with a regex. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module skill_t27_idea_factory; + +pub const KIND : str = "skill"; +pub const ID : str = "t27/idea-factory"; +pub const NAME : str = "Idea Factory"; +pub const REPO : str = "t27"; +; No SKILL.md exists for this card. +pub const SOURCE : str = ""; +pub const SUMMARY_EN : str = "Unpack any idea, Latin or Russian, into a 1080x1920 particle reel in one of five style presets: every letter, card and glow is a particle springing into a 5x7 font, a bar cuts a scene, and the reel closes on the address"; +pub const COMMAND : str = "/idea-factory"; +; Specs this skill stands on: the factory, its font and its particle design system. +pub const SPECS : [3]str = ["specs/t27f/idea_factory.t27", "specs/t27f/glyph57.t27", "specs/t27f/particle_ds.t27"]; +; The five presets, index = the preset number idea_factory takes. +pub const PRESETS : [5]str = ["noir-gold", "neon-vortex", "trit-charge", "paper-ink", "rain"]; +pub const TAGS : [5]str = ["social-visual", "reel", "factory", "particles", "t27f"]; +pub const ENABLED : bool = true; +; Default budget; no SKILL.md declares a timeout. +pub const TIMEOUT_MIN : u16 = 30; + +test basic_constants { + assert KIND == "skill"; + assert ID == "t27/idea-factory"; + assert NAME == "Idea Factory"; + assert REPO == "t27"; + assert SOURCE == ""; + assert COMMAND == "/idea-factory"; + assert ENABLED == true; + assert TIMEOUT_MIN == 30; + assert TAGS.len == 5; + assert TAGS[0] == "social-visual"; + assert TAGS[2] == "factory"; + assert SPECS.len == 3; + assert SPECS[0] == "specs/t27f/idea_factory.t27"; + assert SPECS[1] == "specs/t27f/glyph57.t27"; + assert SPECS[2] == "specs/t27f/particle_ds.t27"; +} + +test presets_in_preset_number_order { + assert PRESETS.len == 5; + assert PRESETS[0] == "noir-gold"; + assert PRESETS[1] == "neon-vortex"; + assert PRESETS[2] == "trit-charge"; + assert PRESETS[3] == "paper-ink"; + assert PRESETS[4] == "rain"; +} diff --git a/specs/t27f/glyph57.t27 b/specs/t27f/glyph57.t27 new file mode 100644 index 0000000000..8c40966697 --- /dev/null +++ b/specs/t27f/glyph57.t27 @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/glyph57.t27 -- the 5x7 particle font of the OS design system: UTF-8 in, lit cells +// out, Latin and Cyrillic. Issue gHashTag/t27#10788, epic #10560. +// +// WHY: the idea factory (idea_factory.t27) set its text in scene_raster's 3x5 font, which is ASCII +// only and too small for Zh, Sh, Shch or Yu. The owner's reels are Russian; until now their text +// was a TTF font drawn by ffmpeg drawtext -- not particles, and nothing an FPGA could draw. +// +// THE PIECES, all integer (lookups and compares, no division by a variable): +// decode cp_len / cp_at / cp_count read UTF-8 bytes. A byte that does not start a well-formed +// sequence reads as one character, BAD, so a walk never stalls and never skips text. +// fold Latin a..z, Cyrillic a..ya and yo fold to upper case: the font has capitals only. +// glyph ASCII 32..95 are glyphs 0..63; the 21 Cyrillic capitals with no Latin look-alike +// are glyphs 64..84; the 11 look-alikes (A Ve Ie Ka Em En O Er Es Te Ha) and Yo reuse +// the Latin glyph through CYR_MAP. Anything else draws as '?'. +// +// SOURCES ARE ASCII (L3), AND t27c DOES NOT LOWER "\xNN" IN A STRING (it keeps the backslash), so +// non-ASCII text is a u8 array of its UTF-8 bytes: Zh (U+0416) is [208, 150]. +// +// NOT CLAIMED: no lower-case glyphs (they fold); Yo is drawn as Ie, as on small LCD fonts; no +// glyph outside Latin, digits, ASCII punctuation and the Russian alphabet. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module glyph57; + +use t27f::scene_raster::fmod; + +pub const ID : str = "glyph57"; +pub const VERSION : u8 = 1; + +pub const GW : i32 = 5; // glyph width, font pixels +pub const GH : i32 = 7; // glyph height +pub const GLYPHS57 : i32 = 85; // 64 ASCII + 21 Cyrillic +pub const BAD : i32 = 65533; // U+FFFD, what a malformed byte reads as +pub const QMARK : i32 = 31; // the glyph of '?' +pub const CYR_A : i32 = 1040; // U+0410, Cyrillic capital A +pub const CYR_YO : i32 = 1025; // U+0401, Cyrillic capital Yo + +// The weight of pixel column gx in a row: bit 4 is the left pixel. +pub const PW5 : [5]i32 = [16, 8, 4, 2, 1]; + +// ---- the font: 85 glyphs of seven rows ---- +pub const FONT57 : [595]u8 = [ + 0, 0, 0, 0, 0, 0, 0, // space + 4, 4, 4, 4, 4, 0, 4, // ! + 10, 10, 0, 0, 0, 0, 0, // " + 10, 10, 31, 10, 31, 10, 10, // # + 4, 15, 20, 14, 5, 30, 4, // $ + 24, 25, 2, 4, 8, 19, 3, // % + 12, 18, 20, 8, 21, 18, 13, // & + 4, 4, 0, 0, 0, 0, 0, // ' + 2, 4, 8, 8, 8, 4, 2, // ( + 8, 4, 2, 2, 2, 4, 8, // ) + 0, 4, 21, 14, 21, 4, 0, // * + 0, 4, 4, 31, 4, 4, 0, // + + 0, 0, 0, 0, 12, 4, 8, // , + 0, 0, 0, 31, 0, 0, 0, // - + 0, 0, 0, 0, 0, 12, 12, // . + 0, 1, 2, 4, 8, 16, 0, // / + 14, 17, 19, 21, 25, 17, 14, // 0 + 4, 12, 4, 4, 4, 4, 14, // 1 + 14, 17, 1, 2, 4, 8, 31, // 2 + 31, 2, 4, 2, 1, 17, 14, // 3 + 2, 6, 10, 18, 31, 2, 2, // 4 + 31, 16, 30, 1, 1, 17, 14, // 5 + 6, 8, 16, 30, 17, 17, 14, // 6 + 31, 1, 2, 4, 8, 8, 8, // 7 + 14, 17, 17, 14, 17, 17, 14, // 8 + 14, 17, 17, 15, 1, 2, 12, // 9 + 0, 12, 12, 0, 12, 12, 0, // : + 0, 12, 12, 0, 12, 4, 8, // ; + 2, 4, 8, 16, 8, 4, 2, // < + 0, 0, 31, 0, 31, 0, 0, // = + 8, 4, 2, 1, 2, 4, 8, // > + 14, 17, 1, 2, 4, 0, 4, // ? + 14, 17, 1, 13, 21, 21, 14, // @ + 14, 17, 17, 17, 31, 17, 17, // A + 30, 17, 17, 30, 17, 17, 30, // B + 14, 17, 16, 16, 16, 17, 14, // C + 28, 18, 17, 17, 17, 18, 28, // D + 31, 16, 16, 30, 16, 16, 31, // E + 31, 16, 16, 30, 16, 16, 16, // F + 14, 17, 16, 23, 17, 17, 15, // G + 17, 17, 17, 31, 17, 17, 17, // H + 14, 4, 4, 4, 4, 4, 14, // I + 7, 2, 2, 2, 2, 18, 12, // J + 17, 18, 20, 24, 20, 18, 17, // K + 16, 16, 16, 16, 16, 16, 31, // L + 17, 27, 21, 21, 17, 17, 17, // M + 17, 17, 25, 21, 19, 17, 17, // N + 14, 17, 17, 17, 17, 17, 14, // O + 30, 17, 17, 30, 16, 16, 16, // P + 14, 17, 17, 17, 21, 18, 13, // Q + 30, 17, 17, 30, 20, 18, 17, // R + 15, 16, 16, 14, 1, 1, 30, // S + 31, 4, 4, 4, 4, 4, 4, // T + 17, 17, 17, 17, 17, 17, 14, // U + 17, 17, 17, 17, 17, 10, 4, // V + 17, 17, 17, 21, 21, 21, 10, // W + 17, 17, 10, 4, 10, 17, 17, // X + 17, 17, 17, 10, 4, 4, 4, // Y + 31, 1, 2, 4, 8, 16, 31, // Z + 14, 8, 8, 8, 8, 8, 14, // [ + 0, 16, 8, 4, 2, 1, 0, // backslash + 14, 2, 2, 2, 2, 2, 14, // ] + 4, 10, 17, 0, 0, 0, 0, // ^ + 0, 0, 0, 0, 0, 0, 31, // _ + 31, 16, 16, 30, 17, 17, 30, // 64 Be + 31, 16, 16, 16, 16, 16, 16, // 65 Ge + 6, 10, 10, 10, 10, 31, 17, // 66 De + 21, 21, 14, 4, 14, 21, 21, // 67 Zhe + 14, 17, 1, 6, 1, 17, 14, // 68 Ze + 17, 17, 19, 21, 25, 17, 17, // 69 I + 10, 4, 17, 19, 21, 25, 17, // 70 short I + 7, 9, 9, 9, 9, 9, 17, // 71 El + 31, 17, 17, 17, 17, 17, 17, // 72 Pe + 17, 17, 17, 15, 1, 17, 14, // 73 U + 4, 14, 21, 21, 21, 14, 4, // 74 Ef + 18, 18, 18, 18, 18, 31, 1, // 75 Tse + 17, 17, 17, 15, 1, 1, 1, // 76 Che + 21, 21, 21, 21, 21, 21, 31, // 77 Sha + 21, 21, 21, 21, 21, 31, 1, // 78 Shcha + 24, 8, 8, 14, 9, 9, 14, // 79 hard sign + 17, 17, 17, 29, 19, 19, 29, // 80 Yeru + 16, 16, 16, 30, 17, 17, 30, // 81 soft sign + 14, 17, 1, 7, 1, 17, 14, // 82 E + 18, 21, 21, 29, 21, 21, 18, // 83 Yu + 15, 17, 17, 15, 5, 9, 17, // 84 Ya +]; + +// The glyph of Cyrillic capital U+0410 + k, A..Ya. Below 64 a Latin look-alike, else its own. +pub const CYR_MAP : [32]u8 = [ + 33, 64, 34, 65, 66, 37, 67, 68, // A Be Ve Ge De Ie Zhe Ze + 69, 70, 43, 71, 45, 40, 47, 72, // I shortI Ka El Em En O Pe + 48, 35, 52, 73, 74, 56, 75, 76, // Er Es Te U Ef Ha Tse Che + 77, 78, 79, 80, 81, 82, 83, 84, // Sha Shcha hard Yeru soft E Yu Ya +]; + +// ---- decode ---- + +fn byte_at(t: []const u8, i: i32) -> i32 { + return @as(i32, t[@as(usize, @intCast(i))]); +} +fn is_cont(b: i32) -> bool { + return b >= 128 && b < 192; +} + +// How many bytes the character at byte i takes: 1..4, or 1 for a malformed byte; 0 past the end. +pub fn cp_len(t: []const u8, i: i32) -> i32 { + const n = @as(i32, @intCast(t.len)); + if (i < 0 || i >= n) { return 0; } + const b = byte_at(t, i); + var k : i32 = 1; + if (b >= 248) { + k = 1; + } else if (b >= 240) { + k = 4; + } else if (b >= 224) { + k = 3; + } else if (b >= 192) { + k = 2; + } + if (i + k > n) { return 1; } + var j : i32 = 1; + while (j < k) { + if (!is_cont(byte_at(t, i + j))) { return 1; } + j = j + 1; + } + return k; +} + +// The code point at byte i; BAD for a malformed byte, 0 past the end. +pub fn cp_at(t: []const u8, i: i32) -> i32 { + const k = cp_len(t, i); + if (k == 0) { return 0; } + const b = byte_at(t, i); + if (k == 1) { + if (b < 128) { return b; } + return BAD; + } + const c1 = byte_at(t, i + 1) - 128; + if (k == 2) { return (b - 192) * 64 + c1; } + const c2 = byte_at(t, i + 2) - 128; + if (k == 3) { return (b - 224) * 4096 + c1 * 64 + c2; } + const c3 = byte_at(t, i + 3) - 128; + return (b - 240) * 262144 + c1 * 4096 + c2 * 64 + c3; +} + +// The number of characters in bytes a..b of t (a walk from a, by cp_len). +pub fn cp_count(t: []const u8, a: i32, b: i32) -> i32 { + var i : i32 = a; + var n : i32 = 0; + while (i < b) { + const k = cp_len(t, i); + if (k == 0) { return n; } + i = i + k; + n = n + 1; + } + return n; +} + +// ---- fold and glyph ---- + +// Upper case: Latin a..z, Cyrillic a..ya (U+0430..U+044F) and yo (U+0451). +pub fn fold(c: i32) -> i32 { + if (c >= 97 && c <= 122) { return c - 32; } + if (c >= 1072 && c <= 1103) { return c - 32; } + if (c == 1105) { return CYR_YO; } + return c; +} + +// The glyph that draws code point c; '?' for one the font does not have. +pub fn glyph57_of(c: i32) -> i32 { + const f = fold(c); + if (f >= 32 && f <= 95) { return f - 32; } + if (f >= CYR_A && f < CYR_A + 32) { return @as(i32, CYR_MAP[@as(usize, @intCast(f - CYR_A))]); } + if (f == CYR_YO) { return 37; } + return QMARK; +} + +// Whether pixel (gx, gy) of glyph g is lit, gx 0..4 and gy 0..6. +pub fn glyph57_px(g: i32, gx: i32, gy: i32) -> bool { + if (g < 0 || g >= GLYPHS57) { return false; } + if (gx < 0 || gx >= GW || gy < 0 || gy >= GH) { return false; } + const row = @as(i32, FONT57[@as(usize, @intCast(g * GH + gy))]); + return fmod(@divTrunc(row, PW5[@as(usize, @intCast(gx))]), 2) == 1; +} + +// Whether pixel (gx, gy) of code point c is lit. +pub fn glyph57_on(c: i32, gx: i32, gy: i32) -> bool { + return glyph57_px(glyph57_of(c), gx, gy); +} + +// The number of lit pixels of glyph g. +pub fn glyph57_lit(g: i32) -> i32 { + var n : i32 = 0; + var gy : i32 = 0; + while (gy < GH) { + var gx : i32 = 0; + while (gx < GW) { + if (glyph57_px(g, gx, gy)) { n = n + 1; } + gx = gx + 1; + } + gy = gy + 1; + } + return n; +} + +fn same_glyph(a: i32, b: i32) -> bool { + var gy : i32 = 0; + while (gy < GH) { + if (FONT57[@as(usize, @intCast(a * GH + gy))] != FONT57[@as(usize, @intCast(b * GH + gy))]) { return false; } + gy = gy + 1; + } + return true; +} + +// ---- tests ---- + +// "A", Zhe (D0 96), the euro sign (E2 82 AC); a stray continuation, "A", a cut-off lead; a lead +// byte followed by "A". +pub const U8_MIX : [6]u8 = [65, 208, 150, 226, 130, 172]; +pub const U8_BAD : [3]u8 = [150, 65, 208]; +pub const U8_CUT : [2]u8 = [208, 65]; + +test "UTF-8 decodes one, two and three bytes, and a malformed byte reads as one BAD character" { + const t = U8_MIX[0..6]; + assert(t.len == 6); + assert(cp_len(t, 0) == 1); + assert(cp_at(t, 0) == 65); + assert(cp_len(t, 1) == 2); + assert(cp_at(t, 1) == 1046); // Zhe, U+0416 + assert(cp_len(t, 3) == 3); + assert(cp_at(t, 3) == 8364); // the euro sign, U+20AC + assert(cp_count(t, 0, 6) == 3); + assert(cp_len(t, 6) == 0); + const s = U8_BAD[0..3]; + assert(cp_len(s, 0) == 1); + assert(cp_at(s, 0) == BAD); + assert(cp_at(s, 1) == 65); + assert(cp_len(s, 2) == 1); // a lead byte cut off by the end + assert(cp_at(s, 2) == BAD); + assert(cp_count(s, 0, 3) == 3); + const e = U8_CUT[0..2]; + assert(cp_len(e, 0) == 1); // a lead byte whose next byte is not a continuation + assert(cp_count(e, 0, 2) == 2); +} + +test "fold maps every lower-case letter to its capital and leaves the rest" { + assert(fold(97) == 65); + assert(fold(122) == 90); + assert(fold(1072) == 1040); + assert(fold(1103) == 1071); + assert(fold(1105) == CYR_YO); + assert(fold(1040) == 1040); + assert(fold(48) == 48); + assert(fold(124) == 124); +} + +test "the font table: 85 glyphs of seven 5-bit rows, a blank space, every other glyph lit" { + assert(GLYPHS57 * GH == 595); + var i : usize = 0; + while (i < 595) { + assert(@as(i32, FONT57[i]) <= 31); + i = i + 1; + } + assert(glyph57_lit(0) == 0); + var g : i32 = 1; + while (g < GLYPHS57) { + assert(glyph57_lit(g) > 0); + g = g + 1; + } + assert(glyph57_of(63) == QMARK); +} + +test "no two glyphs share a bitmap" { + var a : i32 = 0; + while (a < GLYPHS57) { + var b : i32 = a + 1; + while (b < GLYPHS57) { + assert(!same_glyph(a, b)); + b = b + 1; + } + a = a + 1; + } +} + +test "the Cyrillic map: 11 Latin look-alikes, and each of the 21 own glyphs used exactly once" { + var latin : i32 = 0; + var k : usize = 0; + while (k < 32) { + if (CYR_MAP[k] < 64) { latin = latin + 1; } + k = k + 1; + } + assert(latin == 11); + var g : i32 = 64; + while (g < GLYPHS57) { + var uses : i32 = 0; + var m : usize = 0; + while (m < 32) { + if (@as(i32, CYR_MAP[m]) == g) { uses = uses + 1; } + m = m + 1; + } + assert(uses == 1); + g = g + 1; + } + assert(glyph57_of(1040) == glyph57_of(65)); // A + assert(glyph57_of(1056) == glyph57_of(80)); // Er draws as P + assert(glyph57_of(1061) == glyph57_of(88)); // Ha draws as X + assert(glyph57_of(CYR_YO) == glyph57_of(69)); // Yo draws as E +} + +test "a lower-case letter lights exactly the pixels of its capital, Latin and Cyrillic" { + var c : i32 = 0; + while (c < 32) { + var gy : i32 = 0; + while (gy < GH) { + var gx : i32 = 0; + while (gx < GW) { + assert(glyph57_on(1072 + c, gx, gy) == glyph57_on(1040 + c, gx, gy)); + if (c < 26) { + assert(glyph57_on(97 + c, gx, gy) == glyph57_on(65 + c, gx, gy)); + } + gx = gx + 1; + } + gy = gy + 1; + } + c = c + 1; + } +} + +test "an unknown code point draws as a question mark, and pixels outside the box are dark" { + assert(glyph57_of(19968) == QMARK); // U+4E00 + assert(glyph57_of(BAD) == QMARK); + assert(glyph57_of(126) == QMARK); // '~' is not in the font + assert(!glyph57_on(65, 5, 0)); + assert(!glyph57_on(65, 0, 7)); + assert(!glyph57_on(65, 0 - 1, 0)); + assert(glyph57_on(65, 0, 6)); // A's left leg reaches the base line +} diff --git a/specs/t27f/idea_factory.t27 b/specs/t27f/idea_factory.t27 index 8affe44e9f..5c05863743 100644 --- a/specs/t27f/idea_factory.t27 +++ b/specs/t27f/idea_factory.t27 @@ -9,10 +9,10 @@ // was a hand-built module with its own f64 constants (token_floor, reel_particles, lesson_reel). // // THE FACTORY, in four stages, each a pure function the tests pin: -// wrap the idea is cut into lines of at most MAXC characters (greedy, by words; a longer -// word is split) and lines into scenes of at most MAXL; an explicit `|` cuts a scene. -// layout a scene's lines are set in the 3x5 font of scene_raster, centred, at the largest cell -// size S that fits the text box; every lit font pixel is a cell, a target for particles. +// wrap the idea, UTF-8, is cut into lines of at most MAXC characters (greedy, by words; +// a longer word is split) and lines into scenes of at most MAXL; a `|` cuts a scene. +// layout a scene's lines are set in glyph57's 5x7 Latin and Cyrillic font, centred, at the +// largest cell size S that fits the text box; every lit font pixel is a cell, a target. // assign at each scene cut every particle gets a new target: a sub-slot of a cell, or -- when // the preset has a card -- a point on the card's border around the text. The closing // scene is the address, CTA. @@ -23,13 +23,21 @@ // THE NUMBERS: layout and assignment run once per scene cut and may divide by a variable (the cell // size); the per-step and per-pixel paths are particle_ds's -- integer, Q8, table lookups. // -// NOT CLAIMED: the font is ASCII (no Cyrillic); no voice or music (lesson_reel mode 5 has those); -// gen-verilog has not been run on this module and no board has drawn it. +// A RUSSIAN IDEA is a u8 array of UTF-8 bytes (L3 keeps sources ASCII, and t27c keeps "\xNN" as +// four characters): RU_IDEA below, passed as RU_IDEA[0..80]. +// +// NOT CLAIMED: capitals only (lower case folds); no voice or music (lesson_reel mode 5 has those); +// the Verilog is emitted, not synthesised, and no board has drawn it. // phi^2 + 1/phi^2 = 3 | TRINITY module idea_factory; -use t27f::scene_raster::glyph_on; +use t27f::glyph57::glyph57_on; +use t27f::glyph57::cp_len; +use t27f::glyph57::cp_at; +use t27f::glyph57::cp_count; +use t27f::glyph57::GW; +use t27f::glyph57::GH; use t27f::scene_raster::rnd; use t27f::scene_raster::lcg; use t27f::scene_raster::clamp; @@ -71,6 +79,21 @@ pub const VERSION : u8 = 1; // ---- the factory's input: the lab recipe rewrites these two lines per render ---- pub const IDEA : str = "PARTICLES BUILD EVERY FRAME|ONE SPEC MANY STYLES"; pub const PRESET : usize = 0; + +// The Russian sample, "CHASTITSY STROYAT KADR|ODNA SPEKA MNOGO STILEJ" -- particles build the +// frame, one spec many styles -- as UTF-8 bytes; RU_IDEA[0..36] is its first scene alone. +pub const RU_IDEA : [80]u8 = [ + 208, 167, 208, 144, 208, 161, 208, 162, 208, 152, 208, 166, 208, 171, 32, + 208, 161, 208, 162, 208, 160, 208, 158, 208, 175, 208, 162, 32, + 208, 154, 208, 144, 208, 148, 208, 160, 124, + 208, 158, 208, 148, 208, 157, 208, 144, 32, + 208, 161, 208, 159, 208, 149, 208, 154, 208, 144, 32, + 208, 156, 208, 157, 208, 158, 208, 147, 208, 158, 32, + 208, 161, 208, 162, 208, 152, 208, 155, 208, 149, 208, 153 +]; +pub const RU_ZHA_UP : [4]u8 = [208, 150, 208, 144]; +pub const RU_ZHA_LO : [4]u8 = [208, 182, 208, 176]; +pub const RU_ZHZH : [4]u8 = [208, 150, 208, 150]; pub const CTA : str = "T27.AI"; // ---- the frame ---- @@ -86,16 +109,18 @@ pub const CY : i32 = 864; // 0.45 of the height, under the key // ---- the wrap and the layout ---- pub const MAXC : i32 = 9; // characters a line pub const MAXL : i32 = 4; // lines a scene -pub const LINE_PITCH : i32 = 7; // font pixels from one line's top to the next's +pub const ADV : i32 = 6; // font pixels from one letter's left to the next's +pub const LINE_PITCH : i32 = 9; // font pixels from one line's top to the next's pub const TEXT_W : i32 = 907; // 0.84 of the width pub const TEXT_H : i32 = 768; // 0.40 of the height pub const CELL_MAX : i32 = 40; // the largest font pixel, px -pub const MAXCELLS : i32 = 1024; // MAXL * MAXC * 15 = 540 is the most a scene can light +pub const MAXCELLS : i32 = 1280; // MAXL * MAXC * GW * GH = 1260 is the most a scene can light // ---- wrap ---- -// The k-th line of text t: what 0 = its first byte, 1 = its length in bytes, 2 = its scene; -1 when -// there is no such line. what 3 = the number of lines, what 4 = the number of scenes (k unused). +// The k-th line of text t: what 0 = its first byte, 1 = its length in bytes, 2 = its scene, 5 = its +// length in characters; -1 when there is no such line. what 3 = the number of lines, what 4 = the +// number of scenes (k unused). Widths are counted in characters, positions in bytes. pub fn wrap_walk(t: []const u8, k: i32, what: i32) -> i32 { const n = @as(i32, @intCast(t.len)); var line : i32 = 0; @@ -121,10 +146,12 @@ pub fn wrap_walk(t: []const u8, k: i32, what: i32) -> i32 { i = i + 1; } else { var j : i32 = i; - while (j < n && j - i < MAXC && t[@as(usize, @intCast(j))] != 32 && t[@as(usize, @intCast(j))] != 124) { - j = j + 1; + var w : i32 = 0; + while (j < n && w < MAXC && t[@as(usize, @intCast(j))] != 32 && t[@as(usize, @intCast(j))] != 124) { + j = j + cp_len(t, j); + w = w + 1; } - if (ls >= 0 && j - ls > MAXC) { + if (ls >= 0 && cp_count(t, ls, j) > MAXC) { flush = true; } else { if (ls < 0) { ls = i; } @@ -138,6 +165,7 @@ pub fn wrap_walk(t: []const u8, k: i32, what: i32) -> i32 { if (what == 0) { return ls; } if (what == 1) { return le - ls; } if (what == 2) { return scene; } + if (what == 5) { return cp_count(t, ls, le); } } line = line + 1; in_scene = in_scene + 1; @@ -181,12 +209,12 @@ pub fn scene_cell(t: []const u8, s: i32, c: i32, what: i32) -> i32 { if (wrap_walk(t, k, 2) == s) { if (first < 0) { first = k; } nl = nl + 1; - maxlen = imax(maxlen, wrap_walk(t, k, 1)); + maxlen = imax(maxlen, wrap_walk(t, k, 5)); } k = k + 1; } if (nl == 0) { return 0; } - const cols = maxlen * 4 - 1; + const cols = maxlen * ADV - 1; const rows = nl * LINE_PITCH - 2; const cs = imin(imin(@divTrunc(TEXT_W, cols), @divTrunc(TEXT_H, rows)), CELL_MAX); const ox = CX - @divTrunc(cols * cs, 2); @@ -199,19 +227,19 @@ pub fn scene_cell(t: []const u8, s: i32, c: i32, what: i32) -> i32 { var n : i32 = 0; var li : i32 = 0; while (li < nl) { - const st = wrap_walk(t, first + li, 0); - const ln = wrap_walk(t, first + li, 1); - const lox = CX - @divTrunc((ln * 4 - 1) * cs, 2); + var bi : i32 = wrap_walk(t, first + li, 0); + const ln = wrap_walk(t, first + li, 5); + const lox = CX - @divTrunc((ln * ADV - 1) * cs, 2); var ci : i32 = 0; while (ci < ln) { - const ch = @as(i32, t[@as(usize, @intCast(st + ci))]); + const ch = cp_at(t, bi); var gy : i32 = 0; - while (gy < 5) { + while (gy < GH) { var gx : i32 = 0; - while (gx < 3) { - if (glyph_on(ch, gx, gy)) { + while (gx < GW) { + if (glyph57_on(ch, gx, gy)) { if (n == c) { - if (what == 0) { return lox + (ci * 4 + gx) * cs + @divTrunc(cs, 2); } + if (what == 0) { return lox + (ci * ADV + gx) * cs + @divTrunc(cs, 2); } if (what == 1) { return oy + (li * LINE_PITCH + gy) * cs + @divTrunc(cs, 2); } } n = n + 1; @@ -220,6 +248,7 @@ pub fn scene_cell(t: []const u8, s: i32, c: i32, what: i32) -> i32 { } gy = gy + 1; } + bi = bi + cp_len(t, bi); ci = ci + 1; } li = li + 1; @@ -291,8 +320,8 @@ pub fn main() -> void { var cr : [3072]i32 = undefined; var cg : [3072]i32 = undefined; var cb : [3072]i32 = undefined; - var cellx : [1024]i32 = undefined; - var celly : [1024]i32 = undefined; + var cellx : [1280]i32 = undefined; + var celly : [1280]i32 = undefined; var kt : [1024]i32 = undefined; var tt : [1024]i32 = undefined; var accr : [1080]i32 = undefined; @@ -514,6 +543,48 @@ test "cells are the font's lit pixels: AA lights twice A, a space lights none" { assert(scene_cell("a", 0, 0, 2) == a); } +// "CHASTITSY STROYAT KADR" -- particles build the frame -- in Cyrillic capitals, UTF-8. +test "a Russian idea wraps by letters, not bytes: a 7-letter, 14-byte word keeps one line" { + const t = RU_IDEA[0..36]; + assert(t.len == 36); + assert(line_count(t) == 3); + assert(wrap_walk(t, 0, 5) == 7); + assert(wrap_walk(t, 0, 1) == 14); + assert(wrap_walk(t, 1, 5) == 6); + assert(wrap_walk(t, 2, 0) == 28); + assert(wrap_walk(t, 2, 5) == 4); + assert(scene_count(t) == 1); + const r = RU_IDEA[0..80]; + assert(line_count(r) == 7); + assert(scene_count(r) == 2); + assert(wrap_walk(r, 3, 5) == 4); // ODNA opens scene 1 + assert(wrap_walk(r, 6, 5) == 6); // STILEJ, 12 bytes + assert(wrap_walk(r, 6, 1) == 12); + const n = scene_cell(t, 0, 0, 2); + assert(n > 0 && n <= MAXCELLS); + var c : i32 = 0; + while (c < n) { + const cx = scene_cell(t, 0, c, 0); + assert(cx > scene_cell(t, 0, 0, 3) && cx < scene_cell(t, 0, 0, 5)); + c = c + 1; + } +} + +test "Russian lower case lights the cells of its capitals, and ZheZhe lights twice Zhe" { + const up = RU_ZHA_UP[0..4]; + const lo = RU_ZHA_LO[0..4]; + const n = scene_cell(up, 0, 0, 2); + assert(n > 0); + assert(scene_cell(lo, 0, 0, 2) == n); + var c : i32 = 0; + while (c < n) { + assert(scene_cell(lo, 0, c, 0) == scene_cell(up, 0, c, 0)); + assert(scene_cell(lo, 0, c, 1) == scene_cell(up, 0, c, 1)); + c = c + 1; + } + assert(scene_cell(RU_ZHZH[0..4], 0, 0, 2) == 2 * scene_cell(RU_ZHZH[0..2], 0, 0, 2)); +} + test "every target of every scene lies inside the frame, and the card frames the text" { var s : i32 = 0; while (s < reel_scenes(IDEA)) {